美文网首页Java
记一次ThreadLocal引发的线上故障,年终奖没了,可能还面

记一次ThreadLocal引发的线上故障,年终奖没了,可能还面

作者: 让我来搞这个bug | 来源:发表于2022-05-12 13:18 被阅读0次

    事情起因

    耗子逗猫 —— 没事找事

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

    image.png

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

    祸端就此埋下了!

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

    而查询订单接口层层调用的时候,调用了好几次查询用户信息的接口。当然可以改成再最上层查询一次,然后层层往下传递,这样一来改的地方比较多,也很麻烦。

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

    ThreadLocal是线程私有的,调用结束后,线程销毁了,ThreadLocal里面数据也跟着没了。

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

    image.png

    动手实践

    我先写一个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);
    }
    

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

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

    image.png

    事与愿违

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

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

    我一脸懵逼,没碰到过这种情况,逐渐反馈的用户越来越多,我已经不知所措了。

    领导当机立断,小灯,你小子搞什么飞机,赶紧回滚服务。

    image.png

    半个小时后,回滚完毕,用户的情绪逐渐平复下来。

    故障复盘

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

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

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

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

    一个线程处理完一个请求,并不会被销毁。可能导致多个用户请求共用一个线程,最后出现数据越权,看到了别的用户的订单。

    image.png

    解决方案

    解决办法就是,在使用完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,或者资损大于50w,故障定级为P1,全年绩效C。

    本来想优化程序性能,提高访问速度,给领导一个好印象,显得自己技术能力强,工作积极主动。

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

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

    image.png

    事故总结

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

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

    原文链接:https://juejin.cn/post/7095627880482209822

    相关文章

      网友评论

        本文标题:记一次ThreadLocal引发的线上故障,年终奖没了,可能还面

        本文链接:https://www.haomeiwen.com/subject/pdisurtx.html