1 幂等性
“幂等”这个词原是数学领域中的概念,指的是某些操作或函数能够被执行多次,但每次得到的结果都是不变的。在计算机领域中,幂等性的含义稍微有一些不同:
- 在命令式编程语言(比如 C)中,若一个子程序是幂等的,那它必然不能修改系统状态。这样不管运行这个子程序多少次,与该子程序关联的那部分系统状态保持不变。
- 在函数式编程语言(比如 Scala 或 Haskell)中,很多纯函数(pure function)天然就是幂等的,它们不执行任何的 side effect。
幂等性有很多好处,其最大的优势在于我们可以安全地重试任何幂等性操作,反正它们也不会破坏我们的系统状态。
2 幂等Producer
2.1 简介
在 Kafka 中,Producer 默认不是幂等性的,但我们可以创建幂等性 Producer。它其实是 0.11.0.0
版本引入的新功能。在 0.11 之后,指定 Producer 幂等性的方法很简单,仅需要设置一个参数即可,即 props.put(“enable.idempotence”, ture)
,或 props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true)
。
2.2 基本原理
enable.idempotence 被设置成 true 后,Producer 自动升级成幂等性 Producer,其他所有的代码逻辑都不需要改变。Kafka 自动帮你做消息的重复去重。底层具体的原理很简单,就是经典的用空间去换时间的优化思路,即在 Broker 端多保存一些字段。当 Producer 发送了具有相同字段值的消息后,Broker 能够自动知晓这些消息已经重复了,于是可以在后台默默地把它们“丢弃”掉。当然,实际的实现原理并没有这么简单,但你大致可以这么理解。
- 每条消息都有一个主键,这个主键由
<PID, Partition, SeqNumber>
组成。
PID
:ProducerID
,每个生产者启动时,Kafka 都会给它分配一个ID
,ProducerID
是生产者的唯一标识,需要注意的是,Kafka
重启也会重新分配PID
。Partition
:消息需要发往的分区号。SeqNumber
:生产者,他会记录自己所发送的消息,给他们分配一个自增的ID
,这个ID
就是SeqNumber
,是该消息的唯一标识,每发送一条消息,序列号加 1。
- 对于主键相同的数据,kafka 是不会重复持久化的,它只会接收一条。
2.3 作用范围
它只能保证单分区上的幂等性
,即一个幂等性 Producer 能够保证某个主题的一个分区上不出现重复消息,它无法实现多个分区的幂等性。其次,它只能实现单会话上的幂等性,不能实现跨会话的幂等性。这里的会话,你可以理解为 Producer 进程的一次运行。当你重启了 Producer 进程之后,这种幂等性保证就丧失了。
3 事务Producer
3.1 简介
Kafka 自 0.11 版本
开始也提供了对事务的支持,目前主要是在 read committed
隔离级别上做事情。它能保证多条消息原子性地写入到目标分区,同时也能保证 Consumer 只能看到事务成功提交的消息。
事务型 Producer 能够保证将消息原子性地写入到多个分区中。这批消息要么全部写入成功,要么全部失败。另外,事务型 Producer 也不惧进程的重启。Producer 重启回来后,Kafka 依然保证它们发送消息的精确一次处理。
3.2 事务API
设置事务型 Producer 的方法也很简单,满足两个要求即可:
- 和幂等性 Producer 一样,开启
enable.idempotence = true
。 - 设置 Producer 端参数
transactional. id
。最好为其设置一个有意义的名字。
生产端
此外,你还需要在 Producer 代码中做一些调整,如这段代码所示:
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(record1);
producer.send(record2);
producer.commitTransaction();
} catch (KafkaException e) {
producer.abortTransaction();
}
这段代码能够保证 Record1 和 Record2 被当作一个事务统一提交到 Kafka,要么它们全部提交成功,要么全部写入失败。
消费端
实际上即使写入失败,Kafka 也会把它们写入到底层的日志中,也就是说 Consumer 还是会看到这些消息。因此在 Consumer 端,读取事务型 Producer 发送的消息也是需要一些变更的。
修改起来也很简单,设置 isolation.level
参数的值即可。当前这个参数有两个取值:
read_uncommitted
:这是默认值,表明 Consumer 能够读取到 Kafka 写入的任何消息,不论事务型 Producer 提交事务还是终止事务,其写入的消息都可以读取。很显然,如果你用了事务型 Producer,那么对应的 Consumer 就不要使用这个值。read_committed
:表明 Consumer 只会读取事务型 Producer 成功提交事务写入的消息。当然了,它也能看到非事务型 Producer 写入的所有消息。
3.3 事务实现原理
kafka事务的实现引入了事务协调器
,如下图所示:
- 生产者使用事务必须配置事务id, kafka根据事务id计算分配事务协调器节点
- 事务协调器返回pid(幂等性中需要)
- 开始发送消息到topic中,不过这些消息与普通的消息不同,它们带着一个字段标识自己是事务消息
- 当生产者事务内的消息发送完毕,会向事务协调器发送
commit
或abort
请求,等待 kafka 响应 - 事务协调器收到请求后先持久化到内置事务主题
__transaction_state
中。 - 事务协调器后台会跟topic通信,告诉它们事务是成功还是失败的。
- 如果是成功,topic会汇报自己已经收到消息,协调者收到主题的回应便确认了事务完成,并持久化这一结果。
- 如果是失败的,主题会把这个事务内的消息丢弃,并汇报给协调者,协调者收到所有结果后再持久化这一信息,事务结束。
- 持久化第6步中的事务成功或者失败的信息, 如果
kafka broker
配置max.transaction.timeout.ms
之前既不提交也不中止事务,kafka broker
将中止事务本身。 此属性的默认值为 15 分钟。
注意:__transaction_state
默认有50个分区,每个分区负责一部分事务。事务划分是根据transactional.id
的hashcode
值%50
,计算出该事务属于哪个分区。 该分区Leader
副本所在的broker节点即为这个transactional.id
对应的Transaction Coordinator
节点,这也是上面第一步中的计算逻辑。