事务提交之后,再处理外部副作用

设想一个收藏服务:保存书签后,需要给订阅者发一条通知。如果先发送通知,再提交数据库,提交失败时就会出现“收到通知却查不到书签”。反过来先提交、再发送,也可能在两步之间退出,导致通知遗漏。

本篇目标: 认识数据库事务能够覆盖的范围,并用发件箱思路为外部动作留下恢复依据。

把确定的提交与外部动作分开
把确定的提交与外部动作分开放大图解 ↗

先把事务边界画出来

事务把一组数据库修改组成一个整体,提交和回滚围绕这个整体生效。一个普通数据库事务不会自动包含邮件服务、远程 HTTP 请求或另一个独立系统。PostgreSQL:事务

因此,不能把“放进 try/catch”当成跨系统事务。异常被捕获,只说明程序得知了失败,不说明外部动作已经撤销。

在同一次提交中保存事件意图

一种常见办法是把待处理事件与业务记录一起保存。下面用简化 SQL 表达关系,字段和对象均为演示用途。

BEGIN;
INSERT INTO bookmarks (id, url) VALUES ($1, $2);
INSERT INTO outbox (event_id, kind, payload, state)
VALUES ($3, 'bookmark.created', $4, 'pending');
COMMIT;

后台工作者随后读取待处理事件,调用通知服务,再记录处理结果。这样,即使进程在提交之后退出,待处理意图仍在数据库中,后续可以继续。

发件箱仍然需要幂等

工作者可能已经成功发送,却在保存成功状态前退出。恢复后再次领取同一事件,仍然可能重复发送。应把稳定事件标识交给接收方,或在能够控制的边界里识别重复处理。

多工作者还需要可靠的领取机制,避免同一条事件同时被多个执行者认领。可以使用行锁或租约等方式,但具体选择应与数据库和任务模型一致。

别忽略长期积压

可恢复不等于会自动恢复。需要观察待处理数量、最老事件年龄、连续失败次数,以及人工重试是否安全。超过重试预算的事件应保留可定位原因,不能只从队列里消失。

这套思路适合“允许最终完成、能够识别重复”的外部动作。如果业务要求更强的一致性,需要进一步明确补偿、确认或人工决策,而不是把普通发件箱称作端到端的恰好一次执行。

见字如晤

图解