问题描述
有一个接口,根据ThreadLocal中是否存在某个变量,来执行不同代码分支,存在该变量走A代码分支,否则走B代码分支。
刚上线时,接口正常运行,一段时间后,出现问题,所有请求都走到A分支,但是理论上ThreadLocal不应该存在该变量。
相同前后端代码,测试环境无法复现问题。
问题原因
tomcat的线程池大小是有限的,如果请求数大于tomcat线程池大小,就会出现同一个线程处理不同的请求,那么后面的请求就可能看到前面的请求往ThreadLocal放的脏数据。
之所以运行一段时间后,生产环境问题必现,而测试环境无法复现,是因为生产环境tomcat线程池所有线程的ThreadLocal都存在脏数据,而测试环境没有。
因此,每次请求(http或者websocket)处理完毕后,需要清理ThreadLocal。
问题解决
ThreadLocalUtils工具类
@Slf4j
public class ThreadLocalUtils {
private ThreadLocalUtils() {
}
private static ThreadLocal<HttpServletRequest> request = new ThreadLocal<>();
private static ThreadLocal<String> string = new ThreadLocal<>();
private static ThreadLocal<HashMap> map = ThreadLocal.withInitial(() -> new HashMap(16));
public static void setRequest(HttpServletRequest httpServletRequest){
request.set(httpServletRequest);
}
public static HttpServletRequest getRequest(){
return request.get();
}
public static void setString(String str) {
string.set(str);
}
public static String getString() {
return string.get();
}
public static void setMap(String key, Object value) {
map.get().put(key, value);
}
public static Object getMap(String key) {
return map.get().get(key);
}
public static HashMap getMapAll() {
return map.get();
}
/**
* tomcat的线程池大小是有限的,如果请求数大于tomcat线程池大小,就会出现同一个线程处理不同的请求,那么后面的请求就可能看到前面的请求往ThreadLocal放的脏数据。
* 因此,每次请求(http或者websocket)处理完毕,需要清理ThreadLocal
*/
public static void clear() {
// 注意:谨慎使用api,仅清理当前线程即可
request.set(null);
string.set(null);
map.get().clear();
log.info("ThreadLocal clear successfully");
}
}
在拦截器中清理ThreadLocal
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 只拦截http请求,不拦截websocket
ThreadLocalUtils.clear();
}
websocket相关问题
上述sprintboot拦截器无法拦截websocket请求,因此websocket连接断开时,需要另外清理ThreadLocal。
经测试(springboot版本1.5.3.RELEASE),同一个websocket连接,后端处理客户端发来的消息的线程并不是同一个,客户端主动关闭ws连接时,后端处理onClose的线程也不是处理onOpen时的线程。
因此,后端不能在onClose方法里清理ThreadLocal,要根据业务逻辑,在合适的时机清理ThreadLocal。
网友评论