ChatGPT、Codex排障:数据库明明提交成功了,为什么消息队列里却没有这条事件?
最近用ChatGPT、Codex排查分布式系统问题时,有一类故障特别容易让人误判:
数据库里的数据明明已经写成功了,但消息队列里却没有对应事件。
比如订单服务里已经能查到:
order_id = 10241
status = CREATED
页面也显示:
订单创建成功。
但下游库存服务始终没有收到:
OrderCreated
结果就变成:
订单存在。
库存没扣。
积分没加。
通知没发。
下游系统像完全不知道这笔订单存在。
如果只看业务代码,很容易觉得逻辑没问题:
saveOrder()
publishEvent()
先写数据库。
再发消息。
看起来非常自然。
但问题恰恰就在这里:
数据库提交成功,不代表消息一定发送成功。
这类问题本质上不是普通代码Bug。
而是一个典型的:
Dual Write——双写一致性问题
一、为什么“先写数据库,再发消息”并不安全?
假设订单创建流程是:
1. INSERT order
2. COMMIT
3. publish OrderCreated
正常情况下当然没问题。
但考虑一个很小的失败窗口:
数据库提交成功
↓
进程突然崩溃
↓
消息还没来得及发送
最终结果就是:
数据库:
order = CREATED
消息队列:
没有 OrderCreated
这时候你重启服务也没用。
因为数据库事务已经结束。
系统不会自动知道:
“刚才还有一条消息没发。”
这就是最麻烦的地方。
二、为什么ChatGPT、Codex也很容易漏掉这个问题?
因为AI读代码时,很容易沿着正常执行路径理解:
保存订单
↓
发送事件
↓
返回成功
从静态代码上看:
两步都有。
没有明显异常。
单元测试也可能全部通过。
但真实问题并不发生在某一行代码内部。
而是发生在:
两步之间。
也就是:
DB COMMIT
↓
Failure Window
↓
MQ PUBLISH
Agent如果只验证:
“数据库函数有没有成功。”
“MQ发送代码有没有调用。”
往往会得出:
代码没有问题。
但真正应该问的是:
如果系统刚好在这两步之间失败,会发生什么?
这才是双写问题的核心。
三、那把顺序反过来行不行?
很多人看到这里会想到:
既然“先写DB再发MQ”可能丢消息,
那改成:
1. publish message
2. save database
是不是就好了?
还是不行。
因为这次失败窗口变成:
消息发送成功
↓
数据库写入失败
于是下游已经收到:
OrderCreated
开始:
扣库存。
发通知。
计算积分。
但数据库里:
订单根本不存在。
所以你会发现:
无论顺序怎么换,
只要是:
两个独立系统分别写入
就始终存在中间失败窗口。
四、问题真正难在哪:两个系统没有共同事务
数据库事务可以保证:
BEGIN
INSERT
UPDATE
COMMIT
要么全部成功。
要么全部回滚。
但数据库和消息队列通常不是同一个事务系统。
数据库不知道:
MQ有没有成功。
MQ也不知道:
数据库有没有Commit。
所以代码里写:
transaction {
saveOrder()
sendMessage()
}
并不意味着:
数据库事务能够自动把MQ也一起回滚。
这也是很多排查最容易误解的一点:
本地事务 ≠ 跨系统原子事务
只要系统涉及:
数据库 + MQ
数据库 + Redis
数据库 + 第三方API
数据库 + 搜索引擎
类似问题都会出现。
五、真实系统里会表现成什么?
双写问题通常不会表现成:
“整个服务挂了。”
反而经常表现成一些很奇怪的局部异常。
比如:
订单存在,但库存没变化
数据库写成功。
库存事件丢失。
支付成功,但订单状态没更新
支付系统完成。
状态同步事件失败。
用户注册成功,但欢迎邮件没发
用户记录已经存在。
异步通知事件没进入队列。
数据库已经修改,但搜索结果还是旧的
主库更新成功。
同步到搜索引擎的事件丢了。
这些问题最大的特点是:
单独看任何一个系统都可能是“正常的”。
只有把两个系统放在一起看,
你才会发现状态不一致。
六、ChatGPT、Codex排查时应该先画出“两个状态”
遇到这类问题,我不建议一开始就让Agent继续改代码。
先把状态拆出来。
比如订单场景:
状态A:订单是否写入数据库?
状态B:OrderCreated是否进入消息队列?
然后列出4种情况:
DB成功 + MQ成功 正常
DB失败 + MQ失败 正常失败
DB成功 + MQ失败 异常
DB失败 + MQ成功 异常
真正要关注的是后面两种:
状态失配
这样Agent会马上意识到:
问题不是简单的“发送消息失败”。
而是:
两个系统之间没有共同的一致性保障。
七、真正常见的解决办法:Transactional Outbox
这类问题里,一个非常常见的解决方案就是:
Transactional Outbox
思路其实并不复杂。
不要在数据库提交以后立刻直接依赖MQ。
而是在同一个数据库事务里同时写:
业务数据。
和一条待发送事件。
例如:
BEGIN
INSERT INTO orders ...
INSERT INTO outbox_events
(
event_type,
aggregate_id,
payload,
status
)
COMMIT
因为这两张表在同一个数据库事务里:
要么一起成功。
要么一起失败。
这时候至少可以保证:
只要订单存在,就一定存在一条对应的待发送事件。
八、然后由Relay负责把Outbox送进MQ
接下来再由独立任务扫描:
outbox_events
找到:
status = PENDING
然后发送到消息队列。
流程变成:
业务事务
↓
订单 + Outbox同时落库
↓
Relay读取Outbox
↓
发送MQ
↓
标记SENT
如果发送过程中服务崩溃:
没关系。
因为Outbox记录还在。
重启以后继续发送即可。
这就是关键变化:
原来的系统是:
失败以后不知道漏了什么。
用了Outbox以后变成:
失败以后还有一个可恢复状态。
九、但Outbox并不意味着“发送一次就结束”
这里又会出现另一个问题。
假设Relay:
消息已经成功发送到MQ。
但还没来得及把Outbox标记为SENT。
这时候进程崩了。
重启以后看到:
status = PENDING
它会再次发送。
于是同一条消息可能出现:
重复投递。
所以Outbox通常解决的是:
不丢。
并不天然保证:
绝不重复。
这就必须和昨天讲的另一个机制连起来:
Idempotency——幂等
消费者应该能承受:
同一个事件收到两次。
十、真正可靠的设计往往是“至少一次 + 幂等”
现实系统里,很多消息系统更容易做到:
At-Least-Once Delivery
也就是:
宁愿消息重复。
也尽量不要丢。
所以消费者需要根据:
event_id。
order_id。
业务唯一键。
判断:
这条事件是不是已经处理过。
例如:
event_id = evt_83921
消费者第一次处理成功以后记录:
evt_83921 = processed
第二次再收到:
直接忽略。
这样:
Outbox负责降低丢消息风险。
幂等负责降低重复执行风险。
两者结合,整个链路才更完整。
十一、让Codex排查时,不要只问“为什么消息没发出去”
更有效的排查方式是直接要求它回答:
1. 数据库事务在哪一行真正Commit?
不是调用save()。
而是真正提交完成的位置。
2. MQ发送发生在Commit之前还是之后?
这个顺序非常重要。
3. 两者之间有没有失败窗口?
比如:
进程崩溃。
网络异常。
Timeout。
MQ不可用。
4. 失败以后系统有没有可恢复记录?
如果没有:
那很可能就是一次不可恢复的事件丢失。
5. 重试以后是否会产生重复副作用?
这决定消费者是否需要幂等。
这样Agent分析的就不再只是几行代码。
而是:
整个一致性链路。
十二、验证时一定要主动制造失败
正常跑100遍都成功,并不能证明这个设计安全。
真正有效的测试应该是:
故意在关键位置失败。
例如:
数据库Commit以后立即Kill进程。
MQ发送前断网。
MQ发送成功后、更新Outbox状态前崩溃。
消费者处理一半时失败。
然后观察:
订单是否存在。
Outbox是否存在。
消息是否最终能重新发送。
重复消息会不会导致重复业务操作。
这类测试比单纯:
assert messageSent == true
有价值太多。
因为分布式一致性真正的问题,
几乎都藏在失败窗口里。
十三、可以自己测一个指标:跨系统失配率
这篇我建议只看一个核心指标:
Cross-System Mismatch Rate——跨系统失配率
统计那些本来应该同时保持一致的业务动作。
比如:
订单写库 + OrderCreated事件。
100次业务执行以后,
有2次出现:
数据库已经成功。
但消息最终没有进入正确处理链路。
那么:
跨系统失配率 = 2%。
这个数字看起来不高。
但如果一天有10万笔订单:
2%意味着:
2000笔状态可能不一致。
对于:
支付。
库存。
订单。
账户余额。
哪怕0.1%都可能非常严重。
十四、失配率高,真正应该优化什么?
如果这个指标还很高,
优先检查:
有没有Transactional Outbox。
有没有可靠Relay。
消费者有没有幂等。
事件有没有唯一ID。
失败任务能不能恢复。
有没有Dead Letter Queue。
有没有补偿机制。
不要第一反应就是:
换更强模型。
因为这里的问题不是Agent推理能力不足。
而是:
系统本身没有设计好失败状态。
十五、Plus和Pro怎么判断?
如果你的跨系统失配率还比较高:
数据库和消息队列之间经常出现:
漏事件。
重复事件。
状态不一致。
失败后无法恢复。
那当前真正限制效率的不是AI容量。
而是:
分布式一致性机制还不稳定。
这种阶段Plus通常已经足够。
更值得先完善:
Transactional Outbox。
幂等。
状态机。
Relay重试。
异常恢复。
否则增加更多AI容量,
只是让Agent更快地在一个不可靠的系统上继续修改。
如果你的跨系统失配率已经长期接近0:
失败路径有明确恢复方式。
消息允许安全重试。
消费者具备幂等。
Outbox、补偿和审计都比较完善。
同时又长期存在:
大量复杂工程任务排队。
多个Agent持续处理项目。
AI执行容量真正成为瓶颈。
这时候Pro才更容易放大真实吞吐。
因为Agent工作的基础已经稳定。
最后
数据库已经提交成功,
并不意味着整个业务已经成功。
在越来越多由ChatGPT、Codex参与开发和排障的分布式系统里,
真正容易出问题的,往往不是某一行代码。
而是:
两个系统之间那几毫秒的失败窗口。
数据库成功。
MQ失败。
或者MQ成功。
数据库失败。
只要这两个状态不能被统一恢复,
系统迟早就会出现:
“明明成功了一半,却再也补不回来”的问题。
所以排查这类故障时,
不要只问:
消息发送代码有没有执行。
更应该问:
如果系统在两个系统之间突然失败,状态还能不能恢复?
这往往才是问题真正的根。
持续分享 Codex、大模型开发与 AI 编程实战内容。
长期深度使用各类代码大模型,也整理了稳定的Plus/Pro会员订阅渠道,有需要可自取。
更多推荐



所有评论(0)