ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

kafka重复消费

kafka重复消费

目录

1、生产者重复发送消息

生产者幂等性

生产者事务消息

2、消费者重复消费消息


导致重复消费的情况

1、生产者重复发送消息

2、消费者重复消费消息

1、生产者重复发送消息

生产者发送了消息,没有收到broker正确的应答响应,导致生产者重试。

生产者发出一条消息,broker落盘后因为网络原因,发送端得到一个发送失败的响应或超时网络中断,然后生产者收到一个可恢复的Exception,重试发送消息导致重复。

重试过程

1、new KafkaProducer() 后创建一个后台线程 KafkaThread 负责发送消息到Kafka集群

2、调用 KafkaProducer.send() 发送消息,实际上只是把消息保存到 RecordAccumulator 中;

3、RecordAccumulator 某一个批次被填满或者等待时间超过了所设定的阈值的时候,此时KafkaThread 线程就开始拉取消息,发送消息到Kafka集群

4、如果发送成功,那么返回成功;

5、如果发送失败,那么判断是否允许重试。如果不允许重试,那么返回失败的结果;如果允许重试,把消息再保存到 RecordAccumulator 中,等待后台线程 KafkaThread 再次发送。

在Kafka中消息存在三种语义:

1、最多一次(At most once):消息可能会丢失,但绝不会被重复发送 ----> Producer禁止重试

2、至少一次(At least once):消息不会丢失,但有可能被重复发送 ----> Producer开启了重试机制,当发送的消息没有收到Kafka集群成功的应答时会再次发送数据

3、仅有一次(Exactly once):消息不会丢失,也不会被重复发送 ----> Kafka通过幂等性和事务这两个机制保证了精准一次

生产者幂等性

kafka为了实现幂等性,在 0.11.0 版本之后,它在底层设计架构中引入了ProducerID和SequenceNumber。

1、ProducerID:在每个新的 Producer 初始化时,会被分配一个唯一的 ProducerID,这个 ProducerID 对客户端使用者是不可见的。

2、SequenceNumber:对于每个 ProducerID,Producer发送数据的每个 Topic 和 Partition 都对应一个从 0 开始单调递增的 SequenceNumber 值。

异常情况如下图所示:

当 Producer 发送消息 (x2,y2) 给 Broker 时,Broker 接收到消息并将其追加到消息流中。此时,Broker 返回 Ack 信号给 Producer 时,发生异常导致

Producer 接收 Ack 信号失败。对于 Producer 来说,会触发重试机制,将消息 (x2,y2) 再次发送,

但是,由于引入了幂等性,在每条消息中附带 PID(ProducerID) 和 equenceNumber。相同的 PID 和 SequenceNumber 发送给 Broker,

而之前 Broker 缓存过之前发送的相同的消息,那么在消息流中的消息就只有一条 (x2,y2),不会出现重复发送的情况。

注意:幂等生产者只能保证单个主题的单一分区内消息不重复;其次,它只能实现单会话上的幂等性,不能实现跨会话的幂等性,这里的会话即可以理解为:Producer 进程的一次运行。当重启了 Producer 进程之后,则幂等性保证就失效了。

如果我想实现多分区以及多会话上的消息无重复,应该怎么做呢?答案就是事务!

生产者事务消息

Kafka的事务特性本质上是支持了Kafka跨分区和Topic的原子写操作。通过事务机制,KAFKA 可以实现对多个 topic 的多个 partition 的原子性的写入,即处于同一个事务内的所有消息,不管最终需要落地到哪个 topic 的哪个 partition, 最终结果都是要么全部写成功,要么全部写失败(Atomic multi-partition writes)

Kafka事务的回滚,并不是删除已写入的数据,而是将写入数据的事务标记为 Rollback/Abort 从而在读数据时过滤该数据。

// @Transactional // 方式一需要添加该注解,但是添加该注解事务会自动回滚 @Test public void sendMsgTransaction() throws ExecutionException, InterruptedException { // 方式一 /*kafkaTemplate.send("topic-02" , 0 , null , "msg11....") ; int a = 1 / 0 ; kafkaTemplate.send("topic-02" , 1 , null , "msg12....") ;*/ // 方式二 kafkaTemplate.executeInTransaction(operations -> { operations.send("topic-02" , 0 , null , "msg09....") ; int a = 1 / 0 ; operations.send("topic-02" , 1 , null , "msg10....") ; return true ; }) ; }
2、消费者重复消费消息

# 解决方案

1、消费端手动移位提交

2、消费端做幂等处理

# 幂等操作

1、新增时,先查一下,如果有了就不操作了

2、更新,需要set为某个值,不需要处理,天然幂等

3、生产者每次传递一个唯一id,消费消息时,先去redis中查一下是否已经消费,如果没有,就消费消息,然后把id存入到redis中;如果存在,就不用处理了

4、基于数据库的唯一索引, 新增的数据在数据库中存在重复的直接报错,不会出现脏数据

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进