幂等与去重
用途:让“重复投递”变成无害事件
为什么必须做
「为什么必须做」小节链接至少一次投递 + 重试机制意味着重复不可避免。重复来源:生产者重试(超时但实际成功)、Broker 主从切换、消费者崩溃在提交位点前、再均衡、人工重放。去重责任在消费方,中间件不提供端到端恰好一次。
四种实现
「四种实现」小节链接| 方法 | 做法 | 适用 |
|---|---|---|
| 唯一约束 | 业务键建唯一索引,插入冲突即视为重复 | 新增类操作(登记、流水) |
| 状态机守卫 | 条件更新 WHERE status = 期望前态,影响行数 0 即已处理 |
状态推进类(支付、发货) |
| 去重表/缓存 | 处理前查 msg_dedup(biz_key),处理后写入(带 TTL) |
无法用唯一约束的复杂动作 |
| 版本号/时间戳 | 携带版本,旧版本请求被拒绝 | 乱序 + 重复同时存在 |
幂等键必须是业务键(订单号 + 操作类型 + 版本),不是消息 ID:业务重试会产生新消息 ID,而同一业务动作的键不变。
原子性要求
「原子性要求」小节链接“查一下有没有处理过,再处理”在并发下不成立。正确做法:
- 数据库唯一约束或条件更新(单语句原子)。
- Redis
SET key NX EX抢占 + 处理 + 失败时释放(注意锁失效仍需 DB 兜底)。 - 先落“处理中”记录(唯一键),再执行,最后更新为成功/失败——把状态机做进去。
与补偿、对账的配合
「与补偿、对账的配合」小节链接消息到达 → 幂等检查 → 业务处理(事务)→ 记录完成 → 失败则重试/死信 → 定期对账兜底对账负责发现“幂等逻辑漏掉的不一致”(例如消息根本没到),是最后一层。
设计清单
「设计清单」小节链接- 每个消费者组声明其幂等键与实现方式
- 重复消息的处理结果与首次一致(不能返回不同状态码导致上游误判)
- 幂等记录有 TTL/归档策略,避免无限增长
- 人工重放工具复用同一幂等逻辑
- 压测覆盖“同一消息并发到达”场景
常见误区
「常见误区」小节链接| 误区 | 修正 |
|---|---|
| 用消息 ID 去重 | 业务重试会产生新 ID,防不住 |
| 只靠“先查后插” | 并发下重复,必须唯一约束或条件更新 |
| 幂等表无过期 | 表无限膨胀,查询变慢 |
| 认为加了锁就幂等 | 锁失效/主从切换仍会重复,DB 约束才是权威 |
| 重复时抛异常 | 应返回“已成功”,否则上游持续重试 |
延伸阅读
「延伸阅读」小节链接- Martin Kleppmann《数据密集型应用系统设计》第 9 章(幂等消费者与恰好一次投递)
- Apache RocketMQ 官方文档:消费重试与幂等建议