redisson-tomcat会话共享之session失效BUG
一、使用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.jpg2. 拿session id去访问server2,正常
server2.jpg3. 拿session id再去访问server3,问题出现!
server3.jpgtomcat返回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));
}
}