ChatGPT、Codex工程实录:数据库事务已经回滚了,为什么外部操作却撤不回来?
最近用ChatGPT、Codex排查真实项目时,有一类问题特别容易让人产生错觉:
数据库事务明明已经Rollback了,为什么有些事情却还是发生了?
比如一个订单流程:
BEGIN
创建订单
扣减余额
调用短信服务
调用第三方库存接口
后续步骤报错
ROLLBACK
最后去数据库里看:
订单没了。
余额也恢复了。
从数据库角度看,事务确实已经完整回滚。
但用户手机上:
短信已经收到了。
第三方系统里:
库存也已经被扣了。
甚至某些场景里:
支付请求都已经发出去。
这时候很多人第一反应是:
“不是已经Rollback了吗?”
问题就在这里。
数据库事务能够回滚的,只是:
数据库自己控制范围里的状态。
它没办法自动撤回已经发送出去的HTTP请求、短信、Webhook、消息或者第三方操作。
这就是今天真正要讲的问题:
Transaction Boundary——事务边界
一、为什么代码看起来像在一个事务里,实际上并不是?
很多代码会写成类似这样:
@Transactional
createOrder() {
saveOrder();
callInventoryApi();
sendNotification();
updateBalance();
}
从代码结构看:
这些操作都在同一个方法里。
方法上甚至还有:
@Transactional
很容易产生一种感觉:
“这里面的操作要么全部成功,要么全部失败。”
但实际上,数据库事务真正能够控制的只有:
数据库连接里的:
INSERT。
UPDATE。
DELETE。
如果中间执行:
callInventoryApi()
这个请求一旦已经被第三方系统接收并执行:
数据库事务根本不知道:
对方做了什么。
也没有能力在本地ROLLBACK时告诉对方:
“刚才那次操作也请撤销。”
所以:
代码块在一个事务里,不代表所有副作用都属于同一个事务。
二、一个最典型的真实场景
假设创建订单流程是:
1. INSERT order
2. 调用库存服务扣库存
3. INSERT payment_record
4. 后续逻辑异常
5. ROLLBACK
最终本地数据库:
order:不存在
payment_record:不存在
看起来恢复得非常干净。
但库存服务那里:
stock - 1
已经执行完成。
于是整个系统变成:
本地认为:
订单从来没有创建过。
库存系统却认为:
这笔库存已经扣掉。
这种问题真正危险的地方是:
两个系统分别看自己,都可能是正常的。
只有从业务整体来看,状态才是不一致的。
三、为什么ChatGPT、Codex也很容易误判这里?
因为Agent读代码时,很容易看到:
@Transactional
然后默认:
失败以后会自动恢复。
尤其如果后面的异常确实触发了数据库Rollback,它可能进一步判断:
事务机制已经生效。
但真正需要问的是:
事务里到底有哪些操作是数据库本地操作,哪些已经跨出了数据库边界?
比如:
写MySQL。
可以Rollback。
但:
发HTTP请求。
发短信。
发邮件。
调用支付。
写对象存储。
调用第三方SaaS。
这些都属于:
External Side Effect——外部副作用
一旦副作用已经真正发生:
本地数据库事务不会自动帮你撤回。
四、所以事务真正的边界在哪里?
一个数据库事务可以保证:
BEGIN
INSERT A
UPDATE B
DELETE C
COMMIT
这些动作:
要么都成功。
要么都失败。
因为它们都属于同一个数据库事务管理器。
但如果流程变成:
数据库
↓
Redis
↓
第三方API
↓
消息队列
↓
邮件服务
你面对的已经不是:
单个数据库事务。
而是:
分布式状态变化
每个系统都有:
自己的状态。
自己的失败方式。
自己的重试机制。
自己的时间顺序。
这时候简单依赖:
ROLLBACK
已经不够了。
五、为什么把外部调用放在事务里面,反而可能更危险?
很多人会觉得:
既然外部调用很重要,那就放到数据库事务里。
例如:
BEGIN
UPDATE order
callPaymentApi()
UPDATE order_status
COMMIT
表面看起来:
整个流程更完整。
但这里又会产生一个新的问题:
长事务
假设第三方API响应5秒。
那么数据库事务可能一直保持5秒甚至更久。
期间:
锁可能一直不释放。
连接一直占用。
其他请求继续等待。
如果外部服务更慢:
事务会越来越长。
所以把外部调用简单塞进数据库事务,并不能真正获得跨系统原子性。
反而可能同时得到两个问题:
外部副作用无法Rollback + 数据库长事务。
六、那这种问题到底应该怎么设计?
核心思路不是:
“想办法让数据库替你回滚外部世界。”
而是:
承认跨系统操作可能部分成功,然后设计恢复路径。
这其实是很多分布式系统设计的起点。
七、第一种办法:先记录状态,再异步执行外部操作
比如订单创建。
不要直接:
创建订单
↓
扣库存
↓
发通知
可以改成:
创建订单
status = PENDING
COMMIT
数据库先保存一个明确状态。
然后再由后台任务:
扣库存。
调用第三方。
发送通知。
成功以后:
status = COMPLETED
失败以后:
status = FAILED
这样外部操作不再强行绑在一个数据库事务里。
而是变成:
一个可以观察、重试和恢复的状态机。
八、第二种办法:为副作用设计补偿操作
比如:
已经扣库存。
后面的步骤失败。
那系统应该知道:
需要执行:
restoreInventory()
已经创建第三方资源:
需要:
deleteRemoteResource()
已经预扣余额:
需要:
refund()
这种思路就是:
Compensation——补偿
注意:
补偿和Rollback不是一回事。
Rollback是:
事情从来没有发生过。
补偿是:
事情已经发生了,现在再执行一个反向动作尽量恢复业务状态。
这两个概念非常重要。
九、Saga为什么经常出现在这种场景里?
如果一个业务流程横跨多个系统:
订单。
库存。
支付。
通知。
物流。
想靠一个数据库事务控制所有系统,通常不现实。
Saga的核心思路就是:
把一个大事务拆成多个本地事务。
每一步都有:
正向操作。
以及必要时的补偿操作。
例如:
创建订单
↓
扣库存
↓
支付
↓
创建物流
如果支付失败:
补偿库存。
取消订单。
如果物流创建失败:
可能需要:
取消支付。
恢复库存。
取消订单。
也就是说:
系统不再追求:
所有系统瞬间原子一致。
而是追求:
失败以后能够走回一个可接受状态。
十、但补偿也不一定百分百成功
这是工程里最现实的一点。
比如库存扣减成功。
后续失败。
系统开始补偿:
恢复库存。
结果库存服务这时候也挂了。
怎么办?
所以补偿本身也需要:
重试。
状态记录。
告警。
幂等。
人工兜底。
这也是为什么真正成熟的分布式流程里,通常会保存:
当前业务状态
已经完成哪些步骤
哪些补偿已执行
哪些补偿仍失败
而不是只靠:
try-catch。
十一、幂等为什么在这里特别重要?
假设补偿接口是:
restoreInventory(order_id)
第一次调用其实成功了。
但网络超时,调用方没收到结果。
于是系统再次Retry。
如果接口不是幂等的:
库存可能加回来两次。
所以所有可能重复调用的:
执行。
重试。
补偿。
最好都设计成:
Idempotent——幂等
例如根据:
order_id。
event_id。
operation_id。
保证同一个业务动作重复执行,不会产生重复副作用。
十二、真正排查这类问题时,第一步不是看Rollback日志
遇到:
“事务回滚了,但外部状态没恢复”
不要继续盯着:
ROLLBACK
而应该把流程分成两类。
本地事务内动作
数据库:
INSERT。
UPDATE。
DELETE。
外部副作用
HTTP。
消息。
短信。
支付。
库存。
文件。
第三方API。
然后问:
哪些副作用发生在事务提交之前?
它们失败后有没有补偿?
补偿失败怎么办?
这三句话往往比继续看数据库日志更重要。
十三、怎么让Codex更容易发现这种问题?
不要只告诉它:
“事务回滚了,但库存还是少了。”
更有效的是让它检查:
1. 哪些操作属于数据库事务
2. 哪些操作属于外部系统
3. 每个外部操作发生在什么时间
4. 失败后是否存在补偿
5. 重试和补偿是否幂等
这样Agent就不会把:
“数据库事务成功Rollback”
误认为:
“整个业务流程已经回到原点”。
十四、给自己测一个指标:孤立副作用率
这篇我建议只看一个核心指标:
Orphan Side Effect Rate——孤立副作用率
统计那些最终本地事务失败的任务。
看其中有多少仍然留下了:
外部系统里的有效副作用。
比如:
100次最终Rollback的业务执行里。
有3次虽然数据库恢复了,但:
库存已经扣减。
通知已经发出。
第三方资源已经创建。
那么:
孤立副作用率 = 3%。
这个数字看起来不高。
但如果涉及:
支付。
库存。
订单。
账户余额。
哪怕1%都可能非常危险。
十五、孤立副作用率高,应该先改什么?
如果这个指标偏高:
优先检查:
外部API是不是直接放在数据库事务里。
任务有没有状态机。
有没有补偿动作。
补偿能不能重试。
执行和补偿是不是幂等。
失败后有没有可观察状态。
不要第一反应就是:
让ChatGPT、Codex继续修改事务注解。
因为真正的问题可能根本不在:
事务有没有Rollback。
而在:
事务边界之外已经发生了什么。
十六、Plus和Pro怎么判断?
如果你的孤立副作用率还比较高:
业务经常出现:
数据库回滚了。
外部状态却没有恢复。
补偿依赖人工处理。
Retry以后又出现重复操作。
那么当前真正限制效率的不是ChatGPT、Codex容量。
而是:
跨系统失败恢复机制还不完整。
这种阶段Plus通常已经够用。
更值得先完善:
状态机。
补偿。
幂等。
失败恢复。
审计。
否则增加更多AI容量:
只会让Agent更快地产生更多复杂跨系统修改。
如果你的孤立副作用率已经长期接近0:
外部操作都有明确状态。
失败以后可以自动补偿。
补偿本身能够安全重试。
高风险流程也能被追踪和恢复。
同时复杂工程任务长期排队,
AI执行容量才真正成为瓶颈。
这时候Pro才更容易放大真实效率。
因为Agent工作的基础已经从:
“失败以后靠人工救回来”
变成:
“失败本身就是系统设计的一部分”。
最后
数据库事务已经Rollback,
并不意味着:
整个世界都回到了执行之前。
数据库能够撤销的是:
它自己控制范围里的状态。
但短信已经发送。
HTTP请求已经执行。
库存已经被扣。
第三方资源已经创建。
这些事情不会因为:
ROLLBACK
自动消失。
所以以后让ChatGPT、Codex分析这类工程问题时,真正值得问的不是:
“事务为什么没有回滚成功?”
而是:
“哪些操作根本就不在这个事务里?”
当你把这个边界画清楚以后,很多看起来很诡异的分布式问题,其实就已经解释了一半。
持续分享 Codex、大模型开发与 AI 编程实战内容。
长期深度使用各类代码大模型,也整理了稳定的Plus/Pro会员订阅渠道,有需要可自取。
更多推荐




所有评论(0)