原文:
blog.csdn.net/Gaowumao/article/details/124309548

平常做的项目都是在一台应用系统,并且所有的操作都在一台Tomcat服务器上,并不会引发Session共享的问题,所以并不会对我们的系统产生影响,但是当我们部署多个微服务的时候,再搭配Nginx进行负载均衡时,如果不处理分布式Session问题,我们在系统中访问不同功能时就会频繁出现用户登录的操作。
图解分析原因:
分布式 Session - 图1
前提:用户登录功能和图中的商品订单模块、秒杀抢购模块属于单独的微服务模块
用户登录成功后想要访问图中其他两个模块的功能时,由于Nginx使用默认负载均衡策略(轮询),这时请求会按照时间顺序逐一分发到后端应用上,也就是说用户在Tomcat1上登录成功之后,用户的信息放在Tomcat1的Session里,过了一会,用户想要进行秒杀活动的功能操作,请求又被Nginx分发到了Tomcat2,而这时的Tomcat2上的Session里还没有用户信息,于是就是出现让用户重新登录的情况,在微服务分布式项目中,不同的功能模块必然会被分列成各自的微服务,假设访问一个功能都需要重新登录一次,用户的体验必然会大幅度下降!
那如何来解决分布式Session问题呢?

一、解决方案列举

1. Session复制

优点:

  • 无需修改代码,只需要修改Tomcat配置

缺点:

  • Session同步传输占用内网宽带
  • 多台Tomcat同步性能指数级下降
  • Session占用内存,无法有效水平扩展

    2. 前端存储

    优点:

  • 不占用服务器内存

缺点:

  • 存在安全风险
  • 数据大小受cookie限制
  • 占用外网宽带

    3. Session粘滞

    优点:

  • 无需修改代码

  • 服务端可以水平扩展

缺点:

  • 增加新机器,会重新Hash,导致重新登录
  • 应用重新启动后,需要重新登录

    4. 后端集中存储

    优点:

  • 安全

  • 容易水平扩展

优点:

  • 增加复杂度
  • 需要修改代码

    二、Java代码实现解决分布式Session

    1. SpringSession - Redis解决分布式Session
    添加依赖
    1. <!--Redis-->
    2. <dependency>
    3. <groupId>org.springframework.boot</groupId>
    4. <artifactId>spring-boot-starter-data-redis</artifactId>
    5. </dependency>
    6. <!--commons-pools2 对象池依赖-->
    7. <dependency>
    8. <groupId>org.apache.commons</groupId>
    9. <artifactId>commons-pool2</artifactId>
    10. </dependency>
    11. <!--spring session 依赖-->
    12. <dependency>
    13. <groupId>org.springframework.session</groupId>
    14. <artifactId>spring-session-data-redis</artifactId>
    15. </dependency>

添加Redis配置

  1. ## Redis配置
  2. spring:
  3. redis:
  4. # 服务器地址
  5. host: localhost
  6. # 端口
  7. port: 6379
  8. # 数据库
  9. database: 0
  10. # 超时时间
  11. connect-timeout: 10000ms
  12. lettuce:
  13. pool:
  14. # 最大连接数
  15. max-active: 8
  16. # 最大连接阻塞等待时间 默认 -1
  17. max-wait: 10000ms
  18. # 最大空闲时间 默认8
  19. max-idle: 200
  20. # 最小空闲连接 默认8
  21. min-idle: 5

业务逻辑实现

  1. /**
  2. * 登录功能
  3. * @param loginVo
  4. * @return
  5. */
  6. @Override
  7. public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
  8. String username = loginVo.getUserName();
  9. String password = loginVo.getPassword();
  10. User user = userMapper.selectByUserName(username);
  11. if (user == null){
  12. throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
  13. }
  14. //判断密码是否正确
  15. if (!MDUtils.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
  16. throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
  17. }
  18. //使用UUID生成字符串代替Cookie
  19. String ticket = UUIDUtil.uuid();
  20. request.getSession().setAttribute(ticket,user);
  21. CookieUtil.setCookie(request,response,"userTicket",ticket);
  22. return RespBean.success();
  23. }

视图控制层

  1. /**
  2. * 跳转商品列表
  3. * @param session
  4. * @param model
  5. * @return
  6. */
  7. @RequestMapping("/toList")
  8. public String toList(HttpSession session, Model model,@CookieValue("userTicket")String ticket){
  9. if (StringUtils.isEmpty(ticket)){
  10. return "login";
  11. }
  12. User user = (User) session.getAttribute(ticket);
  13. if (user == null){
  14. return "login";
  15. }
  16. model.addAttribute("user",user);
  17. return "goodsList";
  18. }

登录测试
分布式 Session - 图2
打开Redis管理软件发现Session信息已经添加到Redis中了
分布式 Session - 图3
2. Redis解决分布式Session
导入依赖

  1. <!--Redis-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-redis</artifactId>
  5. </dependency>
  6. <!--commons-pools2 对象池依赖-->
  7. <dependency>
  8. <groupId>org.apache.commons</groupId>
  9. <artifactId>commons-pool2</artifactId>
  10. </dependency>

Redis配置参考 【SpringSession - Redis解决分布式Session】
业务逻辑层

  1. @Override
  2. public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
  3. String username = loginVo.getUserName();
  4. String password = loginVo.getPassword();
  5. User user = userMapper.selectByUserName(username);
  6. if (user == null){
  7. throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
  8. }
  9. //判断密码是否正确
  10. if (!MDUtils.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
  11. throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
  12. }
  13. //成功Cookie
  14. String ticket = UUIDUtil.uuid();
  15. //将用户信息存入到redis中
  16. redisTemplate.opsForValue().set("userTicket",ticket);
  17. redisTemplate.opsForValue().set("user:"+ticket,user);
  18. //request.getSession().setAttribute(ticket,user);
  19. CookieUtil.setCookie(request,response,"userTicket",ticket);
  20. return RespBean.success();
  21. }
  22. /**
  23. * 根据cookie获取cookie
  24. * @param ticket
  25. * @return
  26. */
  27. @Override
  28. public User getUserByByCookie(String ticket,HttpServletRequest request,HttpServletResponse response) {
  29. if (StringUtils.isEmpty(ticket)){
  30. return null;
  31. }
  32. User user = (User) redisTemplate.opsForValue().get("user:" + ticket);
  33. if (user == null){
  34. CookieUtil.setCookie(request,response,"userTicket",ticket);
  35. }
  36. return user;
  37. }

视图控制层

  1. /**
  2. * 跳转商品列表
  3. * @param session
  4. * @param model
  5. * @return
  6. */
  7. @RequestMapping("/toList")
  8. public String toList(HttpSession session, Model model,HttpServletRequest request,HttpServletResponse response){
  9. String ticket = (String) redisTemplate.opsForValue().get("userTicket");
  10. if (StringUtils.isEmpty(ticket)){
  11. return "login";
  12. }
  13. //User user = (User) session.getAttribute(ticket);
  14. User user = userService.getUserByByCookie(ticket, request, response);
  15. if (user == null){
  16. return "login";
  17. }
  18. model.addAttribute("user",user);
  19. return "goodsList";
  20. }

测试成功
分布式 Session - 图4
查看Redis管理工具
分布式 Session - 图5