我们常使用 Shiro + redis 的组合解决集群下的 Session 共享问题,这里就不展开如何集成的问题了。
在进行日常优化的过程中,我通过日志发现这么一段日志:
2017-09-17 15:16:07.723 -DEBUG [nio-8080-exec-6] org.apache.shiro.session.mgt.DefaultSessionManager : Creating new EIS record for new session instance [org.apache.shiro.session.mgt.SimpleSession,id=null]
2017-09-17 15:16:07.723 -DEBUG [nio-8080-exec-6] cn.bqjr.eily.shiro.spring.boot.RedisShiroSessionDAO : 创建session:74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.723 -DEBUG [nio-8080-exec-6] cn.bqjr.eily.shiro.spring.boot.RedisShiroSessionDAO : 获取session:74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.723 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Opening RedisConnection
2017-09-17 15:16:07.723 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Closing Redis Connection
2017-09-17 15:16:07.723 -DEBUG [nio-8080-exec-6] cn.bqjr.eily.shiro.spring.boot.RedisShiroSessionDAO : 保存session:74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.724 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Opening RedisConnection
2017-09-17 15:16:07.724 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Closing Redis Connection
2017-09-17 15:16:07.725 -DEBUG [nio-8080-exec-6] cn.bqjr.eily.shiro.spring.boot.RedisShiroSessionDAO : 获取session:74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.725 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Opening RedisConnection
2017-09-17 15:16:07.725 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Closing Redis Connection
2017-09-17 15:16:07.725 -DEBUG [nio-8080-exec-6] cn.bqjr.eily.shiro.spring.boot.RedisShiroSessionDAO : 保存session:74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.725 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Opening RedisConnection
2017-09-17 15:16:07.725 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Closing Redis Connection
2017-09-17 15:16:07.725 -DEBUG [nio-8080-exec-6] org.apache.shiro.web.session.mgt.DefaultWebSessionManager : Session ID cookie is disabled. No cookie has been set for new session with id 74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.725 -DEBUG [nio-8080-exec-6] cn.bqjr.eily.shiro.spring.boot.RedisShiroSessionDAO : 获取session:74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.725 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Opening RedisConnection
2017-09-17 15:16:07.726 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Closing Redis Connection
2017-09-17 15:16:07.726 -DEBUG [nio-8080-exec-6] cn.bqjr.eily.shiro.spring.boot.RedisShiroSessionDAO : 保存session:74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.726 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Opening RedisConnection
2017-09-17 15:16:07.727 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Closing Redis Connection
2017-09-17 15:16:07.727 -DEBUG [nio-8080-exec-6] cn.bqjr.eily.shiro.spring.boot.RedisShiroSessionDAO : 获取session:74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.727 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Opening RedisConnection
2017-09-17 15:16:07.727 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Closing Redis Connection
2017-09-17 15:16:07.727 -DEBUG [nio-8080-exec-6] cn.bqjr.eily.shiro.spring.boot.RedisShiroSessionDAO : 保存session:74a8d2b3-7143-40f1-b7bb-a9596975bc90
2017-09-17 15:16:07.728 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Opening RedisConnection
2017-09-17 15:16:07.728 -DEBUG [nio-8080-exec-6] org.springframework.data.redis.core.RedisConnectionUtils : Closing Redis Connection
值得注意的是,这一切是在单次请求中发生的!由于 Session 都持久化在 redis 中,导致 shiro 在请求处理中需要用到 session 的时候都要从 redis 中取数据并且反序列化,虽然 redis 的存取性能爆表,但是在如此场景中明显是 “铺张浪费” 了。
我们一步步来,先看看能不能减少读取 session 的次数,跟踪源码定位 shiro 获取 session 的方法:
// DefaultSessionManager.class
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = this.getSessionId(sessionKey);
if (sessionId == null) {
log.debug("Unable to resolve session ID from SessionKey [{}]. Returning null to indicate a session could not be found.", sessionKey);
return null;
} else {
Session s = this.retrieveSessionFromDataSource(sessionId);
if (s == null) {
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
} else {
return s;
}
}
}
protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
return this.sessionDAO.readSession(sessionId);
}
这里可以直观地看到,shiro 取得 SessionId 后直接通过 sessionDao 去获取 Session 对象了,由于我们集成了 redis,所以这里的 dao 一般是我们自定义的 RedisSessionDao 了,这就不多展开了。关键看retrieveSession
方法,如何能减少单次请求内调用retrieveSessionFromDataSource
的次数呢?我们惯性思维会想到首次获取到 session 后就将其缓存起来,下次再获取时就直接返回而避免再次调用 redis。这种方式需要注意的就是这个 session 的作用域问题,要避免出现不同请求间 session 对象的隔离。要实现的方法很多,比如 ThreadLocal,但是使用它咱就要考虑数据生命周期或者作用域的问题了,有没有别的更简单点的方式?
答案是必须的,在对retrieveSession
方法进行 debug 的时候,我发现 sessionKey 变量的有趣之处:
在 Web 下使用 shiro 时这个 sessionKey 是 WebSessionKey 类型的,这个类有个我们很熟悉的属性:servletRequest。小伙伴们应该都灵光一现了!直接把 session 对象怼进 request 里去!那么在单次请求周期内我们都可以从 request 中取 session 了,而且请求结束后 request 被销毁,作用域和生命周期的问题都不需要我们考虑了。
显然我们要 Override 这个retrieveSession
方法,为此我们需要使用自定义的 SessionManager,如下:
public class ShiroSessionManager extends DefaultWebSessionManager {
/**
* 获取session
* 优化单次请求需要多次访问redis的问题
* @param sessionKey
* @return
* @throws UnknownSessionException
*/
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
if (request != null && null != sessionId) {
Object sessionObj = request.getAttribute(sessionId.toString());
if (sessionObj != null) {
return (Session) sessionObj;
}
}
Session session = super.retrieveSession(sessionKey);
if (request != null && null != sessionId) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}
剩下的就是在 ShiroConfiguration 中注册这个自定义 SessionManager 即可。
参考地址:http://www.hillfly.com/2017/182.html
l