什么是分布式事务?
事务的特性:原子性、一致性、隔离性、持久性。
单数据源的一致性靠单机事务来保证。涉及到多个数据源的一致性,就要靠分布式事务。
分布式事务用于在分布式系统中保证不同节点之间的数据一致性。分布式事务的实现有很多种,最具有代表性的是由Oracle Tuxedo系统提出的XA分布式事务协议。
XA协议包含两阶段提交(2PC)和三阶段提交(3PC)两种实现。
两阶段提交(2PC)
正向流程:
准备阶段:事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段。
提交阶段:如果事务协调节点在之前所收到都是正向返回,那么它将会向所有事务参与者发出Commit请求。
接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息。
当事务协调者接收到所有事务参与者的“完成”反馈,整个分布式事务完成。
失败流程:
在XA的第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。
于是在第二阶段,事务协调节点向所有的事务参与者发送Abort请求。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作,回滚操作依照Undo Log来进行。
如果是第二个阶段失败呢?只能重试,直到所有参与者都回滚了,或者所有参与者都提交成功。甚至需要人工介入。
XA两阶段提交有哪些不足?
性能问题
XA协议遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。
协调者单点故障问题
事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。
丢失消息导致的不一致问题。
在XA协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
3PC
XA三阶段提交在两阶段提交的基础上增加了CanCommit
阶段(使得参与者可以利用这一个阶段统一各自的状态),并且引入了**超时机制**
(在参与者中也引入了超时机制)。
一旦事物参与者迟迟没有接到协调者的commit请求,会自动进行本地commit。这样有效解决了协调者单点故障的问题。但是性能问题和不一致的问题仍然没有根本解决。超时机制也会带来数据不一致的问题,比如在等待提交命令时候超时了,参与者默认执行的是提交事务操作,但是有可能执行的是回滚操作,这样一来数据就不一致了。
3PC 包含了三个阶段,分别是**准备阶段**
、**预提交阶段**
和**提交阶段**
,对应的英文就是:CanCommit、PreCommit 和 DoCommit。
优缺点
3PC 相对于 2PC 做了一定的改进:引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。
所以 2PC 和 3PC 都不能保证数据100%一致,因此一般都需要有定时扫描补偿机制。
补偿事务(TCC)
2PC 和 3PC 都是**数据库层面**
的,而 TCC 是**业务层面**
的分布式事务,就像我前面说的分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候 TCC 就派上用场了!
过程
TCC事务是Try、Commit、Cancel三种指令的缩写,其逻辑模式类似于XA两阶段提交,但是实现方式是在代码层面来实现。
TCC 指的是Try - Confirm - Cancel。
- Try 指的是预留,即资源的预留和锁定,注意是预留。
- Confirm 指的是确认操作,这一步其实就是真正的执行了。
- Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
从思想上看和 2PC 差不多,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚。
比如说一个事务要执行A、B、C三个操作,那么先对三个操作执行预留动作。如果都预留成功了那么就执行确认操作,如果有一个预留失败那就都执行撤销动作。
优缺点
- TCC流程还是很简单的,难点在于业务上的定义,对于每一个操作都需要定义三个动作分别对应
Try - Confirm - Cancel
。因此 TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。 - 撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。
- 相对于 2PC、3PC ,TCC 适用的范围更大,但是开发量也更大,毕竟都在业务上实现,而且有时候你会发现这三个方法还真不好写。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。
本地消息表(异步确保)
本地消息表其实就是利用了各系统本地的事务
来实现分布式事务。
本地消息表顾名思义就是会有一张存放本地消息的表
,一般都是放在数据库中,然后在执行业务的时候将业务的执行
和将消息放入消息表中
这2个操作放在同一个事务中
,这样就能保证消息放入本地表中业务肯定是执行成功的。然后再去调用下一个操作,如果下一个操作调用成功了,消息表的消息状态可以直接改成已成功。如果调用失败,会有后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。这时候有可能消息对应的操作不成功,因此也**需要重试**
,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
可以看到本地消息表其实实现的是**最终一致性**
,容忍了数据暂时不一致的情况。
MQ事务消息
利用消息中间件来异步完成事务的后一半更新,实现系统的最终一致性。这个方式避免了像XA协议那样的性能问题。
如何通过消息实现事务
- 第一步先给 Broker 发送事务消息即半消息,半消息是指这个消息对消费者来说不可见,发送成功后,发送方再执行本地事务。
- 再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。
RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
- 如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
- 如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
最大努力通知
最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
总结
2PC 和 3PC 是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面。
TCC 是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大,每一个操作都需要实现对应的三个方法。
本地消息、事务消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。
分布式事务本身是一个技术难题,是
没有一种完美的方案应对所有场景的
,具体还是要根据业务场景去抉择。