用事务内事件和消费去重衔接数据库与消息系统,明确至少一次投递下的责任边界。
本文于 2026-09-12 补充整理,按 2026-06 专题归档。示例为方案设计,参数用于说明方法,不代表已上线项目或实测成果。
双写窗口来自两个系统
用户提交任务时,应用需要把任务写入数据库,再通知消息队列。先写库后发消息,可能提交成功但消息丢失;先发消息后提交,消费者可能读到不存在的任务。
Outbox 的做法是把任务记录和待发送事件放进同一个本地事务,由独立投递器稍后发送。AWS 的模式说明介绍了这一核心思想。它提供可靠衔接的基础,但仍需要应用定义重复和顺序的处理方式。
事件与任务一起提交
以下是逻辑表结构示意:
CREATE TABLE task_outbox (
event_id varchar(64) PRIMARY KEY,
aggregate_id varchar(64) NOT NULL,
aggregate_version bigint NOT NULL,
event_type varchar(80) NOT NULL,
payload text NOT NULL,
created_at timestamp with time zone NOT NULL,
published_at timestamp with time zone
);
同一数据库事务中插入任务和对应 outbox 记录。事件负载尽量只包含标识和必要字段,不放访问令牌或大段知识原文。消费者使用事件中的任务 ID,在自己的权限边界内读取当前状态。
事件需要明确 schema 版本。后续代码升级时,旧事件可能仍留在重试队列里,不能按新字段含义直接解释。
投递器会产生重复
投递器发送消息并收到 broker 确认后,更新 published_at。如果发送成功但更新前崩溃,下次会再次发送。这是正常恢复窗口,不能通过提前标记已发送来规避,否则会重新引入消息丢失。
多个投递器可采用短租约抢占记录,或结合数据库锁机制分批认领。不要让数据库事务在整个网络发送期间一直持有大量锁。认领超时、失败退避和卡住记录都要可观察。
消费去重必须绑定业务更新
消费者维护 consumer_name + event_id 唯一记录。在同一数据库事务中完成去重记录插入和本地状态推进,提交成功后才确认消息。若重复事件到达,读取已有处理结果并确认即可。
如果先记录“已消费”再单独执行业务,进程在两者之间崩溃会丢业务;如果先提交业务再记录去重,可能重复业务。所谓幂等消费需要精确到事务边界。
对外部工具调用,本地去重事务仍不能包住远程副作用。应进一步转化为带稳定操作 ID 的执行任务,再由工具幂等和结果对账处理。
顺序应由聚合版本校验
相同任务的事件可能出现版本 3 先于版本 2。消费者不能盲目按到达顺序推进状态。可以按任务 ID 分区降低乱序概率,同时用聚合版本做最终检查。
收到旧版本事件可以判定为已过时;收到超前版本时,选择等待缺失事件或读取任务权威状态进行协调。不能简单把所有超前消息丢掉,否则可能永久失去推进信号。
死信队列中的事件重放应保留原事件 ID,这样去重逻辑仍然有效。人工重放也不能跳过业务状态检查。
验证最重要的三个窗口
分别在数据库提交后、broker 确认后、消费者事务提交后杀掉对应进程。预期是事件最终可见,本地状态不会重复推进,外部写操作受独立幂等保护。
此外监控最老未投递事件的年龄,单看 outbox 行数不够:少量长期卡住的记录也可能对应重要任务。清理历史 outbox 前,要明确审计、重放和备份保留期限。
这套设计体现的是对失败窗口的管理,而不是给系统贴上“消息绝不丢失、恰好一次”的标签。
