前言
前日对JUC进行了一个深度总结,不过现在博主能记得的也不多了,只是这东西,不是看几遍写几遍就能完全记住的,功夫在平时,很多知识点都需要反复的看,不光要看,还要用,这样才能了解其原理,掌握其原理,从而能更好的运用于平时的开发中,提高开发效率。今天给大家带来一篇分布式事务相关的总结,详不详细的不敢说,大家还是自己来看看吧。
事务
虽然我们说的是分布式事务,但在这之前,我们还是要了解一下事务的本质,事务分为两种,一种是我们本篇要说的分布式事务,另一种是本地事务。下面,我们对这两种事务的特点进行说明。
本地事务
本地事务也叫做单机事务。但说起来和单机游戏并不一样,它只是相对于分布式事务的一种描述。学完此篇,相信你对单机事务的理解能够加深不少。
本地事务由四大特点,俗称ACID,分别是:
- A:原子性,即事务中的所有操作,要么全部成功,要么全部失败;
- C:一致性,即要保证数据库内部完整性约束,声明性约束。约束指的是数据定义的组成部分,比如PRIMARY KEY,NOT NULL ,UNIQUE等等,共五大约束;声明性,约束都是声明性的;
- I:隔离性,即对同一资源操作的事务不能同时发生,比如不能喝水的同时还在唱歌,你可以试试看;
- D:持久性,即数据库的这些操作必须永久保存,不论对错。
分布式事务
分布式事务指的是在多个服务或多个数据库架构下产生的事务,有跨数据源的分布式事务,也有跨服务的分布式事务,可能还有其他类型的分布式事务。最典型的案例就是生成订单的时候,在生成订单时,还要修改库存,如果是购物车下单还要删除购物车下单商品,如果账户可充值还要扣除账户余额,是不是很复杂?但这,就是分布式事务的一部分!
这时,你会发现,每一个服务都是一个单独的单机事务,他们共同组成了这个分布式事务,原子性要求他们要么全部成功,要么全部失败,但对于上图的情况,你很难保证原子性,ACID恐怕就无法保证了,而这就是分布式事务要解决的问题。
分布式事务的理论基础
为了解决分布式事务问题,诞生了一些用于解决分布式问题原子性的理论,分别是CAP原则和BASE理论,以及在他们基础上提出的最终解决思路。
CAP原则
CAP定理是在1998年,由加州大学的科学家Eric Brewer提出,主要有灿个特性,分别对照CAP:
- C:一致性,即在分布式系统中的所有数据备份,在同一时刻数据要一致;
- A:可用性,即在访问多个服务的任何节点时,都能得到相应,不论成功还是失败;
- P:分区容错性,即系统中任意信息的丢失或失败不会影响系统的继续运作,其原理是,分布式系统中的某节点出现故障时,形成独立分区,分区即使形成,也要保证整个系统依然可以对外提供服务。
CAP原则的精髓就是要么AP,要么CP,要么AC,但是不存在CAP。关于这点,我们在解决思路上再说。大家看下下面的图,他们的关系如下:
但他们之间存在一个不可调和的矛盾,那就是P一定存在,因为谁也不敢100%保证不会发生服务器故障,这个没有任何争议。这就是我们要说的解决思路,暂且不提,继续往下说。
BASE理论
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
BASE理论有三大特性,分别是:
- BASE是Basically Available(基本可用):分布式系统发生故障时,允许损失部分可用性,但要保证核心功能依然可用,其中,损失包括响应时间上的损失和非核心功能的损失;
- Soft state(软状态):在一定时间内,允许出现数据不一致的临时状态,称为软状态;
- Eventually consistent(最终一致性):在无法保证强一致性的条件下,在软状态结束后,要达到数据的最终一致性。
分布式事务解决思路
上面两个理论看完之后,相信你对解决思路已经有了一个大概的了解,总结起来就是两个模式:AP和CP:
- AP:最终一致性模式,即各子事务分别执行和提交,允许出现软状态,然后采取弥补措施保证数据的最终一致性;
- CP:强一致性模式,即各子事务执行后后想等待,要么全部成功,要么全部失败,失败后回滚。
但无论是AP还是CP,在执行的过程中都离不开一个很重要的东西,那就是事务的协调者TC,我们会在下面的Seata中讲到。在分布式系统下,子系统的事务就是分支事务,有关联的事务统称为全局事务。
Seata核心架构
关于Seata,其实博主在微服务那篇博客中讲到过,也有过使用的案例:Java开发 - 数风流人物,还看“微服务”
有兴趣的可以看看,也可以去搜下其他Seata相关的教程,了解Seata的可以继续往下看。关于Seata,博主就不再详细介绍了,我们直接切入正题,说说Seata的架构 。
在Seata的事务管理中,有三个重要的角色,我们在上面已经提到了一个事务协调者TC,还有一个事务管理器TM和资源管理器RM。他们的作用如下:
- TC:维护全局和分支事务的状态,说白了就是提交或者回滚,类似于接线员;
- TM:定义了全局事务的范围,控制全局事务的开始,提交或者回滚,是真正的管理者;
- RM:用于管理各分支事务的资源,比如向TC注册分支事务,向TC报告分支事务的状态,驱动分支事务的提交或者回滚,是个执行者。
三者的关系结构如下图:
Seata基于此架构又提出了四种用于解决分布式事务的方案:
XA:强一致性分阶段事务模式,强一致性虽有性能损耗,但却没有业务侵入;
AT:最终一致的分阶段事务模式,无业务侵入,是Seata的默认模式;
TCC:最终一致的分阶段事务模式,有业务侵入;
SAGA:长事务模式,有业务侵入。
Seata四大模式
XA模式
原始的XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范提供了支持。
原始的XA其实是一种规范,目前的主流数据库都实现了次规范,其实现原理是基于两阶段提交:
第一阶段:
TC通知分支事务进行事务处理本地事务,并将执行结果上报TC,此过程只执行SQL,不提交,继续持有数据库锁。
第二阶段:
TC根据各分支事务上报的结果进行处理,如果全部执行成功,则通知所有分支事务进行提交,提交后锁释放;如果有一条以上的失败消息,则通知所有分支事务进行回滚,最终释放锁。
原始的XA请看下图:
Seata中的XA是在XA的基础上做了封装和改造,我们来看看其架构图,它和我们前面画的TM,TC,RM的关系图很像:
看图,其两个阶段都在做什么就一目了然了。
虽然XA模式实现简单,使用了强一致性,且没有代码的侵入,但和传统XA一样,第一阶段不提交,所以锁在第二阶段释放,性能略差,此模式也必须依赖关系型数据库才可以使用。
其实现也比较简单,分两步完成:
第一步,开启XA模式:
seata:
data-source-proxy-mode: XA
第二步,给发起全局事务的入口方法添加@GlobalTransactional注解。
关于Seata的使用就不再详细说了,既然你看到这里,就代表Seata你至少是知道怎么用的,比如依赖和配置,不了解的可查看博主在上面放的微服务的链接或自行查找学习。
AT模式
AT模式前面提到过,是分阶段提交的事务模型,它主要解决了XA模式中资源锁定时间太久的问题,因此,它并不是强一致性的,而是最终一致性的,它对代码同样没有侵入,是Seata的默认模式。
下面,我们来看看它的架构流程图:
架构图很清晰,但也略微复杂,我们来画一个简易版的流程图:
这样,流程有没有更清晰一点?画图能省不少文字啊,不过画图确实比较费时间。
注意:这里存在一个问题,就是AT模式因为在第一阶段直接提交后释放的锁,第二阶段无法保证其他事务是否会操作同一条数据,所以会存在脏写的问题。
解决办法就是引入全局锁,在释放DB锁之前,先拿到全局锁,避免在同一时刻有另外一个事务来操作当前数据。这一点要格外注意。
目前使用全局锁@GlobalLock时,限制条件过于苛刻,必须要添加@Transaction注解,同时必须是for update语句,否则
不会触发全局锁检查。更详细的大家在使用过程中再详细了解,本文不再延伸介绍。
使用AT模式也很简单:
seata:
data-source-proxy-mode: AT # 默认就是AT
修改配置即可直接使用,详细代码不再给出,可参考:
详解 Seata AT 模式事务隔离级别与全局锁设计
Seata AT 模式
https://github.com/seata/seata-samples
TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复(其实就是业务侵入)。需要实现三个方法:
- Try:资源的检测和预留;
- Confirm:完成资源操作业务,要求 Try 成功 Confirm 也一定能成功;
- Cancel:预留资源释放,可以认为是回滚操作。
它和AT相比的优点是不依赖undolog,事务回滚通过资源的预留和cancel操作。其架构如下:
看起来和AT及其相似,只是没有了对undolog文件的操作,多了try,confirm和cancel操作。 从图中,我们应该还能看出来一个问题,那就是TCC模式不依赖数据库事务,可用于非事务性数据库。
那么,你在上图还能看出有哪些问题呢?Let us see......
有两个问题需要格外注意:空回滚和事务悬挂。
空回滚
我们假设在try阶段发生了阻塞,这可能会导致全局事务超时,进而导致cancel操作被触发,在try操作没有执行的情况下,执行cancel的回滚操作就是空回滚。如果不对空回滚加以防范的话,可能会造成资源的无效释放,即在没有预留资源的情况下就释放资源,造成异常。
解决办法是:执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则不应执行空回滚。
事务悬挂
对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。
解决办法是:执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂。
总结
总的来说,这俩其实算是一个问题,可以通过创建一张新表,字段自己随便定,只要每个事务不一样即可,推荐使用全局事务id,三个函数中的操作如下:
- try中记录事务状态到表中;
- confirm中根据全局事务id删除这条数据;
- cancel中恢复记录表中的数据到初始状态;
判断空回滚方法:
- cancel业务中,根据xid查询数据,如果为null则说明try还没做,可不执行空回滚,然后根据全局事务id插入一条状态数据留给try做空回滚判断;
- try业务中,根据xid查询数据 ,如果已经全局事务id,则存在Cancel已经执行的情况,可拒绝执行try业务
- 以上两条满足一条即可,如果空回滚做了,try绝对不能执行,但若是经过判断避免了,那try继续,否则try执行了,真到了要执行cancel的时候是要正常执行的。
SAGA模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
看图,分布式事务执行过程中,各节点依次执行正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任意一个正向操作执行失败,那么分布式事务会在当前节点,也就是FAILS的地方通过补偿服务退回去,执行前面各节点的逆向回滚操作,把已提交的节点的操作回滚,使分布式事务回到初始状态。
适用场景:
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
优势:
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,高吞吐
- 补偿服务易于实现
缺点:
- 不保证隔离性,其实还是脏写问题
- 其软状态持续时间不可预测
关于Saga,更详细内容可见这里:Seata Saga 模式
博主个人感觉Sage模式是四大模式中最复杂的了,说实话,没用过,和前三种比起来,Saga更难理解,博主也没有理解透,惭愧惭愧!!!但有一点博主是明确的,其补偿服务是另类的回滚,可以通过一个单独的节点完成此操作。大家还是自己看吧,不要被误导,谁要是搞懂了回来给博主科普科普。
四大模式对比
XA | AT | TCC | SAGA | |
一致性 | 强一致性 | 弱一致性 | 弱一致性 | 最终一致性 |
隔离性 | 完全隔离 | 基于全局锁隔离 | 基于资源预留隔离 | 无隔离 |
代码侵入 | 无 | 无 | 有,try,confirm,cancel | 有,编写状态机和补偿服务 |
性能 | 差 | 好 | 非常好 | 非常好 |
使用场景 | 对一致性和隔离型要求高的事务 | 基于关系型数据库的大多数分布式事务场景都可以,比较通用 |
|
|
Seata高可用架构了解
这个东西说起来其实很简单,但做起来嘛又很麻烦,博主只简单的提一嘴,毕竟做起来是有成本在里面的,各公司做起来还可能不一样。
使用Seata就一定离不开类似Nacos的注册中心,使用分布式事务,就一定离不开类似Dubbo的远程调用服务。然后集群搞起来,高可用性的主从架构搞起来,再土豪一点的话搞异地容灾,其实都不复杂,就是砸钱。
关于这些内容,不再本篇详细介绍,准备单独出,各位,咱今天就到这里吧。
结语
看到这里,想必你对分布式事务的解决方案就多多少少有些了解了,虽然没有提供详细的代码,但在各位略懂Seata的情况下,你基本已经可以通过默认的AT模式来解决大部分问题了,这篇博客虽然才写了短短五六千字,但却浓缩了精华,觉得不错,就给博主点个赞吧!