消息重复
同一条消息为什么会到两次、什么时候会到、该怎么办 —— 因为「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标识的是一次投递尝试。它是消费侧的围栏令牌,按设计每次重投都会变。拿它当业务键,等于保证重复一定漏过去。
标准形态是带去重表的幂等消费:
- 收到消息。
- 查
business_key是否已处理过。 - 是 → ACK 返回(不产生副作用)。
- 否 → 施加副作用,记下
business_key,再 ACK。
business_key 要在 ACK 之前落库 —— 这样第 4 步两半之间崩溃,重投时仍能正确判定。
怎么检测
| 信号 | 说明什么 |
|---|---|
mortarmq_group_retries_total 上涨 | 正在重投 —— 下游要准备接重复 |
mortarmq_group_in_flight 一直贴着窗口上限 | 处理太慢 → 租约超时 → 接着就是重复 |
同一个 producer_seq 配上不同的 delivery_id | 这就是特征:同一条业务消息,两次投递尝试 |
第三行是判定性的。delivery_id 不同而 producer_seq 相同,不是缺陷报告 —— 是系统按规范在工作。
相关
- 台账为什么能扛崩溃:基础概念 §投递台账
- 生产者幂等字段:线协议 §2.1
- 处理器写法:最佳实践 §1.1 与 §2.1