当前位置:   article > 正文

一碰就头疼的 Kafka 消息重复问题,立马解决_kafka消息怎么能保证不重复发送

kafka消息怎么能保证不重复发送

一、前言

数据重复这个问题其实也是挺正常,全链路都有可能会导致数据重复。

通常,消息消费时候都会设置一定重试次数来避免网络波动造成的影响,同时带来副作用是可能出现消息重复。

整理下消息重复的几个场景:

1.「生产端:」 遇到异常,基本解决措施都是 「重试」

  • 场景一:leader分区不可用了,抛 LeaderNotAvailableException 异常,等待选出新 leader 分区。

  • 场景二:Controller 所在 Broker 挂了,抛 NotControllerException 异常,等待 Controller 重新选举。

  • 场景三:网络异常、断网、网络分区、丢包等,抛 NetworkException 异常,等待网络恢复。

2.「消费端:」 poll 一批数据,处理完毕还没提交 offset ,机子宕机重启了,又会 poll 上批数据,再度消费就造成了消息重复。

怎么解决?

「先来了解下消息的三种投递语义:」

  • 最多一次(at most once): 消息只发一次,消息可能会丢失,但绝不会被重复发送。例如:mqtt 中 QoS = 0

  • 至少一次(at least once): 消息至少发一次,消息不会丢失,但有可能被重复发送。例如:mqtt 中 QoS = 1

  • 精确一次(exactly once): 消息精确发一次,消息不会丢失,也不会被重复发送。例如:mqtt 中 QoS = 2

了解了这三种语义,再来看如何解决消息重复,即如何实现精准一次,可分为三种方法:
  1. Kafka 幂等性 Producer: 保证生产端发送消息幂等。局限性,是只能保证单分区且单会话(重启后就算新会话)

  2. Kafka 事务: 保证生产端发送消息幂等。解决幂等 Producer 的局限性。

  3. 消费端幂等: 保证消费端接收消息幂等。蔸底方案。

1)Kafka 幂等性 Producer

「幂等性指」:无论执行多少次同样的运算,结果都是相同的。即一条命令,任意多次执行所产生的影响均与一次执行的影响相同。

「幂等性使用示例:在生产端添加对应配置即可」

  1. Properties props = new Properties();  
  2. props.put("enable.idempotence", ture); // 1. 设置幂等  
  3. props.put("acks""all"); // 2. 当 enable.idempotence 为 true,这里默认为 all  
  4. props.put("max.in.flight.requests.per.connection"5); // 3. 注意

1.设置幂等,启动幂等。

2.配置 acks,注意:一定要设置 acks=all,否则会抛异常。

3.配置max.in.flight.requests.per.connection 需要 <= 5,否则会抛异常 OutOfOrderSequenceException

  • 0.11 >= Kafka < 1.1max.in.flight.request.per.connection = 1

  • Kafka >= 1.1max.in.flight.request.per.connection <= 5

为了更好理解,需要了解下Kafka 幂等机制:

1.Producer 每次启动后,会向 Broker 申请一个全局唯一的 pid。(重启后 pid 会变化,这也是弊端之一)

2.Sequence Numbe:针对每个 <Topic, Partition> 都对应一个从0开始单调递增的 Sequence,同时 Broker端会缓存这个 seq num

3.判断是否重复: 拿 <pid, seq num> 去 Broker 里对应的队列 ProducerStateEntry.Queue(默认队列长度为 5)查询是否存在

  • 如果 nextSeq == lastSeq + 1,即 服务端seq + 1 == 生产传入seq,则接收。

  • 如果 nextSeq == 0 && lastSeq == Int.MaxValue,即刚初始化,也接收。

  • 反之,要么重复,要么丢消息,均拒绝。

这种设计针对解决了两个问题:
  1. 「消息重复:」 场景 Broker 保存消息后还没发送 ack 就宕机了,这时候 Producer 就会重试,这就造成消息重复。

  2. 「消息乱序:」 避免场景,前一条消息发送失败而其后一条发送成功,前一条消息重试后成功,造成的消息乱序。

那什么时候该使用幂等:
  1. 如果已经使用 acks=all,使用幂等也可以。

  2. 如果已经使用 acks=0 或者 acks=1,说明你的系统追求高性能,对数据一致性要求不高。不要使用幂等。

2)Kafka 事务

使用 Kafka 事务解决幂等的弊端:单会话且单分区幂等。

Tips:」 这块篇幅较长,这先稍微提及下使用,之后另起一篇。

「事务使用示例:分为生产端 和 消费端」

  1. Properties props = new Properties();  
  2. props.put("enable.idempotence", ture); // 1. 设置幂等  
  3. props.put("acks""all"); // 2. 当 enable.idempotence 为 true,这里默认为 all  
  4. props.put("max.in.flight.requests.per.connection"5); // 3. 最大等待数  
  5. props.put("transactional.id""my-transactional-id"); // 4. 设定事务 id  
  6.   
  7. Producer<StringString> producer = new KafkaProducer<StringString>(props);  
  8.   
  9. // 初始化事务  
  10. producer.initTransactions();  
  11.   
  12. try{  
  13.     // 开始事务  
  14.     producer.beginTransaction();  
  15.   
  16.     // 发送数据  
  17.     producer.send(new ProducerRecord<StringString>("Topic""Key""Value"));  
  18.    
  19.     // 数据发送及 Offset 发送均成功的情况下,提交事务  
  20.     producer.commitTransaction();  
  21. } catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {  
  22.     // 数据发送或者 Offset 发送出现异常时,终止事务  
  23.     producer.abortTransaction();  
  24. finally {  
  25.     // 关闭 Producer 和 Consumer  
  26.     producer.close();  
  27.     consumer.close();  
  28. }

这里消费端Consumer 需要设置下配置:isolation.level 参数

  • read_uncommitted:」 这是默认值,表明 Consumer 能够读取到 Kafka 写入的任何消息,不论事务型 Producer 提交事务还是终止事务,其写入的消息都可以读取。如果你用了事务型 Producer,那么对应的 Consumer 就不要使用这个值。

  • read_committed:」 表明 Consumer 只会读取事务型 Producer 成功提交事务写入的消息。当然了,它也能看到非事务型 Producer 写入的所有消息。

3)消费端幂等

“如何解决消息重复?” 这个问题,其实换一种说法:就是如何解决消费端幂等性问题。

只要消费端具备了幂等性,那么重复消费消息的问题也就解决了。

「典型的方案是使用:消息表,来去重:」

  • 上述栗子中,消费端拉取到一条消息后,开启事务,将消息Id 新增到本地消息表中,同时更新订单信息。

  • 如果消息重复,则新增操作 insert 会异常,同时触发事务回滚。

二、案例:Kafka 幂等性 Producer 使用

环境搭建可参考:https://developer.confluent.io/tutorials/message-ordering/kafka.html#view-all-records-in-the-topic

「准备工作如下:」

1、Zookeeper:本地使用 Docker 启动

  1. $ docker run -d --name zookeeper -p 2181:2181 zookeeper  
  2. a86dff3689b68f6af7eb3da5a21c2dba06e9623f3c961154a8bbbe3e9991dea4

2、Kafka:版本 2.7.1,源码编译启动(看上文源码搭建启动)

3、启动生产者:Kafka 源码中 exmaple 中

4、启动消息者:可以用 Kafka 提供的脚本

  1. # 举个栗子:topic 需要自己去修改  
  2. $ cd ./kafka-2.7.1-src/bin  
  3. $ ./kafka-console-producer.sh --broker-list localhost:9092 --topic test_topic

创建topic :1副本,2 分区

  1. $ ./kafka-topics.sh --bootstrap-server localhost:9092 --topic myTopic --create --replication-factor 1 --partitions 2  
  2.   
  3. # 查看  
  4. $ ./kafka-topics.sh --bootstrap-server broker:9092 --topic myTopic --describe

「生产者代码:」

  1. public class KafkaProducerApplication {  
  2.   
  3.     private final Producer<StringString> producer;  
  4.     final String outTopic;  
  5.   
  6.     public KafkaProducerApplication(final Producer<StringString> producer,  
  7.                                     final String topic) {  
  8.         this.producer = producer;  
  9.         outTopic = topic;  
  10.     }  
  11.   
  12.     public void produce(final String message) {  
  13.         final String[] parts = message.split("-");  
  14.         final String keyvalue;  
  15.         if (parts.length > 1) {  
  16.             key = parts[0];  
  17.             value = parts[1];  
  18.         } else {  
  19.             key = null;  
  20.             value = parts[0];  
  21.         }  
  22.         final ProducerRecord<StringString> producerRecord  
  23.             = new ProducerRecord<>(outTopic, keyvalue);  
  24.         producer.send(producerRecord,  
  25.                 (recordMetadata, e) -> {  
  26.                     if(e != null) {  
  27.                         e.printStackTrace();  
  28.                     } else {  
  29.                         System.out.println("key/value " + key + "/" + value + "\twritten to topic[partition] " + recordMetadata.topic() + "[" + recordMetadata.partition() + "] at offset " + recordMetadata.offset());  
  30.                     }  
  31.                 }  
  32.         );  
  33.     }  
  34.   
  35.     public void shutdown() {  
  36.         producer.close();  
  37.     }  
  38.   
  39.     public static void main(String[] args) {  
  40.   
  41.         final Properties props = new Properties();  
  42.   
  43.         props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");  
  44.         props.put(ProducerConfig.ACKS_CONFIG, "all");  
  45.   
  46.         props.put(ProducerConfig.CLIENT_ID_CONFIG, "myApp");  
  47.         props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);  
  48.         props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);  
  49.   
  50.         final String topic = "myTopic";  
  51.         final Producer<StringString> producer = new KafkaProducer<>(props);  
  52.         final KafkaProducerApplication producerApp = new KafkaProducerApplication(producer, topic);  
  53.   
  54.         String filePath = "/home/donald/Documents/Code/Source/kafka-2.7.1-src/examples/src/main/java/kafka/examples/input.txt";  
  55.         try {  
  56.             List<String> linesToProduce = Files.readAllLines(Paths.get(filePath));  
  57.             linesToProduce.stream().filter(l -> !l.trim().isEmpty())  
  58.                     .forEach(producerApp::produce);  
  59.             System.out.println("Offsets and timestamps committed in batch from " + filePath);  
  60.         } catch (IOException e) {  
  61.             System.err.printf("Error reading file %s due to %s %n", filePath, e);  
  62.         } finally {  
  63.             producerApp.shutdown();  
  64.         }  
  65.     }  
  66. }

「启动生产者后,控制台输出如下:」

「启动消费者:」

$ ./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic myTopic

修改配置 acks

启用幂等的情况下,调整acks 配置,生产者启动后结果是怎样的:

  • 修改配置 acks = 1

  • 修改配置 acks = 0

会直接报错:

  1. Exception in thread "main" org.apache.kafka.common.config.ConfigException: Must set acks to all in order to use the idempotent producer.  
  2. Otherwise we cannot guarantee idempotence.

修改配置 max.in.flight.requests.per.connection

「启用幂等的情况下,调整此配置,结果是怎样的:」

将  max.in.flight.requests.per.connection > 5 会怎样?

「当然会报错:」

Caused by: org.apache.kafka.common.config.ConfigException: Must set max.in.flight.requests.per.connection to at most 5 to use the idempotent producer.

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/738081
推荐阅读
相关标签
  

闽ICP备14008679号