先来讲一下什么是本地事物和分布式事物:平时我们写java代码在事物都会都会加@Transactional注解来保证事物的一致性,如下就是两个事物,如果都是通过这个注解的话,只要方法异常就回滚,看似没有问题;但是在分布式事物过程中,只要涉及到网络就是不可靠的,比如在方法中,远程调用成功了,但是因为网络原因迟迟没有返回,导致你认为是失败了,整个事物回滚,最终出现事物不一致;就如下图,李四金额增加成功了,由于网络原因返回超时了,但是张三回滚了,事物不一致,那么我们该如何解决分布式事物的问题呢?
目前我们都是采用CAP的AP比较多,在AP的前提下,我们引出了BASE(基本可用、软状态、最终一致性)理论,然后保证消息的最终一致性。BASE理论就是牺牲强一致性获得可用性,当出现故障的时候要允许部分不可用,保证基本核心功能可用,允许数据在一段时间内是不一致的,但是最终要一致,软状态就是引入一些中间状态,最终这个中间状态会变成最终状态,比如引入支付中,用户可以去查询,但是最终一定是支付成功或者支付失败;
那么针对上面场景,业界分布式事物解决方案又2PC、TCC、可靠消息最终一致性、最大努力通知等;
2PC:(简单来说就是准备阶段和提交阶段,在准备阶段先各自去执行本地事物,如果失败了,那就回滚,再次期间所有的本地事物都会加锁,直到整个分布式系统的事物完成,性能较差,但是思路比较简单,依赖数据库要实现XA协议)
两阶段提交提交协议 2PC(准备阶段 prepare 提交阶段 commit ):需要有两个角色协调者和参与者,一个协调者(Coordinator)来统一掌控所有参与者(worker)的操作结果;
常用的数据库Mysql、Oracle中也有两阶段提交协议,总的来说就是在准备阶段,协调者会给每个参与者发送prepare消息,让他们各自去执行对应的本地事物,如果所有参与者都执行成功并且返回消息,则协调者就会发送commit命令,如果失败者发送回滚命令;
3PC:
三段提交(3PC)是二阶段提交(2PC)的一种改进版本 ,为解决两阶段提交协议的阻塞问题,上边提到两段提交,当协调者崩溃时,参与者不能做出最后的选择,就会一直保持阻塞锁定资源。
虽然 3PC 用超时机制,解决了协调者故障后参与者的阻塞问题,但与此同时却多了一次网络通信,性能上反而变得更差,也不太推荐。
TCC:(性能好、但是代码侵入强、需要处理空回滚、悬挂、幂等问题,常见实现有seata和Hmily)(其实就是分为T、C、C三个阶段,在try阶段所有分支事物都要尝试各自去执行自己的分支事物,如果成功了,则都进入提交阶段,如果失败了,则都执行cancel方法进行补偿;TCC主要在业务层,对代码侵入大,所以代码灵活度高,因为不涉及锁,也不依赖数据库本身的事物,所以性能比传统的2PC方案要高)
所谓的 TCC 编程模式,也是两阶段提交的一个变种,不同的是 TCC 为在业务层编写代码实现的两阶段提交。TCC 分别指 Try、Confirm、Cancel ,一个业务操作要对应的写这三个方法。要处理幂等校验、try悬挂问题、cancel空回滚问题
以下单扣库存为例,Try 阶段去占库存,Confirm 阶段则实际扣库存,如果库存扣减失败 Cancel 阶段进行回滚,释放库存。
TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则所有事物都通过 Cancel 来进行回滚补偿,这也就是常说的补偿性事务。
原本一个方法,现在却需要三个方法来支持,可以看到 TCC 对业务的侵入性很强,而且这种模式并不能很好地被复用,会导致开发量激增。还要考虑到网络波动等原因,为保证请求一定送达都会有重试机制,所以考虑到接口的幂等性。
可靠性消息最终一致性(本地消息表方案和RocketMQ事物消息方案):总的来说就是发起方一定会把消息发送给消息中间件然后同时执行完本地事物,消息中间件一定会把消息发送给远端服务,远端服务最终要保证消息一致。但是消息生产者无法回滚,
远端一定要保证最终一致性;
本地消息表方案:总的来说就是A和B有一个分布式事物,A在执行完本地事物会把消息插入到本地数据库消息表,同时开启定时任务把消息发送给B,当B收到消息以后处理完会把处理的消息也更新到自己的本地消息表B和A系统的表(刷成完成),表示事物完成,
如果B操作失败了,那么A就会扫描到有未处理的事物,然后发给B继续处理。---------因为性能原因,目前使用较少,主要采用MQ的事物消息
RocketMQ事物消息(Kafka现在也支持事物消息了,需要实现消息回调和事物回查)
订单系统向 MQ 发送一条预备扣减库存消息,MQ 保存预备消息并返回成功 ACK,订单消息实现消息回调,订单系统接收到预备消息执行成功 ACK,订单系统执行本地下单操作,下单成功以后,就会返回commit给消息队列,同时
为防止消息发送成功而本地事务失败,订单系统会实现 MQ 的回调接口,其内不断的检查本地事务是否执行成功,如果失败则 rollback 回滚预备消息;成功则对消息进行最终 commit 提交。
MQ事务消息:前提消息系统需要支持事务如RocketMQ,在本地事务执行前,发送事务消息prepare,本地事务执行成功,发送事务消息commit,实现分布式事务最终一致性。如果事务消息commit失败,RocketMQ会回查消息发送者确保消息正常提交,如果步骤5执行失败,进行重试,达到最终一致性。
基于消息中间件的两阶段提交方案,通常用在高并发场景下使用,牺牲数据的强一致性换取性能的大幅提升,不过实现这种方式的成本和复杂度是比较高的,还要看实际业务情况。
最大努力通知(主要是通知):就是一般支付宝和微信支付成功以后,它需要做回调通知你,它只能是最大努力通知你。它也会用消息队列,支付成功以后,它会把消息写入消息队列,然后自己有一个通知应用去消费,消费就会通知你,如果消费失败会利用MQ的
重试机制,所以通过多次重试通知提高一致性(消费端要实现幂等),同时它在支付成功以后会开始业务补偿查询接口,给业务方去主动查询。
2PC | TCC | 可靠消息 | 最大努力通知 | |
一致性 | 强一致性 | 强一致性(需要自己保证) | 最终一致性 | 最终一致性 |
吞吐量 | 低 | 中 | 高 | 高 |
实现复杂度 | 易 | 难(需要写TCC) | 中 | 易 |
Seata:
Seata 也是从两段提交演变而来的一种分布式事务解决方案,提供了 AT、TCC、SAGA 和XA 等事务模式,这里重点介绍 AT模式。既然 Seata 是两段提交,那我们看看它在每个阶段都做了点啥?下边我们还以下单扣库存、扣余额举例。
先介绍 Seata 分布式事务的几种角色:
Transaction Coordinator(TC): 全局事务协调者,用来协调全局事务和各个分支事务(不同服务)的状态, 驱动全局事务和各个分支事务的回滚或提交,此外TC需要单独安装。
Transaction Manage(TM): 事务管理者,业务层中用来开启/提交/回滚一个整体事务(在调用服务的方法中用注解开启事务)。
Resource Manager(RM): 资源管理者,一般指业务数据库代表了一个分支事务(Branch Transaction),管理分支事务与 TC 进行协调注册分支事务并且汇报分支事务的状态,驱动分支事务的提交或回滚。
Seata 实现分布式事务,设计了一个关键角色 UNDO_LOG (回滚日志记录表),我们在每个应用分布式事务的业务库中创建这张表,这个表的核心作用就是,将业务数据在更新前后的数据镜像组织成回滚日志,备份在 UNDO_LOG 表中,以便业务异常能随时回滚。