账号登录后,用户不主动退出账号,在使用过程中,session/token登录信息失效,需要重新登录
这个是一个线上问题,排查的过程。
在pc端,用户登录后,用户不主动退出账号,在使用过程中,有一定几率会提示需要登录信息失效。token失效的情况,
问题现象:
1、初始现象:登录后,无论是否关闭浏览器,鉴权session不定期被重置失效时间,重置时间不固定;
2、进一步排查,连接redis主节点并执行monitor后,发现对应session修改来自于sys和cms两个项目
3、具体现象:当A账号被登录,B账号登录或则注销时,有一定几率A账号的session被修改。其中登录出现的概率最大;
4、进一步缩减涉及服务范围,只部署鉴权,等少量服务,仍然存在被修改的情况;
问题解决办法:
1、检查代码,发现里面有多个拦截器和过滤器对session有更新时间操作对最后一个执行的过滤器(AuthFilter)中,完成chain.doFilter(request, response);后增加Threadlocal.remove()操作后解决问题:
chain.doFilter(request, response);
CurrentAuthUserManager.removeCurrentAuthUser();
sessionManager.removeCurrentSession();
//其中removeCurrentAuthUser()和removeCurrentSession()分别执行的是userid和sessionid的threadlocal.remove()操作
问题原因:
1、ThreadLocal源码set()/get()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
即ThreadLocal获取的ThreadLocalMap其实都是Thread的私有成员变量,并通过ThreadLocalMap的get(key)和set(key)方式获取对应值,其中key为Thread对象。通过这种方式获取的线程的私有成员。
//Thread源码中,ThreadLocalMap为Thread成员
ThreadLocal.ThreadLocalMap threadLocals = null;
带来的问题问题是:如果Thread不销毁,那么ThreadLocal .set()的值将一直存在
2、springboot内置tomcat的线程池Springboot内置tomcat默认为线程池模式,默认线程池配置:
maxThreads:处理的最大并发请求数,默认值200
minSpareThreads:最小线程数始终保持运行,默认值10
根据minSpareThreads=10可知,在http并发低于10的时候,线程是不会被销毁的,会持续在线程池中重复使用。如果请求并发大于10,那么会创建更多线程直到200,然后在空闲一定时间会再被销毁,但是10个以内的线程将一直不会被销毁!
和第一点ThreadLocal代码综合可知:由于tomcat线程池不会销毁线程,导致10次以后ThreadLocal.get()始终未上一次该线程生效set()的值,引起逻辑异常重现示例代码:
@RestController
@RequestMapping("/thlocal")
@Slf4j
public class ThreadLocalTest {
private static ThreadLocal<String> thlocal = new ThreadLocal<>();
@RequestMapping(value = "/print", method = {RequestMethod.GET, RequestMethod.POST})
public String thlocalPrint() {
Thread curt = Thread.currentThread();
String rsp = "not exist";
if (null == thlocal.get()) {
thlocal.set(String.valueOf(DggKeyWorker.nextId()));
} else {
rsp = "exist:" + thlocal.get();
}
rsp = String.format("ThreadId:%s %s", curt.getId(), rsp);
log.info(rsp);
return rsp;
}
}
对应日志
ThreadId:62 not exist
ThreadId:63 not exist
ThreadId:64 not exist
ThreadId:65 not exist
ThreadId:66 not exist
ThreadId:67 not exist
ThreadId:68 not exist
ThreadId:69 not exist
ThreadId:70 not exist
ThreadId:71 not exist
ThreadId:62 exist:7845178637774557184
ThreadId:63 exist:7845178641041920000
ThreadId:64 exist:7845178643764023296
ThreadId:65 exist:7845178645232029696
ThreadId:66 exist:7845178652232323072
ThreadId:67 exist:7845178654644047872
ThreadId:68 exist:7845178655801675776
ThreadId:69 exist:7845178657135464448
ThreadId:70 exist:7845178658607665152
ThreadId:71 exist:7845178660130197504
ThreadId:62 exist:7845178637774557184
ThreadId:63 exist:7845178641041920000
ThreadId:64 exist:7845178643764023296
ThreadId:65 exist:7845178645232029696
ThreadId:66 exist:7845178652232323072
ThreadId:67 exist:7845178654644047872
ThreadId:68 exist:7845178655801675776
ThreadId:69 exist:7845178657135464448
ThreadLocal使用建议:
使用完毕后,无论应用的线程模型如何,均执行remove()操作