一、消息传输保障概念

消息中间件的消息传输保障有以下三个层级


( 1 ) at most once :至多 一次。消息可能会丢失,但绝对不会重复传输。


( 2) at least once 最少一次。消息绝不会丢失,但可能会重复传输。


(3) exactly once :恰好 一次。每条消息肯定会被传输一次且仅传输一次。


 


 



kafka中保障级别

生产者



kafka生产者  提供的消息传输保障为 at least once, 当生产者向 Kafka 发送消息时,一旦消息被成功提 交到日志文件,由于多副本机制的存在,这条消息就不会丢失。如果生产者发送消息到 Kafka 之后,遇到了网络问题而造成通信中断,那么生产者就无法判断该消息是否己经提交。虽然 Kafka 无法确定网络故障期间发生了什么,但生产者可以进行 多次重试来确保消息 已经写入 Kafka, 这个重试的过程中有可能会造成消息的重复写入



 

消费者

对消费者而言,消费者处理消息和提交消费位移的顺序在很大程度上决定了消费者提供哪种消息传输保障 如果消费者在拉取完消息之后 ,应用逻辑先处理消息后提交消费位移,那么在消息处理之后且在位移提交之前消费者看机了,待它重新上线之后,会从上一次位移提交的位置拉取,这样就出现了重复消费,因为有部分消息已经处理过了只是还没来得及提交消费 位移,此时就对应 at least once 。如果消费者在拉完消息之后,应用逻辑先提交消费位移后进行消息处理,那么在位移提交之后且在消息处理完成之前消费者岩了,待它重新上线之后,会从己提交的位移处开始重新消费,但之前尚有部分消息未进行消费 ,如此就会发生消息丢失, 此时就对应 at most once

Kafka 0.11.0.0 版本开始引 入了幂等和事 务这两个特性,以此来实现 exactly once



 



二、幕等性

1.简介

Producer 的幂等性指的是当发送同一条消息时,数据在 Server 端只会被持久化一次,数据不丟不重,但是这里的幂等性是有条件的:

  • 只能保证 Producer 在单个会话内不丟不重,如果 Producer 出现意外挂掉再重启是无法保证的(幂等性情况下,是无法获取之前的状态信息,因此是无法做到跨会话级别的不丢不重);
  • 幂等性不能跨多个 Topic-Partition,只能保证单个 partition 内的幂等性,当涉及多个 Topic-Partition 时,这中间的状态并没有同步

开启方式:



properties . put(ProducerConfig .E NABLE_IDEMPOTENCE CONFIG, true);



 



 



2.幂等性实现方式

PID 和 Sequence Number

为了实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。

PID。每个新的Producer在初始化的时候会被分配一个唯一的PID,这个PID对用户是不可见的。

Sequence Numbler。(对于每个PID,该Producer发送数据的每个都对应一个从0开始单调递增的Sequence Number。

Broker端在缓存中保存了这seq number,对于接收的每条消息,如果其序号比Broker缓存中序号大于1则接受它,否则将其丢弃。这样就可以实现了消息重复提交了。

三、事务



幂等性并不能跨多 个分区运作,而 事务 可以弥补这个缺陷 。事务可 以保证对多 个分区写 入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功、部分失败的可能。



kafka是消费和生产 并存: 应用程序从某个主题中消费消息 然后经过一 系列转换后写入 另一个主题 ,消费者可能在提 交消费 位移的过程中出现问题而导致 重复消 费, 也有 可能生产者重复生产消息 Kafka 中的 事务可以使应用程序将消费消息、生产消息提交消费位移当作原子操作来处理,同时成功或失败,即使该生产或消费会跨多个分区



 



1.开启方式:



事务要求生产者开启幕等特性,因 通过将 transactional id 参数设置为非空从而开 启事务特性的同时 需要将 e nab le.id empot ence 设置为 true 如果未显式设置 KafkaProducer,则 默认会将它 的值设 true



transactionalld PID 一一对应 ,两 者之 间所 不同的是 transactionalld 由用户显式 设置 PID 是由 Kafk 部分配的 。另 外,为了保证新的 生产者启动后具有相同 transactionalld 的旧生 产者能够立即失效



 

2.事务的执行流程

(1)在 Producer 的配置中配置 transactional.id

(2)通过 initTransactions() 初始化事务状态信息

(3)通过 beginTransaction() 标识一个事务的开始

(4)通过 commitTransaction() 或 abortTransaction() 对事务进行 commit 或终止

Properties props = new Properties();
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("client.id", "ProducerTranscationnalExample");
props.put("bootstrap.servers", "localhost:9092");
//配置transactional.id
props.put("transactional.id", "test-transactional");
props.put("acks", "all");
KafkaProducer producer = new KafkaProducer(props);
//初始化事务
producer.initTransactions();

try {
    String msg = "matt test";
    //开启事务
    producer.beginTransaction();
    producer.send(new ProducerRecord(topic, "0", msg.toString()));
    producer.send(new ProducerRecord(topic, "1", msg.toString()));
    producer.send(new ProducerRecord(topic, "2", msg.toString()));
    producer.commitTransaction();
} catch (ProducerFencedException e1) {
    e1.printStackTrace();
    producer.close();
} catch (KafkaException e2) {
    e2.printStackTrace();
    //终止事务
    producer.abortTransaction();
}
producer.close();

3.消费者事务存在问题:



而从消费者的角度分析, 事务能保证的语义相对偏弱。出于以下原因, Kafka 并不能保证 己提交的事务中的所有消息都能够被消费



(1)对采用日志压缩策略的主题而言,事务中的某些消息有可能被清理(相同key 的消息, 后写入的消息会覆盖前面写入的消息)。



(2)事务中消息可能分布在同一个分区的多个日志分段( LogSegment )中,当老的日志分 段被删除时,对应的消息可能会丢失。



(3)消费者可以通过 seekO 方法访问任意 offset 的消息,从而可能遗漏事务中的部分消息。



(4)消费者在消费时可能没有分配到事务内的所有分区,如 此它也 就不能读取事务中的所有消息。



 



4.kafka事务性要解决的问题

  1. 在写多个 Topic-Partition 时,执行的一批写入操作,有可能出现部分 Topic-Partition 写入成功,部分写入失败(比如达到重试次数),这相当于出现了中间的状态,这并不是我们期望的结果;如果启用事务性的话,涉及到多个 Topic-Partition 的写入时,这个事务操作要么会全部成功,要么会全部失败,不会出现上面的情况(部分成功、部分失败),如果有 Topic-Partition 无法写入,那么当前这个事务操作会直接 abort;
  2. Producer 应用中间挂之后再恢复,无法做到 Exactly-Once 语义保证,其实应用做到端到端的 Exactly-Once,仅仅靠 Kafka 是无法做到的,还需要应用本身做相应的容错设计,以 Flink 为例,其容错设计就是 checkpoint 机制,作业保证在每次 checkpoint 成功时,它之前的处理都是 Exactly-Once 的,如果中间出现了故障,恢复之后,只需要接着上次 checkpoint 的记录做恢复即可,对于失败前那个未完成的事务执行回滚操作(abort)就可以了,这样的话就是实现了 Flink + Kafka 端到端的 Exactly-Once