ChatGPT、Codex排障:单线程测试都通过了,为什么一到并发场景Bug又出现?
最近用ChatGPT、Codex排查真实项目时,有一种问题特别容易让人产生错觉:
本地测试全绿,单线程跑也完全正常,一到真实并发场景,Bug又回来了。
比如:
单个请求修改库存,没有问题。
单个用户提交订单,没有问题。
单线程执行计数逻辑,也没有问题。
相关单元测试、接口测试甚至全部通过。
可一旦两个、十个甚至几十个请求同时进来,就开始出现:
库存被多扣。
订单重复创建。
状态互相覆盖。
计数结果少了一部分。
同一个任务被执行两次。
这时候很多人的第一反应是:
是不是Codex修得还不彻底?
但这种问题真正麻烦的地方在于:
代码可能在“一个请求一次执行”的情况下完全正确。
错误只会发生在:
两个或者多个执行路径同时碰到同一个状态时。
所以这里需要先分清一个非常重要的概念:
Sequential Correctness ≠ Concurrent Correctness
单线程正确,不代表并发正确。
一、先看一个最典型的例子
假设现在有一个库存:
stock = 1
用户购买时,代码逻辑很简单:
读取库存
如果 stock > 0
stock = stock - 1
保存库存
单线程测试:
请求A进入。
读取库存:1。
判断通过。
减1。
保存0。
完全正确。
测试也一定通过。
但如果请求A和请求B几乎同时进来呢?
可能变成:
请求A读取:
stock = 1
请求B也读取:
stock = 1
A判断库存大于0。
B也判断库存大于0。
A减1,保存0。
B也按照自己刚才读到的1减1,再保存0。
最终数据库里:
stock = 0
看起来库存甚至没有变成负数。
但实际上:
两个用户都已经购买成功。
真正错误的不是减法。
而是:
两个请求都在基于同一个旧状态做决定。
这就是典型的:
Race Condition——竞态条件
二、为什么单元测试特别容易漏掉这种问题?
因为绝大多数普通测试,本质上都是:
一步一步执行。
比如:
准备数据。
调用函数。
检查结果。
整个过程中只有一个执行流。
测试看到的是:
A结束以后,B才开始。
但真实线上系统往往是:
A还没结束。
B已经进入。
C也在读取同一份数据。
所以测试环境验证的是:
顺序世界。
而线上运行的是:
并发世界。
如果代码逻辑依赖:
“我读取以后,数据不会被别人改”
这种隐含假设,
那么单线程测试再完整,也很难证明并发安全。
三、为什么AI修Bug时更容易忽略这一层?
因为Agent非常擅长沿着当前失败路径解决问题。
比如测试告诉它:
expected stock = 0
received stock = 1
它会分析:
哪一行更新失败。
哪个函数没有保存。
哪个条件判断不正确。
这些都属于:
局部执行逻辑。
但并发Bug真正关键的问题往往不是:
这一行写错。
而是:
这几行代码在两个执行流同时进入时,会发生什么。
如果测试本身没有模拟并发,
Agent也很容易默认:
当前操作是独占执行的。
于是代码在测试里会非常漂亮。
一上真实流量:
问题重新出现。
四、更深一层:很多并发Bug,本质上都是“检查”和“修改”之间存在时间窗口
比如:
if balance >= amount:
balance -= amount
单看没有问题。
真正危险的地方是:
判断:
balance >= amount
和后面的:
balance -= amount
不是一个不可分割操作。
中间存在时间窗口。
第一个线程检查完以后,
第二个线程也可能进来。
这时候两个执行流都会觉得:
当前条件成立。
所以很多并发Bug真正需要解决的并不是:
“判断条件写得更严一点。”
而是:
如何保证“检查 + 修改”是一个原子过程?
这就是并发控制的核心。
五、最常见的第一类问题:Lost Update——更新丢失
比如两个请求同时修改计数器。
初始值:
count = 100
线程A读取100。
线程B也读取100。
A执行:
100 + 1 = 101
保存101。
B也执行:
100 + 1 = 101
再保存101。
理论上执行两次以后应该是:
102
最终却只有:
101
其中一次更新被覆盖了。
代码单独执行没有任何问题。
问题只存在于:
多个执行流共享同一个状态。
六、第二类问题:重复创建
例如:
用户点击一次付款。
前端因为网络慢又重试一次。
两个请求几乎同时进入。
后端逻辑都是:
查询订单是否存在。
不存在。
创建订单。
单线程完全正确。
并发情况下:
请求A查询:
不存在。
请求B查询:
也不存在。
A创建。
B也创建。
最终:
两个订单。
所以真正的问题是:
“先查有没有,再决定创建”并不是并发安全的。
这类问题往往需要:
唯一约束。
幂等Key。
事务。
原子写入。
而不是继续给查询逻辑加更多判断。
七、第三类问题:状态覆盖
比如一个任务状态:
pending
线程A准备更新成:
processing
线程B准备更新成:
cancelled
如果两边没有版本控制或者状态机约束:
谁最后写入,
谁就覆盖前面的结果。
于是你会看到:
明明任务已经取消,
过一会又变成processing。
或者:
明明已经完成,
状态却被旧请求重新改回处理中。
这类问题最容易出现于:
异步任务。
消息队列。
Webhook。
支付回调。
多个Agent同时修改共享状态。
八、为什么“加锁”也不是万能答案?
看到并发问题以后,很多人第一反应是:
那就加锁。
但锁本身也可能带来新的问题:
锁范围太大。
吞吐量下降。
锁顺序不一致。
死锁。
锁没有覆盖所有入口。
分布式环境下,本地锁根本无效。
所以真正需要先判断的是:
哪一段状态必须被保护?
而不是:
哪里报错就在哪里加锁。
九、数据库事务为什么也可能不够?
很多人觉得:
放进Transaction就安全了。
其实也不一定。
事务只能保证一部分一致性。
如果两个事务同时:
读到同一个值。
然后分别修改。
不同隔离级别下,仍然可能产生:
Lost Update。
幻读。
重复写入。
所以并发问题往往需要结合:
事务隔离级别。
行锁。
乐观锁。
版本字段。
唯一约束。
原子UPDATE。
一起考虑。
十、一个很实用的办法:把“读 → 判断 → 写”画出来
遇到这类Bug时,我不建议第一步直接让Codex修改代码。
先把执行过程画成:
Read
↓
Check
↓
Compute
↓
Write
然后问:
如果两个请求同时执行,
每一个步骤之间能不能被另一个请求插进来?
比如:
A Read
B Read
A Check
B Check
A Write
B Write
只要把执行顺序展开,很多并发Bug会非常明显。
十一、第二个办法:让Codex主动模拟Interleaving
可以直接要求Agent分析:
假设请求A和请求B同时执行,请列出可能的执行交错顺序,并判断哪些顺序会产生错误结果。
这个方法非常有效。
因为并发Bug最核心的东西就是:
Interleaving——执行交错
并不是代码逻辑本身错误。
而是不同线程、请求或任务之间的执行顺序不同以后:
结果改变了。
十二、第三个办法:测试必须真正制造并发
普通测试这样写:
run A
run B
并没有意义。
需要的是:
A ────────>
B ────────>
尽量同时进入关键区域。
例如:
同时启动多个请求。
使用Barrier让线程在同一个点一起继续。
重复运行数百次。
随机延迟。
提高问题出现概率。
因为很多竞态问题并不是每次都会发生。
它可能:
100次才出现1次。
如果只跑一遍:
测试很容易全部通过。
十三、为什么这类Bug经常“本地复现不了”?
因为并发Bug特别依赖时序。
你的本机可能:
CPU快。
负载低。
网络稳定。
请求恰好没有重叠。
线上却可能:
大量请求同时进入。
数据库响应变慢。
线程调度不同。
网络延迟变化。
于是原来只有几毫秒的竞态窗口,被放大了。
这就是为什么:
本地跑100次没问题,不代表线上一定没问题。
并发问题通常需要:
压力。
随机性。
重复执行。
才能真正验证。
十四、第四个办法:优先使用“原子约束”,不要只依赖代码判断
比如防止重复订单。
与其完全依赖:
if not exists:
create
更可靠的方式可能是:
数据库唯一约束。
这样即使两个请求同时创建:
数据库也会阻止第二个。
又比如库存扣减:
可以使用:
带条件的原子UPDATE。
而不是:
先SELECT,再UPDATE。
很多时候:
最可靠的并发控制不是让代码更聪明,而是让底层系统保证不变量。
十五、第五个办法:明确哪些状态必须满足“不变量”
比如库存系统最核心的不变量:
stock >= 0
支付系统可能是:
同一个 payment_id 只能成功一次
任务系统可能是:
completed 状态不能重新回到 processing
如果这些Invariant能够写进:
数据库约束。
状态机。
测试。
Agent以后修改相关代码时,就更容易知道:
哪些东西绝对不能被破坏。
十六、为什么Agent越能自己执行,并发安全反而越重要?
因为未来不仅用户请求在并发。
Agent本身也会带来更多并发。
例如:
Agent A修改任务状态。
Agent B同时处理另一个相关任务。
后台Worker也在运行。
CI或者自动化任务又触发新的操作。
以前一个开发者一次只做一个任务。
现在系统可能同时存在:
多个自动执行主体。
这意味着:
并发不再只是“高流量网站”的问题。
多Agent Workflow本身也在增加:
状态竞争的机会。
十七、给自己测一个指标:并发失效率
这篇我建议只看一个指标:
Concurrent Failure Rate——并发失效率
选几个关键流程:
库存。
订单。
权限。
任务状态。
计数。
然后在并发条件下重复执行。
统计:
并发运行时出现状态错误、重复写入、更新丢失或结果不一致的次数 ÷ 总并发验证次数
比如:
对一个关键接口做100组并发测试。
其中出现4次:
重复创建。
状态错误。
数据不一致。
那么:
并发失效率 = 4%。
看起来4%不高。
但线上如果一天有10万次调用:
4%已经非常严重。
十八、并发失效率高于5%,说明什么?
如果核心流程一上并发就容易出现问题:
说明当前系统依赖了太多:
先读再写。
隐式顺序。
无保护共享状态。
这种情况下真正需要优化的是:
并发控制。
事务。
幂等。
原子操作。
状态机。
而不是继续堆更多AI任务。
十九、并发失效率1%—5%,最容易被误判为“偶发Bug”
这种最危险。
因为问题不是每次出现。
开发者很容易觉得:
再跑一次就好了。
但这种低概率竞态一旦进入高流量环境:
最终几乎一定会遇到。
所以这种阶段应该重点:
重复运行。
增加压力。
扩大时序随机性。
不要因为99次成功就忽略那1次错误。
二十、并发失效率长期接近0,才能说明流程真正稳定
如果关键操作在:
高并发。
重复执行。
随机延迟。
压力测试。
下都能保持:
状态一致。
没有重复副作用。
没有更新丢失。
说明并发控制已经比较成熟。
这个时候Agent进一步参与更多自动化任务,风险才更低。
二十一、Plus和Pro怎么判断?
如果你的并发失效率还比较高:
本地测试全部通过。
一上真实并发就出现:
重复写。
状态覆盖。
计数错误。
那当前真正的瓶颈并不是AI额度。
而是:
系统本身缺少并发安全保证。
这种阶段Plus通常已经足够。
更应该先让ChatGPT、Codex帮助你完善:
并发测试。
事务边界。
锁策略。
幂等Key。
唯一约束。
状态机。
因为增加更多Agent容量,甚至可能进一步提高并发操作数量,让问题暴露得更严重。
如果你的并发失效率已经长期接近0:
关键状态都有明确约束。
事务和幂等成熟。
高并发验证稳定。
Agent参与自动化也很少制造共享状态问题。
同时你又长期存在:
大型项目分析。
长任务。
多个稳定Agent任务排队。
AI侧容量真正限制开发吞吐。
这时候Pro才更容易带来真实价值。
因为增加的是:
建立在稳定并发基础上的AI产能。
而不是放大一个本来就存在竞态问题的系统。
二十二、并发Bug真正难的地方,是每一行代码都可能“看起来没错”
这是这类问题最值得注意的地方。
很多竞态问题,你单独看:
if判断没错。
计算没错。
数据库写入也没错。
每一步都合理。
只有把:
请求A。
请求B。
放在同一条时间线上,
错误才会出现。
所以以后让Codex处理这类问题,不能只问:
这一段代码对不对?
还要再问:
如果两个人同时执行这段代码,它还对不对?
这两个问题完全不是一回事。
最后
ChatGPT、Codex越来越能快速修复代码以后,测试全绿很容易给人一种感觉:
这个问题已经解决了。
但如果项目真正运行在:
多用户。
多线程。
多进程。
多个Worker。
多个Agent。
这样的环境里,
单线程正确只是第一步。
真正可靠的代码还必须回答:
多个执行主体同时进入时,系统还能不能保持同一个规则?
库存只能卖一次。
订单不能重复创建。
状态不能互相覆盖。
计数不能丢。
这些问题,普通单线程测试很难替你证明。
所以以后Codex修完一个涉及共享状态的Bug以后,我觉得很值得再问一句:
如果两个请求在完全相同的一毫秒进入,这段代码还成立吗?
如果这个答案还不确定,
那么测试虽然已经全绿,
真正的Bug可能只是:
还没有等到并发把它重新叫出来。
持续分享 Codex、大模型开发与 AI 编程实战内容。
长期深度使用各类代码大模型,也整理了稳定的Plus/Pro会员订阅渠道,有需要可自取。
更多推荐


所有评论(0)