详解Java分布式系统中session一致性问题

 更新时间:2021年04月22日 16:13:18   作者:JiaJian  
这篇文章主要介绍了Java分布式系统中session一致性问题,对分布式系统感兴趣的同学,要仔细看一下

业务场景

在单机系统中,用户登陆之后,服务端会保存用户的会话信息,只要用户不退出重新登陆,在一段时间内用户可以一直访问该网站,无需重复登陆。用户的信息存在服务端的 session 中,session中可以存放服务端需要的一些用户信息,例如用户ID,所属公司companyId,所属部门deptId等等。

但是随着业务的发展,技术架构需要调整,原来的单机系统逐渐被更换,架构由单机扩展到分布式,甚至当下流行的微服务。虽然在用户端看来系统仍然是一个整体,但在技术端来说业务则被拆分成多个模块,各个模块之间相互独立,甚至不在同一台物理机器上,模块之间通过 RPC 进行通信。

那么原来单机只需一份的 session, 如何满足在多系统的运行下保证会话一致性呢?单独保存在任何一个系统中都不合适,而且每个单独模块系统也可能是分布式形式的,是由集群组成。那么session的分配就更复杂了。

Redis 实现

针对以上问题,我们可能会从以下几个方面想到解决的方法,每个服务端存储一份,通过同步的方式保证一致性,但是这种方式有个很明显的缺点:session的同步需要数据传输,占内网带宽,有时延,网络不稳定的时候会造成部分系统同步延迟,那么就不能保证 session 一致性。而且所有服务端都包含所有session数据,数据量受内存限制,无法水平扩展。

那么我们是否可以单独将 session 信息存储在某一个独立的介质中,介质可以是DB也可以是缓存。

考虑到如下业务:登陆的时候我们经常会给用户一个过期时间(一般移动端常设置为7天或者一个月甚至更久),到期后用户需要输入登陆信息重新登陆,即会话过期。这种到期的设置我们自然想到了Redis的 key expire功能,所以最终我们可以将Redis引入进来实现我们的这种需求。系统如下图所示:

我们只需在用户首次登陆的时候将用户信息放到 Token并缓存到 Redis 中,同时设置一个过期时间,伪代码如下:

@Override
public Map login(UserDto dto) {
    Map<String, Object> restMap = new HashMap<>();
    
    // 校验登陆信息
    User user = checkLoginInfo(dto);

     //删除旧的token
    String token = (String) redisUtils.get(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName());
    
    if (!ObjectUtils.isEmpty(token)) {
        redisUtils.delete(CacheConstants.USER_TOKEN_KEY_WEB + token);
    }
    // 唯一签名信息
    String signStr = user.getCompanyId() + user.getUserName() + dto.getPassword() + DateUtils.now().getTime();
    token = MD5Utils.md5(signStr);
    // 设置用户 token
    redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_WEB + token, user.getId(), LOGIN_EXPIRED_TIME);
    //缓存新的token
    redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName(), token, LOGIN_EXPIRED_TIME);
    dto.setCompanyId(user.getCompanyId());
    dto.setId(user.getId());
    restMap.put("token", token);
    restMap.put("userName", user.getUserName());
    return restMap;
}

那么在系统中如何使用呢,我们可以定义一个拦截器 SessionInterceptor,当访问 web 接口的时候检验用户的 token 信息,判断用户是否登陆,未登录的情况下一些业务接口是无法访问的,以及在登陆的情况下拿到我们需要的用户信息,如 userId。

public class SessionInterceptor {

    @Autowired
    private RedisUtils redisUtils;
    
    @Autowired
    private UserService userService;

    @Pointcut("execution(* com.jajian.demo.web.*.controller.*.*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void controllerMethodPointcut() {

    }

    @Around("controllerMethodPointcut()")
    public Object Interceptor(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        
        Signature signature = proceedingJoinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        if (targetMethod.getDeclaringClass().isAnnotationPresent(NoLogin.class) || targetMethod.isAnnotationPresent(NoLogin.class)) {
            return proceedingJoinPoint.proceed();
        }
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        String token = request.getHeader("token");

        if(StringUtils.isEmpty(token)){
            Log.debug("验证token", "token验证失败,{}", "token不存在");
            throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
        }
        Integer userId= (Integer)redisUtils.get(CacheConstants.USER_TOKEN_KEY_WEB + token);
       
        if (null == userId) {
            Log.debug("验证token", "token验证失败,{}", "token超时");
            throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
        }
        User user = userService.getById(userId.longValue());
        if (ObjectUtils.isEmpty(user)){
            Log.debug("验证token", "token验证失败,{}", "用户信息不存在");
            throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
        }
        if (user.getStatus() == UserStatusEnum.NO.getCode() || user.getDeleteFlag() == DeleteFlagEnum.YES.getCode()){
            Log.debug("验证token", "token验证失败,用户信息异常 userName : {}, status : {},deleteFlag : {}", user.getUserName(),user.getStatus(), user.getDeleteFlag());
            throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
        }
        return proceedingJoinPoint.proceed();
    }
    
}

以上实现方式简单易用,而且Redis 在分布式系统中的使用率也很高,所以无需额外的技术引入。可以支持水平扩展,数据库或缓存水平切分即可,服务端重启或者扩容都不会有session丢失的情况发生。

以上就是详解Java分布式系统中session一致性问题的详细内容,更多关于Java分布式系统的资料请关注脚本之家其它相关文章!

相关文章

  • Mybatis基于TypeHandler实现敏感数据加密

    Mybatis基于TypeHandler实现敏感数据加密

    业务场景中经常会遇到诸如用户手机号,身份证号,银行卡号,邮箱,地址,密码等等信息,属于敏感信息,本文就来介绍一下Mybatis基于TypeHandler实现敏感数据加密,感兴趣的可以了解一下
    2023-10-10
  • 使用SpringBoot2.x配置静态文件缓存

    使用SpringBoot2.x配置静态文件缓存

    这篇文章主要介绍了使用SpringBoot2.x配置静态文件缓存的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • java开发RocketMQ生产者高可用示例详解

    java开发RocketMQ生产者高可用示例详解

    这篇文章主要为大家介绍了java开发RocketMQ生产者高可用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • 全解史上最快的JOSN解析库alibaba Fastjson

    全解史上最快的JOSN解析库alibaba Fastjson

    这篇文章主要介绍了史上最快的JOSN解析库alibaba Fastjson,对FastJson感兴趣的同学,一定要看一下
    2021-04-04
  • Java如何实现读取txt文件内容并生成Word文档

    Java如何实现读取txt文件内容并生成Word文档

    本文主要介绍了通过Java实现读取txt文件中的内容,并将内容生成Word文档。文章的代码非常详细,具有一定的学习价值,感兴趣的小伙伴可以了解一下
    2021-12-12
  • MyBatis快速入门之环境搭建和单表映射

    MyBatis快速入门之环境搭建和单表映射

    一说起对象关系映射框架,大家第一时间想到的肯定是Hibernate。Hibernate作为一个著名的框架,功能十分强大。但是由于Hibernate如此强大的功能,导致了它的缺点。好吧,不多说了,具体详情大家通过本文一起学习吧
    2017-03-03
  • 200行Java代码编写一个计算器程序

    200行Java代码编写一个计算器程序

    本篇文章给大家分享的只用200行java代码,实现一个计算器程序,不仅能够计算加减乘除,还能够匹配小括号。实现代码超简单,需要的朋友参考下吧
    2017-12-12
  • Java实现JS中的escape和UNescape代码分享

    Java实现JS中的escape和UNescape代码分享

    在PHP和Python中都有类似JS中的escape和UNescape函数的功能,那么Java语言中到底有没有类似的方法呢?本文就来介绍一下Java实现JS中的escape和UNescape转码方法,需要的朋友可以参考下
    2017-09-09
  • springmvc的文件保存方法详解

    springmvc的文件保存方法详解

    这篇文章主要介绍了springmvc的文件保存方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • Javas使用Redlock实现分布式锁过程解析

    Javas使用Redlock实现分布式锁过程解析

    这篇文章主要介绍了Javas使用Redlock实现分布式锁过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-08-08

最新评论