您现在的位置是:探索 >>正文
程序员修复
探索77863人已围观
简介凌晨两点十七分,我盯着屏幕上那行红色的报错信息,手指在键盘上悬了半分钟没落下去。`NullPointerException at com.xxx.service.impl.OrderServiceIm ...
凌晨两点十七分,程序我盯着屏幕上那行红色的员修报错信息,手指在键盘上悬了半分钟没落下去。程序`NullPointerException at com.xxx.service.impl.OrderServiceImpl.calculatePrice(OrderServiceImpl.java:247)`。员修就这一行,程序把整个支付流程卡死了。员修
白天测试环境跑得好好的程序,预发环境一上就炸。员修运维群里有人@我,程序问是员修不是代码有问题。我没回,程序先把日志捞出来看了一遍。员修日志里订单号、程序用户ID、员修时间戳都对得上,程序唯独到了计算价格那一步,对象突然就空了。可代码逻辑明明写了判空——我翻到247行,盯着那行 `order.getItems().stream()` 看了足足十秒,脑子里闪过一个念头:并发。
这个接口白天QPS不到50,预发环境压测直接拉到800。`order`对象是从缓存里取的,缓存用的是本地Caffeine,过期时间设了30秒。问题就出在这儿——第一个线程拿到order,开始遍历items,遍历到一半,缓存过期了,Caffeine异步刷新把order对象替换成了新的。旧对象被GC回收,但第一个线程还在用引用,`getItems()`返回的list内部数组被清空,stream一跑就NPE。
这种问题很难复现。你本地跑一百遍都没事,因为QPS上不去,缓存根本来不及过期。我去年在上一家公司也遇到过类似的,那次是Guava Cache,场景是用户session刷新,也是并发读写导致对象状态不一致。当时我们加了个`synchronized`块,性能直接掉了一半。这次我学乖了,没急着改代码,先写了个复现脚本,用JMeter模拟并发,把Caffeine的`expireAfterWrite`改成1秒,跑十次能复现三次。
复现了就好办。方案有三个:第一,缓存里存不可变对象,每次刷新生成新实例,旧线程继续用旧的,新线程用新的,互不干扰。第二,加读写锁,读的时候不允许刷新。第三,把`getItems()`改成返回副本。我选了第一个,因为改动最小,而且Caffeine本身就支持`refreshAfterWrite`,配合`buildAsync`可以做到无锁刷新。改完压测跑了一小时,QPS稳在1200,NPE再没出现。
修完这个bug,我靠在椅背上发了会儿呆。程序员修bug,修到最后其实是在修自己对系统的理解。你以为是代码写错了,其实是并发模型没吃透;你以为是缓存配置问题,其实是对象生命周期管理没想清楚。每一次NPE背后,都藏着一个你之前忽略的时序问题。
天亮的时候,运维群里有人发了句“支付恢复了”。我没说话,把复现脚本和修复方案整理成文档,丢进了团队wiki。下次再有人踩这个坑,至少能少熬一个通宵。程序员这行,修复的不只是代码,还有自己那点侥幸心理——总觉得“我写的逻辑没问题”,可系统跑起来,从来不按你写的逻辑走。
Tags:
上一篇:篡改猴插件
相关文章
海洋渗透能
探索海洋渗透能,说白了就是淡水往咸水里冲的时候那股劲儿。这事儿听着新鲜,其实原理特简单——初中物理就讲过渗透压,半透膜把淡水和海水隔开,淡水拼命往盐度高的那边钻,压力攒够了,推动涡轮机发电。就这么个玩意儿 ...
【探索】
阅读更多国信证券金太阳下载官网电脑版
探索打开浏览器,输入“国信证券金太阳下载官网电脑版”,搜出来的结果里混着广告、仿冒站点和旧版本链接,稍不留神就点错。这年头,连下载个官方软件都成了技术活。但如果你是个老股民,大概率绕不开这个名字——金太阳 ...
【探索】
阅读更多餐饮招商加盟
探索下午三点,我坐在一家连锁快餐店的角落,对面是加盟商老周。他刚算完账,把计算器推过来,屏幕上显示着这个月的净利润——四万二。老周去年从互联网大厂裁员出来,用补偿金加上父母的积蓄开了这家店。“比上班强,” ...
【探索】
阅读更多