一、分布式事务介绍
1、事务
事务提供一种机制将一个业务涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。
2、本地事务4大特性
什么是本地事务?
本地事务就是用关系数据库来控制事务,关系数据库通常都具有ACID特性,传统的单体应用通常会将数据全部存储在一个数据库中,会借助关系数据库来完成事务控制。
本地事务参考:并发时情况下,事务引发的问题演示
ACID:
A:原子性(Atomicity),一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。 事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。 就像你买东西要么交钱收货一起都执行,要么发不出货,就退钱。 C:一致性(Consistency),事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。 如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。 如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。 I:隔离性(Isolation),指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。 由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,
事务不会查看到中间状态的数据。 打个比方,你买东西这个事情,是不影响其他人的。
D:持久性(Durability),指的是只要事务成功结束,它对数据库所做的更新就必须***保存下来。 即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。 打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。
3、分布式事务
什么是分布式系统?
部署在不同结点上的系统通过网络交互来完成协同工作的系统。比如:充值加积分的业务,用户在充值系统向自己的账户充钱,在积分系统中自己积分相应的增加。充值系统和积分系统是两个不同的系统,一次充值加积分的业务就需要这两个系统协同工作来完成。
什么是分布式事务?
在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务,如下图:
另外一种分布式事务的表现是,一个应用程序使用了多个数据源连接了不同的数据库,当一次事务需要操作多个数据源,此时也属于分布式事务,当系统作了数据库拆分后会出现此种情况。
上面两种分布式事务表现形式以第一种据多。
分布式事务有哪些场景?
1) 电商系统中的下单扣库存
电商系统中,订单系统和库存系统是两个系统,一次下单的操作由两个系统协同完成
2)金融系统中的银行卡充值
在金融系统中通过银行卡向平台充值需要通过银行系统和金融系统协同完成。
3)教育系统中下单选课业务
在线教育系统中,用户购买课程,下单支付成功后学生选课成功,此事务由订单系统和选课系统协同完成。
4) SNS系统的消息发送
在社交系统中发送站内消息同时发送手机短信,一次消息发送由站内消息系统和手机通信系统协同完成。
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
什么时候会产生分布式事务呢?
1.多个Service
2.多个Resource
如下订单支付业务流程:
4、CAP定理
关于CAP理论,记住以下内容即可:分布式系统在设计时,只能在CAP中满足两种,无法兼顾三种。分区容错性(P)必须要满足,故只有AP和CP,对于 AP 来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择。CP:放弃可用性,加强一致性和分区容忍性,一些强一致性要求的系统按CP进行设计,比如跨行转账。
CAP 定理,又被叫作布鲁尔定理。对于设计分布式系统(不仅仅是分布式事务)的架构师来说,CAP 就是你的入门理论。
CAP理论是:分布式系统在设计时只能在一致性(Consistency)、可用性(Availability)、分区容忍性(PartitionTolerance)中满足两种,无法兼顾三种。
通过下图理解CAP理论:
一个服务有三个不同的节点
1、(数据)一致性(Consistency):服务A、B、C三个结点都存储了用户数据, 三个结点的数据需要保持同一时刻数据一致性。(多个节点的数据一致)
2、(服务)可用性(Availability):服务A、B、C三个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。(只有多个节点才能保证服务的可用性)
3、(网络)分区容忍性(或容错性)(Partition Tolerance):分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障。
分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区导致数据的不完整及无法访问等问题。
分布式系统不可避免的出现了多个系统通过网络协同工作的场景,结点之间难免会出现网络中断、网延延迟等现象,这种现象一旦出现就导致数据被分散在不同的结点上,这就是网络分区(network parition)。
通常情况下,网络分区指的是在分布式集群中,节点之间由于网络不通,导致集群中节点形成不同的子集,子集中节点间的网络相通,而子集和子集间网络不通。网络分区是子集与子集之间在网络上相互隔离了。
C (一致性):对于数据分布在不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。
合理的时间指的是请求不能被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回 50,而不是返回 40。
P (分区容错性):当出现网络分区后,系统能够继续工作。打个比方,这里集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。
分布式系统能否兼顾C、A、P?
首先对于分布式系统,分区是必然存在的,所谓分区指的是分布式系统可能出现的子区域网络不通,成为孤立区域的的情况。
那么分区容错性(P)就必须要满足(P 出现的概率实在是太小了),因为如果要牺牲分区容错性,就得把服务和资源放到一个机器,或者一个“同生共死”的集群,那就违背了分布式的初衷。
那么满足分区容错的基础上(分布式系统必须进行分区),能不能同时满足一致性
和可用性
?
假如现在有两个分区N1
和N2
,N1和N2分别有不同的分区存储D1和D2,以及不同的服务S1和S2。
- 在满足
一致性
的时候,N1和N2的数据要求值一样的,D1=D2。 - 在满足
可用性
的时候,无论访问N1还是N2,都能获取及时的响应。
假如现在有这样的场景:
- 用户访问了N1,修改了D1的数据。
- 用户再次访问,请求落在了N2。此时D1和D2的数据不一致。
接下来:
- 保证
一致性
:此时D1和D2数据不一致,要保证一致性就不能返回不一致的数据,可用性
无法保证。 - 保证
可用性
:立即响应,可用性得到了保证,但是此时响应的数据和D1不一致,一致性
无法保证。
所以,可以看出,分区容错的前提下,一致性
和可用性
是矛盾的。
在保证分区容忍性的前提下一致性和可用性无法兼顾,如果要提高系统的可用性就要增加多个(分区)结点,如果要保证数据的一致性就要实现每个结点的数据一致,结点越多可用性越好,但是数据一致性越差。所以,在进行分布式系统设计时,同时满足“一致性”、“可用性”和“分区容忍性”三者是几乎不可能的。
CAP有哪些组合方式?
1、CA:放弃分区容忍性,加强一致性和可用性,关系数据库按照CA进行设计。
理论上放弃P(分区容错性),则C(强一致性)和A(可用性)是可以保证的。实际上分区是不可避免的,严格上CA指的是允许分区后各子系统依然保持CA。
CA模型的常见应用:
- 集群数据库
- xFS文件系统
2、AP:放弃一致性,加强可用性和分区容忍性,追求最终一致性,很多NoSQL数据库按照AP进行设计。
要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。
说明:这里放弃一致性是指放弃强一致性,强一致性就是写入成功立刻要查询出最新数据。追求最终一致性是指允许暂时的数据不一致,只要最终在用户接受的时间内数据 一致即可。
AP模型常见应用:
- Web缓存
- DNS
3、CP:放弃可用性,加强一致性和分区容忍性,一些强一致性要求的系统按CP进行设计,比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。
放弃A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。
CP模型的常见应用:
- 分布式数据库
- 分布式锁
说明:由于网络问题的存在CP系统可能会出现待等待超时,如果没有处理超时问题则整理系统会出现阻塞。
总结:在分布式系统设计中AP的应用较多,即保证分区容忍性和可用性,牺牲数据的强一致性(写操作后立刻读取到最新数据),保证数据最终一致性。比如:订单退款,今日退款成功,明日账户到账,只要在预定的用户可以接受的时间内退款事务走完即可。
熟悉 CAP 的人都知道,三者不能共有,如果感兴趣可以搜索 CAP 的证明,在分布式系统中,网络无法 100% 可靠,分区其实是一个必然现象。
如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是 A 又不允许,所以分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
举个大家更熟悉的例子,像我们熟悉的注册中心ZooKeeper
、Eureka
、Nacos
中:
- ZooKeeper 保证的是 CP
- Eureka 保证的则是 AP
- Nacos 不仅支持 CP 也支持 AP
对于 CP 来说,放弃可用性,追求一致性和分区容错性,我们的 ZooKeeper 其实就是追求的强一致。
对于 AP 来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的 BASE 也是根据 AP 来扩展。
顺便一提,CAP 理论中是忽略网络延迟,也就是当事务提交时,从节点 A 复制到节点 B 没有延迟,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。
同时 CAP 中选择两个,比如你选择了 CP,并不是叫你放弃 A。因为 P 出现的概率实在是太小了,大部分的时间你仍然需要保证 CA。
就算分区出现了你也要为后来的 A 做准备,比如通过一些日志的手段,使其他机器恢复至可用。
二、分布式事务解决方案
有了上面的理论基础后,这里开始介绍几种常见的分布式事务的解决方案。
1、两阶段提交协议(2PC)
为解决分布式系统的数据一致性问题出现了两阶段提交协议(2 Phase Commitment Protocol),两阶段提交由协调者和参与者组成,共经过两个阶段和三个操作,部分关系数据库如Oracle、MySQL支持两阶段提交协议,本节讲解关系数据库两阶段提交协议。
参考:2PC:https://en.wikipedia.org/wiki/Two-phase_commit_protocol
2PC协议流程图:
1)第一阶段:准备阶段(prepare)
协调者通知参与者准备提交订单,参与者开始投票。参与者完成准备工作向协调者回应Yes。
2)第二阶段:提交(commit)/回滚(rollback)阶段
协调者根据参与者的投票结果发起最终的提交指令。如果有参与者没有准备好则发起回滚指令。
一个下单减库存的例子:
1、应用程序连接两个数据源。
2、应用程序通过事务协调器向两个库发起prepare,两个数据库收到消息分别执行本地事务(记录日志),但不提交,如果执行成功则回复yes,否则回复no。
3、事务协调器收到回复,只要有一方回复no则分别向参与者发起回滚事务,参与者开始回滚事务。
4、事务协调器收到回复,全部回复yes,此时向参与者发起提交事务。如果参与者有一方提交事务失败则由事务协调器发起回滚事务。
两阶段提交协议(Two Phase Commitment Protocol)中,涉及到两种角色
一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚)
多个事务参与者(participants):即本地事务执行者
总共处理步骤有两个
1)投票阶段(voting phase):协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;
如图所示 1-2为第一阶段,2-3为第二阶段
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。部分关系数据库支持(Oracle、MySQL等)。
缺点: 牺牲了可用性,对性能影响较大,不适合高并发高性能场景,如果分布式系统跨接口调用,目前 .NET 界还没有实现方案。整个事务的执行需要由协调者在多个节点之间去协调,增加了事务的执行时间,性能低下。
2、事务补偿(TCC)
TCC事务补偿是基于2PC实现的业务层事务控制方案,它是Try、Confirm和Cancel三个单词的首字母,含义如下:
1、Try 检查及预留业务资源
完成提交事务前的检查,并预留好资源。
2、Confirm 确定执行业务操作
对try阶段预留的资源正式执行。
3、Cancel 取消执行业务操作
对try阶段预留的资源释放。
下边用一个下单减库存的业务为例来说明:
1、Try
下单业务由订单服务和库存服务协同完成,在try阶段订单服务和库存服务完成检查和预留资源。订单服务检查当前是否满足提交订单的条件(比如:当前存在未完成订单的不允许提交新订单)。库存服务检查当前是否有充足的库存,并锁定资源。
2、Confirm
订单服务和库存服务成功完成Try后开始正式执行资源操作。订单服务向订单写一条订单信息。库存服务减去库存。
3、Cancel
如果订单服务和库存服务有一方出现失败则全部取消操作。订单服务需要删除新增的订单信息。库存服务将减去的库存再还原。
优点:最终保证数据的一致性,在业务层实现事务控制,灵活性好。
缺点:开发成本高,每个事务操作每个参与者都需要实现try/confirm/cancel三个接口。
注意:TCC的try/confirm/cancel接口都要实现幂等性,在为try、confirm、cancel失败后要不断重试。
什么是幂等性?
幂等性是指同一个操作无论请求多少次,其结果都相同。
幂等操作实现方式有:
1、操作之前在业务方法进行判断如果执行过了就不再执行。
2、缓存所有请求和处理的结果,已经处理的请求则直接返回结果。
3 、在数据库表中加一个状态字段(未处理,已处理),数据操作时判断未处理时再处理。
TCC是一种比较成熟的分布式事务解决方案,可用于解决跨库操作的数据一致性问题;
TCC是服务化的两阶段编程模型,其Try、Confirm、Cancel 3个方法均由业务编码实现;
其中Try操作作为一阶段,负责资源的检查和预留,Confirm操作作为二阶段提交操作,执行真正的业务,Cancel是预留资源的取消;
如下图所示,业务实现TCC服务之后,该TCC服务将作为分布式事务的其中一个资源,参与到整个分布式事务中;事务管理器分2阶段协调TCC服务,在第一阶段调用所有TCC服务的Try方法,在第二阶段执行所有TCC服务的Confirm或者Cancel方法;
操作方法 | 含义 |
---|---|
Try | 预留业务资源/数据效验-尝试检查当前操作是否可执行 |
Confirm | 确认执行业务操作,实际提交数据,不做任何业务检查,try成功,confirm必定成功,需保证幂等 |
Cancel | 取消执行业务操作,实际回滚数据,需保证幂等 |
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2(confirm),3(cancel)步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。还存在非幂等问题。
3、消息队列实现最终一致
本方案是将分布式事务拆分成多个本地事务来完成,并且由消息队列异步协调完成,如下图:
下边以下单减少库存为例来说明:
1、订单服务和库存服务完成检查和预留资源。
2、订单服务在本地事务中完成添加订单表记录和添加“减少库存任务消息”。
3、由定时任务根据消息表的记录发送给MQ通知库存服务执行减库存操作。
4、库存服务执行减少库存,并且记录执行消息状态(为避免重复执行消息,在执行减库存之前查询是否执行过此消息)
5、库存服务向MQ发送完成减少库存的消息。
6、订单服务接收到完成库存减少的消息后删除原来添加的“减少库存任务消息”。
实现最终事务一致要求:预留资源成功理论上要求正式执行成功,如果执行失败会进行重试,要求业务执行方法实现幂等。‘
优点 :由MQ按异步的方式协调完成事务,性能较高。不用实现try/confirm/cancel接口,开发成本比TCC低。
缺点:此方式基于关系数据库本地事务来实现,会出现频繁读写数据库记录,浪费数据库资源,另外对于高并发操作不是最佳方案。
三、Seata分布式事务中间件
1、Seata介绍
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。
学习文档:https://seata.io/zh-cn/docs/overview/what-is-seata.html
Seata特色功能
在dubbo或springcloud中,使用Seata来控制分布式事务是非常轻松的,直接使用一个注解就可以实现。
2、Seata模式讲解
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
相关术语:
TC - 事务协调者 维护全局和分支事务的状态,驱动全局事务提交或回滚。 TM - 事务管理器 定义全局事务的范围:开始全局事务、提交或回滚全局事务。 RM - 资源管理器 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
1)、AT模式(推荐使用)
提供无侵入自动补偿的事务模式,但是需要在每个数据库中创建一张日志表,用于记录当前日志操作的信息,比如锁定数据方便回滚。
前提:
1.基于支持本地 ACID 事务的关系型数据库。 2.Java 应用程序,通过 JDBC 访问数据库。
整体机制:
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
写隔离:
1.一阶段本地事务提交前,需要确保先拿到 全局锁 。 2.拿不到 全局锁 ,不能提交本地事务。 3.拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
(1)、情况一:tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
(2)、情况二:如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁(此时拥有全局锁),进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
读隔离:
查数据一般是不需要隔离的,什么时候需要隔离呢?如果希望查数据时不希望别人进行修改,此时需要实现读隔离。
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
工作机制:
以一个示例来说明整个 AT 分支的工作过程。
业务表:product
Field | Type | Key |
---|---|---|
id | bigint(20) | PRI |
name | varchar(100) | |
since | varchar(100) |
AT 分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
一阶段:
过程:
1.解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
2.查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据(即得出修改前的数据)。
select id, name, since from product where name = 'TXC';
得到前镜像:
id | name | since |
---|---|---|
1 | TXC | 2014 |
3.执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
4.查询后镜像:根据前镜像的结果,通过 主键 定位数据(得到修改之后的数据)。
select id, name, since from product where id = 1`;
得到后镜像:
id | name | since |
---|---|---|
1 | GTS | 2014 |
5.插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG
表中。
{ "branchId": 641789253, "undoItems": [{ "afterImage": { "rows": [{ "fields": [{ "name": "id", "type": 4, "value": 1 }, { "name": "name", "type": 12, "value": "GTS" }, { "name": "since", "type": 12, "value": "2014" }] }], "tableName": "product" }, "beforeImage": { "rows": [{ "fields": [{ "name": "id", "type": 4, "value": 1 }, { "name": "name", "type": 12, "value": "TXC" }, { "name": "since", "type": 12, "value": "2014" }] }], "tableName": "product" }, "sqlType": "UPDATE" }], "xid": "xid:xxx" }
6.提交前(在一个事务中提交),向 TC 注册分支:申请 product
表中,主键值等于 1 的记录的 全局锁 。
7.本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
8.将本地事务提交的结果上报给 TC。
二阶段-(异常)回滚:
1.收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
2.通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
3.数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改(手动修改)。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
4.根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
5.提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
二阶段-提交:
1.收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
2.异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
回滚日志表:
UNDO_LOG Table:不同数据库在类型上会略有差别。
以 MySQL 为例:(xid为全局事务的唯一id)不同的版本表不一样
Field | Type |
---|---|
branch_id | bigint PK |
xid | varchar(100) |
context | varchar(128) |
rollback_info | longblob |
log_status | tinyint |
log_created | datetime |
log_modified | datetime |
-- 注意此处0.7.0+ 增加字段 context CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
只要你做分布式事务,则每个数据库都要创建这张表。
2)、TCC模式
回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 Manual (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,**自动** 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,**自动** 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
3)、Saga模式
Saga模式是Seata提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
理论基础:Hector & Kenneth 发表论⽂ Sagas (1987)
使用场景:
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
优势:
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,高吞吐
- 补偿服务易于实现
缺点:
- 不保证隔离性(应对方案见用户文档)
3、抢单分布式事务实现(Seata在项目中的使用场景)
非热点商品抢单的时候,我们这里采用Seata分布式事务控制库存减少,当库存递减成功的时候,执行创建订单,当库存减少失败的时候,不执行创建订单,但有可能存在这么一种现象,库存减少成功了,但创建订单失败了,此时需要回滚库存,这时需要跨应用管理事务,因此可以使用Seata实现。
库存递减调用商品微服务,下单调用订单微服务。
1)、分布式事务实现
Seata分布式事务实现(使用AT模式),分如下几个步骤:
1.在每个需要控制分布式事务的数据库中添加日志表undo_log 2.安装TC(实现全局事务的管理) 3.配置数据源-代理数据源 4.配置微服务与Seata的TC交互信息以及注册地址 5.使用全局分布式事务的方法上添加注解@GlobalTransactional
1)、日志表添加
商品微服务和订单微服务都创建undo_log表
CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
2)、安装TC事务协调者(Seata-server)
我们这里采用Docker安装,安装命令如下:
docker run -d --name seata-server -p 8191:8191 -e SEATA_PORT=8191 seataio/seata-server:latest
TC下载地址:https://github.com/seata/seata/releases
建议下0.6.0以上的版本,因为0.6以下的版本不太稳定。最好不要下载0.8以下的版本。
下载后如下所示:
3)、配置数据源
由于分布式事务相对来说比较独立,可以创建一个独立的工程seckill-seata,谁需要实现分布式事务,直接依赖这个工程就可以了。
在需要执行分布式事务控制的工程pom.xml
中添加如下依赖:
<!--Seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
在seckill-goods
和seckill-order
中添加配置类(代理数据源配置),代码如下:
@Configuration public class DataSourcesProxyConfig { // 配置数据源 @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } // 配置代理数据源 @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(); } // 持久层用的是mybatis,SqlSessionFactory需要注入DataSource,将注入的DataSource换成代理数据源 @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); return sqlSessionFactoryBean.getObject(); } }
通过注解@ConfigurationProperties(prefix="配置文件中的key的前缀")可以将配置文件中的配置自动与实体进行映射
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://db-server:3306/seckill_goods?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: 123456
4)、配置注册信息
在seckill-goods
和seckill-order
中添加文件registry.conf
,内容如下:
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "file" file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" file { name = "file.conf" } }
在seckill-goods
和seckill-order
中添加文件file.conf
,内容如下:
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup 组名自定义,这里为seata_seckill_transaction vgroup_mapping.seata_seckill_transaction = "default" #only support single node (Seata-server的地址) default.grouplist = "192.168.211.137:8191" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 } ## transaction log store store { ## store mode: file、db mode = "file" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } } lock { ## the lock store mode: local、remote mode = "remote" remote { ## store locks in the seata's server } } recovery { committing-retry-delay = 30 asyn-committing-retry-delay = 30 rollbacking-retry-delay = 30 timeout-retry-delay = 30 } transaction { undo.data.validation = true undo.log.serialization = "jackson" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 }
5)配置事务分组信息
seckill-goods和seckill-order两个微服务都要依赖seckill-seata
<dependency> <groupId>com.seckill</groupId> <artifactId>seckill-seata</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
在seckill-goods
和seckill-order
的bootstrap.yml中添如下代码:
spring:
cloud:
alibaba:
seata:
tx-service-group: seata_seckill_transaction
6)方法开启全局事务
在seckill-order
的com.seckill.order.service.impl.OrderServiceImpl#add
上@GlobalTransactional
全局事务注解,代码如下:
小知识点:
如果Seata是低版本,此时事务会失效,事务失效的方式可以通过AOP方式解决,参考地址如下: https://seata.io/zh-cn/blog/seata-spring-boot-aop-aspectj.html