《分布式事务解决方案》
将一个整体模块拆分为多个微服务,某些业务场景需要同时操作多个原子服务的数据,分布式事务就是用来保证多个原子服务数据源一致性的解决方案。
分布式事务产生的原因?
数据库分库分表:由于单表的数据量巨大需要分库分表,分库分表之后,此时一个操作可能涉及访问多个数据库,为了保证数据一致性,就需要用到分布式事务。 应用SOA化:指的是业务的服务化,将一个整体的系统拆分为多个子系统,每个子系统都有自己的数据库,为了保证数据一致性,就需要用到分布式事务。
常见的分布式解决方案
1. 两阶段提交/XA(2PC)
两阶段提交的算法思路可以概括为:参与者将操作成功或失败的结果通知协调者,再由协调者根据所有参与者的反馈情况决定个参与者是否要提交操作还是中止操作。
第一阶段:请求阶段(投票阶段)
- 协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果。
- 事务参与者收到请求之后,本地执行事务,但不提交。
- 参与者将自己事务执行情况反馈给协调者,同时等待协调者的下一步通知。
第二阶段:提交阶段(执行阶段)
在第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在三种可能:
- 所有的参与者回复能够正常执行事务。
- 协调者向各个参与者发送commit通知,请求提交事务。
- 参与者收到事务提交通知之后,执行commit操作。
- 参与者向协调者返回事务commit结果信息。
- 一个或多个参与者回复事务执行失败。
- 协调者等待超时。
- 协调者向各个参与者发送事务rollback通知,请求回滚事务。
- 参与者收到事务回滚通知之后,执行rollback操作。
- 参与者向协调者返回事务rollback结果信息。
两阶段提交的缺点
- 同步阻塞:执行过程中,所有参与者的节点都是事务阻塞型的。当参与者占用公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 单点故障:由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞,尤其时在第二阶段,协调者发生故障,那么所有的参与者都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
- 数据不一致:在第二阶段中,当协调者想参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接收到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接收到commit请求的节点则无法提交事务。于是,整个分布式系统就出现了数据不一致的现象。
两阶段提交无法解决的问题
当协调者和参与者同时出现故障时,两阶段提交无法保证事务的完整性。如果调者在发出commit消息之后宕机,而唯一接收到commit消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,因为没人知道事务是否已经被提交。
2. 三阶段提交
第一阶段:CanCommit
协调者向参与者发送事务执行请求CanCommit,参与者如果可以提交就返回YES响应,否则就返回NO响应。
第二阶段:PreCommit
协调者根据参与者反馈的结果来决定是否继续执行事务的PreCommit操作,根据协调者反馈的结果,有以下两种可能:
- 假如协调者收到参与者的反馈结果都是YES,那么就会执行PreCommit操作。
- 发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段。
- 事务预提交:参与者接收到PreCommit请求后,执行事务操作。
- 响应反馈:事务操作执行成功,则返回ACK响应,然后等待协调者的下一步通知。
- 假如有任何一个参与者向协调者发送了NO响应,或者等待超时之后,协调者没有收到参与者的响应,那么就中断事务。
- 发送中断请求:协调者向所有参与者发送中断请求。
- 中断事务:参与者收到中断请求之后(或超时之后,仍未收到协调者的请求),执行事务中断操作。
第三阶段:DoCommit
- 执行提交
- 发送提交请求:协调者收到ACK之后,向所有的参与者发送DoCommit请求。
- 事务提交:参与者收到DoCommit请求之后,提交事务。
- 响应反馈:事务提交之后,向协调者发送ACK响应。
- 完成事务:协调者收到ACK响应之后,完成事务。
- 中断事务 在第二阶段,协调者没有收到参与者发送的ACK响应,那么就会执行中断事务。
3. 补偿事务(TCC)
TCC是一种比较成熟的分布式事务解决方案,可用于解决跨库操作的数据一致性问题,适用于公司内部对一致性、实时性要求较高的业务场景。其中Try、Confirm、Cancel 3个方法均由业务编码实现。其中Try操作为第一阶段,负责资源的检查和预留;Confirm操作为第二阶段,执行真正的业务操作;Cancel时执行取消(回滚)操作。 业务实现TCC服务之后,该TCC服务将作为分布式事务的其中一个资源,参与到整个分布式事务中;事务管理器分两阶段协调的TCC服务,第一阶段调用所有TCC服务的Try方法,在第二阶段执行所有TCC服务的Confirm或者Cancel方法。
实现TCC服务时注意事项
- 业务操作分两阶段完成 接入TCC前,业务操作只需要一步就能完成,但是在接入TCC之后,需要考虑如何将其分成2阶段完成,把资源的检查和预留放在一阶段的Try操作中进行,把真正的业务操作的执行放在二阶段的Confirm操作中进行。TCC服务要保证第一阶段Try操作成功之后,二阶段Confirm操作一定能成功。
2. 允许空回滚事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行。
3. 防悬挂控制事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况;用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求。
4. 幂等控制无论是网络数据包重传,还是异常事务的补偿执行,都会导致TCC服务的Try、Confirm或者Cancel操作被重复执行;用户在实现TCC服务时,需要考虑幂等控制,即Try、Confirm、Cancel 执行一次和执行多次的业务结果是一样的。
举例,假入 Bob 要向 Smith 转账,思路大概是: 1. 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。 2. 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。 3. 如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
4. 本地消息表
基于本地消息的最终一致性方案的最核心做法就是在执行业务操作的时候,记录一条消息数据到DB,并且消息数据的记录与业务数据的记录必须在同一个事务内完成,这是该方案的前提核心保障。在记录完成后消息数据后,后面我们就可以通过一个定时任务到DB中去轮询状态为待发送的消息,然后将消息投递给MQ。这个过程中可能存在消息投递失败的可能,此时就依靠 重试机制 来保证,直到成功收到MQ的ACK确认之后,再将消息状态更新或者消息清除;而后面消息的消费失败的话,则依赖MQ本身的重试来完成,其最后做到两边系统数据的最终一致性。基于本地消息服务的方案虽然可以做到消息的最终一致性,但是它有一个比较严重的弊端,每个业务系统在使用该方案时,都需要在对应的业务库创建一张消息表来存储消息。
5. MQ事务消息
RocketMQ中间件能够支持一种事务消息机制,确保本地操作和发送消息的异步处理达到本地事务的结果一致。
- 第一阶段,生产者在执行事务之前,首先向MQ发送一个Prepare消息(消息保存在broker中,不会被消息者看到),RocketMQ能够拿到消息的地址。
- 第二阶段,生产者执行本地事务操作。
- 第三阶段,确认消息发送,通过第一阶段拿到的地址取访问消息,并修改状态。
注:如果确认消息发送失败, RocketMQ会定期扫描消息集群中的事务消息 ,如果发现了Prepare消息,它会向消息的发送者确认本地事务是否已执行成功,然后再根据业务实现的策略决定时继续发送还是回滚(消息生产者需要设置监听)。
6. 最大努力通知
举例,订单支付之后,支付宝向商户推送支付结果,如果商户没有回复Success,支付宝会重复推送N次支付结果。
感谢您耐心看完了文章...
关注作者:JAVA高级程序员
我会不定期在微头条发放:(Java工程化、分布式架构、高并发、高性能、深入浅出、微服务架构、Spring、MyBatis、Netty、源码分析)等技术学习资料,以及Java进阶学习路线图。