围绕至少一次投递设计幂等消费、重试和死信处理。
图:事务 Outbox 解决发送空隙,消费端幂等保证重复传输只产生一个业务结果;点击查看原图。
核心思路
生产者使用业务唯一键,消费者先判断是否处理过。重试采用退避并设置上限,永久失败进入死信队列等待人工或补偿。
落地步骤
监控积压深度、最老消息年龄和失败率。消息顺序、吞吐与可用性之间存在权衡,应按业务选择分区键。
验证清单
实施后不要只确认“能运行”,还应检查错误率、延迟、资源占用和异常路径。把验证命令、预期结果和回滚方式写进变更记录,后续维护会轻松很多。
小结
好的工程实践不是堆叠工具,而是用清晰边界、可观察结果和可逆变更降低长期成本。先解决最重要的问题,再逐步自动化。
可执行实践
生产者以业务键或 Outbox 保证消息与事务关系,消费者实现幂等并在成功提交业务状态后确认。重试采用退避,上限后进入死信。
验证与退出条件
验证重复、乱序、延迟和毒消息。监控积压量、最老消息年龄、处理延迟和死信增长;分区键必须符合真正需要的顺序边界。
实践场景
以“保证消息至少一次投递下的业务唯一性”为例。实施前先保存当前版本、配置和关键指标基线,写明成功条件与停止条件;涉及生产数据或外部写入时,先在隔离环境验证。
分步实施
生产者使用事务消息或 outbox,消费者以业务键去重;成功提交后再确认消息,失败分类重试并设置死信处理。每完成一步都记录可观察结果,不把命令返回零等同于业务正确。
故障演练
重复投递、乱序、毒消息和处理后确认前宕机都要演练,验证数据库只产生一次业务结果。演练应使用可清理的测试数据,并验证告警、日志和恢复路径,而不只是确认系统最终又能访问。
观测与回滚
观察积压、重试、死信和端到端延迟;异常时暂停消费而不是删除队列或跳过失败消息。变更单中应写清负责人、观察窗口、阈值和回滚触发器,避免故障发生后再临时争论。
完成后把使用的版本、关键命令、观测窗口和结果写入变更记录。没有验证证据的“已完成”不能作为上线结论。
第二轮实践复核
可复用交付物: 消息契约、幂等表、重试策略和死信处置记录。复用时先固定软件版本、依赖锁文件、测试数据规模和运行环境;文章中的命令包含占位符,必须替换成自己的非生产目标后再执行。
最低验证: 重复投递同一业务键并在提交后确认前终止消费者,最终业务结果仍唯一
执行记录要保存命令、退出码、关键输出、观察窗口和回滚结果。若验证会产生写入,先使用隔离环境或 dry-run;任何失败都不能通过扩大权限、关闭校验或跳过测试来“解决”。
完整实战:订单已支付事件
订单服务更新状态后发布 OrderPaid,积分服务和通知服务各自消费。最危险的空隙是“数据库提交成功,但消息发送失败”。使用事务 Outbox 把业务更新和待发布事件写入同一数据库事务:
1 | BEGIN; |
独立发布器读取 outbox、发送到消息系统并标记已发布。发布器在确认前崩溃会造成重复发送,所以消费者仍必须幂等。不要宣称端到端“恰好一次”;通常可实现的是至少一次传输,加业务键去重后的单一业务结果。
消费者的幂等边界
积分服务在同一事务中登记 event_id 并增加积分,唯一约束阻止重复:
1 | BEGIN; |
如果副作用是调用第三方 API,还需传递稳定幂等键,或建立本地动作状态机进行对账;仅在本地记“消费过”不能证明第三方动作只执行一次。
| 故障 | 预期行为 | 必留证据 |
|---|---|---|
| 提交后发布器崩溃 | 恢复后重发 | outbox 延迟、event_id |
| 消费完成前进程退出 | 消息重投,业务不重复 | 消费尝试和幂等命中 |
| 下游持续失败 | 有限重试后进入隔离队列 | 原因、次数、首次/末次时间 |
| 消息乱序 | 旧版本不覆盖新状态 | aggregate version |
| 消息格式升级 | 新旧消费者均可解析 | schema 版本和契约测试 |
积压、重放与隔离队列
监控队列深度不够,还要看最老消息年龄、生产/消费速率差、处理 P95、重试率和隔离队列增长。重放前确认消费者版本、数据时间范围和副作用策略,先对少量事件 dry-run;隔离队列不是墓地,必须有负责人、告警和处理时限。
灰度期间按事件版本和消费者组观察。回滚消费者时必须仍能读取灰度期间产生的新事件,或在生产者端保持旧 schema;不要同时删除字段、回滚代码和清空积压,这会失去恢复依据。
一手资料
资料与版本边界复核日期:2026-08-22。工具行为可能演进,实施前以锁定版本对应的官方文档为准。
评论