Skip to content

案例3:复杂 Bug 调试

场景:生产环境 API 随机返回 500 错误,无法稳定复现,日志信息不完整。
挑战:间歇性错误、日志不够、根因在复杂的并发逻辑里


第一步:提供所有可用信息

你(附上日志):

生产环境有个随机出现的 500 错误,大约每 100 次请求出现 2-3 次。
我贴一下错误日志:

[粘贴日志片段]

已知信息:
- 只在高并发时出现(超过 50 个并发请求)
- 只影响 /api/orders/create 端点
- 重启服务后短暂消失,但过一段时间又会出现
- 数据库没有报错

你能看出这是什么类型的问题吗?有什么思路?

Claude(分析):

从日志模式分析,这很可能是竞态条件(Race Condition)。

证据:
1. "Database locked" 错误有时候出现(SQLite 的锁竞争)
2. 只在高并发出现
3. 重启后消失(说明是运行时状态问题,不是启动配置)

建议检查方向:
1. orders/create 路由里是否有非原子的"先读后写"操作
2. 是否有共享的内存状态(全局变量、模块级缓存)
3. 数据库事务是否正确隔离了并发写入

我需要看 orders/create 的路由代码和相关 service。

第二步:深度代码分析

你:

好,相关代码在 src/routes/orders.ts 和 src/services/orderService.ts。

帮我深入分析:
1. 找出所有可能的竞态条件
2. 找出没有事务保护的数据库写入
3. 找出共享状态

分析完列出所有可疑点,从最可能到最不可能排序。

Claude 分析后输出(摘要):

发现 3 个可疑点(按可能性排序):

1. [高] orderService.ts 第 89-103 行
   先 SELECT 检查库存,再 UPDATE 扣减,没有事务
   在高并发下,两个请求都可能读到"库存充足",然后都扣减
   → 典型的 TOCTOU 竞态条件

2. [中] orderService.ts 第 45 行
   全局变量 `let pendingOrders = 0` 在并发请求中被读写
   没有加锁,可能导致计数错误

3. [低] routes/orders.ts 第 23 行
   requestId 用 Date.now() 生成,高并发时可能重复

关键技巧:让 Claude 排序可疑点

不要让 Claude 一次修所有问题。先分析,排序,然后从最可能的开始修,每次修一个,验证后再继续。


第三步:先写能复现的测试

根据分析,问题最可能是库存扣减的竞态条件。

在修复之前,先写一个测试来复现这个 bug:
- 模拟 10 个并发请求同时下单
- 库存只有 5
- 预期结果:只有 5 个成功,5 个失败
- 实际结果(修复前):可能超过 5 个成功

用 Jest 实现这个并发测试。

第四步:修复

你:

测试确实失败了,复现了 bug。

现在修复第一个问题:库存扣减的竞态条件。

方案:用数据库事务 + 行锁,把"检查库存"和"扣减库存"变成原子操作。

修改 orderService.ts,不要改其他文件。改完跑并发测试,验证测试通过。

Claude 修复并跑测试,输出:

修复完成。代码改动:

[展示 diff]

测试结果:
✓ 并发 10 请求,库存 5 → 5 成功,5 失败 ✅
✓ 所有原有测试通过 ✅

第五步:修复其他问题

第一个问题修了,继续修第二个:
pendingOrders 全局变量的并发问题。

用原子操作或者锁来保护它,或者考虑是否真的需要这个全局状态。
修完跑测试。

第六步:部署验证计划

你:

两个问题都修了,测试全绿。

在部署前,帮我制定一个验证计划:
1. 应该监控哪些指标
2. 如果修复没有生效,怎么快速回滚
3. 建议增加什么日志,方便下次排查类似问题

另外,commit 信息里要说明:
- 问题根本原因
- 修复方案
- 如何验证

调试思路总结

阶段提示词技巧
收集信息提供尽量多的上下文:日志、复现条件、已知信息
分析阶段让 Claude 给「可能性排序」,而不是直接修所有问题
复现优先先写能失败的测试,再修复,有了测试才能验证修复
逐一修复一次只修一个问题,修完验证再继续
部署计划修复后讨论监控和回滚策略,不要「假设没问题」

可复用的 Debug 提示词

有个 bug,情况如下:

现象:[具体症状]
复现条件:[什么时候出现]
已知信息:[你知道的一切]

日志:
[粘贴相关日志]

请:
1. 分析可能的根本原因(按可能性排序)
2. 不要急着修,先分析
3. 分析完后,我来决定从哪个方向开始

基于官方文档整理,持续更新