凌晨两点半,debug到怀疑人生,突然悟了
今天这个bug,真的给我整破防了。
下午三点接到一个线上告警,某个接口偶尔返回500,频率不高,大概千分之几。看了下日志,报错信息是NullPointerException,堆栈指向一个工具类方法。
第一反应:这有啥难的,加个判空不就完了。
然后我就开始复现。
本地跑,没问题。
压测环境跑,没问题。
测试环境跑,还是没问题。
行吧,那就加日志呗。打了十几个log.info,部署上去,等。
等了两个小时,终于抓到一次。日志显示,那个对象确实是null。但问题是——这个对象在方法入口明明是有值的,中间经过了两层调用,怎么就没了呢?
我开始逐行排查。看了半天,突然注意到一个细节:这个方法被一个@Async注解的方法调用了。
当时心里咯噔一下。
因为那个对象是ThreadLocal里取的。
@Async会开新线程,新线程拿不到主线程的ThreadLocal,所以取出来就是null。
就这么个玩意儿,我折腾了快四个小时。
说实话,这种坑不是没踩过,但每次踩到还是会觉得自己像个新手。你说这玩意儿它报错吧,它也不报错,就是偶尔返回个null,你甚至不知道问题出在哪一层。
后来我想了想,其实根本原因就是:我对这段代码的调用链路不够熟。那个@Async是半年前另一个同事加的,我当时review的时候压根没注意到它会影响ThreadLocal的传递。
代码这种东西,真的是你不动它,它就不动你。但只要你碰了,之前埋的雷就全炸了。
最后改法也很简单,把ThreadLocal的值在异步调用前取出来,作为参数传进去,不依赖线程上下文了。
改完上线,观察了一晚上,没再出过。
现在八点了,我终于能睡了。
写这篇东西也不是想教谁什么,就是记录一下今天这个坑。以后要是再碰到类似的问题,至少能少花两个小时。
程序员嘛,不就是在一个又一个坑里爬出来,然后拍拍灰继续干。