一次自己修出来的线上事故
上一篇复盘了插件内存泄漏的修复。修完打包、安装、验证——一切正常。两天后用户反馈:插件的 i18n Inlay 功能整体消失了,一个都不剩。
更迷惑的是:功能是”全有或全无”式消失,不是部分失效。这种症状通常指向初始化链路被拦腰截断,而不是散点的功能 bug。
这篇文章复盘这次自己引入的事故:一个后台线程上的 read-action 断言异常,如何静默杀掉整个功能域——以及为什么这类异常在开发时根本看不见。
业务代码里大量存在这种框架式调用——方法名藏在字符串字面量里:
1 | // MyBatis 风格:字符串 = Dao 接口方法名 |
IDE 原生对这种字符串无能为力。插件的诉求是四向导航:
| 位置 | 操作 | 期望 |
|---|---|---|
Java "getXxx" 字符串 |
Ctrl+B | 跳 Dao 方法 / Moc XML |
| Dao 方法声明处 | Ctrl+B / Find Usages | 反查字符串 |
Mapper XML <select id> |
Ctrl+B | 弹「Dao 方法 + 调用点」 |
Moc XML name 属性 |
Ctrl+B | 弹 Java 调用点 |
这篇文章记录实现过程中的三次架构演进——每一次都是被真实现象(选择框不弹、进度条死循环、几万文件搜索)逼出来的重新设计。
团队自研的 IDEA 插件更新后,出现了一个很诡异的症状:什么都没做,只是打开了几个文件,IDEA 就卡得不行。CPU 风扇狂转、界面掉帧、时不时冻结几秒。
比”卡”更有价值的是手里的 GC 数据。连续抓了几轮 jstat -gcutil:
1 | S0 S1 E O M CCS YGC YGCT FGC FGCT GCT |
只看数字就能拆出三层信号。这篇文章完整复盘这次排查:从 jstat 解读、到沿执行路径找代码证据、到三类修复的设计与验证。
老 MES 系统里”导出”大概是最容易被低估的功能:早期数据量小,一个同步接口把数据查出来写成 Excel 直接回传,皆大欢喜。等到单表日志类数据涨到千万行、报表动辄几十万行时,这条链路开始连环爆雷。本文记录一次完整的导出链路改造:从同步超时丢文件,到「提交即返回 + 游标流式读取 + 异步写入 + 桌面通知取件」的全套架构,包括关键实现细节、踩过的每一个坑,以及清楚留着的优化债。
改造前的链路是典型的”一把梭”:
1 | 用户点导出 → 请求线程分页循环查全量 → 攒成大 List → 写 Excel → HTTP 响应回传 |
数据量上来后暴露的问题按出现频率排序:
List<Map> + 写缓冲),几个人并发导出大报表,堆直接见顶;期间打过一层补丁:同步导出超时后转后台继续写,完成后上传文件、发通知。这解决了一部分问题,但”用户先同步等 30 秒”的体验和分页翻页式读取的额外开销还在。于是有了这次彻底的改造。
仅修改某个类的实现逻辑或修复bug而不重新编译构建整个项目时,可以使用命令行工具手动替换class文件。


1 | pv dengqimes_20250519_144357.sql | mysql --database=dengqimes-test --user=zy_mes -pr4UQBwBV --host=127.0.0.1 --port=3306 --default-character-set=utf8mb4 |
使用 pv | mysql的组合,老版mysql在shell命令处理参数时,会忽略 dengqimes-test 中的 -,导致实际上mysql将备份还原到了数据库 dengqimes 上。而正式环境和测试环境数据库名恰好仅用-加后缀进行区分。

| 客户端请求路径 | 后端接收路径 |
|---|---|
https://域名/J5BfNtYvHW/api |
http://后端:54321/J5BfNtYvHW/api |
https://域名/api |
http://后端:54321/J5BfNtYvHW/api |
要实现 所有客户端请求路径 最终在后端统一映射到 /J5BfNtYvHW/api,可以通过以下两种方案实现:
1 | server { |
路径映射效果:
| 客户端请求路径 | 后端接收路径 |
|---|---|
https://域名/J5BfNtYvHW/api |
http://后端:54321/J5BfNtYvHW/api |
https://域名/api |
http://后端:54321/J5BfNtYvHW/api |
1 | server { |
路径映射效果
| 客户端请求路径 | 后端接收路径 |
|---|---|
https://域名/J5BfNtYvHW/api |
http://后端:54321/J5BfNtYvHW/api |
https://域名/api |
http://后端:54321/J5BfNtYvHW/api |

AUTO_INCREMENT字段1 | select max(id) from biz_wms_delivery_order_barcode; |
才几万行数据,表ID已经增长到快100万了
1 | <update id="insertDeliveryOrder" parameterType="Map"> |
三个表同样的现象,自增ID不连续。
使用 INSERT INTO ... ON DUPLICATE KEY UPDATE 或 INSERT IGNORE INTO 语句的坑。
使用 INSERT INTO ... ON DUPLICATE KEY UPDATE 语句:
即使在唯一键冲突时执行更新操作,没有实际插入数据库,AUTO_INCREMENT 计数器仍会递增,导致ID值跳跃。
1 | insert into biz_wms_delivery_order( |
尤其是在批量操作的时候,假设批量插入100条数据,其中5条插入成功,95条已存在执行更新操作,自增主键ID会在此次语句执行完后增大100而不是5,即使实际上只插入了5条新数据。
可以手动多执行几次同样的SQL,通过 show create table 语句查看表的 AUTO_INCREMENT 属性验证。
使用 INSERT IGNORE INTO 语句,也是一样。假设批量插入100条数据,其中5条插入成功,另外95条由于主键冲突被忽略,实际 AUTO_INCREMENT 增大了100。
表结构,查看 AUTO_INCREMENT
1 | show create table biz_wms_delivery_order_barcode; |
网上查询可知以上是mysql默认的自增ID分配策略导致的问题,会直接预先分配ID,导致
AUTO_INCREMENT增长。虽然调整innodb_autoinc_lock_mode配置,可以修改该策略,但是修改该参数会影响并发性能。
因为需要定时从SRM同步,每次执行 INSERT INTO ... ON DUPLICATE KEY UPDATE 会有大量已存在的数据需要更新,导致ID跳跃的非常大。
最终还是要修改业务表的插入逻辑,不使用 INSERT INTO ... ON DUPLICATE KEY UPDATE ,将其拆分为插入和更新两部分。
方案: 在插入前先执行查询,判断记录是否存在;如果存在,则执行更新;如果不存在,则执行插入。
优点: 避免了自增ID的跳跃。
缺点: 增加了查询和判断的开销,增加了代码,可能影响性能,但影响不大可以接受。