消息消费如何保证幂等,避免重复消费导致数据不一致?
简化版
消息消费要通过业务唯一键、消费记录表、数据库唯一约束、状态机和乐观锁保证幂等。因为 MQ 通常只能保证至少一次投递,消费者必须能接受同一消息重复到达,并保证重复执行不会产生重复扣款、重复发券、重复加积分。
详细版
幂等消费常见方案:
- 消费记录表:
consumer_group + message_id唯一。 - 业务唯一键:例如
payment_id + points_type。 - 数据库唯一约束:防止并发重复插入。
- 状态机条件更新:只有合法状态才能流转。
- 乐观锁版本号:防止旧消息覆盖新状态。
- 下游接口也传幂等号:跨服务保证只处理一次。
不建议只依赖 Redis 去重,除非能接受过期或丢失风险。关键业务最好以数据库唯一约束或业务流水作为最终防线。
完整版教学
一、为什么消息会重复消费
消息队列为了可靠性,常见语义是至少一次投递。也就是说,它宁愿让消费者重复收到,也不轻易丢消息。
重复原因很多:
- 消费者处理成功,但提交 offset 或 ack 失败。
- 消费者处理过程中重启,消息被重新投递。
- Broker 重平衡,分区被其他消费者接管。
- 生产者重复发送了同一业务事件。
所以消费者不能假设“我只会收到一次”。
二、消费记录表怎么做
消费记录表适合通用去重。
设计唯一键:
consumer_group + message_id
消费开始时先插入记录,插入成功说明第一次消费;如果唯一键冲突,说明已经处理过或正在处理。
但要注意,如果插入消费记录成功后业务处理失败,记录状态要能区分 PROCESSING、SUCCESS、FAILED,否则可能误判为已成功。
三、业务唯一键更可靠
很多时候,业务唯一键比消息 ID 更重要。因为同一个业务事件可能被不同消息 ID 重发。
例如支付成功后加积分,真正要防重的是同一笔支付不能加两次积分,所以唯一键应该是:
payment_id + points_scene
即使消息 ID 不同,只要 payment_id 相同,也不会重复加积分。
四、状态机可以避免旧消息覆盖新状态
订单状态消费要特别注意顺序。可能先收到“订单已发货”,后收到延迟的“订单已支付”。
状态机应该规定只能从低阶状态流转到合法下一状态:
WAIT_PAY -> PAID -> SHIPPED -> FINISHED
如果订单已经是 SHIPPED,再收到 PAID 事件,不能把状态改回去。
五、Redis 去重的边界
Redis 去重速度快,适合短期防重,例如防止短时间重复点击。但如果 Redis 数据过期、淘汰或故障,去重信息可能丢失。
关键资金、积分、库存类消费,最好让数据库唯一约束作为最终防线。Redis 可以做第一层过滤,数据库保证最终正确。
六、幂等消费的事务边界
消费记录和业务更新最好在同一个本地事务里完成。比如积分消费场景中,插入消费记录、增加积分、写积分流水应该一起提交。否则可能出现消费记录成功但积分没加,后续消息又被当成已消费跳过。
如果业务更新和消费记录不在同一个库,就要使用可靠消息、补偿或把业务唯一约束放到真正产生副作用的那张表上。幂等不是单独插一条日志就结束,关键是防住最终副作用重复或丢失。
还要注意失败返回语义:已经幂等处理过的重复消息,通常应该返回消费成功,让 MQ 提交进度,而不是抛异常导致它无限重投。
七、常见误区与追问
这道题要紧扣「消费者幂等」本身回答,不能把它混成泛泛的数据一致性套话。面试官通常会追问“写入怎么确认、失败怎么补、旧数据怎么防、成本在哪里”,所以回答要覆盖一致性目标、写入顺序、消息投递、幂等、补偿、对账和读写体验。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 消费者幂等保证同一消息被重复投递或重复消费时,业务结果只生效一次 | 不要停在名词解释 |
| 流程机制 | 接收消息 -> 解析业务唯一键 -> 检查去重记录或状态 -> 执行业务更新 -> 记录处理结果 -> 重复消息直接确认 | 要说清触发点、状态变化、确认点和失败兜底 |
| 工程取舍 | 同一扣款消息重复投递 3 次,账户余额只能扣一次,后两次应识别为已处理 | 一致性方案本质是在强一致成本、系统可用性、延迟和业务可接受的不一致窗口之间取舍 |
消费者幂等 面试拆解:
1. 接收消息
2. 解析业务唯一键
3. 检查去重记录或状态
4. 执行业务更新
5. 记录处理结果
6. 重复消息直接确认
记忆钩子:先给结论,再拆流程,再讲数字例子和失败边界;回答「消费者幂等」时要围绕题目问法收束,不要把相邻概念堆成一段没有重点的名词清单。
- 误区:MQ 能保证消息只投递一次。 多数 MQ 实际提供至少一次投递,重复消费是常态风险。
- 误区:消费代码不报错就不需要幂等。 ACK 丢失、消费者重启和重平衡都可能导致重复投递。
- 误区:只按 messageId 去重就够了。 有些重发会生成新 messageId,更稳的是业务唯一键。
- 追问:常见幂等手段? 去重表、唯一约束、状态机、乐观锁、版本号和业务流水号。
- 追问:先执行业务还是先写去重? 要放在同一本地事务里,避免去重成功业务失败或反过来。
- 追问:幂等失败会怎样? 轻则重复通知,重则重复扣款、重复发货或状态回退。
八、加强记忆
消息幂等的核心是“重复消息一定会来,业务结果不能重复”。通用消息 ID 去重能挡一层,业务唯一键和数据库唯一约束更可靠;涉及状态流转时,还要用状态机防止旧消息把新状态覆盖掉。