接上篇《Kafka技术知识总结之一——Kafka 的元素,组成,架构》
参考地址:
《【干货】Kafka 事务特性分析》
Kafka 事务与数据库的事务定义基本类似,主要是一个原子性:多个操作要么全部成功,要么全部失败。Kafka 中的事务可以使应用程序将消费消息、生产消息、提交消费位移当作原子操作来处理。
为了实现事务,Producer 应用程序必须做到:
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, “transacetionId”);
注:
- transactionalId 与 PID 一一对应,为了保证新的 Producer 启动之后,具有相同的 transactionalId 的旧生产者立即失效,每个 Producer 通过 transactionalId 获取 PID 的时候,还会获取一个单调递增的 producer epoch。
- Kafka 的事务主要是针对 Producer 而言的。对于 Consumer,考虑到日志压缩(相同 Key 的日志被新消息覆盖)、可追溯的 seek() 等原因,Consumer 关于事务语义较弱。
- 对于 Kafka Consumer,在实现事务配置时,一定要关闭自动提交的选项,即 props.put(“ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false”);
消费-转换-生产模式是一种常见的,又比较复杂的情况,由于同时存在消费与生产,所以整个过程通常需要事务化。
通常在实现该模式时,需要同时构建一个用于拉取原消息的 Consumer,一个将原消息处理后,将处理后消息投递出去的 Producer。在代码上主要有五个步骤:
// 初始化事务
producer.initTransactions();
// 开启事务
producer.beginTransaction();
// 消费 - 生产模型
producer.send(producerRecord);
// 提交消费位移
producer.sendOffsetsToTransaction(offsets, "groupId");
// 提交事务
producer.commitTransaction();
上述过程全部被 try… catch…,如果中间出现错误,需要在 catch 块中执行:
// 中止事务
producer.abortTransaction();
实现 Kafka 事务,主要使用到 Broker 端的事务协调器 (TransactionCoordinator)。每个 Producer 都会被指定一个特定的 TransactionalCoordinator,用来负责处理其事务,与消费者 Rebalance 时的 GroupCoordinator 作用类似。实现事务的流程如下图所示:
基本步骤如下:
生产者首先会发起一个查找事务协调者 (TransactionalCoordinator) 的请求 (FindCoordinatorRequest),Broker 集群根据 Request 中包含的 transactionalId 查找对应的 TransactionalCoordinator 节点并返回给 Producer。
生产者获得协调者信息后,向刚刚找到的 TransactionalCoordinator 发送 InitProducerIdRequest 请求,为当前 Producer 分配一个 Producer ID。分两种情况:
注:如果 TransactionalCoordinator 第一次收到包含该 transactionalId 的消息,则将相关消息存入主题 __transaction_state 中。
生产者通过方法 producer.beginTransaction()
启动事务,此时只是生产者内部状态记录为事务开始。对于事务协调者,直到生产者发送第一条消息,才认为已经发起了事务。
前面的阶段都是开始阶段,该阶段包含了整个事务的处理过程,消费者和生产者互相配合,共同完成事务。需要做如下工作:
producer.send()
方法,发送数据到分区;producer.senOffsetsToTransaction()
接口,发送分区的 Offset 信息到事务协调者,协调者将分区信息增加到事务中;producer.commitTransaction()
或 abortTransaction()
方法,提交或回滚事务;参考地址:《利用事务消息实现分布式事务》
很多场景下,我们发消息的过程,目的往往是通知另外一个系统或者模块去更新数据。消息队列中的事务,主要**解决消息生产者和消息消费者的数据一致性问题**。
举一个例子:用户在电商 APP 上购物时,先把商品加到购物车里,然后几件商品一起下单,最后支付,完成购物流程。
这个过程中有一个需要用到消息队列的步骤:订单系统创建订单后,发消息给购物车系统,将已下单的商品从购物车中删除。因为从购物车删除已下单商品这个步骤,并不是用户下单支付这个主要流程中必要的步骤,使用消息队列来异步清理购物车是更加合理。
对于订单系统,它创建订单的过程实际执行了 2 个步骤的操作:
对于购物车系统:订阅相应的主题,接收订单创建的消息,然后清理购物车,在购物车中删除订单的商品。
在分布式系统中,上面提到的步骤,任何一个都有可能失败,如果不做任何处理,那就有可能出现订单数据与购物车数据不一致的情况,比如:
所以我们需要解决的问题为:在上述任意步骤都有可能失败的情况下,还要保证订单库和购物车库这两个库的数据一致性。所以在这种跨库的事务操作中,需要使用到分布式事务。分布式事务见数据库篇,在多种适用于不同场景下的分布式事务方法中,其中一种方式是消息事务。
事务消息需要消息队列提供相应的功能才能实现,kafka 和 RocketMQ 都提供了事务相关功能。依旧以上面的订单系统为例,有两个操作:在本地数据库中插入订单数据,以及向消息队列中发送订单信息,订单系统如何才能保证这两个操作同时成功,同时失败呢?
producer.beginTransaction();
producer.send();
方法;注:此外,该流程也可以用于支付流水的业务场景:保证存入一条支付流水,以及发送支付流水消息,两者之间的原子性。(来自于笔者阿里二面面试题)