幂等消费者
解释 at-least-once 消息系统中消费者为什么必须容忍重复交付,以及唯一约束、消息去重表、状态机和业务幂等如何避免重复副作用。
#concept
#growing
#backend
#architecture
[!info] related notes
- 所属 MOC:Messaging & Queues MOC
- 前置:消息投递语义、接口幂等设计
- 具体实现:Apache Kafka、RabbitMQ
- 跨系统:分布式事务
幂等消费者
[!abstract] 一句话定义 幂等消费者允许同一逻辑消息被投递和处理多次,但最终业务状态与成功处理一次相同。它防的是重复副作用,不只是“看见相同 Message ID 就跳过”。
为什么重复是正常情况
Consumer updates DB successfully
→ crashes before ACK / Offset Commit
→ Broker redelivers
这是 At-least-once 的正常故障窗口。因此消息系统“没有重复”不应成为业务正确性的前提。
三种常用实现
1. 业务唯一约束
例如订单支付事件最终创建一条支付记录:
UNIQUE (payment_id)
重复 INSERT 由数据库约束拒绝或转为 Upsert。优点是正确性靠近最终事实。
2. Processed Message 表
transaction:
insert (consumer_name, message_id) unique
update business state
commit
如果 Message ID 已存在,说明该 Consumer 已成功处理。去重记录与业务更新必须在同一事务,否则仍有窗口。
3. 状态机 / 条件更新
UPDATE orders
SET status = 'paid', version = version + 1
WHERE id = ?
AND status = 'pending'
AND version = ?;
重复或过期事件无法再次推进状态。适合有明确业务阶段的实体。
为什么只用 Redis TTL 去重不够
SETNX dedup:<message_id> EX 1d
可作为性能优化,但强正确性风险包括:
- TTL 到期后历史消息 Replay;
- Redis 故障或 Eviction;
- 标记成功后业务 DB 写失败;
- 先写业务再写 Redis 的反向窗口;
- 多环境或 Consumer Scope 混淆。
关键业务应让去重和业务状态在同一权威事务边界内。
Message ID 与 Business Key
Message ID 识别一次发布;Business Key 识别同一逻辑操作。
例如 Producer 因请求重试生成两个不同 Message ID,但都代表同一个 payment_id。仅按 Message ID 去重仍会重复扣款。
Transport identity → message_id
Business identity → payment_id / command_id / idempotency_key
高风险场景通常需要业务 Key 兜底。
幂等不等于可交换
两条不同事件可能都各自幂等,却不能乱序:
OrderCancelled(version 3)
OrderPaid(version 2) arrives later
如果只按 Message ID 去重,旧 Paid 仍可能覆盖新状态。还需要 Entity Version、Sequence 或合法状态转换检查。
外部副作用
调用支付、邮件等外部 API 时:
- 传递稳定 Idempotency Key;
- 保存请求与结果;
- Timeout 后查询状态或 Reconcile;
- 不生成新 Key 盲目重试;
- 明确哪些动作不可逆,需要补偿而不是重复执行。
Inbox / Outbox 组合
Producer DB
→ business update + Outbox in one transaction
→ publish
Consumer DB
→ Inbox / Processed Message + business update in one transaction
这不能消除所有重复,但把重复变成可控、可审计的正常路径。
设计检查表
- Message ID 和业务 Idempotency Key 区分清楚。
- 去重记录与业务副作用处在同一事务或可 Reconcile 边界。
- 重复处理返回已有结果,而不是简单吞掉必要响应。
- 过期/乱序消息有 Version / State Machine 防护。
- 外部 API 使用稳定 Key 并处理 Unknown Outcome。
- 去重数据的 Retention 覆盖最大 Replay 窗口。
- 监控 Duplicate Rate、Conflict、Rejected Stale Event 和 Reconciliation。