4、幂等性Producer原理:enable.idempotence配置、Producer ID与序列号机制

4.1 为什么需要幂等性?

做金融系统的朋友应该都懂,消息重复是噩梦。

我记得刚接手一个交易系统时,遇到过一个诡异的问题:明明下游只消费了一次,数据库里却插入了两条重复记录。排查了半天,发现是Producer重试导致的——网络抖动了一下,消息实际已经写进去了,但Producer没收到ack,于是又发了一遍。

嗯,这就是典型的「至少一次」语义带来的副作用。

在金融场景里,重复下单、重复扣款、重复转账,哪个都受不了。所以Kafka引入了幂等性Producer,说白了就是:无论重试多少次,消息在Broker端只会被持久化一次

4.2 enable.idempotence配置

开启幂等性,只需要一行配置:

props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

就这么简单?对,但背后的事情不简单。

当你把这个参数设为true时,Kafka会自动帮你做几件事:

  • acks强制设为all(你改不了,改了会报错)
  • retries设为Integer.MAX_VALUE(无限重试)
  • max.in.flight.requests.per.connection设为5(之前是1,现在放宽了)

我个人的习惯是,只要涉及金融交易类数据,一律开启幂等性。性能开销其实很小,但带来的数据一致性保障是巨大的。

注意: 如果你手动设置了 acks=1acks=0,同时又开启了幂等性,Kafka会直接抛异常。别问我怎么知道的——我曾经在配置中心改参数时踩过这个坑。

4.3 Producer ID(PID)机制

开启幂等性后,每个Producer在初始化时,Broker会分配一个全局唯一的Producer ID(简称PID)。

这个PID长什么样?其实就是一个long类型的数字,比如10001。它存储在Broker的内存里,Producer挂了重启后会重新申请一个新的PID。

你想想看,有了这个PID,Broker就能区分「这是同一个Producer发的消息」还是「另一个Producer发的消息」了。

我画了一张图,帮你理解PID在整个流程中的位置:

幂等性Producer核心流程 Producer PID: 10001 序列号: 0,1,2,... 消息批次 PID: 10001 Seq: 5 Broker 分区: 0 已接收最大Seq: 4 新Seq: 5 → 接受 去重判断逻辑 Seq > 已接收最大Seq → 写入 每个Producer-Partition组合维护一个独立的序列号空间

4.4 序列号机制

光有PID还不够。同一个Producer可能发多条消息到同一个分区,怎么保证这些消息不重复?

答案是:序列号(Sequence Number)

每个Producer在向某个分区发送消息时,会为每条消息(或每个批次)分配一个从0开始递增的序列号。Broker端会为每个组合维护一个已接收的最大序列号

判断逻辑很简单:

  • 如果新消息的序列号 = 已接收最大序列号 + 1,正常写入
  • 如果新消息的序列号 <= 已接收最大序列号,说明是重复消息,直接丢弃
  • 如果新消息的序列号 > 已接收最大序列号 + 1,说明中间有消息丢失,Broker会返回OutOfOrderSequenceException

说白了,Broker就是靠这个递增的序列号来做去重的。你重试一万次,序列号不变,Broker一看「哦,这个我已经收过了」,直接忽略。

核心要点: 幂等性去重是基于 <PID, 分区, 序列号> 三元组的。同一个Producer发往同一个分区的消息,序列号必须严格递增且不重复。

4.5 我在项目中遇到的坑

讲几个实战中容易踩的点:

坑一:Producer重启后PID变了

Producer挂了重启,会重新申请PID。新的PID意味着Broker端之前记录的序列号信息全部作废。所以幂等性只能保证单会话内的精确一次,跨会话是不行的。

我曾经在日志里看到过重复数据,排查了半天才发现是Producer重启导致的。解决方案?要么用事务性Producer,要么下游自己做幂等。

坑二:max.in.flight.requests.per.connection 设为5

开启幂等性后,这个参数默认是5。什么意思?就是允许5个未确认的请求同时在途。之前没开启幂等性时,为了保证顺序,很多人会把这个值设为1。现在有了序列号机制,Broker可以检测乱序,所以可以放宽到5,吞吐量能提升不少。

坑三:不要和事务混为一谈

幂等性Producer只解决单分区内的消息重复问题。如果你需要跨分区、跨Topic的原子性写入,那得用Kafka事务。我见过有人开了幂等性就觉得万事大吉了,结果跨分区消费时还是出现了数据不一致。

4.6 性能影响有多大?

很多人担心开启幂等性会影响性能。我直接说结论:影响微乎其微

对比项 未开启幂等性 开启幂等性
额外内存开销 每个Producer维护序列号状态,约几十KB
网络开销 每个批次多带8字节PID + 4字节序列号
Broker端开销 内存中维护的序列号状态
吞吐量影响 基准 几乎无影响(实测<1%)
我的建议: 只要你的业务对数据一致性有要求,直接开启幂等性。别纠结那点性能损耗。金融系统里,一条重复数据的代价远大于这点CPU和内存开销。

4.7 代码示例

最后给个完整的配置示例,你们可以直接拿去用:

Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, 
          "org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, 
          "org.apache.kafka.common.serialization.StringSerializer");

// 开启幂等性 —— 就这一行
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

// 下面这些参数会被自动覆盖,但显式写出来更清晰
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);

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

// 正常发送即可,幂等性在底层自动生效
producer.send(new ProducerRecord<>("my-topic", "key", "value"));

嗯,代码就这么简单。但背后的原理,我希望你能吃透。毕竟在金融系统里,每一笔数据都可能是真金白银,容不得半点马虎。


专注资料整理