Java

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

2019-12-09  本文已影响0人  学不会1996

一、使用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));
        }
    }
上一篇下一篇

猜你喜欢

热点阅读