外卖等实践项目问题记录(1)
ThreadLocal内存泄漏问题
最近在复盘重新黑马还有尚硅谷一些项目,发现我们在使用获取当前用户id这项功能的时候主要是使用ThreadLocal来进行存取
定义:
public class BaseContext { public static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id) { threadLocal.set(id); } public static Long getCurrentId() { return threadLocal.get(); } public static void removeCurrentId() { threadLocal.remove(); } }
存入:
/** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenUserInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } //1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getUserTokenName()); //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString()); log.info("当前用户的id:", userId); BaseContext.setCurrentId(userId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } }
获取:
//清空当前用户的购物车数据 shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId());
但我们知道ThreadLocal是存在内存泄漏问题的
原因分析
ThreadLocal的工作原理 ThreadLocal通过为每个线程维护一个独立的变量副本,实现了线程间的数据隔离。具体来说,ThreadLocal的值存储在Thread对象的ThreadLocalMap中,键是ThreadLocal实例,值是存储的数据。
内存泄漏的可能性
如果ThreadLocal变量没有被正确清理(调用remove()方法),即使ThreadLocal实例本身被回收,ThreadLocalMap中的键(弱引用)可能已经被回收,但值仍然存在,导致无法释放内存。这种情况尤其容易发生在使用线程池的场景中,因为线程池中的线程是复用的,线程的生命周期很长。
我们整个项目里面只用到了存取ThreadLocal中的线程id,却并没有在方法结束后对ThreadLocal进行remove这样会造成一定的风险。
如果线程池中的线程被复用,而ThreadLocal没有及时清理,可能会导致数据污染。例如,前一个请求的用户ID残留到了下一个请求中。
解决方案
在本项目中,我们可以修改原本的拦截器,在请求结束时统一清理ThreadLocal中的数据。例如:
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("请求结束,执行afterCompletion方法..."); // 在这里编写请求结束后的逻辑,例如清理ThreadLocal数据 BaseContext.removeCurrentId(); // 清理当前用户ID }
使用afterCompletion方法
afterCompletion的作用 afterCompletion方法在请求处理完成(包括视图渲染)后执行,无论请求是否成功或抛出异常。它适合用于清理资源、记录日志等操作。
不知道我这样做的是否正确,欢迎各位大佬指教!
#实习##苍穹外卖项目包装#