记一次ThreadLocal导致的生产事故

1.背景

前段时间发生了一起偶现生产事故,客服查询用户的订单列表时发现多出来了很多不应对其展示的订单。后经排查是由于ThreadLocal使用不当造成的。

2.过程

改动前

大致伪代码如下:

java 复制代码
ThreadLocal<Boolean> IS_HIGHVENDER = ThreadLocal.withInitial(() -> false);
        
        Set<String> v_set = new HashSet<>();// set里面有一些指定的 v
        String v = "xx";//客服自身的某属性
        if(v_set.contains(v)){
            IS_HIGHVENDER.set(true);
        }
      try {
          List orderList = new ArrayList(); // 查出来的订单列表
		boolean other = false;// 其他过滤条件
          if(!IS_HIGHVENDER.get() || other ){
              orderList = new ArrayList(); // 对订单列表有一些过滤逻辑
          }
      }finally {
          IS_HIGHVENDER.remove();
      }
        return orderList;

改动后

大致伪代码:

java 复制代码
ThreadLocal<Boolean> IS_HIGHVENDER = ThreadLocal.withInitial(() -> false);

        Set<String> v_set = new HashSet<>();// set里面有一些指定的 v
        String v = "xx";//客服自身的某属性
        if(v_set.contains(v)){
            IS_HIGHVENDER.set(true);
        }
      try {
          Callable callable1 = new Callable() {
              @Override
              public Object call() throws Exception {
                  try {
                      List orderList = new ArrayList(); // 查出来的订单列表
						boolean other = false;// 其他过滤条件
                      if (!IS_HIGHVENDER.get() || other) {
                          orderList = new ArrayList(); // 对订单列表有一些过滤逻辑
                      }
                      return orderList;
                  }finally {
                      IS_HIGHVENDER.remove();
                  }
              }
          };
          
          Callable callable2 = new Callable() {
              @Override
              public Object call() throws Exception {
               // 一些其他业务
                  return null;
              }
          };
          // 把两个任务丢线程池去执行了
         // 得到两个任务的结果后进行一些业务处理,
          return orderList; // 返回任务1中的订单列表
      }

分析

改动前是直接在一个线程中执行该业务逻辑,完事儿后在finally 中调用ThreadLocal的remove方法。

改动后因为增加了其他的业务逻辑,将原查询订单列表的逻辑与新增的其他逻辑分成两个异步任务进行处理,在子任务1中任务完成后有ThreadLocal的remove方法调用,但是把原来在外层的finally 中的remove调用给干掉了。

  1. 使用的ThreadLocal,子线程不会自动传递该变量;所以改动后的子任务1中,IS_HIGHVENDER.get()始终为初始值false;
  2. 子任务内的remove只会影响到子线程内的变量,不会影响到外层的ThreadLocal;
  3. 外层的remove去掉后,外层的ThreadLocal中的值只能通过判断符合v_set.contains(v)条件才会更改,不然就保持是之前的值。

3.回顾

  1. 记录的时候离事发已经过去一个多月了,上面的伪代码是真的伪,只能按大概记忆编一下了,不保真。
  2. 这个业务改动前不是我写的,不清楚之前的逻辑;改动后也不是我写的,也不清楚改动后的逻辑,所以有点儿麻烦。
  3. 收到事故反馈时,前两天才有过上线,就是上线的这个改动,所以排查范围一下就缩小了。
  4. 事故现象是查到了不该查到的订单,近一步缩小范围到过滤逻辑上。
  5. 其他过滤条件都是针对订单本身某些属性做的判断,可能性不大。
  6. 一番排除下来这个ThreadLocal变量嫌疑就比较大了。
  7. 使用的是ThreadLocal,不是如InheritableThreadLocal这种可在线程间复制传递的,这好像并不是主因。
  8. 又发现IS_HIGHVENDER这个变量,只有一处调了set方法,一处调了remove方法,多处调了get方法。这个只有一处remove就感觉有点怪了,毕竟现在两个线程中都用到了。
  9. 外层的remove被干掉了,没有清除它的值,这可能性是极大的。

4.终

最初为啥要使用 ThreadLocal来存储这个变量已经不可考了,看起来貌似没太大必要使用ThreadLocal。

最终找人修复后,看了下修复结果:去除了该ThreadLocal变量,直接 boolean isHighvender = v_set.contains(v); 将isHighvender 变量传递下去进行过滤逻辑判断。

相关推荐
wangbing11254 小时前
springboot应用关闭时触发动作
java·spring boot·后端
weixin_460443567 小时前
2026年新修订《网络安全法》实施:Java私有化考试系统如何做好SBOM、漏洞治理与安全升级?
java·开发语言·在线考试系统·企业培训考试系统·宏远培训考试系统·国企培训考试系统·国产化培训系统
海鸥-w10 小时前
什么是AOP
java
Cc.Y10 小时前
Java零基础入门:可变字符串与包装类:StringBuilder、StringBuffer 与 通讯录管理系统实战
java·开发语言·python
paopaokaka_luck10 小时前
基于springboot+vue的智慧家政管理系统(AI问答、 WebSocket实时沟通、ECharts运营分析、订单规则与服务追踪)
java·javascript·spring boot·数据分析·echarts·mybatis
sm_9267870510 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
阿森_dev10 小时前
2027 届毕设选题怎么选:30 个 Java 方向选题的实现难度排序
java·开发语言·spring boot·毕业设计·课程设计
Sarvartha11 小时前
Lambda 表达式和函数式接口
java·开发语言
计算机毕业设计杰瑞11 小时前
【计算机毕设精品】基于SpringBoot的一体化教务与学业质量管理系统的设计与实现,附源码_增删改查_java项目_可完美运行部署_网站项目
java·spring boot·课程设计
colinu11 小时前
C++ 异步任务与线程生命周期:`std::async`、`future`、`thread`、`jthread` 与协作式退出
java·jvm·c++