什么是事务
什么是事务呢?事务是并发控制的单位,是用户定义的一个操作序列。有四个特性(ACID):
- 原子性(Atomicity): 事务是数据库的逻辑工作单位,事务中包括的诸操作要么全做,要么全不做。
- 一致性(Consistency): 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
- 隔离性(Isolation): 一个事务的执行不能被其他事务干扰。
- 持续性/永久性(Durability): 一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。
以上是书面解释,简单来说就是把你的操作统一化,要么所有操作都成功,要么就都不成功,如果执行中有某一项操作失败,其之前所有的操作都回滚到未执行这一系列操作之前的状态。
消息队列中的事务
主要解决的是消息生产者和消息消费者的数据一致性问题
举个例子,一般来说,用户在电商 APP 上购物时,先把商品加到购物车里,然后几件商品一起下单,最后支付,完成购物流程,就可以愉快地等待收货了
这个过程中有一个需要用到消息队列的步骤,订单系统创建订单后,发消息给购物车系统,将已下单的商品从购物车中删除。因为从购物车删除已下单商品这个步骤,并不是用户下单支付这个主要流程中必需的步骤,使用消息队列来异步清理购物车是更加合理的设计。
什么是分布式事务
之前提到的事务ACID大部分传统的单体关系型数据库(Mysql Oracle)都完整的实现了 ACID,但是,对于分布式系统来说,严格的实现 ACID 这四个特性几乎是不可能的,或者说实现的代价太大,大到我们无法接受。
分布式事务就是要在分布式系统中的实现事务。在分布式系统中,在保证可用性和不严重牺牲性能的前提下,光是要实现数据的一致性就已经非常困难了,所以出现了很多“残血版”的一致性,比如顺序一致性、最终一致性等等。
严格实现分布式事务不太可能,大家常说的分布式事务也是不完整实现,在不同场景中通过一些妥协来解决实际问题。
常见的有
2PC(Two-phase Commit)二阶段提交
2PC
准备阶段(Prepare phase)
事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
提交阶段(Commit phase)
如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者*发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
TCC(Try-Confirm-Cancel) (预处理-提交-取消)
TCC模式
- 主业务服务:主业务服务为整个业务活动的发起方,如前面提到的组合支付场景,支付系统即是主业务服务。
- 从业务服务:从业务服务负责提供TCC业务操作,是整个业务活动的操作方。从业务服务必须实现Try、Confirm和Cancel三个接口,供主业务服务调用。由于Confirm和Cancel操作可能被重复调用,故要求Confirm和Cancel两个接口必须是幂等的。前面的组合支付场景中的余额系统和红包系统即为从业务服务。
- 业务活动管理器:业务活动管理器管理控制整个业务活动,包括记录维护TCC全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时确认所有的TCC型操作的confirm操作,在业务活动取消时调用所有TCC型操作的cancel操作。
可见整个TCC事务对于主业务服务来说是透明的,其中业务活动管理器和从业务服务各自干了一部分工作。
Try:预留业务资源
-
尝试执行业务。
-
完成所有业务检查(一致性)
-
预留必须业务资源(准隔离性)
Confirm:确认执行业务
- 真正执行业务
- 不做任何业务检查
- 只使用Try阶段预留的业务资源
Cancel:取消执行业务
- 释放Try阶段预留的业务资源
优点
- 解决了跨应用业务操作的原子性问题,在诸如组合支付、账务拆分场景非常实用。
- TCC实际上把数据库层的二阶段提交上提到了应用层来实现,对于数据库来说是一阶段提交,规避了数据库层的2PC性能低下问题
缺点
- TCC的Try、Confirm和Cancel操作功能需业务提供,开发成本高
事务消息
事务消息是分布式事务
事务消息适用的场景主要是那些需要异步更新数据,并且对数据实时性要求不太高的场景。比如在创建订单后,如果出现短暂的几秒,购物车里的商品没有被及时情况,也不是完全不可接受的,只要最终购物车的数据和订单数据保持一致就可。
消息队列是如何实现分布式事务的
那购物车和订单的例子
- 订单系统在消息队列上开启一个事务
- 发送一个“半消息”(半消息不是说消息内容不完整,它包含的内容就是完整的消息内容,半消息和普通消息的唯一区别是,在事务提交之前,对于消费者来说,这个消息是不可见的。)
- 订单系统执行本地事务,创建一条订单记录,并提交订单库的数据库事务。
- 根据本地事务的执行结果(SUCCESS OR FAILD)提交或者回滚事务消息
- 投递消息
- 订单创建成功(提交事务消息)-----> 购物车系统消费消息继续下面的流程
- 订单创建失败(回滚事务消息)----->购物车系统则不会接收到消息
这里吗就存在一个问题,如果在第四步提交事务消息失败了,成功与否,购物车系统都收不到消息
Kafka 和 RocketMQ 给出了 2 种不同的解决方案
Kafka 的解决方案比较简单粗暴,直接抛出异常,让用户自行处理。我们可以在业务代码中反复重试提交,直到提交成功,或者删除之前创建的订单进行补偿。
RocketMQ 中的事务实现中,增加了事务反查的机制来解决事务消息提交失败的问题。如果 Producer 也就是订单系统,在提交或者回滚事务消息时发生网络异常,RocketMQ 的 Broker 没有收到提交或者回滚的请求,Broker 会定期去 Producer 上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。(下面主要介绍实现方式)
RocketMQ 中的分布式事务实现
为了支撑这个事务反查机制,我们的业务代码需要实现一个反查本地事务状态的接口,告知 RocketMQ 本地事务是成功还是失败。
反查本地事务的逻辑也很简单,我们只要根据消息中的订单 ID,在订单库中查询这个订单是否存在即可,如果订单存在则返回成功,否则返回失败。RocketMQ 会自动根据事务反查的结果提交或者回滚事务消息。
反查本地事务的实现,并不依赖消息的发送方,也就是订单服务的某个实例节点上的任何数据。这种情况下,即使是发送事务消息的那个订单服务节点宕机了,RocketMQ 依然可以通过其他订单服务的节点来执行反查,确保事务的完整性。
滚事务消息。
反查本地事务的实现,并不依赖消息的发送方,也就是订单服务的某个实例节点上的任何数据。这种情况下,即使是发送事务消息的那个订单服务节点宕机了,RocketMQ 依然可以通过其他订单服务的节点来执行反查,确保事务的完整性。