基于本文回答
0
评论

在Kafka的事务机制中,Transaction Coordinator是如何通过类似于两阶段提交(2PC)的机制保证跨Partition写入原子性的?

知识点图片

在 Kafka 中,实现跨 Partition 的写入原子性(即 Exactly-Once Semantics,EOS 的核心部分)依赖于Transaction Coordinator(事务协调器,简称 TC)和内部的 __transaction_state 主题

Kafka 的事务机制确实采用了类似于两阶段提交(2PC)的协议,但为了适应高吞吐量的流处理场景,它对传统的 2PC 进行了改良(去除了同步阻塞和投票阶段)。

以下是 Transaction Coordinator 如何通过这种“类 2PC”机制保证跨 Partition 写入原子性的详细原理解析:


核心组件概述

  1. Transactional ID (TID):开发者指定的全局唯一事务 ID。
  2. Producer ID (PID) & Epoch:Kafka 自动分配的生产者 ID 和纪元,用于防范“僵尸生产者”(Zombie Fencing)。
  3. Transaction Coordinator (TC):运行在 Broker 上的后台组件,负责管理事务状态。
  4. __transaction_state:Kafka 的内部 Topic,充当 TC 的预写式日志(WAL),持久化记录事务的各种状态转变。

事务的完整生命周期与 2PC 执行流

假设一个 Producer 要在一个事务中向 Partition A 和 Partition B 分别写入一条消息。整个过程如下:

0. 初始化与开启事务

  • initTransactions():Producer 向 TC 发送自己的 TID。TC 为其分配 PID 和 Epoch,并将映射关系写入 __transaction_state。此时如果之前有同名 TID 的“僵尸” Producer,其 Epoch 会过期,从而被拒绝写入。
  • beginTransaction():Producer 本地标记事务开始。

1. 注册参与者(事务准备期)

在 Producer 真正向 Partition A 和 B 发送数据之前,它必须先向 TC 发送 AddPartitionsToTxnRequest

  • TC 收到请求后,将 Partition A 和 B 记录为当前事务的参与者(Participants)
  • TC 会在 __transaction_state 中记录当前事务的状态为 Ongoing

2. 写入实际数据

Producer 直接向 Partition A 和 Partition B 的 Leader Broker 发送实际的消息数据。

  • 这些消息带有 PID、Epoch 和序列号。
  • 注意: 此时数据已经写入到 Partition 的日志中了,但是对配置了 read_committed 的 Consumer 来说,这些数据是不可见的。

3. 类两阶段提交(2PC)的核心过程

当 Producer 调用 commitTransaction()(或 abortTransaction())时,真正的 2PC 机制开始运转:

Phase 1:准备阶段(Prepare)—— 记录决议
  1. Producer 向 TC 发送 EndTxnRequest(请求 Commit 或 Abort)。
  2. TC 接收到请求后,第一件事不是去通知 Partition,而是先写自己的日志
  3. TC 向内部主题 __transaction_state 写入一条 PrepareCommit(或 PrepareAbort)记录。
  4. 原子性分水岭: 一旦 PrepareCommit 成功写入 __transaction_state这个事务就一定会被提交。即使此时 TC 宕机,新的 TC 选举出来后读取到这条记录,也会继续完成后续的提交动作。这就保证了单点故障下的原子性。
Phase 2:提交/回滚阶段(Commit/Rollback)—— 写入控制标记
  1. TC 查阅在“第1步”中注册的参与者列表(Partition A 和 B)。
  2. TC 向 Partition A 和 B 的 Leader 异步发送 WriteTxnMarkerRequest
  3. Partition Leader 收到请求后,在自己的数据日志(Log)中写入一条特殊的非数据消息,称为 控制记录(Control Record / Transaction Marker)。这个标记是 <COMMIT><ABORT>
  4. 当所有的参与者(Partition A 和 B)都成功写入了控制记录后,TC 向 __transaction_state 写入最终的 CompleteCommit(或 CompleteAbort)记录,标记事务彻底结束。

消费者(Consumer)如何配合实现原子性?

光有写入侧的 2PC 是不够的,读取侧也必须配合。

如果 Consumer 的隔离级别设置为 read_committed

  • Kafka 会维护一个叫做 LSO (Last Stable Offset) 的水位线。
  • LSO 之前的消息都是已提交的(或者非事务消息)。
  • Consumer 消费时,如果遇到属于某个活跃事务(未遇到 Commit/Abort 标记)的数据,Consumer 会将其缓存或暂不返回给用户,直到在 Log 中读取到 <COMMIT> 标记,才会将这批数据交给应用;如果读取到 <ABORT> 标记,这批数据就会被直接丢弃。

通过这种方式,不管跨了多少个 Partition,只要 <COMMIT> 标记没有写入,Consumer 就看不见这些数据;一旦 Phase 1 完成,TC 保证所有 Partition 最终都会被写上 <COMMIT> 标记,从而实现了跨 Partition 的原子性(要么全部可见,要么全不可见)


Kafka 的 2PC 与 传统数据库 2PC 的区别

Kafka 之所以能保持极高的吞吐量,是因为它对传统的 2PC 做了流处理场景下的改造:

特性 传统数据库 2PC (如 XA 协议) Kafka 的类 2PC 机制
Prepare 阶段 协调者需要向所有参与者询问:“你们能提交吗?”,需要等待所有节点投票 无需投票。只要 Producer 把数据成功写给 Partition,TC 直接在本地记录 PrepareCommit,单向决定。
锁机制 强锁。事务未提交前,参与者(数据行/表)会被阻塞加锁,严重影响并发。 无锁。数据直接追加写入 Partition,并发的其他事务继续追加写。隔离性通过 Consumer 端的 LSO 和控制标记来过滤。
性能 极差,受限于最慢的参与者网络和磁盘 I/O。 极高。完全基于追加写(Append-only log)和异步 Marker 下发,不阻塞其他事务。

总结

Kafka 的 Transaction Coordinator 通过充当“大脑”,结合充当“预写日志”的 __transaction_state 主题,实现了一个高效的异步 2PC 模型。Phase 1(PrepareCommit)保证了即使宕机决议也不会丢失,Phase 2(Transaction Marker)保证了数据可见性在各个 Partition 上的统一。 搭配 Consumer 端的 read_committed 隔离级别,最终完美实现了跨 Partition 的原子写入。

右滑查看面试常问