使用spring框架开发业务代码的时候,会把业务处理Service
类交给spring ioc
管理,最终它会是一个单例bean,为了避免线程安全问题,则不好在这些类中定义成员变量,于是我们会想到使用 ThreadLocal
包裹成一个变量。ThreadLocal
适用于变量在线程间隔离,而在方法或类间共享的场景。如果用户信息获取比较昂贵(例如需要调接口从其它微服务中查询),那么在 ThreadLocal
中缓存数据是比较合适的做法,但,这么做有可能会出现用户信息错乱的Bug。可以通过如下的方式复现:
使用springboot快速构建一个web应用程序,创建OrderController
类
@RestController
public class OrderController {
final ThreadLocal<String> userThreadLocal = ThreadLocal.withInitial(() -> null);
@Autowired(required = false)
private OrderService orderService;
@GetMapping("/order/wrong")
public String createOrder(OrderCreateRequest request) {
// 设置用户信息之前先查一次
final String currentUser = userThreadLocal.get();
System.out.println("threadLocal中的用户为:" + currentUser);
// 使用userService查询当前用户信息并缓存到userThreadLocal中
String user = "user from userService";
userThreadLocal.set(user);
System.out.println(Thread.currentThread().getName() + "中用户信息为:" + userThreadLocal.get());
return user;
}
}
按理说,在查询用户之前第一次获取的值始终应该为null
,但是在测试中发现,偶尔有几次不是null
。为了方便发现这个问题,我们在配置文件中设置一下tomcat的线程数:
server:
tomcat:
threads:
max: 1
测试结果如下:
仅第一次线程中的用户信息为 null
,后面每次新请求过来,线程ThreadLocal
中似乎已经有值了。
原因大概大家已经知道了,程序运行在tomcat
中,执行程序的线程是tomcat
的工作线程,而tomcat的工作线程是基于线程池的。线程池会重用固定的几个线程,我们将 server.tomcat.threads.max
设置为1,把工作线程池最大线程数设置为1,这样始终是同一个线程在处理请求,因此下一个请求的线程中会遗留上一次请求的信息。
解决方案只需要在每次请求结束之后将ThreadLocal
清空或者在请求之前将其清空即可。
@GetMapping("/order/wrong")
public String createOrder(OrderCreateRequest request) {
try {
// 设置用户信息之前先查一次
final String currentUser = userThreadLocal.get();
System.out.println("threadLocal中的用户为:" + currentUser);
// 使用userService查询当前用户信息并缓存到userThreadLocal中
String user = "user from userService";
userThreadLocal.set(user);
System.out.println(Thread.currentThread().getName() + "中用户信息为:" + userThreadLocal.get());
return user;
} finally {
userThreadLocal.remove();
}
}
版权声明:本文为jisuanji12306原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。