乐趣区

关于java:记一次ThreadLocal引发的线上故障年终奖没了可能还面临辞退

记一次惨痛的线上 ThreadLocal 引发的故障,3 个月的年终奖没了,可能还会面临解雇。

事件起因

耗子逗猫 —— 没事找事

前几天,在工作不太忙的时候,为了展现我在工作中积极主动,技术能力较强,并给领导留个好印象,我就去翻翻我的项目代码有没有可优化的空间。

没想到,我真让我找着啦。

祸端就此埋下了!

有用户反馈查问订单列表接口有点慢,我就去打印每一步的耗时信息。发现查问订单之前,须要先依据用户 ID 查问用户信息,而查问用户信息接口须要调用用户团队提供的服务,有时候网络较慢的时候,耗时达到 200 毫秒。

而查问订单接口层层调用的时候,调用了好几次查问用户信息接口。当然能够改成再最上层查问一次,而后层层往下传递,这样一来改的中央比拟多,也很麻烦。

我推敲着能不能加个本地缓存,把用户信息缓存起来,这样就不必每次去调用用户服务查问了。刚好就想到了应用ThreadLocal,据说高级程序员都用ThreadLocal,我也想用一下试试。

ThreadLocal是线程公有的,调用完结后,线程销毁了,ThreadLocal外面数据也跟着没了。

听着 ThreadLocal 是线程平安的,应该没什么问题。

入手实际

我先写一个 ThreadLocal 的工具类,用来存储和获取用户信息:

/**
 * @author 一灯
 * @apiNote 本地缓存用户信息
 **/
public class ThreadLocalUtil {

    // 应用 ThreadLocal 存储用户信息
    private static final ThreadLocal<User> threadLocal = new ThreadLocal<>();

    /**
     * 获取用户信息
     */
    public static User getUser() {
        // 如果 ThreadLocal 中没有用户信息,就从 request 申请解析进去放进去
        if (threadLocal.get() == null) {threadLocal.set(UserUtil.parseUserFromRequest());
        }
        return threadLocal.get();}

}

而后在查问订单接口外面,调用这个工具类的办法获取用户信息,最初依据用户信息查问订单信息,完满。

/**
 * 获取订单列表办法
 */
public List<Order> getOrderList() {
    // 1. 从 ThreadLocal 缓存中获取用户信息
    User user = ThreadLocalUtil.getUser();
    // 2. 依据用户信息,调用用户服务获取订单列表
    return orderService.getOrderList(user);
}

自测、提测、验收、上线,接口访问速度“嗖”一下就下来了,所有看上去都是那么完满。

我曾经开始空想,升职加薪,迎娶白富美,走上人生巅峰了。

大失所望

上线一个小时后,值班群炸了。

陆续开始有用户反馈本人刚下的订单不见了,其余用户也有反馈本人的订单列表莫名其妙多了一些订单。

我一脸懵逼,没碰到过这种状况,逐步反馈的用户越来越多,我曾经手足无措了。

领导毅然决然,小灯,你小子搞什么飞机,连忙回滚服务。

半个小时后,回滚结束,用户的情绪逐步平复下来。

故障复盘

线上故障解决后,紧接着就开始排查问题产生的起因。

通过无数次打日志、debug,终于定位到问题了。

ThreadLocal的确是线程公有的,并且会在线程销毁后,ThreadLocal外面的数据也会被清理掉。

然而问题就出在,无论咱们服务端用的是 Tomcat、Jetty、SpringBoot、Dubbo 等,都不会来一个申请就创立一个线程,而是创立一个线程池,所有申请共享这这个线程池里的线程。

一个线程解决完一个申请,并不会被销毁。可能导致多个用户申请共用一个线程,最初呈现数据越权,看到了别的用户的订单。

解决方案

解决办法就是,在应用完 ThreadLocal 后,再调用 remove 办法革除 ThreadLocal 数据。

/**
 * @author 一灯
 * @apiNote 本地缓存用户信息
 **/
public class ThreadLocalUtil {

    // 应用 ThreadLocal 存储用户信息
    private static final ThreadLocal<User> threadLocal = new ThreadLocal<>();

    /**
     * 获取用户信息
     */
    public static User getUser() {
        // 如果 ThreadLocal 中没有用户信息,就从 request 申请解析进去放进去
        if (threadLocal.get() == null) {threadLocal.set(UserUtil.parseUserFromRequest());
        }
        return threadLocal.get();}

    /**
     * 删除用户信息
     */
    public static void removeUser() {threadLocal.remove();
    }

}

应用 try/catch 包裹业务代码,而后在 finally 中革除 ThreadLocal 数据。

/**
 * 获取订单列表
 */
public List<Order> getOrderList() {
    // 1. 从 ThreadLocal 缓存中获取用户信息
    User user = ThreadLocalUtil.getUser();
    // 2. 依据用户信息,调用用户服务获取订单列表
    try {return orderService.getOrderList(user);
    } catch (Exception e) {throw new RuntimeException(e.getMessage());
    } finally {
        // 3. 应用完 ThreadLocal 后,删除用户信息
        ThreadLocalUtil.removeUser();}
    return null;
}

故障定级

影响用户超过 10w,或者谬误数据超过 10w,或者资损大于 100w,故障定级为 P1,全年绩效 C。

原本想优化程序性能,进步访问速度,给领导一个好印象,好显得本人技术能力强,工作积极主动。

这下好了,岂但年终奖没了,工作还可能保不住了。

睡觉没盖屁股——我是露大脸了!

事变总结

通过这次事变,我总结了以下几点教训:

  1. 没事儿别瞎逞能。
  2. 没有金刚钻,别揽瓷器活。
  3. 不求有功,但求无过。
  4. 灯子,重构优化的水太深,你把握不住。

文章继续更新,能够微信搜一搜「一灯架构」第一工夫浏览更多技术干货。

退出移动版