最近用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会员订阅渠道,有需要可自取。

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐