tomcat默认使用线程池来管理线程,即当收到一个请求时,如果线程池存在空闲线程,则会从中取出一个空闲线程来处理该请求。当一个线程在处理请求时,其他请求就不会被分配至该线程。
例如有a,b,c三个空闲线程:
1、当request1到来时,分配a线程来处理
2、同时有request2到来,此时a线程在处理中,因此只有bc两个空闲线程,则会从中选一个处理request2。
3、request1处理完成,request3来了,则就可能分配给空闲线程a来处理。
问题来了,如果项目中使用了ThreadLocal,则在步骤1中会存入a线程对应的数据,步骤2中会存入b线程对应的数据,二者互不干扰。当到步骤3时,由于复用了线程a,因此request3这个请求处理过程中,可以直接通过ThreadLocal取到步骤1中放入的数据。
因此,在写代码时,注意在请求处理结束之前,把ThreadLocal中存入的数据清空,防止后续线程复用时造成干扰。
上案列(黑马点评):
在MvcConfig中配置了两个拦截器类
一个负责拦截一切路径 ,一个负责拦截需要登陆的路径。
通过设置order属性,让我们拦截一切路径的拦截器先执行
preHandle在controller方法前执行
我们用户的数据(如果登陆了的话)现在存在redis中(过期时间默认30分钟)
然后就可以取出来存入threadLocal中
afterCompletion在controller方法完成后执行
用于清除threadLocal中的用户数据(对应第一段的理论)
package com.hmdp.utils; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; import com.hmdp.dto.UserDTO; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY; import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL; public class RefreshTokenInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { return true; } // 2.基于TOKEN获取redis中的用户 String key = LOGIN_USER_KEY + token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); // 3.判断用户是否存在 if (userMap.isEmpty()) { return true; } // 5.将查询到的hash数据转为UserDTO UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.存在,保存用户信息到 ThreadLocal UserHolder.saveUser(userDTO); // 7.刷新token有效期 stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserHolder.removeUser(); } }
在第二个拦截器LoginInterceptor中
就可以从threadLocal中取出用户数据
package com.hmdp.utils; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.判断是否需要拦截(ThreadLocal中是否有用户) if (UserHolder.getUser() == null) { // 没有,需要拦截,设置状态码 response.setStatus(401); // 拦截 return false; } // 有用户,则放行 return true; } }
总结以下:
用户的每一次请求来之后,都对应tomcat中线程池的一个线程去执行。
可以利用threadLocal的隔离性,在线程的threadLocalMap中存入用户信息
并且用户登陆后信息也会保存在redis中以实现单点登陆
这两个地方清除用户数据的时机不同
threadLocal:用户每次请求结束后清除,不然可能当前这个tomcat的线程a保存有用户数据,然后别的用户也刚好用到了这个tomcat中的线程a,就发生了歧义
redis:用户数据过期后(30分钟),可以由拦截器刷新过期时间,这样我们的用户在过期时间内不管发送什么请求,都能在第一个拦截器中获取到,然后把它设置进threadLocal中