欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-03 22:38:16
业务场景在单机系统中,用户登陆之后,服务端会保存用户的会话信息,只要用户不退出重新登陆,在一段时间内用户可以一直访问该网站,无需重复登陆。用户的信息存在服务端的 session 中,session中可...

业务场景

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

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

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

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

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

redis 实现

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

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

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

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

我们只需在用户首次登陆的时候将用户信息放到 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分布式系统的资料请关注其它相关文章!