重试机制与幂等性:如何避免消息重复

消息发送失败怎么办?重试。但重试可能带来重复。重复又怎么办?幂等性来解决。

这章我们就把这两个机制掰开揉碎,讲清楚它们怎么配合,又有什么坑。

retries 与 retry.backoff.ms:重试不是无限循环

先说说 retries。这个参数控制的是——当生产者发送消息失败时,最多重试几次。

核心要点:Kafka 生产者的重试,不是客户端自己在那傻等。它背后有一套完整的重试管理器,会按照你设定的次数和间隔,重新发送那些未确认的批次。

我个人习惯,生产环境至少设到 Integer.MAX_VALUE。为什么?因为很多失败是暂时的——比如 leader 选举、网络抖动。你重试几次可能就成功了。如果设成 3 或 5,一旦超过次数,消息就丢了。

但重试不能太快。这就引出了 retry.backoff.ms。

参数 默认值 生产环境建议 说明
retries 2147483647(Integer.MAX_VALUE) Integer.MAX_VALUE 配合幂等性使用,无限重试直到成功
retry.backoff.ms 100 200-500 两次重试之间的等待时间,避免雪崩

我曾经遇到过一个案例:某团队把 retry.backoff.ms 设成 10ms,结果 broker 负载一高,生产者疯狂重试,直接把集群打挂了。你想想看,1000 个生产者同时重试,间隔 10ms,那是什么概念?

避坑指南:重试间隔太短,等于没有间隔。建议至少 200ms。如果业务对延迟不敏感,500ms 更安全。

enable.idempotence=true 的原理

幂等性,说白了就是——同一个消息,不管发多少次,broker 只保存一份。

它的核心机制是 producer ID(PID) 和 序列号(sequence number)。

  • 每个生产者初始化时,broker 会分配一个唯一的 PID。
  • 每个分区发送的消息,都有一个从 0 开始递增的序列号。
  • broker 会缓存每个 PID+分区 最近 5 个序列号。
  • 如果收到重复的序列号,直接丢弃,不写入。

举个例子:你发了一条序列号为 100 的消息,broker 写入了。网络超时,你重试,又发了序列号 100 的消息。broker 一看——这个序列号我已经写过了,直接返回成功,不写磁盘。

提示:幂等性只保证单分区内的不重复。跨分区的事务性消息,需要用到事务 API,那是另一回事。

如何配置幂等性

配置其实很简单:

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

// 开启幂等性
props.put("enable.idempotence", true);

// 配合重试
props.put("retries", Integer.MAX_VALUE);
props.put("retry.backoff.ms", 200);

// 必须设置 acks=all
props.put("acks", "all");

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

注意,开启幂等性后,acks 会被强制设为 all。你手动设成其他值也没用,Kafka 会覆盖。

为什么?因为幂等性需要所有副本都确认,才能保证序列号的一致性。如果只等 leader 确认,leader 挂了,新 leader 可能没有完整的序列号缓存,幂等性就失效了。

消息重复的几种场景

开启幂等性就万事大吉了吗?不是的。我总结了几种常见的重复场景:

  1. 生产者重试导致的重复——幂等性可以解决。
  2. 消费者重平衡导致的重复——幂等性管不了,需要消费者做去重。
  3. 事务性消息的重复——需要事务 API + 幂等性配合。
  4. broker 故障恢复后的重复——如果 broker 的序列号缓存丢失,可能产生重复。

嗯,这里要注意:幂等性不是万能的。它解决的是「生产者重试导致的消息重复」,不是「消费者重复消费」。后者需要你在消费者端做幂等处理——比如用数据库的唯一索引、或者 Redis 的去重表。

重试 + 幂等性的完整流程

我把整个流程画了一张图,方便你理解:

重试 + 幂等性完整流程 生产者 发送消息(seq=100) Broker(检查序列号) 序列号是否已存在? 是 → 丢弃,返回成功 否 → 写入,返回成功 收到成功响应 超时/失败 → 重试(retry.backoff.ms) 说明:重试时携带相同的序列号,Broker 通过序列号去重,保证消息不重复

生产环境的最佳实践

最后,我总结了几条实战经验:

  • 永远开启幂等性——性能开销极小(约 1-2%),但收益巨大。没有理由不开。
  • 重试次数设到最大——配合幂等性,重试不会导致重复,所以放心重试。
  • 重试间隔别太短——200ms 起步,根据你的网络延迟和 broker 负载调整。
  • 消费者端也要做幂等——幂等性只保生产者到 broker 这一段,消费者到应用这一段需要你自己处理。
  • 监控重试次数——如果发现大量重试,说明集群或网络有问题,不要只靠重试硬扛。

小技巧:你可以通过 Kafka 的 metrics 接口监控 record-error-rate 和 retry-rate。如果 retry-rate 持续高于 1%,就该排查问题了。

重试和幂等性,就像一对搭档。重试保证消息不丢,幂等性保证消息不重复。两者配合,才能做到「至少一次」语义下的精确一次。