一提及事务,你可能天然会联想到数据库。的确,咱们平常使用事务的场景,绝大部分都是在操做数据库的时候。像 MySQL、Oracle 这些主流的关系型数据库,也都提供了完整的事务实现。那消息队列为何也须要事务呢?数据库
其实不少场景下,咱们“发消息”这个过程,目的每每是通知另一个系统或者模块去更新数据,消息队列中的“事务”,主要解决的是消息生产者和消息消费者的数据一致性问题。服务器
依然拿咱们熟悉的电商来举个例子。通常来讲,用户在电商 APP 上购物时,先把商品加到购物车里,而后几件商品一块儿下单,最后支付,完成购物流程,就能够愉快地等待收货了。网络
这个过程当中有一个须要用到消息队列的步骤,订单系统建立订单后,发消息给购物车系统,将已下单的商品从购物车中删除。由于从购物车删除已下单商品这个步骤,并非用户下单支付这个主要流程中必需的步骤,使用消息队列来异步清理购物车是更加合理的设计。并发
对于订单系统来讲,它建立订单的过程当中实际上执行了 2 个步骤的操做:异步
购物车系统订阅相应的主题,接收订单建立的消息,而后清理购物车,在购物车中删除订单中的商品。分布式
在分布式系统中,上面提到的这些步骤,任何一个步骤都有可能失败,若是不作任何处理,那就有可能出现订单数据与购物车数据不一致的状况,好比说:性能
那咱们须要解决的问题能够总结为:在上述任意步骤都有可能失败的状况下,还要保证订单库和购物车库这两个库的数据一致性。学习
对于购物车系统收到订单建立成功消息清理购物车这个操做来讲,失败的处理比较简单,只要成功执行购物车清理后再提交消费确认便可,若是失败,因为没有提交消费确认,消息队列会自动重试。spa
问题的关键点集中在订单系统,建立订单和发送消息这两个步骤要么都操做成功,要么都操做失败,不容许一个成功而另外一个失败的状况出现。设计
这就是事务须要解决的问题。
那什么是事务呢?若是咱们须要对若干数据进行更新操做,为了保证这些数据的完整性和一致性,咱们但愿这些更新操做要么都成功,要么都失败。至于更新的数据,不仅局限于数据库中的数据,能够是磁盘上的一个文件,也能够是远端的一个服务,或者以其余形式存储的数据。
这就是一般咱们理解的事务。其实这段对事务的描述不是太准确也不完整,可是,它更易于理解,大致上也是正确的。因此我仍是倾向于这样来说“事务”这个比较抽象的概念。
一个严格意义的事务实现,应该具备 4 个属性:原子性、一致性、隔离性、持久性。这四个属性一般称为 ACID 特性。
原子性,是指一个事务操做不可分割,要么成功,要么失败,不能有一半成功一半失败的状况。
一致性,是指这些数据在事务执行完成这个时间点以前,读到的必定是更新前的数据,以后读到的必定是更新后的数据,不该该存在一个时刻,让用户读到更新过程当中的数据。
隔离性,是指一个事务的执行不能被其余事务干扰。即一个事务内部的操做及使用的数据对正在进行的其余事务是隔离的,并发执行的各个事务之间不能互相干扰,这个有点儿像咱们打网游中的副本,咱们在副本中打的怪和掉的装备,与其余副本没有任何关联也不会互相影响。
持久性,是指一个事务一旦完成提交,后续的其余操做和故障都不会对事务的结果产生任何影响。
大部分传统的单体关系型数据库都完整的实现了 ACID,可是,对于分布式系统来讲,严格的实现 ACID 这四个特性几乎是不可能的,或者说实现的代价太大,大到咱们没法接受。
分布式事务就是要在分布式系统中的实现事务。在分布式系统中,在保证可用性和不严重牺牲性能的前提下,光是要实现数据的一致性就已经很是困难了,因此出现了不少“残血版”的一致性,好比顺序一致性、最终一致性等等。
显然实现严格的分布式事务是更加不可能完成的任务。因此,目前你们所说的分布式事务,更多状况下,是在分布式系统中事务的不完整实现。在不一样的应用场景中,有不一样的实现,目的都是经过一些妥协来解决实际问题。
在实际应用中,比较常见的分布式事务实现有 2PC(Two-phase Commit,也叫二阶段提交)、TCC(Try-Confirm-Cancel) 和事务消息。每一种实现都有其特定的使用场景,也有各自的问题,都不是完美的解决方案。
事务消息适用的场景主要是那些须要异步更新数据,而且对数据实时性要求不过高的场景。好比咱们在开始时提到的那个例子,在建立订单后,若是出现短暂的几秒,购物车里的商品没有被及时清空,也不是彻底不可接受的,只要最终购物车的数据和订单数据保持一致就能够了。
2PC 和 TCC 不是咱们本次课程讨论的内容,就不展开讲了,感兴趣的同窗能够自行学习。
事务消息须要消息队列提供相应的功能才能实现,Kafka 和 RocketMQ 都提供了事务相关功能。
回到订单和购物车这个例子,咱们一块儿来看下如何用消息队列来实现分布式事务。
首先,订单系统在消息队列上开启一个事务。而后订单系统给消息服务器发送一个“半消息”,这个半消息不是说消息内容不完整,它包含的内容就是完整的消息内容,半消息和普通消息的惟一区别是,在事务提交以前,对于消费者来讲,这个消息是不可见的。
半消息发送成功后,订单系统就能够执行本地事务了,在订单库中建立一条订单记录,并提交订单库的数据库事务。而后根据本地事务的执行结果决定提交或者回滚事务消息。若是订单建立成功,那就提交事务消息,购物车系统就能够消费到这条消息继续后续的流程。若是订单建立失败,那就回滚事务消息,购物车系统就不会收到这条消息。这样就基本实现了“要么都成功,要么都失败”的一致性要求。
若是你足够细心,可能已经发现了,这个实现过程当中,有一个问题是没有解决的。若是在第四步提交事务消息时失败了怎么办?对于这个问题,Kafka 和 RocketMQ 给出了 2 种不一样的解决方案。
Kafka 的解决方案比较简单粗暴,直接抛出异常,让用户自行处理。咱们能够在业务代码中反复重试提交,直到提交成功,或者删除以前建立的订单进行补偿。RocketMQ 则给出了另一种解决方案。
在 RocketMQ 中的事务实现中,增长了事务反查的机制来解决事务消息提交失败的问题。若是 Producer 也就是订单系统,在提交或者回滚事务消息时发生网络异常,RocketMQ 的 Broker 没有收到提交或者回滚的请求,Broker 会按期去 Producer 上反查这个事务对应的本地事务的状态,而后根据反查结果决定提交或者回滚这个事务。
为了支撑这个事务反查机制,咱们的业务代码须要实现一个反查本地事务状态的接口,告知 RocketMQ 本地事务是成功仍是失败。
在咱们这个例子中,反查本地事务的逻辑也很简单,咱们只要根据消息中的订单 ID,在订单库中查询这个订单是否存在便可,若是订单存在则返回成功,不然返回失败。RocketMQ 会自动根据事务反查的结果提交或者回滚事务消息。
这个反查本地事务的实现,并不依赖消息的发送方,也就是订单服务的某个实例节点上的任何数据。这种状况下,即便是发送事务消息的那个订单服务节点宕机了,RocketMQ 依然能够经过其余订单服务的节点来执行反查,确保事务的完整性。
综合上面讲的通用事务消息的实现和 RocketMQ 的事务反查机制,使用 RocketMQ 事务消息功能实现分布式事务的流程以下图:
咱们经过一个订单购物车的例子,学习了事务的 ACID 四个特性,以及如何使用消息队列来实现分布式事务。
而后咱们给出了现有的几种分布式事务的解决方案,包括事务消息,可是这几种方案都不能解决分布式系统中的全部问题,每一种方案都有局限性和特定的适用场景。
最后,咱们一块儿学习了 RocketMQ 的事务反查机制,这种机制经过按期反查事务状态,来补偿提交事务消息可能出现的通讯失败。在 Kafka 的事务功能中,并无相似的反查机制,须要用户自行去解决这个问题。
可是,这不表明 RocketMQ 的事务功能比 Kafka 更好,只能说在咱们这个例子的场景下,更适合使用 RocketMQ。Kafka 对于事务的定义、实现和适用场景,和 RocketMQ 有比较大的差别,后面的课程中,咱们会专门讲到 Kafka 的事务的实现原理。