Skip to content

消息重复 ​

同一条消息为什么会到两次、什么时候会到、该怎么办 —— 因为「at-least-once」只有在你知道第二份什么时候出现时才有用。

契约的准确说法 ​

MortarMQ 保证 at-least-once。意思是:被接受的消息(已返回 PUB_OK)至少会被投递一次 —— 也可能更多次。消费者组级别的 exactly-once 不提供。

这个保证是不对称的,而这个不对称正是整个设计:

  • 生产者侧有去重。 重发是安全的。
  • 消费者侧没有。 处理两次是预期之内的;你的处理函数必须能容忍。

什么时候会出现重复 ​

触发发生了什么是 bug 吗
ACK 到达前租约超时delivery_lease_ms(30 s)到期,即使消费者已经处理过,消息仍被重投否 —— broker 分不清「慢」和「崩了」
消费者处理完、ACK 还没持久化就崩了台账里有 DELIVER 没有 ACK;重启后定时器重建,消息重投否 —— 这正是台账与记录分离的原因
消费者处理并 ACK,但连接断了ACK 可能没到 broker,随后重投否
broker kill -9 在途从台账重放恢复定时器,未完成的投递继续否 —— 这就是仿真注入的场景

规律是:每一处重复都发生在 broker 分不清「已处理」与「还没处理」的地方。 它不去猜,而是重投。

该怎么办 ​

生产者:让发布幂等 ​

同时设 producer_id 和 producer_seq。broker 按 (topic_id, partition, producer_id) 窗口去重,超时后的重试不会写重。

text
producer_id=0 && producer_seq=0    → 允许,计入非幂等
producer_id=0 && producer_seq>0    → 拒绝,0x0013
其他                                → 按窗口去重

消费者:用幂等处理器 ​

副作用的幂等键用你自己的业务标识 —— 订单号、支付流水号 —— 绝不用 delivery_id。

delivery_id 标识的是一次投递尝试。它是消费侧的围栏令牌,按设计每次重投都会变。拿它当业务键,等于保证重复一定漏过去。

标准形态是带去重表的幂等消费:

  1. 收到消息。
  2. 查 business_key 是否已处理过。
  3. 是 → ACK 返回(不产生副作用)。
  4. 否 → 施加副作用,记下 business_key,再 ACK。

business_key 要在 ACK 之前落库 —— 这样第 4 步两半之间崩溃,重投时仍能正确判定。

怎么检测 ​

信号说明什么
mortarmq_group_retries_total 上涨正在重投 —— 下游要准备接重复
mortarmq_group_in_flight 一直贴着窗口上限处理太慢 → 租约超时 → 接着就是重复
同一个 producer_seq 配上不同的 delivery_id这就是特征:同一条业务消息,两次投递尝试

第三行是判定性的。delivery_id 不同而 producer_seq 相同,不是缺陷报告 —— 是系统按规范在工作。

相关 ​