消息队列中的事务,主要解决生产者和消费者的数据一致性问题。
以电商为例,用户在电商APP上购物,加商品到购物车,再几件商品一起下单,支付。
这个过程中,订单系统会通过消息队列发订单消息给购物车模块,其消费订单消息从购物车中移除下单商品。
因为购物车删商品不是下单必须流程的缘故,因此会有上述的异步删购物车数据的操作。
订单系统 ===== 创建 ====> 订单库
====发送订单创建消息 ==> 消息队列 ====> 购物车系统 ==清除购物车==> 购物车库
对于订单系统来说,它创建订单的过程实际上执行了2个步骤的操作:
1. 订单库创建数据
2. 发送订单创建消息
对于购物车系统,对订阅的主题,接收消息后只有一个操作就是移除购物车订单上的商品记录。
分布式系统中,任何步骤的失败,如果不做处理,会出现系统间数据不一致。
1. 比如插入了新商品订单,没有删除购物车商品记录。
2. 没有新增商品订单,购物车商品记录被删除了。
待解决问题可归纳为:上述任意步骤都可能失败时,保证订单库和购物车库这两个库的数据一致性。
对于购物车系统删除购物车商品记录失败的话,可以不回复确认,消息队列服务端会重发,成功处理后回复确认即可。
对于订单系统,插商品订单记录操作和发消息两个步骤一致成功和一致失败的保证则较为困难,也是事务需要解决的问题。
什么是分布式事务?
较为浅显的说明:对若干数据进行更新操作,希望若干操作要么都成功,要么都失败。
严格定义:ACID。
A:若干操作要么都成功,要么都失败
C:若干操作完成前的时间点,读到的是完成前的数据状态,完成后则是完成后的数据状态,不存在中间状态被读到。
I:各事务内部操作的数据对其他事务是隔离的,即并发事务操作的共同数据是互相隔离的。
D:一个事务一旦完成提交,后续其他故障或操作不影响本事务的结果。(???)
大部分传统单体RDBM都完整实现了ACID。对于分布式系统,严格实现ACID几乎不可能,或者说因为代价过大导致无法接受。
分布式事务就是要在分布式系统中实现事务。在保证高可用、高性能下,光一致性就较为困难,因此有很多“残血版”:最终一致性,顺序一致性。(????)
目前所说分布式事务,都是在分布式系统中事务的不完整实现:在不同应用场景中,有不同的实现,目的是通过一些妥协来解决实际问题。(????)
比如在实际应用中,有2PC(???)、TCC(???)、事务消息。每一种实现都有其特定的使用场景和各自的问题,没有银弹。
事务消息适合的场景为需要异步更新数据,并对数据实时性要求不高的场景。即只要求达到一个最终一致性,并对达到的时延要求不高,比如购物车内的商品删除延迟个几秒都没有关系,只要最终订单数据与购物车商品数据一致即可。
消息队列是如何实现分布式事务的?
事务消息需要消息队列提供相应的功能才能实现,Kafka和RocketMQ都提供了事务相关功能。
===========3. 执行本地事务,创建订单==========> 订单库
订单系统 == 1. 开启事务 ==>
== 2. 发送半消息 ==> 消息队列 ==5. 投递消息==> 购物车系统
== 4. 提交或回滚 ==>
半消息和普通消息的唯一区别是在消息投递前,对于消费者来说,半消息不可见。
上述过程,第4步的操作与第3步本地事务的提交与回滚操作保持一致。
上述过程存在一个问题,在于第4步的操作可能发生失败。Kafka处理失败直接抛异常由使用方重试提交直到成功,或者删除第3步的订单进行补偿。RocketMQ给出了另一种方案。
RocketMQ中的分布式事务实现
在RocketMQ中的事务实现中,增加了事务反查机制解决事务消息提交失败问题。如果Producer也就是订单系统,在提交或者回滚事务消息时发生网络异常,RocketMQ的Broker没有收到提交或者回滚的请求,Broker会定期去Producer去反查这个事务状态,根据反查结果自动提交或者回滚这个消息事务。
由业务代码实现反查本地事务状态的接口,提供给RMQ告知本地事务成功与否。
前述订单系统为例,Broker通过半消息中的订单ID去调用接口,查订单库是否存在该订单。
挂了任意的订单节点,只要还有存活的节点都不影响反查,也就是开启事务去发半消息的节点挂了也无所谓。
综合前述通用事务消息的实现和RocketMQ的事务反查机制,使用RocketMQ实现分布式事务的流程
小结
我们通过一个订单购物车的例子,学习了事务的ACID,和使用消息队列实现分布式事务的方案。
几种现有的分布式事务的解决方案,包括事务消息,但是这几种方案都不能解决分布式系统中所有问题,每个方案都有适用场景和局限性。
最后一起学习了RocketMQ中事务反查机制,以此补偿事务消息的提交或回滚时的通信失败。在Kafka的事务功能中,并没有类似的反查机制,需要用户自行去解决这个问题。