美文网首页Java
redisson-tomcat会话共享之session失效BUG

redisson-tomcat会话共享之session失效BUG

作者: 学不会1996 | 来源:发表于2019-12-09 21:20 被阅读0次

    一、使用redisson-tomcat

    使用redisson-tomcat很简单,只需要两个步骤:

    1. 添加会话管理器

    在tomcat/conf/context.xml增加配置:

    <Manager className="org.redisson.tomcat.RedissonSessionManager"
      configPath="${catalina.base}/redisson.conf" 
      readMode="REDIS" updateMode="DEFAULT" broadcastSessionEvents="false"/>
    
    2. 拷贝两个jar包到tomcat/lib目录下

    (rv表示redisson的版本号,tv表示tomcat的版本号)

    redisson-all-rv.jar
    redisson-tomcat-tv-rv.jar
    

    参考https://github.com/redisson/redisson/tree/master/redisson-tomcat

    二、问题出现

    运维伙伴配置好负载均衡后(集群环境,一台一台发布,发布前剔除负载,发布后加入负载),我便开始测试。

    一开始正常,但多次调试后,偶尔会出现session失效的问题,再次多次尝试后找到了session失效的触发条件:
    第一次切换负载时,能正常访问,但第二次切换负载时,session会失效。

    三、slb、nginx的问题?

    由于tomcat上层有slb做负载均衡、nginx做反向代理,首先得排查是不是它们引起的问题。
    但这很难,于是我换了个思路,绕过上层直接访问tomcat,查看是否有问题。
    排查流程:

    1. 在server 1上创建session
    server1.jpg
    2. 拿session id去访问server2,正常
    server2.jpg
    3. 拿session id再去访问server3,问题出现!
    server3.jpg

    tomcat返回Set-Cookie响应头,说明session已经失效,并重新创建了一个新的session。
    所以,问题不是出在slb和nginx上。

    四、 查看redis

    考虑一番,打算直接从数据上查看是否有异样,重复上述步骤:

    1. 在server 1上创建session
    server1.jpg

    查看redis,一切正常:


    redis1.jpg
    2. 拿session id去访问server2
    server2.jpg

    查看redis,发现问题:


    redis2.jpg

    什么???
    session的isValid变成false了,意味着session在第一次切负载的时候就已经失效了!

    3. 拿session id再去访问server3,问题再次复现
    server3.jpg

    响应头依然有Set-Cookie,表示session的确失效了。
    此时基本上确定问题是出在redisson-tomcat了。

    五、查看源码

    在第一次getSession的时候,会调用sessionManager的createSession方法。
    在切换负载的时候,会携带session id去访问另外一台tomcat,调用sessionManager的findSession方法:
    乍眼一看,没有问题呀。

        @Override
        public Session findSession(String id) throws IOException {
            Session result = super.findSession(id);
            if (result == null) {
                if (id != null) {
                    Map<String, Object> attrs = new HashMap<String, Object>();
                    try {
                        if (readMode == ReadMode.MEMORY) {
                            attrs = getMap(id).readAllMap();
                        } else {
                            attrs = getMap(id).getAll(RedissonSession.ATTRS);
                        }
                    } catch (Exception e) {
                        log.error("Can't read session object by id " + id, e);
                    }
                    if (attrs.isEmpty() || !Boolean.valueOf(String.valueOf(attrs.get("session:isValid")))) {
                        log.info("Session " + id + " can't be found");
                        return null;
                    }
                    RedissonSession session = (RedissonSession) createEmptySession();
                    session.setId(id);
                    session.setManager(this);
                    session.load(attrs);
                    session.access();
                    session.endAccess();
                    return session;
                }
                return null;
            }
            result.access();
            result.endAccess();
            return result;
        }
    

    关键在于session.setId,调用了sessionManager的add(session)方法:
    但是,到这一步也没问题。

    StandardSession.java:
        public void setId(String id) {
            this.setId(id, true);
        }
        public void setId(String id, boolean notify) {
            if (this.id != null && this.manager != null) {
                this.manager.remove(this);
            }
            this.id = id;
            if (this.manager != null) {
                this.manager.add(this);
            }
            if (notify) {
                this.tellNew();
            }
        }
    ManagerBase.java
        public void add(Session session) {
            this.sessions.put(session.getIdInternal(), session);
            int size = this.getActiveSessions();
            if (size > this.maxActive) {
                Object var3 = this.maxActiveUpdateLock;
                synchronized(this.maxActiveUpdateLock) {
                    if (size > this.maxActive) {
                        this.maxActive = size;
                    }
                }
            }
        }
    

    问题在于,RedissonTomcat重写了sessionManager的add方法:
    它调用了RedissonSession的自定义方法save

        public void add(Session session) {
            super.add(session);
            ((RedissonSession)session).save();
        }
    

    Redisson的save方法将所有字段同步到redis:

        public void save() {
            if (this.map == null) {
                this.map = this.redissonManager.getMap(this.id);
            }
            Map<String, Object> newMap = new HashMap();
            newMap.put("session:creationTime", this.creationTime);
            newMap.put("session:lastAccessedTime", this.lastAccessedTime);
            newMap.put("session:thisAccessedTime", this.thisAccessedTime);
            newMap.put("session:maxInactiveInterval", this.maxInactiveInterval);
            newMap.put("session:isValid", this.isValid);
            newMap.put("session:isNew", this.isNew);
            if (this.attrs != null) {
                Iterator var2 = this.attrs.entrySet().iterator();
                while(var2.hasNext()) {
                    Entry<String, Object> entry = (Entry)var2.next();
                    newMap.put(entry.getKey(), entry.getValue());
                }
            }
            this.map.putAll(newMap);
            if (this.readMode == ReadMode.MEMORY) {
                this.topic.publish(this.createPutAllMessage(newMap));
            }
            this.expireSession();
        }
    

    回过头看findSession,在还没有loadAttr的时候,就调用了setId方法,将一大堆还没有初始化好的值同步到了redis,导致session的isValid被置为false:

        @Override
        public Session findSession(String id) throws IOException {
    略......
                    RedissonSession session = (RedissonSession) createEmptySession();
                    session.setId(id);
                    session.setManager(this);
                    session.load(attrs);
                    session.access();
                    session.endAccess();
                    return session;
    略....
        }
    

    查看redisson的release记录,在最新版本已经修复了:


    redisson.jpg

    我们看一下,redisson是怎么修复的,仅仅是交换了setId和load(attrs)的顺序:


    fix.jpg

    六、解决问题

    redisson 3.x版本最低要求jdk1.8,然而我们项目用的是jdk1.7。
    于是我使用updateMode=AFTER_REQUEST模式暂时解决了这个问题。
    AFTER_REQUEST原理是在tomcat容器的pipeline增加了一个Valve:

            if (updateMode == UpdateMode.AFTER_REQUEST) {
                getEngine().getPipeline().addValve(new UpdateValve(this));
            }
    

    UpdateValve在请求结束后,同步所有字段到redis:

        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
    略......
            try {
                getNext().invoke(request, response);
            } finally {
                manager.store(request.getSession(false));
            }
        }
    

    相关文章

      网友评论

        本文标题:redisson-tomcat会话共享之session失效BUG

        本文链接:https://www.haomeiwen.com/subject/mdrigctx.html