3. Kafka 事务核心概念:事务性生产者、事务协调器、事务日志
Kafka 事务机制是构建端到端精确一次语义(EOS)的基石。它确保跨多个分区和主题的消息写入要么全部成功,要么全部失败,并且不会出现重复或丢失。理解其核心组件——事务性生产者、事务协调器与事务日志——是掌握事务边界的关键。
3.1 事务性生产者(Transactional Producer)
事务性生产者是事务的发起者。与普通生产者不同,它必须调用 initTransactions() 进行初始化,并在发送消息前后使用 beginTransaction() 和 commitTransaction() / abortTransaction() 来界定事务边界。
关键行为: 事务性生产者会为每条消息附加一个单调递增的 生产者 ID(PID) 和 序列号(Sequence Number),Broker 利用这些信息进行幂等去重,确保即使生产者重试,消息也仅被写入一次。
事务性生产者工作流程
- 初始化: 调用
initTransactions(),向协调器注册 PID 并获取事务超时时间。 - 开启事务: 调用
beginTransaction(),标记事务开始。 - 发送消息: 将消息发送到目标主题分区,此时消息处于“未提交”状态,对消费者不可见。
- 提交/中止: 调用
commitTransaction()或abortTransaction(),协调器将事务结果写入事务日志。
// 事务性生产者示例(Java)
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("transactional.id", "my-transactional-id");
props.put("enable.idempotence", "true");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(new ProducerRecord<>("topic-a", "key1", "value1"));
producer.send(new ProducerRecord<>("topic-b", "key2", "value2"));
producer.commitTransaction();
} catch (Exception e) {
producer.abortTransaction();
} finally {
producer.close();
}
3.2 事务协调器(Transaction Coordinator)
每个 Kafka Broker 上都运行着一个事务协调器组件。它负责管理事务的生命周期,包括分配 PID、持久化事务状态、处理超时以及执行提交或中止的协调工作。
| 职责 | 说明 |
|---|---|
| PID 分配 | 为每个 transactional.id 分配唯一的 PID,并维护 PID 与事务 ID 的映射关系。 |
| 事务状态管理 | 跟踪事务的当前状态(如 Ongoing、PrepareCommit、CompletedCommit、Aborted)。 |
| 事务超时处理 | 如果事务在超时时间内未完成,协调器主动中止该事务,防止悬挂事务。 |
| 提交/中止协调 | 在提交阶段,协调器向所有参与分区写入事务标记(Commit Marker),通知消费者该事务已提交。 |
定位方式: 生产者通过
transactional.id 的哈希值计算目标分区,从而找到对应的协调器 Broker。公式为:abs(hash(transactional.id)) % numPartitions,其中分区来自内部主题 __transaction_state。
3.3 事务日志(Transaction Log)
事务日志是一个名为 __transaction_state 的内部主题,默认有 50 个分区,每个分区存储一组事务的元数据。它是事务协调器的持久化存储,确保协调器故障后能够恢复事务状态。
事务日志存储的关键信息
- 事务 ID 与 PID 映射: 记录每个
transactional.id对应的 PID 和当前 epoch。 - 事务状态: 当前事务处于哪个阶段(Empty、Ongoing、PrepareCommit、CompletedCommit、Dead)。
- 参与分区列表: 该事务涉及的所有主题分区。
- 最后心跳时间戳: 用于检测事务超时。
注意: 事务日志的副本因子建议设置为 3,且 min.insync.replicas 应至少为 2,以防止协调器故障导致事务状态丢失。如果事务日志丢失,所有未完成的事务将被强制中止。
3.4 事务状态机与核心流程
Kafka 事务遵循严格的状态机转换。下图展示了事务从创建到完成的核心状态流转,以及协调器与生产者之间的交互。
3.5 事务边界与幂等性保障
事务机制与幂等性生产者紧密配合。每个事务性生产者都自动启用了幂等性(enable.idempotence=true),通过 PID + 序列号实现去重。事务边界则进一步将多个幂等写入操作原子化。
| 特性 | 幂等生产者 | 事务性生产者 |
|---|---|---|
| 作用范围 | 单次生产者会话内的单个分区 | 跨分区、跨主题的多次写入 |
| 去重粒度 | PID + 序列号 | PID + 序列号 + 事务 ID |
| 原子性 | 不支持 | 支持(全部成功或全部失败) |
| 消费者可见性 | 消息立即可见 | 仅提交后的消息可见 |
实践建议: 在需要跨分区原子写入的场景(如支付系统、订单状态同步)中,必须使用事务性生产者。对于单分区单消息的写入,幂等生产者已足够,无需引入事务开销。
3.6 事务超时与故障恢复
事务协调器通过 transaction.timeout.ms 参数控制事务的最大存活时间。如果生产者崩溃或网络分区导致事务长时间未完成,协调器会主动中止事务,释放资源。
- 超时检测: 协调器定期扫描事务日志,检查最后心跳时间戳是否超过超时阈值。
- 僵尸生产者防护: 每个
transactional.id关联一个 epoch 计数器。当新生产者实例启动时,epoch 递增,旧实例的请求会被拒绝,防止僵尸生产者写入脏数据。 - 恢复流程: 协调器故障后,新的协调器通过读取事务日志恢复所有未完成事务的状态,并重新执行超时检测。
关键参数: 建议将
transaction.timeout.ms 设置为 60000(60 秒),既避免事务长时间占用资源,又给正常业务处理留出足够时间。对于长时间运行的事务,生产者应定期调用 sendOffsetsToTransaction() 发送心跳,防止超时。