幂等性 Producer 只能保证单分区、单会话上的消息幂等性;而事务能够保证跨分区、跨会话间的幂等性。

 

所谓的消息交付可靠性保障,是指 Kafka 对 Producer 和 Consumer 要处理的消息提供什么样的承诺。

 

常见的承诺有以下三种:

最多一次(at most once):消息可能会丢失,但绝不会被重复发送。

至少一次(at least once):消息不会丢失,但有可能被重复发送。  [kafka默认选项]

精确一次(exactly once):消息不会丢失,也不会被重复发送。

 

只有 Broker 成功“提交”消息且 Producer 接到 Broker 的应答才会认为该消息成功发送。不过倘若消息成功“提交”,但 Broker 的应答没有成功发送回 Producer 端(比如网络出现瞬时抖动),那么 Producer 就无法确定消息是否真的提交成功了。因此,它只能选择重试,也就是再次发送相同的消息。这就是 Kafka 默认提供至少一次可靠性保障的原因,不过这会导致消息重复发送。

 

Kafka 也可以提供最多一次交付保障,只需要让 Producer 禁止重试即可。这样一来,消息要么写入成功,要么写入失败,但绝不会重复发送。我们通常不会希望出现消息丢失的情况,但一些场景里偶发的消息丢失其实是被允许的,相反,消息重复是绝对要避免的。此时,使用最多一次交付保障就是最恰当的。

 

Kafka 做到精确一次: 幂等性(Idempotence)和事务(Transaction)

 

幂等性(Idempotence)定义

“幂等”这个词原是数学领域中的概念,指的是某些操作或函数能够被执行多次,但每次得到的结果都是不变的。

 

在计算机领域中,幂等性的含义稍微有一些不同:

在命令式编程语言(比如 C)中,若一个子程序是幂等的,那它必然不能修改系统状态。这样不管运行这个子程序多少次,与该子程序关联的那部分系统状态保持不变。

在函数式编程语言(比如 Scala 或 Haskell)中,很多纯函数(pure function)天然就是幂等的,它们不执行任何的 side effect。

幂等性有很多好处,其最大的优势在于我们可以安全地重试任何幂等性操作,反正它们也不会破坏我们的系统状态。

 

幂等性 Producer

在 Kafka 中,Producer 默认不是幂等性的,但我们可以创建幂等性 Producer.
指定 Producer 幂等性的方法很简单,仅需要设置一个参数即可,

props.put(“enable.idempotence”, ture),或

props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true)。

enable.idempotence 被设置成 true 后,Producer 自动升级成幂等性 Producer,其他所有的代码逻辑都不需要改变。

Kafka 自动帮你做消息的重复去重。

底层具体的原理很简单,就是经典的用空间去换时间的优化思路,即在 Broker 端多保存一些字段。当 Producer 发送了具有相同字段值的消息后,Broker 能够自动知晓这些消息已经重复了,于是可以在后台默默地把它们“丢弃”掉。当然,实际的实现原理并没有这么简单,但你大致可以这么理解。

首先,它只能保证单分区上的幂等性,即一个幂等性 Producer 能够保证某个主题的一个分区上不出现重复消息,它无法实现多个分区的幂等性。其次,它只能实现单会话上的幂等性,不能实现跨会话的幂等性。这里的会话,你可以理解为 Producer 进程的一次运行。当你重启了 Producer 进程之后,这种幂等性保证就丧失了。

 

实现多分区以及多会话上的消息无重复,应该怎么做呢?

答案就是事务(transaction)或者依赖事务型 Producer。

 

事务定义

事务提供的安全性保障是经典的 ACID,

原子性(Atomicity)、一致性 (Consistency)隔离性 (Isolation) 持久性 (Durability)

 

已提交读(read committed)隔离级别

所谓的 read committed,指的是当读取数据库时,只能看到已提交的数据,即无脏读。

同时,当写入数据库时,只能覆盖掉已提交的数据,即无脏写。

 

目前kafka是已提交读(read committed)隔离级别,

能保证多条消息原子性地写入到目标分区,同时也能保证 Consumer 只能看到事务成功提交的消息。

事务型 Producer

事务型 Producer 能够保证将消息原子性地写入到多个分区中。这批消息要么全部写入成功,要么全部失败。另外,事务型 Producer 也不惧进程的重启。Producer 重启回来后,Kafka 依然保证它们发送消息的精确一次处理。

 

设置事务型 Producer 的方法也很简单,满足两个要求即可:

和幂等性 Producer 一样,开启 enable.idempotence = true。

设置 Producer 端参数 transactional. id,最好为其设置一个有意义的名字。
 

代码样例

producer.initTransactions();
try {
            producer.beginTransaction();
            producer.send(record1);
            producer.send(record2);
            producer.commitTransaction();
} catch (KafkaException e) {
            producer.abortTransaction();
}

 

和普通 Producer 代码相比,事务型 Producer 的显著特点是调用了一些事务 API:

initTransaction : 事务初始化

beginTransaction : 事务开始

commitTransaction : 事务提交

abortTransaction : 事务终止

这段代码能够保证 Record1 和 Record2 被当作一个事务统一提交到 Kafka,要么它们全部提交成功,要么全部写入失败。

实际上即使写入失败,Kafka 也会把它们写入到底层的日志中,也就是说 Consumer 还是会看到这些消息。

因此在 Consumer 端,读取事务型 Producer 发送的消息也是需要一些变更的。

修改起来也很简单,设置 isolation.level 参数的值即可。

 

当前这个参数有两个取值:

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

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

 

 

 

Kafka核心技术与实战 - 胡夕