大家好,我是易安!明天咱们谈一谈音讯队列中的事务音讯这个话题。
一说起事务,你可能天然会联想到数据库。咱们日常应用事务的场景,绝大部分都是在操作数据库的时候。像 MySQL、Oracle 这些支流的关系型数据库,也都提供了残缺的事务实现。那音讯队列为什么也须要事务呢?
其实很多场景下,咱们“发消息”这个过程,目标往往是告诉另外一个零碎或者模块去更新数据,音讯队列中的“事务”,次要解决的是音讯生产者和音讯消费者的数据一致性问题。
拿电商产品来举个例子。一般来说,用户在电商 APP 上购物时,先把商品加到购物车里,而后几件商品一起下单,最初领取,实现购物流程,就能够欢快地期待收货了。
这个过程中有一个须要用到音讯队列的步骤,订单零碎创立订单后,发消息给购物车零碎,将已下单的商品从购物车中删除。因为从购物车删除已下单商品这个步骤,并不是用户下单领取这个次要流程中必须的步骤,应用音讯队列来异步清理购物车是更加正当的设计。
对于订单零碎来说,它创立订单的过程中实际上执行了 2 个步骤的操作:
- 在订单库中插入一条订单数据,创立订单;
- 发消息给音讯队列,音讯的内容就是刚刚创立的订单。
购物车零碎订阅相应的主题,接管订单创立的音讯,而后清理购物车,在购物车中删除订单中的商品。
在分布式系统中,下面提到的这些步骤,任何一个步骤都有可能失败,如果不做任何解决,那就有可能呈现订单数据与购物车数据不统一的状况,比如说:
- 创立了订单,没有清理购物车;
- 订单没创立胜利,购物车外面的商品却被清掉了。
那咱们须要解决的问题能够总结为:在上述任意步骤都有可能失败的状况下,还要保障订单库和购物车库这两个库的数据一致性。
对于购物车零碎收到订单创立胜利音讯清理购物车这个操作来说,失败的解决比较简单,只有胜利执行购物车清理后再提交生产确认即可,如果失败,因为没有提交生产确认,音讯队列会主动重试。
问题的关键点集中在订单零碎,创立订单和发送音讯这两个步骤要么都操作胜利,要么都操作失败,不容许一个胜利而另一个失败的状况呈现。
这就是事务须要解决的问题。
什么是分布式事务?
那什么是事务呢?如果咱们须要对若干数据进行更新操作,为了保障这些数据的完整性和一致性,咱们心愿这些更新操作 要么都胜利,要么都失败。 至于更新的数据,不只局限于数据库中的数据,能够是磁盘上的一个文件,也能够是远端的一个服务,或者以其余模式存储的数据。
这就是通常咱们了解的事务。其实这段对事务的形容不是太精确也不残缺,然而,它更易于了解,大体上也是正确的。所以我还是偏向于这样来讲“事务”这个比拟形象的概念。
一个严格意义的事务实现,应该具备 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 个性。
- 原子性,是指一个事务操作不可分割,要么胜利,要么失败,不能有一半胜利一半失败的状况。
- 一致性,是指这些数据在事务执行实现这个工夫点之前,读到的肯定是更新前的数据,之后读到的肯定是更新后的数据,不应该存在一个时刻,让用户读到更新过程中的数据。
- 隔离性,是指一个事务的执行不能被其余事务烦扰。即一个事务外部的操作及应用的数据对正在进行的其余事务是隔离的,并发执行的各个事务之间不能相互烦扰,这个有点儿像咱们打网游中的正本,咱们在正本中打的怪和掉的配备,与其余正本没有任何关联也不会相互影响。
- 长久 性,是指一个事务一旦实现提交,后续的其余操作和故障都不会对事务的后果产生任何影响。
大部分传统的单体关系型数据库都残缺的实现了 ACID,然而,对于分布式系统来说,严格的实现 ACID 这四个个性简直是不可能的,或者说实现的代价太大,大到咱们无奈承受。
分布式事务就是要在分布式系统中的实现事务。在分布式系统中,在保障可用性和不重大就义性能的前提下,光是要实现数据的一致性就曾经十分艰难了,所以呈现了很多变种版的一致性,比方程序一致性、最终一致性等等。
显然实现严格的分布式事务是更加不可能实现的工作。所以,目前大家所说的分布式事务,更多状况下,是在分布式系统中事务的不残缺实现。在不同的利用场景中,有不同的实现,目标都是通过一些斗争来解决理论问题。
在理论利用中,比拟常见的分布式事务实现有 2PC(Two-phase Commit,也叫二阶段提交)、TCC(Try-Confirm-Cancel)和事务音讯。每一种实现都有其特定的应用场景,也有各自的问题,都不是完满的解决方案。
事务音讯实用的场景次要是那些须要异步更新数据,并且对数据实时性要求不太高的场景。比方咱们在开始时提到的那个例子,在创立订单后,如果呈现短暂的几秒,购物车里的商品没有被及时清空,也不是齐全不可承受的,只有最终购物车的数据和订单数据保持一致就能够了。
音讯队列如何实现分布式事务?
事务音讯须要音讯队列提供相应的性能能力实现,Kafka 和 RocketMQ 都提供了事务相干性能。
回到订单和购物车这个例子,咱们一起来看下如何用音讯队列来实现分布式事务,这里以 RocketMQ 来举例。
首先,订单零碎在音讯队列上开启一个事务。而后订单零碎给音讯服务器发送一个“半音讯”,这个半音讯不是说音讯内容不残缺,它蕴含的内容就是残缺的音讯内容,半音讯和一般音讯的惟一区别是,在事务提交之前,对于消费者来说,这个音讯是不可见的。
半音讯发送胜利后,订单零碎就能够执行本地事务了,在订单库中创立一条订单记录,并提交订单库的数据库事务。而后依据本地事务的执行后果决定提交或者回滚事务音讯。如果订单创立胜利,那就提交事务音讯,购物车零碎就能够生产到这条音讯持续后续的流程。如果订单创立失败,那就回滚事务音讯,购物车零碎就不会收到这条音讯。这样就根本实现了“要么都胜利,要么都失败”的一致性要求。
如果你足够仔细,可能曾经发现了,这个实现过程中,有一个问题是没有解决的。如果在第四步提交事务音讯时失败了怎么办?对于这个问题,Kafka 和 RocketMQ 给出了 2 种不同的解决方案。
Kafka 的解决方案比较简单粗犷,间接抛出异样,让用户自行处理。咱们能够在业务代码中重复重试提交,直到提交胜利,或者删除之前创立的订单进行弥补。RocketMQ 则给出了另外一种解决方案。
RocketMQ 中的分布式事务实现
在 RocketMQ 中的事务实现中,减少了事务反查的机制来解决事务音讯提交失败的问题。如果 Producer 也就是订单零碎,在提交或者回滚事务音讯时产生网络异样,RocketMQ 的 Broker 没有收到提交或者回滚的申请,Broker 会定期去 Producer 上反查这个事务对应的本地事务的状态,而后依据反查后果决定提交或者回滚这个事务。
为了撑持这个事务反查机制,咱们的业务代码须要实现一个反查本地事务状态的接口,告知 RocketMQ 本地事务是胜利还是失败。
在咱们这个例子中,反查本地事务的逻辑也很简略,咱们只有依据音讯中的订单 ID,在订单库中查问这个订单是否存在即可,如果订单存在则返回胜利,否则返回失败。RocketMQ 会主动依据事务反查的后果提交或者回滚事务音讯。
这个反查本地事务的实现,并不依赖音讯的发送方,也就是订单服务的某个实例节点上的任何数据。这种状况下,即便是发送事务音讯的那个订单服务节点宕机了,RocketMQ 仍然能够通过其余订单服务的节点来执行反查,确保事务的完整性。
综合下面讲的通用事务音讯的实现和 RocketMQ 的事务反查机制,应用 RocketMQ 事务音讯性能实现分布式事务的流程如下图:
总结
本文通过一个订单购物车的例子,学习了事务的 ACID 四个个性,以及如何应用音讯队列来实现分布式事务。而后我给出了现有的几种分布式事务的解决方案,包含事务音讯,然而这几种计划都不能解决分布式系统中的所有问题,每一种计划都有局限性和特定的实用场景。
最初,咱们一起学习了 RocketMQ 的事务反查机制,这种机制通过定期反查事务状态,来弥补提交事务音讯可能呈现的通信失败。在 Kafka 的事务性能中,并没有相似的反查机制,须要用户自行去解决这个问题。然而,这不代表 RocketMQ 的事务性能比 Kafka 更好,只能说在咱们这个例子的场景下,更适宜应用 RocketMQ。Kafka 对于事务的定义、实现和实用场景,和 RocketMQ 有比拟大的差别。
本文由 mdnice 多平台公布