1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        最全分布式Session解決方案

        共 8880字,需瀏覽 18分鐘

         ·

        2021-10-12 14:02

        點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

        Java術(shù)?|?

        考慮一個(gè)場(chǎng)景,用戶在進(jìn)行下單操作之前后臺(tái)需要校驗(yàn)該用戶是否登錄,若未登錄則不允許提交訂單,這在傳統(tǒng)的單體應(yīng)用中非常容易實(shí)現(xiàn),只需在提交訂單之前判斷Session中的用戶信息是否登錄即可,但在分布式應(yīng)用中,這顯然是一個(gè)待解決的問題。


        NO.1


        分布式應(yīng)用下Session存在的問題


        在分布式架構(gòu)中,一個(gè)應(yīng)用往往被劃分為多個(gè)子模塊,比如:登錄注冊(cè)模塊和訂單模塊,當(dāng)應(yīng)用被拆分后,隨之而來的便是數(shù)據(jù)的共享問題:

        一般我們都在登錄注冊(cè)模塊中將用戶的登錄狀態(tài)保存到Session中,然而當(dāng)用戶進(jìn)行下單操作時(shí),由于訂單模塊是獨(dú)立的,它無法獲取到登錄注冊(cè)模塊中保存的Session,所以訂單模塊是無法判斷用戶是否登錄的。而為了保證系統(tǒng)的高可用,一個(gè)模塊往往被部署多份形成集群,這些模塊之間的數(shù)據(jù)共享也是一個(gè)問題:

        用戶在一個(gè)模塊中登錄成功后,很可能在下次訪問時(shí)請(qǐng)求被負(fù)載均衡到其它的集群模塊中,這樣會(huì)導(dǎo)致無法讀取到Session,使得用戶又得重新登錄一次系統(tǒng)。


        NO.2


        Session共享問題的案例演示


        下面編寫一個(gè)案例進(jìn)行演示,首先創(chuàng)建一個(gè)SpringBoot應(yīng)用,實(shí)現(xiàn)登錄模塊:

        @RestController
        public class LoginController {

        @Autowired
        private ServiceOrderClient serviceOrderClient;

        @GetMapping("/login")
        public Result login(User user, HttpSession session) {
        String username = user.getUsername();
        String password = user.getPassword();
        Result result = new Result();
        if ("admin".equals(username) && "admin".equals(password)) {
        result.setCode(200);
        result.setMessage("登錄成功");
        session.setAttribute("user", user);
        } else {
        result.setCode(-1);
        result.setMessage("登錄失敗");
        }
        }
        }

        再創(chuàng)建一個(gè)SpringBoot應(yīng)用,實(shí)現(xiàn)訂單模塊:

        @RestController
        public class OrderController {

        @GetMapping("/order/test")
        public String order(@CookieValue("JSESSIONID") String jSessionId) {
        return "success";
        }
        }

        代碼都非常簡(jiǎn)單,我們主要是觀察Session的問題,在登錄模塊中編寫遠(yuǎn)程調(diào)用接口:

        @FeignClient("service-order")
        public interface ServiceOrderClient {

        @GetMapping("/order/test")
        String order();
        }

        將這兩個(gè)應(yīng)用都注冊(cè)到Nacos中,其它代碼我就不貼出來了,都比較簡(jiǎn)單。分別啟動(dòng)這兩個(gè)項(xiàng)目,并訪問 http://localhost:8080/test ,會(huì)發(fā)現(xiàn)訪問是不成功的:控制臺(tái)輸出的結(jié)果:

        2021-09-21 16:51:43.155  WARN 20908 --- [nio-9000-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingRequestCookieException: Missing cookie 'JSESSIONID' for method parameter of type String]

        找不到名為?JSESSIONID?的Cookie,我們知道,服務(wù)端是通過JSESSIONID來找到該用戶對(duì)應(yīng)的Session信息的,既然JSESSIONID都獲取不到,就更不用說用戶信息了,這就是Session不共享的問題。


        NO.3


        Redis解決Session共享問題


        對(duì)于分布式應(yīng)用中的Session問題,其實(shí)也非常簡(jiǎn)單,無非就是不能共享到Session,所以,我們可以類比緩存的思想,將Session放入緩存中,其它服務(wù)想要獲取Session也從緩存中拿,這樣就實(shí)現(xiàn)了Session的共享。改進(jìn)一下登錄模塊:

        @GetMapping("/login")
        public Result login(User user, HttpSession session) {
        String username = user.getUsername();
        String password = user.getPassword();
        Result result = new Result();
        if ("admin".equals(username) && "admin".equals(password)) {
        result.setCode(200);
        result.setMessage("登錄成功");
        String json = JSONObject.toJSONString(user);
        redisTemplate.opsForValue().set("session", json);
        } else {
        result.setCode(-1);
        result.setMessage("登錄失敗");
        }
        return result;
        }

        當(dāng)我們?cè)L問登錄接口?http://localhost:8080/login?username=admin&password=admin[1]?時(shí),就會(huì)向Redis保存一份Session的值:此時(shí)若是其它服務(wù)需要Session,只要從Redis中讀取即可,修改一下訂單模塊:

        @RestController
        public class OrderController {

        @GetMapping("/order/test")
        public String order() {
        return "success";
        }
        }

        在訂單模塊中添加一個(gè)登錄的攔截器:

        public class LoginInterceptor implements HandlerInterceptor {

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 手動(dòng)獲取StringRedisTemplate對(duì)象
        StringRedisTemplate redisTemplate = SpringBeanOperator.getBean(StringRedisTemplate.class);
        String json = redisTemplate.opsForValue().get("session");
        User user = JSONObject.parseObject(json, User.class);
        System.out.println(user);
        if (user == null) {
        System.out.println("用戶未登錄......");
        return false;
        } else {
        System.out.println("用戶已登錄......");
        return true;
        }
        }
        }

        將攔截器注冊(cè)一下:

        @Configuration
        public class MyWebConfig implements WebMvcConfigurer {

        @Override
        public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
        .addPathPatterns("/**");
        }
        }

        重啟項(xiàng)目,訪問 http://localhost:8080/test ,輸出結(jié)果:

        User(username=admin, password=admin)
        用戶已登錄......


        NO.4


        SpringSession解決Session共享問題

        剛才我們自己使用Redis嘗試著解決了一下Session的共享問題,然而這種方式是有很多缺陷的,首先,我們保存的只是一個(gè)User對(duì)象,并不是Session,所以我們無法標(biāo)識(shí)該用戶,這樣會(huì)導(dǎo)致用戶訪問到了其它用戶的信息,使得系統(tǒng)混亂。我們當(dāng)然可以使用JSESSIONID來標(biāo)識(shí)不同的用戶,但其實(shí),Spring已經(jīng)為我們提供了一個(gè)組件來解決這一問題,那就是SpringSession。

        在兩個(gè)模塊中都引入SpringSession的依賴:


        <groupId>org.springframework.sessiongroupId>
        <artifactId>spring-session-data-redisartifactId>

        在application.yml中配置一下Session的保存方式為Redis:

        spring:  session:
        store-type: redis

        最后在啟動(dòng)類上添加 @EnableRedisHttpSession 注解,這樣SpringSession的整合就完成了。我們修改登錄模塊的代碼:

        @GetMapping("/login")
        public Result login(User user, HttpSession session) {
        String username = user.getUsername();
        String password = user.getPassword();
        Result result = new Result();
        if ("admin".equals(username) && "admin".equals(password)) {
        result.setCode(200);
        result.setMessage("登錄成功");
        session.setAttribute("user",user);
        } else {
        result.setCode(-1);
        result.setMessage("登錄失敗");
        }
        return result;
        }

        按照正常流程將User對(duì)象存入Session,重啟項(xiàng)目并訪問登錄接口,來看看Redis中有什么變化:此時(shí)Redis中已經(jīng)保存了用戶信息,并且還有創(chuàng)建時(shí)間、存活時(shí)間等配置,其它模塊要想獲取到Session中的用戶信息,也只需要按正常流程編寫代碼即可:

        public class LoginInterceptor implements HandlerInterceptor {

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("user");
        System.out.println(user);
        if (user == null) {
        System.out.println("用戶未登錄......");
        return false;
        } else {
        System.out.println("用戶已登錄......");
        return true;
        }
        }
        }

        需要注意的是登錄模塊存入的User對(duì)象需要和其它模塊讀出的User對(duì)象包名一致,所以最好將User類抽取到公共模塊中,提供給所有模塊使用。

        到這里SpringSession就解決了Session共享的問題,你可以運(yùn)行項(xiàng)目測(cè)試一下,訪問 http://localhost:8080/test :結(jié)果出乎意料,控制臺(tái)的結(jié)果是:

        null
        用戶未登錄......

        這就奇怪了,難道是SpringSession沒起作用?我們寫一個(gè)測(cè)試方法測(cè)試一下:

        @GetMapping("/test")
        public String test(HttpSession session) {
        User user = (User) session.getAttribute("user");
        System.out.println(user);
        return "test";
        }

        訪問 http://localhost:9000/test ,得到結(jié)果:

        User(username=admin, password=admin)

        顯然SpringSession是沒有任何問題的,那么問題出在哪里了呢?

        NO.5

        OpenFeign遠(yuǎn)程調(diào)用的坑


        剛才我們進(jìn)行了測(cè)試,發(fā)現(xiàn)在訂單模塊中直接訪問Session可以獲取User對(duì)象,然而通過遠(yuǎn)程調(diào)用,User就獲取不到了,我們可以猜測(cè)這是OpenFeign出現(xiàn)了問題,Debug調(diào)試一下項(xiàng)目,這是遠(yuǎn)程調(diào)用的代碼:我們跟進(jìn)去看看:

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("equals".equals(method.getName())) {
        try {
        Object otherHandler =
        args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
        return equals(otherHandler);
        } catch (IllegalArgumentException e) {
        return false;
        }
        } else if ("hashCode".equals(method.getName())) {
        return hashCode();
        } else if ("toString".equals(method.getName())) {
        return toString();
        }

        return dispatch.get(method).invoke(args);
        }

        該方法中進(jìn)行了一些判斷,最終會(huì)調(diào)用dispatch.get()方法:

        @Override
        public Object invoke(Object[] argv) throws Throwable {
        RequestTemplate template = buildTemplateFromArgs.create(argv);
        Options options = findOptions(argv);
        Retryer retryer = this.retryer.clone();
        while (true) {
        try {
        return executeAndDecode(template, options);
        } catch (RetryableException e) {
        try {
        retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
        Throwable cause = th.getCause();
        if (propagationPolicy == UNWRAP && cause != null) {
        throw cause;
        } else {
        throw th;
        }
        }
        if (logLevel != Logger.Level.NONE) {
        logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
        }
        }
        }

        該方法又會(huì)調(diào)用executeAndDecode():該方法會(huì)封裝一個(gè)請(qǐng)求模板作為目標(biāo)請(qǐng)求進(jìn)行遠(yuǎn)程調(diào)用,然而我們觀察到該請(qǐng)求模板中并沒有任何的參數(shù)和請(qǐng)求頭,而我們知道,Session是依靠JSESSIONID進(jìn)行識(shí)別的,在SpringSession中,Session是依靠SESSION識(shí)別的:由此我們得到結(jié)論,因?yàn)镺penFeign遠(yuǎn)程調(diào)用丟失了請(qǐng)求頭,導(dǎo)致SESSIONID丟失,最終導(dǎo)致訂單模塊無法獲取到User對(duì)象。得知了問題后,解決就非常簡(jiǎn)單了,我們可以創(chuàng)建一個(gè)請(qǐng)求過濾器,它將在請(qǐng)求模板生成前對(duì)請(qǐng)求進(jìn)行處理:

        @Configuration
        public class MyFeignConfig {

        @Bean
        public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
        System.out.println("遠(yuǎn)程調(diào)用前調(diào)用該方法-->requestInterceptor......");
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        String cookie = request.getHeader("Cookie");
        requestTemplate.header("Cookie", cookie);
        };
        }
        }

        將原Request對(duì)象中的Cookie請(qǐng)求頭信息設(shè)置給請(qǐng)求模板,這樣OpenFeign創(chuàng)建的請(qǐng)求就具有了Cookie內(nèi)容,重新啟動(dòng)項(xiàng)目測(cè)試,問題迎刃而解。

        References

        [1]?http://localhost:8080/login?username=admin&password=admin:?http://localhost:8080/login?username=admin&password=admin

        本文作者:汪偉俊?為Java技術(shù)迷專欄作者?投稿,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載

        1、靈魂一問:你的登錄接口真的安全嗎?

        2、HashMap 中這些設(shè)計(jì),絕了~

        3、在 IntelliJ IDEA 中這樣使用 Git,賊方便了!

        4、計(jì)算機(jī)時(shí)間到底是怎么來的?程序員必看的時(shí)間知識(shí)!

        5、這些IDEA的優(yōu)化設(shè)置趕緊安排起來,效率提升杠杠的!

        6、21 款 yyds 的 IDEA插件

        7、真香!用 IDEA 神器看源碼,效率真高!

        點(diǎn)分享

        點(diǎn)收藏

        點(diǎn)點(diǎn)贊

        點(diǎn)在看

        瀏覽 29
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产97自拍 | 成人性生活电影两区 三区 四区 五区 | 成人免费无码淫片在线观看免费 | 男女爽啪啪gif动态第140期 | 亚洲BB | 操亚洲| 114国产精品久久免费观看 | 操屄影视 | 亲嘴伸进内衣揉胸口激烈小说 | 日韩国产精品一级片 |