Skip to content

消息去重 ​

消息去重是幂等在消息场景的落地。队列保证的是消息不丢,不是消息不重复,去重的责任压在消费端自己身上。

1. 重复是怎么产生的 ​

消息场景下的重复,比普通 HTTP 请求更隐蔽,因为它发生在「确认」这个环节的两侧。

第一种,Publisher Confirm 超时。生产者发完消息等确认,超时了以为发送失败,于是重发,但 Broker 其实已经收到了。两条一模一样的内容都进了队列。

第二种,消费者处理完但 ACK 前崩溃。消息已经投递出去,但消费者没来得及确认就挂了,Broker 把消息重新入队,另一个消费者再来一遍。

第三种,ACK 丢失。网络抖动让确认没送达,Broker 误以为消费者没收到,重新投递。

三条的共性是,发端和收端对「是否成功」的判断不一致,重复就从这个缝隙里漏出来。队列的投递语义是 at-least-once,它保证不丢,代价就是容忍重复。要「恰好一次」,只能在业务层补上去重。

2. 去重的前提是消息带指纹 ​

去重的前提是能认出「这是同一条消息」。队列不会替业务判断重复,业务得自己给消息打上指纹。

这个指纹就是 messageId。生产端给每条消息带一个全局唯一 ID,消费端拿它判断是否处理过。指纹的生成责任在生产端。消费端只能被动地根据已有指纹去重,它无法凭空判断两条内容相同的消息是不是同一条,因为业务上完全可能存在两条内容相同、但确实是两次独立操作的消息。

3. 去重手段就是幂等那套 ​

消息场景下的去重手段,就是幂等性设计里的那套。Redis 的 setIfAbsent、数据库唯一约束、乐观锁版本号,通用原理和选型见 幂等性设计,这里不再重复。

这里只补一个消息场景特有的取舍。Redis 去重和数据库去重不是替代关系,是不同场景的分工。Redis 适合高并发、能容忍极端情况下丢一点去重记录的场合,数据库唯一约束适合强一致、绝不允许重复落库的场合。判断的依据是这笔重复的代价有多大,重复扣款不能容忍,就得走数据库约束,重复发一条欢迎短信丢了也就丢了,走 Redis 足够。

4. 去重窗口怎么定 ​

Redis 去重记录不能永久保留,要设 TTL,这个 TTL 就是去重窗口。

窗口设短了,重试消息在窗口过期后才到,被当成新消息处理,去重失效。窗口设长了,Redis 里堆积大量去重记录,浪费内存。这是一对矛盾,破解的办法是找到窗口的安全下限。

下限由三个时间决定。最大消息 TTL,即一条消息从发送到被消费的最长时间,延迟队列的延迟时长属于这类。最大重试时间,消息处理失败后重试的最长间隔。最大处理时间,单条消息处理的最长时间。三者之和再加一点缓冲,就是去重窗口的下限。

判断口诀是,去重窗口宁可长不可短。多占一点内存的代价,远小于重复处理一次业务。

5. 最佳实践 ​

  • 生产端给每条消息带唯一 messageId
  • 消费端做幂等,不依赖 Broker 的「恰好一次」
  • 去重窗口覆盖最大消息 TTL 加最大重试时间
  • 先处理业务再 ack,处理失败不确认

6. 相关章节 ​

去重的通用手段与选型,见 幂等性设计。消息投递的可靠性(Confirm、ACK)见 消息系统高可用实战。重复投递引起的顺序问题,见 消息顺序性。

Released under the MIT License.