跳转到内容

幂等与去重

用途:让“重复投递”变成无害事件

至少一次投递 + 重试机制意味着重复不可避免。重复来源:生产者重试(超时但实际成功)、Broker 主从切换、消费者崩溃在提交位点前、再均衡、人工重放。去重责任在消费方,中间件不提供端到端恰好一次。

方法 做法 适用
唯一约束 业务键建唯一索引,插入冲突即视为重复 新增类操作(登记、流水)
状态机守卫 条件更新 WHERE status = 期望前态,影响行数 0 即已处理 状态推进类(支付、发货)
去重表/缓存 处理前查 msg_dedup(biz_key),处理后写入(带 TTL) 无法用唯一约束的复杂动作
版本号/时间戳 携带版本,旧版本请求被拒绝 乱序 + 重复同时存在

幂等键必须是业务键(订单号 + 操作类型 + 版本),不是消息 ID:业务重试会产生新消息 ID,而同一业务动作的键不变。

“查一下有没有处理过,再处理”在并发下不成立。正确做法:

  1. 数据库唯一约束或条件更新(单语句原子)。
  2. Redis SET key NX EX 抢占 + 处理 + 失败时释放(注意锁失效仍需 DB 兜底)。
  3. 先落“处理中”记录(唯一键),再执行,最后更新为成功/失败——把状态机做进去。
消息到达 → 幂等检查 → 业务处理(事务)→ 记录完成 → 失败则重试/死信 → 定期对账兜底

对账负责发现“幂等逻辑漏掉的不一致”(例如消息根本没到),是最后一层。

  • 每个消费者组声明其幂等键与实现方式
  • 重复消息的处理结果与首次一致(不能返回不同状态码导致上游误判)
  • 幂等记录有 TTL/归档策略,避免无限增长
  • 人工重放工具复用同一幂等逻辑
  • 压测覆盖“同一消息并发到达”场景
误区 修正
用消息 ID 去重 业务重试会产生新 ID,防不住
只靠“先查后插” 并发下重复,必须唯一约束或条件更新
幂等表无过期 表无限膨胀,查询变慢
认为加了锁就幂等 锁失效/主从切换仍会重复,DB 约束才是权威
重复时抛异常 应返回“已成功”,否则上游持续重试
  • Martin Kleppmann《数据密集型应用系统设计》第 9 章(幂等消费者与恰好一次投递)
  • Apache RocketMQ 官方文档:消费重试与幂等建议