详解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分布式系统的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java发送邮箱验证码、session校验功能

    本篇主要描述"发送邮箱验证码.session校验"相关前(html\js)后(java)台代码,业务逻辑示例,闲话少诉,直接上代码. 1.引入的jar包是mail-1.4.jar 2.java底层发送邮箱方法 public boolean sendEMail(Map<String, Object> map) { log.info("電子郵件接口執行開始!"); String from = Cache.getInstance().getParamsCons

  • Java Session验证码案例代码实例解析

    案例 用户输入用户名,密码以及验证码. 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误 如果验证码输入有误,跳转登录页面,提示:验证码错误 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您思路 在生成验证码的时候将它的值存入到session中,在比对的时候再取出来进行对比 代码index.jsp <%-- Created by IntelliJ IDEA. User: tanglei Date: 2020/6/26 Time: 下午12:48 To ch

  • Java跨session实现token接口测试过程图解

    这套框架的报告是自己封装的 一般token会在登录接口返回结果中呈现,从代码层面获取token的方式有很多种,我是使用jsonpath这个json路径语言去匹配token所在路径的key值 没有使用testng.xml的情况下调试testCase,需要设置一下dependsOnMethods,否则token将无法传递给其他test步骤 附上TestUtil.getToken()方法: //获取返回的token ,使用JsonPath获取json路径 public static HashMap<S

  • JavaWeb基于Session实现的用户登陆注销方法示例

    前言 Cookie:cookie是客户端技术,程序把每个用户的数据以cookie的形式写给用户各自的浏览器. 当用户使用浏览器再去访问服务器中的web资源时,就会带着各自的数据区,这样,web资源处理的就是各自的数据了. Session:session是服务器端技术,利用session技术,服务器在运行时可以为每一个用户的浏览器创建其独享的session对象,由于session为用户浏览器独享,所以用户在访问服务器的web资源时,可以把各自的数据放在session中,当用户再次去访问服务器中的其

  • JavaWeb 使用Session实现一次性验证码功能

    表单 <form action="loginServlet" method="post"> 请输入验证码:<input type="text" name="code" /> <img src="getCodeServlet" /><br /> <button type="submit">提交</button> <

  • Java简单实现session保存到redis的方法示例

    本文实例讲述了Java简单实现session保存到redis的方法.分享给大家供大家参考,具体如下: 在负载均衡情况下,如果用户访问不同的机器,如果没有做session同步,用户就会被提出,这样用户体验非常不好,所以我们很有必要做session同步,把session放到reids缓存服务器就能很好的解决问题.下面是代码简单的实现. 一.配置web.xml过滤器: <filter> <filter-name>sessionFilter</filter-name> <

  • Java中session存储Users对象实现记住密码

    之前做过cookie实现记住密码,这次换成session记住密码又做了一遍,有很多收获. 本次博客分为两部分.一部分是cookie和session的区别(面试经常被问道):另一部分是sesion实现记住密码,在Jsp页面和Controller页面的代码编写. 一.cookie和session区别 区别: 1.cookie是浏览器端技术:将数据保存到浏览器中,不安全:不可以保存中文保存时编码: URLEncoder.encode();取值时解码:URLDecoder.decode();. 2.se

  • Java Web会话技术Session的简单使用

    什么是Session Session技术是将信息保存在服务端,而客户端需要接收.记录和回送Session的ID,所以Session通常情况下是借助Cookie技术来传递ID给服务端的,服务端拿到session id之后查询内存中对应的记录. 一个客户端对应一个Session,而一个Session有多个Attribute,每一个Attribute有唯一的name. 编写代码证明提出的观点: protected void doGet(HttpServletRequest req, HttpServl

  • JavaWeb项目打开网页出现Session Error的异常解决方案

    这篇文章主要介绍了JavaWeb项目打开网页出现Session Error的异常解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 找到web.xml配置的原始配置的位置: <servlet> <servlet-name>dwr-invoker</servlet-name> <servlet-class>uk.ltd.getahead.dwr.DWRServlet</servlet-class&g

  • 详解Java分布式Session共享解决方案

    分布式Session一致性? 说白了就是服务器集群Session共享的问题 Session的作用? Session 是客户端与服务器通讯会话跟踪技术,服务器与客户端保持整个通讯的会话基本信息. 客户端在第一次访问服务端的时候,服务端会响应一个sessionId并且将它存入到本地cookie中,在之后的访问会将cookie中的sessionId放入到请求头中去访问服务器,如果通过这个sessionid没有找到对应的数据那么服务器会创建一个新的sessionid并且响应给客户端. 分布式Sessio

  • Java分布式session存储解决方案图解

    前言 本文主要探讨集群后不同Web服务器获取Session数据的问题解决方案. Session Stick Session Stick 方案即将客户端的每次请求都转发至同一台服务器,这就需要负载均衡器能够根据每次请求的会话标识(SessionId)来进行请求转发,如下图所示. 这种方案实现比较简单,对于Web服务器来说和单机的情况一样.但是可能会带来如下问题: 如果有一台服务器宕机或者重启,那么这台机器上的会话数据会全部丢失. 会话标识是应用层信息,那么负载均衡要将同一个会话的请求都保存到同一个

随机推荐