第 11 / 19 篇

第 10 章 把重复变成可承受:幂等与事务发件箱

本章提供数据库幂等与 Outbox 的设计推导,没有附带数据库、CDC 转发器或真实支付调用。第 11 章的 CLI 项目验证日志和消费进度,不是本章事务设计的端到端实现。Kafka 事务的原理说明也不代表本轮做过跨系统事务实验。

用事件 ID 识别同一次事件

幂等的意思是:同一个业务操作执行多次,最终有效结果与执行一次相同。例如对 event_id=e-001 的积分增加只能生效一次。

常见做法是在数据库中建立处理记录表,以 (consumer_name, event_id) 为唯一键。消费者在同一个数据库事务中插入去重记录并更新业务表。唯一键冲突说明这一业务消费者已处理过该事件,可以跳过业务更新。

读取事件
  ↓
开始数据库事务
  ├─ 插入处理凭证(唯一约束)
  └─ 执行业务更新
提交数据库事务
  ↓
提交 Kafka 消费进度

若数据库事务成功、Kafka 进度未提交,重启后会再次读到;数据库唯一约束让第二次更新失效。若业务事务失败,不能提交进度假装成功。

去重表不是随便查一下

“先查没有,再插入”会有并发竞争,必须由数据库唯一约束和事务保证正确性。去重记录保留期还要覆盖可能重放的历史,否则删除去重凭证后,老事件可能再次生效。

外部发短信或支付不能和普通本地数据库事务自然原子化。优先使用对方提供的幂等请求号,或把待执行任务写入本地发件箱,再由独立执行器按幂等规则调用并核对结果。

数据库写成功,消息没发出去怎么办

订单服务面临另一个问题:先写订单库,再发 Kafka,两步之间可能宕机;先发 Kafka,再写库,也可能产生没有真实订单的事件。

事务发件箱(Transactional Outbox)的思路是,在同一个数据库事务里写订单表与 outbox 表。后台转发器或 CDC 工具再把 outbox 的事件送进 Kafka。转发也可能重复,因此消费者仍应幂等。

这提供的是可恢复的协调方式,需要监控未转发事件、重试、顺序和清理,不等于“装一个插件就自动解决所有一致性问题”。

处理失败与坏消息

短暂网络失败可以有限重试并退避;字段格式错误反复重试通常不会自己变好。达到边界后,可以送入死信主题,记录原主题、分区、偏移量、事件 ID、错误原因和失败时间。

死信需要告警、责任人、修复和重放流程。若把同一订单的失败事件移走后继续处理后续事件,业务顺序可能改变。把它作为明确的业务决策,而不是默认“跳过就好”。