心安

解决Shiro多次从redis中读取session的问题

字数统计: 1.4k阅读时长: 5 min
2019/02/12 Share

前言

Shiro是Apache提供的一个非常强大的Java安全框架,提供身份校验、授权、会话管理等功能。
本文示例所使用的框架是Spring boot + Shiro + Redis。其中Shiro作为权限框架,redis负责缓存用户的session。

问题的发现

笔者实现了自定义的SessionDAO,也就是自定义了对于session的增删改查操作,其中关于读取session的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class RedisSessionDAO extends AbstractSessionDAO {
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
return null;
}
byte[] sessionKey = getSessionKey(sessionId);
log.info("doReadSession==================");
return getSession(redisUtils.get(sessionKey));
}
// 其他方法省略
}

由于页面打开缓慢,于是查看控制台日志,发现每次请求会反复打印log,这个方法是根据用户的sessionId去redis中读取用户session,那么也就意味着每次请求都要读取redis很多次。
所以猜想问题应该就出现在这儿,读取redis缓慢造成页面打开缓慢。

寻根溯源

上面说到了每次请求都会多次读取用户的session,而调用这个方法是Shiro框架内部,所以笔者开始翻阅Shiro源码,看一下哪里调用这个方法。
自定义的RedisSessionDAO继承了AbstractSessionDAO,然后在AbstractSessionDAOreadSession(Serializable sessionId)方法中调用了doReadSession(Serializable var1)方法。
继续往上面找,AbstractSessionDAO实现了SessionDAO接口,也就是说某个调用了SessionDAOreadSession(Serializable var1)方法,接着就调用了笔者自定义的doReadSession(Serializable sessionId)方法。
那么哪里维护了SessionDAO呢?这里笔者凭借着多年的经验,猜想是SessionManager,也就是session管理器。
于是乎,笔者就打开了shiro-core的jar包,如下图,里面就有一个名为session的包,肯定就在这里啦。

然后在session包中搜索关键字“sessionManager”,这里笔者使用的开发工具是天下第一开发工具IDEA,直接在侧边栏输入“sessionManager”就可以进行搜索,非常的方便。搜索结果如下图:

可以看到,包含“sessionManager”关键字的总共有七个类。

其中三个是接口,那么很显然,SessionManager是顶级接口,NativeSessionManagerValidatingSessionManager是两个对顶级接口进行功能扩展的接口,我们要找的东西肯定不在这儿。

最上面三个是抽象类,IDEA对普通类和抽象类进行了图表的区分,注意看这三个文件的小图标,就能发现它们是抽象类,这些抽象类应该是对上面的接口进行了简单的实现。

然后最后只有一个DefaultSessionManager类,从名字就可以看得出来,这个是Shiro内部为我们提供的默认的SessionManager。它应该是继承了上面的某个抽象类。

分析到这儿,问题就简单了,SessionDAO应该就存在DefaultSessionManager或者其父类中。
于是打开了DefaultSessionManager,里面果然维护着SessionDAO对象。
然后查找sessionDAO对象的使用情况,发现在retrieveSessionFromDataSource(Serializable sessionId)方法中确实调用了readSession(sessionId)方法。
然后retrieveSession(SessionKey sessionKey)调用了retrieveSessionFromDataSource(Serializable sessionId)方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = 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;
}
Session s = retrieveSessionFromDataSource(sessionId);
if (s == null) {
//session ID was provided, meaning one is expected to be found, but we couldn't find one:
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
}
return s;
}

protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
return sessionDAO.readSession(sessionId);
}

ok,问题到这就找到了。下面我们开始尝试着去解决。

解决思路

通过上面对于源码的分析,不难发现,每次用户的访问,Shiro框架的SessionManager组件都会通过我们的sessionDAO对象去读取session,而我们的session是存储在redis中的,所以就导致了多次访问redis,不仅降低了接口的访问速度,还增加了redis数据库的压力。
这样一来,自然而然的就想到通过缓存来解决这个问题。通过增加一个中间件作为缓存,比如Ehcache或者Memecache。
然后修改SessionDAO的实现逻辑,先去缓存中读取session,如果缓存中不存在session,就去redis读取,然后存储在缓存中,下次就直接从缓存中读取,这样就大大的减轻redis压力,而且访问缓存的速度是原因快于访问数据库的。
这样的实现思路很简单,网上应该也有很多实现的案例,感兴趣的小伙伴可以去了解一下。

本文将会采用更加方便的解决方法,不利用第三方缓存。

上面是retrieveSession(SessionKey sessionKey)方法间接的调用了readSession方法,所以我们尝试在这里做一做文章。
SessionKey有一个子类叫DefaultSessionKeyDefaultSessionKey又有一个子类是WebSessionKey,这里传入的sessionKey对象其实真实类型就是WebSessionKey
WebSessionKey中含有ServletRequest对象,所以我们只需要将ServletRequest当成是缓存就行了,也就是说把session存储在ServletRequest对象中。

下面是具体代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
@Slf4j
public class MySessionManager extends DefaultWebSessionManager {

@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);

ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}

Session session;
if (request != null && sessionId != null) {
session = (Session) request.getAttribute(sessionId.toString());
if (session != null) {
return session;
}
}

session = super.retrieveSession(sessionKey);
if (request != null && sessionId != null) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}

到这里问题差不多就算解决了。解决问题的思路其实很简单,本文的重点其实还是对于问题的发现和寻根溯源,如何找到问题所在。欢迎各位看官在评论区进行讨论,感谢观看~~~

原文作者:XinAnzzZ

原文链接:https://www.yuhangma.com/2019/java/2019-02-12-shiro-session-manager/

发表日期:February 12th 2019, 12:00:00 am

更新日期:February 2nd 2021, 4:44:20 pm

版权声明:(转载本站文章请注明作者和出处 心 安 – XinAnzzZ ,请勿用于任何商业用途)

CATALOG
  1. 1. 前言
  2. 2. 问题的发现
  3. 3. 寻根溯源
  4. 4. 解决思路