前言
温馨提示:文章很干,请多喝水
什么是分布式事务
什么是事务想必大多数朋友应该都很清楚了,不清楚的可以看前面的文章《就这?一篇文章让你读懂 Spring 事务》。
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单来说,就是一个大的操作由 N 个小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。比如存在一个订单的微服务,一个库存的微服务,当订单完成需要同步减少库存,这时候就要在事务上确保完整和一致。
相关理论
关于事务的特性(ACID)和隔离级别这里就不再重复介绍了,可以看前面的文章,这里着重介绍下两个新的知识:CAP 理论和 BASE 理论。
CAP 理论
- 一致性(Consistency):在分布式系统完成某写操作后任何读操作,都应该获取到该写操作写入的那个最新的值。相当于要求分布式系统中的各节点时时刻刻保持数据的一致性。
- 可用性(Availability): 一直可以正常的做读写操作。简单而言就是客户端一直可以正常访问并得到系统的正常响应。用户角度来看就是不会出现系统操作失败或者访问超时等问题。
- 分区容错性(PartitionTolerance):指的分布式系统中的某个节点或者网络分区出现了故障的时候,整个系统仍然能对外提供满足一致性和可用性的服务。也就是说部分故障不影响整体使用。事实上我们在设计分布式系统是都会考虑到 bug、硬件、网络等各种原因造成的故障,所以即使部分节点或者网络出现故障,我们要求整个系统还是要继续使用的
CAP 是一个已经被证实的理论,在分布式系统中最多只能同时满足这三项中的两项,而分区容错性是分布式系统必须满足的,所以在分布式系统中常见的组合就是 CP 和 AP
- CP:放弃可用性,注重一致性和分区容错性,其实这就是所谓的强一致性,可能在银行跨行转账这种强一致业务场景才会用到,具体得根据业务场景做取舍。
- AP:放弃强一致性,注重可用性和分区容错性,这是现在绝大多数分布式业务场景的选择,只要最后能保证最终一致性( BASE 理论)即可。
BASE 理论
- 基本可用(Basically Available):基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
- 软状态(Soft State):软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。MySQL Replication 的异步复制也是一种体现。最终一致性(Eventual Consistency):最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
常见解决方案
1、两阶段提交
两阶段提交(Two-phaseCommit),简称为 2PC,两阶段提交是一种强一致性设计,它引入一个事务协调者的角色来协调管理各个参与者(也可称之为各本地资源)的提交和回滚。
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。
- 准备阶段(Prepare Phase):首先协调器会向所有的参与者发送准备提交或者取消提交的请求,然后会收集参与者的决策。
- 提交阶段(Commit Phase):协调者会收集所有参与者的决策信息,当且仅当所有的参与者向协调器发送确认消息时协调器才会提交请求,否则执行回滚或者取消请求。
2PC 存在的问题:
- 同步阻塞:所有的参与者都是事务同步阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 单点故障:一旦协调者发生故障,系统不可用。
- 数据不一致:当协调者发送 commit 之后,有的参与者收到 commit 消息,事务执行成功,有的没有收到,处于阻塞状态,这段时间会产生数据不一致。
- 不确定性:当协调者发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与协调器同时宕机之后,重新选举的协调器无法确定该条消息是否提交成功。
2PC 的优势在于对业务没有侵入,可以利用数据库自身机制来进行事务的提交和回滚。
常见的基于 2PC 的具体落地方案有:JTA(XA 规范) 和 Seata( AT 模式)。
2、三阶段提交
三阶段提交(Three-phase commit),简称为 3PC,是 2PC 的改进版本。同时在协调者和参与者都引入了超时机制,还在 2PC 中的准备阶段和提交阶段中间增加了一个预提交阶段。
- 准备阶段(CanCommit):协调者向各个参与者发送请求,询问是否可以执行事务,但并不执行事务。
- 预提交阶段(PreCommit):如果从协调者得到的反馈是满足执行条件,那么就发送预提交请求,并开始执行事务;如果从协调者得到的反馈是不满足执行条件或者超时,则发送事务中断请求。
- 提交阶段(DoCommit):如果预提交阶段发送的是预提交请求,那么正常提交事务;如果预提交阶段发送的是事务中断请求,那么直接中断事务。
相对于 2PC,3PC 主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行 commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的中断响应没有及时被参与者接收到,那么参与者在等待超时之后执行了 commit 操作。这样就和其他接到中断命令并执行回滚的参与者之间存在数据不一致的情况。而且 3PC 整体的交互过程更长,性能也会有所下降。
3PC 目前似乎只存在于理论,还没有具体落地方案。
3、TCC
2PC 和 3PC 都是依赖于数据库的事务提交和回滚,但是有时候很多业务并不仅仅只涉及到数据库,可能还会发送短息、消息等等,而 TCC 就是属于业务层面或者说是应用层面的分布式事务。
TCC 方案分为Try-Confirm-Cancel三个阶段,属于补偿性分布式事务。
- Try 阶段:完成所有业务检查(一致性),预留业务资源(准隔离性)
- Confirm 阶段:确认执行业务操作,不再做任何业务检查, 只使用Try阶段预留的业务资源。
- Cancel 阶段:取消Try阶段预留的业务资源。
有的朋友可能会问了,Try 成功了会执行 Confirm,失败了会执行 Cancel,那 Confirm 阶段失败了怎么办?这时候只能设置重试机制,不断重试调失败的 Confirm,直到成功为止,真有怎么也不成功的,就只能人工介入了。
TCC 需要根据每个场景和业务逻辑来设计相应的操作,所以很大程度增加了业务代码的复杂度,对业务有很大的侵入。
虽说对业务有侵入,但是 TCC 没有资源的阻塞,每一个方法都是直接提交事务的,如果出错是通过业务层面的 Cancel 来进行补偿,所以也称补偿性事务方法。
TCC 要注意的几个问题:
- 幂等问题:因为网络调用无法保证请求一定能到达,所以都会有重调机制,因此对于 Try、Confirm、Cancel 三个方法都需要幂等实现,避免重复执行产生错误。
- 空回滚问题:指的是 Try 方法由于网络问题没收到超时了,此时事务管理器就会发出 Cancel 命令,那么需要支持 Cancel 在未执行 Try 的情况下能正常的 Cancel。
- 悬挂问题:这个问题也是指 Try 方法由于网络阻塞超时触发了事务管理器发出了 Cancel 命令,但是执行了 Cancel 命令之后 Try 请求到了。所以空回滚之后还得记录一下,防止 Try 的再调用。
4、本地消息表
本地消息表分布式事务解决方案是国外的 eBay 提出的一套方案。其实就是利用了各系统本地的事务来实现分布式事务,在数据库中存放一张事务消息表,在执行业务操作的时候, 将业务的执行和将消息放入消息表中的操作放在同一个事务中。
本地事务执行成功之后再调用其他服务,如果成功了就将消息表里的消息状态改为成功,如果失败了,则由定时任务去读取本地事务表中未成功的消息,再去调用相应的服务,成功后再次修改状态。
这里也要设置重试机制,一旦有实在不成功的,还需人工介入。这里要注意的是,也要保证对应服务的方法幂等性。
可以看出,本地消息表实现比较简单,是一种最大努力通知思想,实现的是最终一致性,容忍了数据暂时不一致的情况。
缺点是严重依赖数据库。
5、可靠消息最终一致性方案
在上面的本地消息表方案中,生产者需要额外创建消息表,还需要对本地消息表进行轮询,业务负担较重。阿里开源的 RocketMQ 4.3 之后的版本正式支持事务消息,该事务消息本质上是把本地消息表放到 RocketMQ 上,解决生产端的消息发送与本地事务执行的原子性问题。
服务 A,先给 Broker (消息中间件) 发送一个 Half Message(半消息),其实这个半消息已发送到 Broker 端,但是此消息的状态被标记为"不能投递",消费者还看不到,处于这种状态下的消息称为半消息。
发送完 半消息后,服务A 执行业务操作(本地事务),再根据操作结果:如果成功,则向 Broker 发送 一个 Commit 命令,这时半消息就变成了可以被消费者消息;如果失败,则发送一个 RollBack 命令,该消息则会被删除。
如果是 Commit 那么服务 B 就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
如果 RocketMQ 没有收到服务 A 确认状态的消息,那么半消息 RocketMQ 会自动定时轮询回调你的接口,询问这个处理的处理情况。借助这点,服务A实现一个回调,根据实际处理结果 Commit 或者 Rollback,加强一致性判断。
在服务 B 执行的过程中也可能会失败,这时也是需要重试,一直执行不成功也需要人工介入,同时也需要保证服务 B 方法的幂等性。
6、最大努力通知
最大努力通知型( Best-effort delivery)是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。
就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了
事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
总结
其实分布式事务解决方案还有很多,但是各自还是会存在很多问题,极端情况下也都需要人工去处理,而且大大提高了流程的复杂度,会带来很多额外的开销。
所以谨记,在真实的开发过程中,能不使用分布式事务就不要使用!
后面会给大家带来分布式事务的实战,没点关注的可以点个关注,防止走丢了。