消息只被消费一次
MQ 至少一次投递不可避免,业务要靠幂等表做到效果只发生一次。
面试官问题
MQ 怎么保证消息只被消费一次?如果 Consumer 业务处理成功了,但 ACK 丢了,Broker 又重投,怎么保证不会重复扣款或重复发货?
考 MQ 投递语义、ACK 丢失导致重复投递、消费端幂等、去重表设计,以及业务副作用的边界。
先承认 MQ 通常只能保证至少一次投递,不能从 Broker 侧承诺业务效果只发生一次。正确性放在 Consumer:用业务唯一键写幂等表,未处理才执行业务副作用,已处理直接 ack。
业务背景
支付、发货、积分发放这类消费逻辑都有不可重复的业务副作用;MQ 重投时,系统必须保证副作用只生效一次。
- 为什么 MQ 不能直接承诺业务 exactly once?
- ACK 丢失后为什么会重复投递?
- 幂等表如何保证副作用只发生一次?
关键约束
- Broker 重投不可完全避免。
- 业务副作用不能重复执行。
- 幂等判断要和业务更新保持事务一致。
- 重试窗口内要能识别已处理消息。
系统建模
- 1
Broker:至少一次投递消息。
- 2
ACK 信号:可能丢失或超时。
- 3
Consumer:先做幂等判断,再做业务副作用。
- 4
幂等表:用业务唯一键记录处理结果。
- 5
副作用计数器:展示扣款/发货只能增加一次。
动画推演
看 ACK 信号在网络中丢失后 Broker 如何重投;Consumer 第二次收到消息先查幂等表,发现已处理后跳过业务副作用。
- 01 Broker 投递消息。
- 02 Consumer 处理成功但 ACK 丢失。
- 03 Broker 重试导致重复投递。
- 04 Consumer 查询幂等表。
- 05 已处理则跳过业务副作用。
- 06 未处理则执行业务并写入幂等表。
观察 ACK 丢失、Broker 重投、幂等表命中与业务副作用计数的变化。
故障注入 / 错误做法
只要消费成功后 ACK 就行,MQ 会保证消息只投一次;或者用 synchronized 防止同一时刻重复消费。
先承认 MQ 通常只能保证至少一次投递,不能从 Broker 侧承诺业务效果只发生一次。正确性放在 Consumer:用业务唯一键写幂等表,未处理才执行业务副作用,已处理直接 ack。
错误做法是每次收到消息都直接扣款。动画会让 ACK 丢失触发二次投递,展示没有幂等表时副作用计数器如何变成 2。
- ACK 丢失或消费超时会触发重复投递。
- 先执行业务再写幂等表,宕机后可能重复副作用。
- 幂等键选错会把不同业务请求误判为重复。
- 幂等表和业务表不在同一事务会出现中间态。
30 秒面试表达
- 01
我不会说 MQ 天然保证只消费一次,主流 MQ 更现实的是至少一次投递,所以重复投递一定要按会发生来设计。
- 02
Consumer 收到消息后,用业务唯一键先查幂等表;已处理就直接 ack,不再执行业务副作用。
- 03
未处理时,把写幂等记录和业务更新放在同一个本地事务里,事务成功后再 ack。
- 04
这样即使 ACK 丢了 Broker 重投,第二次也只会命中幂等表并跳过业务,达到业务效果只发生一次。
常见追问
幂等表应该用什么 key?▶
用业务唯一键,比如订单号+操作类型、支付流水号、消息业务 id,而不是 MQ 的投递次数或临时 messageId。
先写幂等表还是先执行业务?▶
要放在同一事务里。常见做法是插入幂等记录作为第一步,唯一键冲突说明处理过;随后执行业务更新,事务一起提交。
幂等表会不会很大?▶
会,所以要按业务保留期分区或归档。幂等记录只保留能覆盖最大重试窗口和审计需要的时间。
复盘卡片
- MQ 的至少一次投递是事实,不是异常。
- 业务 exactly once = 消费端幂等 + 本地事务。
- 幂等键必须来自业务,不要依赖投递次数。