分布式事务(基于Seata解决分布式事务的四种模式)
本篇文章讲述了分布式事务的基本概念,以及基于Seata框架解决分布式事务的四种模式:XA,TA,TCC,SAGA模式。
分布式事务
接下来会基于Seata这个框架来进行学习。
一、分布式事务简介:
1.本地事务
本地事务,也就是传统的单机事务,一般关系型数据库事务种都满足了这一点。基本上满足四个原则(ACID):原子性,一致性,隔离性,持久性
- 原子性(A):事务中的一切行为,要么一起成功,要么一起失败。(大家可以认为,有福同享,有难同当。这个应该不用举例。)
- 一致性(C):事务按照预期生效,数据的状态是预期的状态。(这样说太抽象了,举个例子来说就是,张三转账了520给李四,张三这边账户余额扣了520,李四也收到了520,大家都很开心,这是一致。而如果张三扣了520,李四却没收到,这就是不一致)
- 隔离性(I):对同一资源操作的事务不能同时发生。
- 持久性(D):一个事务一旦被提交,它对数据库中的数据的改变就是永久性的,哪怕数据库发生故障也不会有影响。
q:本地事务已经可以很好的完成ACID,为什么要引入分布式事务?
a:一般单体架构中,基于数据库本身的特性,其实已经可以满足ACID了。但是,如果放到微服务中,可能一个业务就会跨越多个服务,每个服务又有各自的数据库。这个时候如果单单靠数据库本身的特性,就无法保证ACID了。
2.分布式事务
分布式事务,就是不再单个服务或者单个数据库中产生的事务,一般有:跨数据源的分布式事务,跨服务的分布式事务。
在我们实际开发中,往往一个业务操作不可能只经过一个服务,也不可能只调用一个数据库。比如,电商平台,我们一整个下单付款业务中,会有以下几个行为:
- 创建订单
- 按照你的订单扣减商品库存
- 从用户的账户余额中扣除金额
完成上面操作我们需要访问3个服务和3个不同的数据库
分别对应:
服务 | DB | 操作 |
---|---|---|
订单服务 | 订单DB | 创建订单 |
账户服务 | 账户余额DB | 扣除余额 |
库存服务 | 商品库存DB | 扣减商品库存 |
按照已有的传统数据库,单个服务和数据库内是一个本地事务,是可以满足ACID原则的,但是如果这三件事整体看作一个”操作“,要满足这个”操作“的原子性,要么行为一起成功,要么一起失败,这就是分布式系统下的事务了。
假设,库存只剩下2个,而我们的订单下单了10个,因为不处于同一个服务,同个数据库,因为各自的本地事务,订单会成功创建写入数据库,而库存却会报错,不能更新,而账户余额也会相应减少,可想而知,这是多大的危害!所以这就是我们使用分布式事务的原因。
二、CAP定理
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分区容错性)
1.CAP参数详解
由图可以看出,这三个指标永远不可能同时做到。
1.一致性(C):用户访问分布式系统中的任意节点,得到的数据必须一致。
假设我们有两个节点,其中初始数据都为1,当我们修改了其中一个节点,修改为2,则两者的数据产生了差异,要想保证一致性,就必须要实现两个节点的数据同步。
2.可用性(A):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
假设我们有三个节点,一开始都可用,但是因为网络故障或其他原因,导致其中有节点不可用,访问则被阻塞或拒绝,则不能满足可用性。
3.分区容错性(P):分区代表因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。这听起来跟可用性的错误差不多,但是它还有个指标叫容错,容错代表在集群出现分区时,整个系统也要持续对外提供服务。这也是它跟可用性的错误差别最大的一块。
假设我们有三个节点,有一个节点因为网络故障或其它原因导致与其他节点断开连接,无法数据同步,但是还可对外提供服务,这就是分区容错性。
2.CAP矛盾
我们都知道,在分布式系统中,他们之间的网络不可能100%保证健康,一定会有故障的时候,所以分区一定会出现,而服务有必须对外保证服务。所以分区容错性(P)不可以避免。
如果此时要保证一致性(C),就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
如果此时要保证可用性(A),就不能等待网络恢复,那节点之间就会出现数据不一致。
所以说,在P一定会出现的情况下,A和C之间只能实现一个。
如此看来,只有理想情况下,CA才可以实现,不考虑由于网络不通或结点挂掉的问题。但是在目前应用的场景中,节点多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到最大化,并要达到良好的响应性能来提高用户体验,因此一般都会做出如下选择:保证 P 和 A ,舍弃 C 的强一致性,但保证最终一致性。所以在这之后又推出了BASE理论
三、BASE理论与解决分布式事务的思路
1.BASE理论(CAP的一种解决思路)
BASE理论是对CAP的一种解决思路,包含三个思想:
- Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
2.解决分布式事务的思路(基于CAP与BASE)
我们分布式事务会出现问题,主要是因为要处理的事务中,往往会包含多个子事务,他们会各自执行与提交,结果他们一些成功,一些失败。他们的状态不一致。我们只需要保证他们最后的状态一致就可以解决了。
2.1.两种模式(AP、CP)
借鉴CAP定理和BASE理论:我们可以采取两种模式:
- AP模式:各个子事务分别执行与提交,可以有失败有成功的。然后我们只需要统计状态采取补救措施恢复数据就可以了,实现**最终一致。**
比如,有A,B两个子事务,A:扣减库存,B:删除金额。他们两个各自执行完后,如果A成功了,直接提交,B没成功,回滚,我们通过他们的状态得需要采取补救措施,只需将A扣减的库存加回来即可。
- CP模式:各个子事务执行后互相等待,等都执行完了,统计状态,决定是同时提交还是同时回滚,**达成强一致。**但是在等待的过程中,处于 弱可用状态。
比如,有A,B两个子事务,A:扣减库存,B:删除金额。他们两个各自执行完后,如果A成功了,等待B执行完成,B执行完了,失败了,则他们一起回滚。
2.2.TC(事务协调者)
从上面的描述来看,我们会发现不管哪种模式,要解决分布式事务,各个子系统之间必须要能感知到彼此的事务状态,这样才能保证状态一致,因此需要一个事务协调者来协调(TC)每一个事务的参与者(子系统事务)。
其中,我们把子系统事务子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务。
3.总结
q:简述BASE理论的三个思想
a:基本可用、软状态、最终一致
q:解决分布式事务的思想和模型
a:首先事务中分为全局事务与分支事务
- 全局事务:整个分布式事务
- 分支事务:分布式事务中的各个子事务
解决分布式事务的思路一般分为两个模式:
- AP模式(最终一致思想)
各分支事务分别执行并提交,如果有失败(不一致)的情况,再想办法恢复数据
- CP模式(强一致思想)
各分支事务执行完后不提交,等待彼此的执行结果,然后一起提交或者一起回滚
四、初识Seata
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。
1.Seata的架构
Seata事务管理中有三个重要的角色:
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
整体的架构如图:
关于这个架构,我个人的想法。TM是管理板块,用来决定,哪些是本次业务中需要用到的事务(分支)。就像订单服务,我们需要1.添加新订单,2.扣除库存,3.扣除金额。而TM就是管理这三个事务,让他们开始执行。而RM则是每个事务的监督者,他会想TC也就是事务协调者报告,自己事务的执行状态,然后TC再通过RM报告过来的状态,从而告诉TM是提交事务还是回滚事务。从而达到一致性。
2.分布式事务的四种解决方案
Seata基于上述架构提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
- TCC模式:最终一致的分阶段事务模式,有业务侵入
- AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
- SAGA模式:长事务模式,有业务侵入
无论哪种方案,都离不开TC,也就是事务的协调者。
关于TC的部署与集成。我放在我的另一篇文章中了。想了解详情的小伙伴,可以移步至:seata的部署与微服务集成(包含多集群异地容灾配置)
这里就不详细讲解了。下面我们就一起学习下Seata中的四种不同的事务模式。
五、Seata解决分布式事务的四种模式
1.XA模式
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
1.1.XA模式的两个阶段
- 成功(事务都执行成功):第一阶段也称为准备阶段,事务协调者(TC)通知RM可以开始准备执行事务,然后RM执行完成后,告知TC自己本次事务的执行状态,TC通过判断每个RM提交过来的状态,进行判断下一阶段要做的事情,也就是告知RM可以去提交事务了。执行流程如图:
- 失败(全局事务中有分支事务执行失败):跟成功状态一样的执行流程,只不过在RM告知TC有失败状态时候,TC在第二阶段会在执行成功的子事务进行回滚还原,执行流程如图:
需要注意的是
在一阶段中:
事务协调者通知每个事物参与者执行本地事务
本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁
在二阶段中分执行成功与失败两种情况执行不同的操作:
如果一阶段都成功,则通知所有事务参与者,提交事务
如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务
1.2.Seata的XA模型
Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:
RM一阶段的工作:
- 注册分支事务到TC
- 执行分支业务sql 但不提交
- 报告执行状态到TC
TC二阶段的工作:
TC检测各分支事务执行状态(分为成功与失败两种):
- 如果都成功,通知所有RM提交事务
- 如果有失败,通知所有RM回滚事务
RM二阶段的工作:
通过接收到的TC指令,选择提交或者回滚事务,完成后才会释放数据库锁
1.3.XA模型的优缺点
优点:
事务的强一致性,满足ACID原则。
常用数据库都支持,实现简单,并且没有代码侵入
缺点:
因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
依赖关系型数据库实现事务
1.4.实现XA模式
Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:
1)修改application.yml文件(每个参与事务的微服务),开启XA模式:
seata:
data-source-proxy-mode: XA
这里我参与的三个事务分别是订单服务(order-service),库存服务(stock-service),余额服务(account),所以分别要配置到这三个服务的yml文件中,如图:
2)给发起全局事务的入口方法添加@GlobalTransactional注解:
本例中是OrderServiceImpl中的create方法,因为他操作了三个库。
@GlobalTransactional负责开启全局事务,只与seata服务端交互,而@Transactional管理的是本地数据库的事务,两者可以同时使用,但是本地事务@Transactional的传播特性需设置为 REQUIRES_NEW
**注意seata的tc集群一定要对应好,不然找不到
3)重启服务并测试
重启三个服务,我们可以看到tc-server的控制台页面显示:
这样就说明成功了。
这时候我们用Postman发送请求:(此时我的数据库里面库存只有4个,而我的请求需要10个,库存扣减肯定会失败)
而这时候,我们来查看idea中各个服务的控制台打印信息:
这样就说明我们的XA模式测试成功了。并且数据库里面的数据并没有发生任何改变。说明三个子事务,成功的事务也进行了回滚
2.AT模式
上面我们提到的XA模式,数据的一致性有很大的保障。但是他必须等待所有事务全部完成,才能释放数据库锁。这无疑不可避免的造成了一些执行快的事务的等待时间。而AT模式解决了这一点。
AT模式同样是分阶段提交的事务模型,不过却弥补了XA模型中资源锁定周期过长的缺陷。
2.1.Seata的AT模型
基本流程图:
我们通过流程图也能看到,其实AT模型也是分为两个阶段。
第一阶段:
首先TM开启全局事务,然后全局事务中的每个子事务被调用,RM开始往TC上注册分支事务,这时候记录undo-log(数据快照),接着执行sql语句并提交,最后向TC报告事务执行状态
第二阶段
- 如果成功,则参与全局事务的子事务一起删除undo-log(数据快照)
- 如果失败,则按照undo-log的记录,参与全局事务的子事务一起回滚。
2.2.详细解析事务整体流程
接下来,我举个例子,方便理解。
假设现在有一个数据表,里面存放着用户的余额。数据表如下:
id | money |
---|---|
1 | 100 |
这个时候我们买了个商品,需要对自己本身的余额进行 -10 的操作,所以我们需要执行的sql语句为:
update tb_account set money = money - 10 where id = 1
AT模式下:
- TM首先发起并注册全局事务到TC
- TM调用分支事务
- 分支事务准备执行业务SQL
- RM拦截业务SQL,根据where条件查询原始数据,形成快照。
{
"id": 1, "money": 100
}
是一个json格式的文件
- RM执行业务SQL,提交本地事务,释放数据库锁。此时
money = 90
- RM报告本地事务执行状态给TC
以上是第一阶段做的事,而接下来第二阶段。分成两种情况
-
TM通知TC事务结束
-
TC检查分支事务状态
此时,如果如果参与全局事务的每个子事务都成功,则立刻删除快照。 如果,有分支事务失败,则需要回滚。读取快照数据(`{"id": 1, "money": 100}`),将快照恢复到数据库。此时数据库再次恢复为100
流程图如下:
2.3.AT与XA的区别
简述AT模式与XA模式最大的区别是什么?
可以看到,我之前一直在重点强调的就是AT模式,他执行完事务后,不会进行等待,而是直接提交。如果最后第二阶段需要回滚,则按照数据快照进行恢复数据。而XA模式,子事务执行完后并不会直接提交,他是在第二阶段,也就是判断所有事务的执行状态后,选择一同回滚或者一同提交。
所以,我们可以得知,AT与XA的区别为:
- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
- XA模式强一致 ;AT模式 最终一致
强一致,简单来说,就是无论什么时候查看数据库,都是正确的数据。而最终一致,要比强一致要差一点。打个比方,订单服务,需要增加一条订单信息,余额扣除100,库存减少1。
- 如果是XA模式的强一致,则是数据库里面一旦更新,就绝对是正确数据。(因为他是第二阶段,判断各个子事务状态后才提交或者回滚)。
- 而如果是AT的最终一致模式,则假设余额扣除了100,已经成功,并且提交。这时候余额数据库已经发生了变化。而等到事务全完成,发现有子事务失败,则余额必须按照数据快照进行恢复。所以数据库又要变回扣除之前的样子。则在这中间的时间,余额数据库,里面的数据并不是正确的。但是事务执行完后,能变成正确的,所以叫**最终一致**
2.4.脏写问题
通过上面解释AT与XA的区别,我们发现因为AT模式的数据为最终一致,所以在未一致的时候,如果有另一个事务也要进行操作,则会出现脏写的问题。如图:
简单来说就是,因为没有保留DB锁,在事务A失败后,要执行恢复数据时,DB正在被其它事务B操作了。此时事务B拿取的值并不是最终的值,又进行了一顿操作,而事务A此时按照原来的快照进行恢复,则会恢复到一开始的最终状态。如果事务B成功了,则事务B的业务sql等于执行无效,如果事务B失败了,则事务A的业务sql虽然失效,但还是因为事务B的数据快照,而“被迫执行成功”。
为了解决脏写的问题,就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
注意:全局锁,并不是DB锁。他只是锁住了当前操作的数据,并不是整张数据表。
2.5.AT模式的优缺点
AT模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好很多
2.6.实现AT模式
其实只要将XA模式改写成AT就可以了。当然默认的就是AT,就不演示了。
3.TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
- Try:资源的检测和预留; 相似AT模式的快照
- Confirm:完成资源操作业务;提交事务,并把资源预留的数据删除。类似于AT模式的提交数据,删除快照
- Cancel:预留资源释放,类似于AT模式的按照快照恢复数据
这么说可能有点抽象,我们几个例子来说明:
举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
- 阶段一(Try):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
我们的余额是100,完全充足,则冻结金额从0增加到30元,可用金额从100减少到70,
此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。
- 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了
此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
- 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
需要回滚,那么就要释放冻结金额,恢复可用金额。
3.1.Seata的TCC模型
Seata中的TCC模型依然延续之前的事务架构,也是TM,RM,TC构成,如图:
3.2.优缺点
TCC模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC的优点是什么?
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
幂等处理:简单来说,就是一个请求调用一次也好,调用多次也好。达成的效果一致。不会因为重复调用而出现问题。Confirm和Cancel可能会执行失败,如果失败,则会一直执行。所以要进行幂等处理,让他们达成的效果是一样的。
3.3.案例介绍
改造account-service服务,利用TCC实现分布式事务
需求如下:
- 修改account-service服务,编写try、confirm、cancel逻辑
- try:添加冻结金额,扣减可用金额
- confirm:删除冻结金额
- cancel:按照冻结金额恢复可用金额
- 保证confirm、cancel的幂等性
- 允许空回滚
- 拒绝业务悬挂
我们先来介绍一下空回滚与业务悬挂。
3.3.1.空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,(没有冻结的数据,也没办法回滚)就是空回滚。
解决办法:
所以我们应该执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。
3.3.2.业务悬挂
业务悬挂是空回滚后,阻塞的try操作恢复,这时候做try的话,事务并没有confirm或者cancel。事务就会一直卡在这里,处于中间状态,这就是业务悬挂。
解决办法:
执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂
3.3.3.实现TCC模式
上面我们了解了空回滚与业务悬挂后,接下来我们来解决这两个问题。
首先我们需要准备一张数据表,用来记录,当前的事务状态,判断当前事务状态是在try,还是confirm,还是cancel。
(1)定义数据表
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) NOT NULL,
`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
`state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
数据库字段解析:
- xid:是全局事务的id
- user_id:该条数据的id
- freeze_money:用来记录用户冻结金额
- state:用来记录事务状态
(2)每个业务的实现详情
-
Try业务
1.记录冻结金额和事务状态到account_freeze表(事务状态为0)
2.扣减account表可用金额 -
Confirm业务
1.根据xid删除account_freeze表的冻结记录 -
Cancel业务
1.修改account_freeze表,冻结金额为0,state为2(将事务的状态记录下来,cancel为2。)
2.修改account表,恢复可用金额 -
如何判断是否空回滚?
在cancel,根据xid查询数据表,判断当前事务状态,如果为null,则说明并没有执行try,只有执行了try,才会有数据,需要空回滚 -
如何避免业务悬挂?
在try业务中,根据xid查询数据表,判断当前事务状态,如果已经存在则证明Cancel已经执行,拒绝执行try业务
(3)声明TCC接口
接下来,我们改造account-service,利用TCC实现余额扣减功能。
TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明。
声明TCC三个接口:
Java代码
@LocalTCC
@LocalTCC
public interface AccountTCCService {
/**
* Try逻辑,@TwoPhaseBusinessAction中的name属性,要对应当前方法名。
* **@TwoPhaseBusinessAction存在哪个方法上,哪个方法就是try方法
*/
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money") int money);
/**
* confirm方法,提交方法。
* 可以重命名,但是必须与@TwoPhaseBusinessAction里的commitMethod指定的名称一致
* @param context 上下文类型,可以获取在try方法的参数
* @return
*/
boolean confirm(BusinessActionContext context);
/**
* 回滚方法。
* 可以重命名,但是必须与@TwoPhaseBusinessAction里的rollbackMethod指定的名称一致
* @param context
* @return
*/
boolean cancel(BusinessActionContext context);
}
(4)编写实现类
在实现类之前,我们需要定义好实体类,以及mapper接口。就是对DB的增加、修改、删除。就不演示了。
下方代码除了实现TCC三个接口外,还实现了幂等,空回滚,防止业务悬挂处理,都做了详细的注释
@Slf4j
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;//注入操作余额表mapper
@Autowired
private AccountFreezeMapper freezeMapper;//注入操作冻结表mapper
/**
* try
*
* @param userId
* @param money
*/
@Override
public void deduct(String userId, int money) {
//1.获取事务ID(通过Seata自带的函数RootContext来获取)
String xid = RootContext.getXID();
//2.避免业务悬挂
AccountFreeze accountFreeze = freezeMapper.selectById(xid);
//2.1.如果查出来不为null,则说明已经cancle过了,避免业务悬挂,我们直接跳过。
if (accountFreeze != null) {
return;
}
//3.执行业务sql
//3.1.增加冻结金额,封装AccountFreeze对象
AccountFreeze freeze = new AccountFreeze();
freeze.setXid(xid);
freeze.setFreezeMoney(money);
freeze.setUserId(userId);
freeze.setState(AccountFreeze.State.TRY);//枚举类型:0.TRY,1.CONFIRM,2.CANCEL
freezeMapper.insert(freeze);
}
/**
* commitMethod
*
* @param context 上下文类型,可以获取在try方法的参数
* @return
*/
@Override
public boolean confirm(BusinessActionContext context) {
//这个幂等可做可不做,因为删除,无论删除多少次,结果都一致。
//执行删除冻结金额(通过上下文对象context获取Xid)
int count = freezeMapper.deleteById(context.getXid());
return count == 1;
}
/**
* rollbackMethod
*
* @param context
* @return
*/
@Override
public boolean cancel(BusinessActionContext context) {
//1.判断是否需要空回滚
AccountFreeze freeze = freezeMapper.selectById(context.getXid());
//如果为null,则说明还没有执行try,说明此时需要空回滚
if (freeze == null) {
//1.1.记录空回滚操作
freeze = new AccountFreeze();
freeze.setXid(context.getXid());
//通过上下文对象获取userId的值
freeze.setUserId(context.getActionContext("userId").toString());
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freezeMapper.insert(freeze);
return true;
}
//幂等判断(防止多次执行,造成的结果不一致。我们只需要判断当前事务状态,如果状态已经执行过了。则不执行)
if (freeze.getState() == AccountFreeze.State.CANCEL) {
//说明已经执行过回滚了,无需重复处理
return true;
}
//2.不需要空回滚,则做数据恢复并删除冻结数据
//2.1.恢复数据
int count = accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
//2.2.将冻结金额清零,状态改为CANCEL
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
return count == 1;
}
}
(5)测试
跟XA模式一样,用Postman发送接口请求,请求一样。
然后我们会看到数据库其实没有发生任何变化,其实就是发生了回滚。回到控制台可以看到详情。
这样就说明我们的TCC模式测试成功了。并且数据库里面的数据并没有发生任何改变。说明三个子事务,成功的事务也进行了回滚
4.Saga模式
Saga算法是一种异步的分布式解决方案。其理论基础在于,其假设所有事件按照顺序推进,总能达到系统的最终一致性,因此 Saga需要服务分别定义提交接口以及补偿接口,当某个事务分支失败时,调用其它的分支的补偿接口来进行回滚。
saga这种事务模式最早来自这篇论文:sagas
在这篇论文里,作者提出了将一个长事务,分拆成多个子事务,每个子事务有正向操作Ti,反向补偿操作Ci。
假如所有的子事务Ti依次成功完成,全局事务完成
假如子事务Ti失败,那么会调用Ci, Ci-1, Ci-2 …进行补偿
Saga也分为两个阶段:
- 一阶段:直接提交本地事务
- 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
优点:
- 事务参与者可以基于事件驱动实现异步调用,吞吐高
- 一阶段直接提交事务,无锁,性能好
- 不用编写TCC中的三个阶段,实现简单
缺点:
- 软状态持续时间不确定,时效性差
- 没有锁,没有事务隔离,会有脏写
不太好意思的说,我也不太会Saga模式,并没有实际使用过,一般在实际使用中,基本上都是TCC,AT模式,XA有时也会用到。个人的看法哈。所以我就不演示了。只能告诉大家有这么个模式。
5.四种模式比较
XA:XA最大的优点就是强一致性,正因为强一致,所以他的隔离性也非常的好,并且不需要进行代码编写(只需要配置XA模式,添加注解即可)。所以他也没有代码侵入。可是他的性能很差,基本上只能使用在对一致性,隔离性要求高的场景。
AT:AT模式是使用最广泛的,虽然是弱一致,但是因为基于全局锁隔离,所以一般情况下,都能满足数据的提交与回滚。而且也没有代码侵入。一般使用在基于关系型数据库的大多数分布式事务的场景。
TCC:TCC的性能要比XA与AT都要好,他也是弱一致性,是基于资源预留的隔离(冻结的数据表),但是他需要编写三个接口,并且按照业务要求实现接口。但是性能非常好。一般使用场景为对性能要求较高的场景,或者有非关系型数据库(Redis缓存之类的)要参与的事务。
SAGA:最终一致,而且并没有隔离。性能非常好。但是代码侵入需要编写状态机和补偿业务。一般使用在业务流程长、业务流程多的场景,或者是参与者包含其它公司或者遗留的系统服务。无法提供TCC模式要求的三个接口。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)