写在前面
消息传输级别
消息队列的主要功能就是将消息从生产者传输到消费者,而衡量一个消息队列传输消息保障一般是有三个级别:
- at most once:至多一次。消息可能会丢失,但绝不会重复传输
- at least once:至少一次。消息绝对不会丢失,但可能会重复传输
- exactly once:恰好一次。每条消息都肯定被且仅被成功传输一次,不会丢失,也不会有多余的重复。
而Kafka提供的消息传输保障级别是:at least once。他的消息传输保障机制主要有以下:
- Broker端针对消息存储有Replica机制,所以消息一旦成功写入Broker,便不会丢失
- Kafka针对Producer提供了消息写入重试机制,但如果因为网络问题导致Producer收到Kafka是否成功写入消息的回调,就会导致Producer重复写入
- Kafka针对Consumer的消息消费使用Offset机制,当Consumer批量拉取消息却没有及时确认Offset,这时发生Rebalance之类,就会触发重新的消费。
Kafka除了基本的at least once,也可以通过消息幂等和事务来实现 exactly once。
消息生产幂等
Kafka可以通过设置producer的客户端参数transactional.id为true来开启消息生产幂等。
Kafka通过引入producer id 和 序列号(sequence number)来实现消息生成的幂等性。这两个概念在接受Kafka消息日志格式V2版本时(这篇文章)有介绍过。这两个概念具体使用如下:
- producer在初始化时,会被分配一个全局唯一的 producer id,这个过程生产者无感。
- producer生产的,发往partition的每条消息,也都会分配一个序列号。序列号从0递增,且按<pid, partition>的维度区分。
- broker在内存中会为每个<pid, partition>维护一个序列号(seq_old),此时,当broker收到一条序列号为seq_new的信息
- 如果seq_new=seq_old+1,broker才会接收该消息。
- 如果seq_new<seq_old+1,说明这条消息是重复消息
- 如果seq_new>seq_old+1,说明消息有乱序,中间区间的消息可能丢失,kafka会返回一个异常。
需要注意,broker存储的序列号,是按照<pid, partition>来划分的,所以Kafka的消息幂等只能保证单producer单分区的幂等。
事务与事务协调器
事务的基本概念
kafka的事务可以跨分区,保证对多个分区的写入操作保持原子性,即多个分区的写入要么全部成功,要么全部失败。注意这里多个分区可以涉及不同的topic的不同分区。
- Kafka的事务可以保证跨生产者会话的消息幂等发送。即kafka保证不会同时有两个相同的transactionalID标识的producer在生产消息
- Kafka的事务也保证跨生产者会话的事务恢复。即当同一个transactionalID的生产者实例意外宕机,那么新的同transactionalID的事务仍然可以继续处理未提交的事务消息。
- Kafka不保证同一个事务的消息都能被消费者消费。这个原因有很多:可能事务消息被压缩处理、消费者可能跳跃消费消息、或者消费者没有被分配某个事务涉及的所有分区,导致无法获取事务所有的消息。
transactionalID
要使用Kafka的事务消息,生产程序需要提供唯一的transactionalID(可以通过客户端的transactional.id设置)。同时要使用事务消息,也需要保证消息幂等配置transactional.id开启。
producer epoch
在开启事物之后,producer在初始化时会通过transactionalID获取producerID,还会获取一个单调递增的producer epoch。
事务的基本使用方法
Kafka提供了事务的5个方法,如下:
- initTransactions():初始化一个事务(要求producer配置了
transactional.id) - beginTransactions():开启事务
- sendOffsetsToTransactions():消费者可调用,用于提交消费事务消息内的offset
- commitTransactions():提交事务
- abortTransactions():终止事务
通常生产端使用事务发送消息的主要逻辑如下:
initTransactions()
beginTransactions()
try {
// get some message for sending
send(message)
// ...
commitTransactions()
} catch (SomeException e) {
abortTransactions()
}
事务的基本实现原理与基本流程
Kafka事务是基于两阶段提交(Two-Phase Commit)协议。
- 开启事务:生产者在开始事务之前调用
beginTransaction()方法,该方法会为该生产者分配一个唯一的事务ID。 - 发送消息:生产者在事务中使用
send()方法发送消息到Kafka主题。这些消息在发送期间只会被缓存在生产者端,而不会立即提交到服务器。 - 预提交:当生产者完成消息发送后,调用
commitTransaction()方法进行预提交。在预提交阶段,生产者将事务中的所有消息写入Kafka主题的特殊事务性分区中。 - 事务协调:Kafka通过事务协调器(Transaction Coordinator)来管理事务的提交和回滚。协调器负责与生产者和消费者进行通信,并确保事务的一致性。
- 决策阶段:在预提交后,事务协调器会协调所有参与该事务的生产者和消费者,并请求它们对事务进行决策。生产者和消费者将根据收到的请求进行回答,表示它们是否同意或拒绝该事务的提交。
- 提交或回滚:根据决策阶段的回答,事务协调器将决定是提交事务还是回滚事务。
- 如果所有参与者都同意提交事务,事务协调器会向Kafka服务器发送
commit请求,将事务中的消息提交到相应的主题分区中。此时事务被视为已提交。 - 如果任何一个参与者拒绝提交事务,事务协调器会向Kafka服务器发送
abort请求,将事务中的消息从特殊事务性分区中移除。此时事务被视为已回滚。
- 如果所有参与者都同意提交事务,事务协调器会向Kafka服务器发送
- 结束事务:无论事务是提交还是回滚,生产者在完成后都应调用
endTransaction()方法来结束事务。这会清除生产者的事务状态并释放相关资源。