执子之手

与子偕老


  • 首页

  • 分类

  • 归档

  • 标签

  • 关于

  • 搜索
close

在Tomcat中使用ThreadLocal以及Session

时间: 2017-05-04   |   分类: 开发     |   阅读: 1878 字 ~4分钟   |   访问: 0

最近运营同事在管理平台(生产环境)上碰到一个问题:登录之后会莫名其妙地变成未登录状态,被踢回登录页面。

管理平台使用的Spring MVC框架实现的后台接口,React实现的前台页面。之前引入React的时候已经做过前后端分离。但是当时考虑到技术栈的原因,没有对登录体系进行彻底改造,没有引入AccessToken来维护登录状态,依然保留了Java的Session机制。考虑到管理平台属于内部使用,访问量不大,因此直接在Nginx层使用iphash进行了Session粘滞,确保同一个用户的请求总是被同一个的后台Tomcat处理,这样就可以使用传统的session机制保持用户登录状态。

排查过程中,由于踢出登录情况比较随机,所以最初怀疑是超时时间设置有问题,但是一直无法重现错误。一直耽搁了好几天,很幸运的,昨天测试的同学终于找到了重现的步骤:在管理平台执行某个数据导出操作,导出操作耗时比较长,在操作没有结束的时候点击其他链接,就会出现未登录踢回登录页面的情况。

1. SessionContext

经过检查代码,发现出现登录异常问题的时候返回的错误码是:LOGIN_OVERTIME,确实是超时的返回代码。但是测试中发现即使刚刚登录,按照上面操作也会出现问题,显然真正的原因不是超时。

没办法,开始扒代码,先找到返回错误码的地方:

 1private BaseResult isLogon() {
 2   BaseResult result = new BaseResult();
 3   UserVO user = SessionContext.getSessionContext().getCurrentUser();
 4   if (user != null) {
 5       result.setSuccess(true);
 6   } else {
 7       result.setCode(ResultCode.LOGIN_OVERTIME.getCode());
 8       result.setMsg(" ");
 9       //result.setMsg(ResultCode.LOGIN_OVERTIME.getDesc());
10   }
11   return result;
12}

逻辑很简单,获取当前的SessionContext,如果currentUser不为空则认为已经登录,否则没有登录。

继续看SessionContext:

 1public class SessionContext {
 2    private transient static final ThreadLocal<SessionContext> SESSION_CONTEXT = new ThreadLocal<SessionContext>();
 3
 4    private HttpServletRequest request;
 5    private HttpServletResponse response;
 6    ...
 7    public UserVO getCurrentUser() {
 8        return (UserVO) request.getSession().getAttribute("currUser");
 9    }
10    	
11    public void setCurrentUser(UserVO user) {
12        request.getSession().setAttribute("currUser", user);
13    }
14    	
15    public void initSession(HttpServletRequest request, HttpServletResponse response) {
16        this.request = request;
17        this.response = response;
18    }
19    ...	
20    public static SessionContext getSessionContext() {
21        if (SESSION_CONTEXT.get() == null) {
22            SessionContext sc = new SessionContext();
23            SESSION_CONTEXT.set(sc);
24        }
25        return SESSION_CONTEXT.get();
26    }
27}

上面的代码的目的是使用ThreadLocal保存SessionContext对象。而SessionContext对象是通过initSession函数注入了request、response后创建的。getCurrentUser获取的是实际上是SessionContext对应的request对象中的内容。

2. 问题原因

扒到以上代码,终于发现问题出在哪儿了:因为Tomcat使用的是线程池(ThreadPool),一个线程池内的线程是复用的,并不能够保证每次web请求都使用同样的线程进行处理;也无法保证一个线程只为一个用户服务。所以在Web容器环境中使用ThreadLocal要特别小心,最好是不用,它和本地环境中的ThreadLocal还是有很多差异的。具体到之前的问题:当一个操作花费时间很长的时候,操作还没有结束,线程依然繁忙,进行第二次请求时,Tomcat会启用新的线程接受处理,但是新的线程ThreadLocal中显然没有对应的SessionContext,自然会被判定为未登录。

3. 修复方法

这部分代码是之前同事遗留下来的,限于时间原因,一直没有仔细看,原来隐藏着这种bug。如果要修复了,方法大概有如下几种:

  1. 就是引入AccessToken机制。用户登录之后分配AccessToken,该AccessToken使用Redis等外部存储进行保存。因为Token每次都跟随请求发送过来,这样就可以摆脱session粘滞的限制;
  2. 如果保持现有的session粘滞配置的话,可以考虑引入redis,通过request.getSession().getId()获得的sessionId作为主键保存currentUser等信息。我们知道Tomcat中SessionID是通过Cookie传递的(JSESSIONID),同时在Tomcat中也开辟了一块内存保存Session相关的信息。因为配置了Session粘滞,所以同一个用户来的请求,总是转发到同一台Tomcat上处理。所以根据请求携带的Cookie可以找到对应的sessionId,也能够找到Tomcat中对应的Session数据,从而找到当前用户的登录状态。

4. 内存泄露

另外,上面的代码中实际上是有内存泄露问题的:request、response对象被赋值到SessionContext对象,然后被注入到了ThreadLocal中。而Tomcat的线程池对象是持久对象,不会很快被释放。因此这两个对象很难被释放掉。当访问量越大,内存消耗会越快。只是目前的管理平台访问量比较小,所以问题不突出,一直没有发现。

总之:在并发状态下使用Tomcat的ThreadLocal是不可靠的。最好的办法是慎用。

#Tomcat# #Java# #并发#
使用Docker+Apache2+WebSVN搭建SVN服务器
Java SSL握手记录分析
  • 文章目录
  • 站点概览
Orchidflower

Orchidflower

Do one thing at a time, and do well.

77 日志
6 分类
84 标签
GitHub 知乎 OSC 豆瓣
  • 1. SessionContext
  • 2. 问题原因
  • 3. 修复方法
  • 4. 内存泄露
© 2009 - 2024 执子之手
Powered by - Hugo v0.113.0
Theme by - NexT
ICP - 鲁ICP备17006463号-1
0%