QQ登录

只需要一步,快速开始

APP扫码登录

只需要一步,快速开始

手机号码,快捷登录

手机号码,快捷登录

查看: 2010|回复: 0

[JAVA/JSP] 详解Java分布式系统中session一致性问题

[复制链接]

等级头衔

积分成就    金币 : 2841
   泡泡 : 1516
   精华 : 6
   在线时间 : 1294 小时
   最后登录 : 2024-11-21

丰功伟绩

优秀达人突出贡献荣誉管理论坛元老

联系方式
发表于 2021-4-22 21:19:07 | 显示全部楼层 |阅读模式
一、业务场景6 [/ v' E% ^8 q6 v) f$ U+ e( P3 F
       在单机系统中,用户登陆之后,服务端会保存用户的会话信息,只要用户不退出重新登陆,在一段时间内用户可以一直访问该网站,无需重复登陆。用户的信息存在服务端的 session 中,session中可以存放服务端需要的一些用户信息,例如用户ID,所属公司companyId,所属部门deptId等等。
- @, W3 p( ^) U+ Q, Q, ? 1.jpg        但是随着业务的发展,技术架构需要调整,原来的单机系统逐渐被更换,架构由单机扩展到分布式,甚至当下流行的微服务。虽然在用户端看来系统仍然是一个整体,但在技术端来说业务则被拆分成多个模块,各个模块之间相互独立,甚至不在同一台物理机器上,模块之间通过 RPC 进行通信。" E7 T) d6 }+ i# K( [
2.jpg " O5 {2 U4 U( i0 Y! u! M& Q& R
       那么原来单机只需一份的 session, 如何满足在多系统的运行下保证会话一致性呢?单独保存在任何一个系统中都不合适,而且每个单独模块系统也可能是分布式形式的,是由集群组成。那么session的分配就更复杂了。  R& C) N6 C% w, K# ~, ?+ D
二、Redis 实现
. _* L0 \6 T2 w( B0 @; |       针对以上问题,我们可能会从以下几个方面想到解决的方法,每个服务端存储一份,通过同步的方式保证一致性,但是这种方式有个很明显的缺点:session的同步需要数据传输,占内网带宽,有时延,网络不稳定的时候会造成部分系统同步延迟,那么就不能保证 session 一致性。而且所有服务端都包含所有session数据,数据量受内存限制,无法水平扩展。
3 x9 b4 w2 t" l- ]& L5 Z       那么我们是否可以单独将 session 信息存储在某一个独立的介质中,介质可以是DB也可以是缓存。
. y4 @( _0 ?$ V1 T* R9 E       考虑到如下业务:登陆的时候我们经常会给用户一个过期时间(一般移动端常设置为7天或者一个月甚至更久),到期后用户需要输入登陆信息重新登陆,即会话过期。这种到期的设置我们自然想到了Redis的 key expire功能,所以最终我们可以将Redis引入进来实现我们的这种需求。系统如下图所示:
6 s5 Z4 o: q( O4 p 3.jpg * i: M- ^( K" M4 y3 |$ H4 V1 t
       我们只需在用户首次登陆的时候将用户信息放到 Token并缓存到 Redis 中,同时设置一个过期时间,伪代码如下:" \8 ]" u& l* m1 ]/ o! M3 y  D
  1. @Override
  2. public Map login(UserDto dto) {
  3.     Map<String, Object> restMap = new HashMap<>();
  4.    
  5.     // 校验登陆信息
  6.     User user = checkLoginInfo(dto);
  7.      //删除旧的token
  8.     String token = (String) redisUtils.get(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName());
  9.    
  10.     if (!ObjectUtils.isEmpty(token)) {
  11.         redisUtils.delete(CacheConstants.USER_TOKEN_KEY_WEB + token);
  12.     }
  13.     // 唯一签名信息
  14.     String signStr = user.getCompanyId() + user.getUserName() + dto.getPassword() + DateUtils.now().getTime();
  15.     token = MD5Utils.md5(signStr);
  16.     // 设置用户 token
  17.     redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_WEB + token, user.getId(), LOGIN_EXPIRED_TIME);
  18.     //缓存新的token
  19.     redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName(), token, LOGIN_EXPIRED_TIME);
  20.     dto.setCompanyId(user.getCompanyId());
  21.     dto.setId(user.getId());
  22.     restMap.put("token", token);
  23.     restMap.put("userName", user.getUserName());
  24.     return restMap;
  25. }
      那么在系统中如何使用呢,我们可以定义一个** SessionInterceptor,当访问 web 接口的时候检验用户的 token 信息,判断用户是否登陆,未登录的情况下一些业务接口是无法访问的,以及在登陆的情况下拿到我们需要的用户信息,如 userId。
" E% S" t6 P* J+ X5 W1 V
  1. public class SessionInterceptor {
  2.     @Autowired
  3.     private RedisUtils redisUtils;
  4.    
  5.     @Autowired
  6.     private UserService userService;
  7.     @Pointcut("execution(* com.jajian.demo.web.*.controller.*.*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
  8.     public void controllerMethodPointcut() {
  9.     }
  10.     @Around("controllerMethodPointcut()")
  11.     public Object Interceptor(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
  12.         
  13.         Signature signature = proceedingJoinPoint.getSignature();
  14.         MethodSignature methodSignature = (MethodSignature) signature;
  15.         Method targetMethod = methodSignature.getMethod();
  16.         if (targetMethod.getDeclaringClass().isAnnotationPresent(NoLogin.class) || targetMethod.isAnnotationPresent(NoLogin.class)) {
  17.             return proceedingJoinPoint.proceed();
  18.         }
  19.         // 从获取RequestAttributes中获取HttpServletRequest的信息
  20.         RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  21.         HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
  22.         String token = request.getHeader("token");
  23.         if(StringUtils.isEmpty(token)){
  24.             Log.debug("验证token", "token验证失败,{}", "token不存在");
  25.             throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
  26.         }
  27.         Integer userId= (Integer)redisUtils.get(CacheConstants.USER_TOKEN_KEY_WEB + token);
  28.       
  29.         if (null == userId) {
  30.             Log.debug("验证token", "token验证失败,{}", "token超时");
  31.             throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
  32.         }
  33.         User user = userService.getById(userId.longValue());
  34.         if (ObjectUtils.isEmpty(user)){
  35.             Log.debug("验证token", "token验证失败,{}", "用户信息不存在");
  36.             throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
  37.         }
  38.         if (user.getStatus() == UserStatusEnum.NO.getCode() || user.getDeleteFlag() == DeleteFlagEnum.YES.getCode()){
  39.             Log.debug("验证token", "token验证失败,用户信息异常 userName : {}, status : {},deleteFlag : {}", user.getUserName(),user.getStatus(), user.getDeleteFlag());
  40.             throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
  41.         }
  42.         return proceedingJoinPoint.proceed();
  43.     }
  44.    
  45. }
      以上实现方式简单易用,而且Redis 在分布式系统中的使用率也很高,所以无需额外的技术引入。可以支持水平扩展,数据库或缓存水平切分即可,服务端重启或者扩容都不会有session丢失的情况发生。/ ?7 l+ y! u3 ~* W) v3 A: F
$ ~2 W. h) E2 e0 H
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|小黑屋|paopaomj.COM ( 渝ICP备18007172号|渝公网安备50010502503914号 )

GMT+8, 2024-11-22 00:43

Powered by paopaomj X3.5 © 2016-2025 sitemap

快速回复 返回顶部 返回列表