赞
踩
目录
- 复习本地事务
- 了解分布式事务问题
1. 本地事务
事务概念:即传统的单机事务,是数据库的概念,表示由一个或多个操作组成的一个业务。比如:银行转账
事务作用:组成事务的多个操作单元,在操作数据库时,要成功都成功,要失败都失败
事务特性:ACID
事务操作,底层会对数据加锁,会影响操作性能
如果多个数据库操作要想属于同一个事务进行管理:就必须使用同一个数据库连接Connection对象
2. 分布式事务
介绍
在分布式环境上同样需要事务来保证数据的一致性。而因为跨数据源或跨服务环境所导致的传统事务不可用,形成的新的事务需求,这样的事务叫分布式事务。
传统事务中,要想让多个操作属于同一事务,就需要使用同一个数据库连接
Connection
对象。但是在分布式环境下,通常是做不到这一点的,必须使用分布式事务。比如:
跨数据源的分布式事务:程序要操作不同服务器上的数据库
跨服务的分布式事务:程序要调用多个服务,每个服务都要操作数据库
综合情况
示例场景
电商行业中比较常见的下单付款案例,包括下面几个行为:
创建新订单
扣减商品库存
从用户账户余额扣除金额
要完成上面的操作,需要访问三个不同的微服务和三个不同的数据库,如图:
![]()
订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。
但是当我们把三件事情看做一个"业务",要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务要解决的问题了
3. 分布式事务的问题演示
准备数据库
使用SQLyog执行SQL脚本《seata-demo.sql》,初始化数据库
导入演示项目
资料里提供了《seata-demo》项目,把这个项目拷贝到你的工作空间目录里
用idea打开这个项目
![]()
启动服务
启动nacos
启动所有微服务
测试下单功能
使用Postman发请求下单
![]()
请求路径是:http://localhost:8082/order
请求方式是:POST
表单参数是:
userId:user202103032042012
commodityCode:100202003032041
count:20
money:200
最终结果是:因为库存量不够,导致扣减库存失败,但是用户的帐户余额已经扣减了。已经出现了分布式事务问题
4. 小结
分布式事务:
跨数据源的事务
跨服务的事务
以上综合
分布式事务要求:
无论是跨数据源还是跨服务,都要做到:要么一起成功,要么一起失败
- 能描述CAP定理
- 理解BASE理论
分布式事务问题的处理,其实就是在数据的一致性与服务的可用性之间做一个权衡:
如果要保证所有子事务的数据一致性:就要舍弃一些服务的可用性。因为数据库事务会对数据行加锁
如果要保证所有服务的可用性:就要考虑一下数据的一致性如何处理
解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导
CAP定理
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。 Eric Brewer 说,这三个指标不可能同时做到,这个结论就叫做 CAP 定理。
CAP三指标介绍
1.C一致性
一致性Consitency,即 用户访问分布式系统中的任意节点,得到的数据必须一致(业务上的一致)。这就要求节点之间必须要及时同步数据。
比如:集群中有两个节点,初始数据是一致的;当修改了其中一个节点的数据时,要把数据变更立即同步到另外一个节点,保证所有节点的数据是一致的。
2.A可用性
可用性Availability,即 用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
3.P分区容错性
分区Partition:因为网络故障或者其它原因,导致分布式系统中的部分节点与其它节点失联,形成独立分区。
分区容错性Partition Tolerance,即 集群出现分区时,整个系统也要持续对外提供服务
![]()
CAP的矛盾
在分布式系统中,分区容错性(P)是必须要保证的。但C和A两个指标就互相矛盾
以上图为例:
因为网络原因形成了两个分区:node01和node02一个分区;node03一个分区
如果我们要修改node02上的数据:
如果要追求一致性:必须等到node02把数据同步到node01、node03,才返回响应。但因为分区了,等待时间不确定,可能要长时间等待==>追求一致性C,舍弃了可用性A
如果要追求可用性:修改了node02的数据就立即返回响应;不能保证数据同步到了node01和node03
追求了可用性A,舍弃了一致性C
所以CAP定理中,P必须保证,而C和A相互矛盾,只能保证一个。即:
CP模式:舍弃可用性,追求一致性
AP模式:舍弃一致性,追求可用性
但是,难道这个AC矛盾就不可调和的吗?并不是,BASE理论就提出了完善和弥补的方案
BASE理论BASE理论,是对CAP定理中的CA矛盾进行权衡之后,提供的一种解决思路。这是指:
采用CP模式,追求一致性,舍弃一定的可用性:
BA (Basically Available),基本可用:分布式系统在出现故障时,允许损失部分可用性,要保证核心可用
响应时间的损失:比如原本要求0.5秒内响应,现在允许5秒内响应
系统功能的损失:出现某些故障时,核心功能保证可用,部分非核心功能允许不可用
采用AP模式,追求可用性,对一致性采用一些补偿措施
S (Soft State),软状态:在一定时间内,允许出现中间状态,比如 数据临时不一致
E (Eventually Consistent),最终一致性:虽然无法保证强一致性,但是在软状态之后,最终达到数据一致
分布式事务的解决思路解决思路
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论。有两种解决思路:
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
几个概念
分支事务RM:在整个业务中,每一个子系统的事务,称为一个分支事务。对应@Transactional
全局事务TM:一个完整的业务,需要众多分支事务共同组成一个全局事务。对应@GlobalTransactional
事务协调者TC:用于在整个全局事务里,管理、协调各个分支事务的状态。对应Seata软件
![]()
小结
什么是CAP定理:
理解CAP
C:一致性。访问系统里任意一个节点,得到的数据必须是一致的
A:可用性。访问系统里任意一个健康节点,都要立即返回响应,不能阻塞、不能拒绝
P:分区容错性。如果系统出现分区(某些节点失联),整个系统必须仍然能够正常提供服务
CAP定理:
一个分布式系统最多只能做到CAP三个指标里的两个指标
通常情况下,P是必须要满足的;然后A和C是矛盾的
如果要追求A可用性:访问任意节点立即必须返回响应,就不能保证数据已经同步完成了。一致性不保证
如果要追求C一致性:访问任意节点必须得到相同数据,就必须等待数据同步完成才能响应。可用性低了
什么是BASE理论:
BASE理论是对CAP矛盾提出的一种解决思路
BA:追求CP一致性,允许舍弃一定的可用性,只要做到基本可用BA即可
允许响应时间稍有延长,允许非核心功能暂不可用
SE:追求AP可用性,允许存在临时不一致的状态,只要做到最终一致即可
S:软状态,表示数据临时不一致的状态
E:最终一致,在一定时间内最终达到了数据的一致
分布式事务的解决思路:
借鉴Base理论
相关的一些概念:
RM:Resource Manager,分支事务。对应代码里的@Transactional
TM:Transaction Manager,全局事务。对应代码里的@GlobalTransactional
TC:Transaction Coordinater,事务协调者,对所有分支事务进行协调管理的。对应软件Seata
- 了解Seata架构相关的角色
- 能够安装部署Seata
- 能够将微服务与Seata集成
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/,其中的文档、博客中提供了大量的使用说明、源码分析。
Seata事务管理中有三个重要的角色:
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
是Seata本身
TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开启全局事务、提交或回滚全局事务。
负责事务的边界。@GlobalTransactional
RM (Resource Manager) - 资源管理器:处理分支事务的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。@Transactional
分支事务
![]()
Seata基于上述架构提供了四种不同的分布式事务解决方案:
XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
TCC模式:最终一致的分阶段事务模式,有业务侵入
SAGA模式:长事务模式,有业务侵入
无论哪种方案,都离不开TC,也就是事务的协调者。
seata-server中分布式事务中充当了TC的角色
下载与安装
下载
下载seata-server包,地址 下载中心
也可以直接使用资料里提供好的程序:《seata-server-1.4.2.zip》
安装
seata-server免安装,直接解压到一个不含中文、空格、特殊字符的目录里即可
准备数据库
tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,包括全局事务、分支事务、全局锁等信息,因此要提前创建好这些表:
用Navicat或其它工具连接本机MySQL,执行脚本:《seata-tc-server.sql》
配置
1.在nacos里添加配置
注意,为了让tc服务的集群可以共享配置,我们选择了nacos作为统一配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好
在nacos中新建配置:
配置的内容如下:
# 数据存储方式,db代表数据库 store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.cj.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&useUnicode=true&rewriteBatchedStatements=true&serverTimezone=UTC store.db.user=root store.db.password=root store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000 # 事务、日志等配置 server.recovery.committingRetryPeriod=1000 server.recovery.asynCommittingRetryPeriod=1000 server.recovery.rollbackingRetryPeriod=1000 server.recovery.timeoutRetryPeriod=1000 server.maxCommitRetryTimeout=-1 server.maxRollbackRetryTimeout=-1 server.rollbackRetryTimeoutUnlockEnable=false server.undo.logSaveDays=7 server.undo.logDeletePeriod=86400000 # 客户端与服务端传输方式 transport.serialization=seata transport.compressor=none # 关闭metrics功能,提高性能 metrics.enabled=false metrics.registryType=compact metrics.exporterList=prometheus metrics.exporterPrometheusPort=98982.seata拉取配置文件并注册服务
修改conf目录下的registry.conf
文件,完整配置如下:
registry { # tc服务的注册中心类型,使用nacos type = "nacos" # 将tc服务注册到nacos,要配置nacos的地址等信息。 ""@DEFAULT_GROUP@seata-tc-server@TJ nacos { # tc服务的应用名称,可以自定义 application = "seata-tc-server" serverAddr = "127.0.0.1:8848" group = "DEFAULT_GROUP" namespace = "" cluster = "TJ" username = "nacos" password = "nacos" } } config { # 读取tc配置文件的方式:从配置中心nacos里读取配置。这样的话,如果tc搭建集群,可以通过配置中心共享配置 type = "nacos" # 要从nacos读取配置文件信息,要配置nacos的地址等信息 nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" dataId = "seataServer.properties" } }
启动TC服务
先启动nacos
启动seata:
进入seata的bin目录,运行其中的
seata-server.bat
验证是否启动:
如果启动成功了,seata-server应该已经注册到nacos注册中心了
我们打开nacos,看一下有没有seata服务
每个需要分布式事务的微服务,都要按照下面的步骤进行配置。
我们以订单服务
order-service
为例进行说明;其它微服务也要做相同配置添加依赖
修改pom.xml,添加依赖
<!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <!--版本较低,1.3.0,因此排除--> <exclusion> <artifactId>seata-spring-boot-starter</artifactId> <groupId>io.seata</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <!--seata starter 采用1.4.2版本--> <version>1.4.2</version> </dependency>配置tc地址
修改application.yaml,配置tc地址。通过注册中心nacos,可以拉取tc服务的地址
seata: # 要去注册中心nacos里,拉取tc服务的地址 registry: type: nacos # tc服务集群注册到了nacos的""@DEFAULT_GROUP@seata-tc-server@TJ # 所以要从nacos中拉取 ""@DEFAULT_GROUP@seata-tc-server@TJ 服务集群 nacos: server-addr: localhost:8848 #nacos地址 namespace: "" #名称空间,没有设置,用"" group: DEFAULT_GROUP #分组,没有设置,默认用DEFAULT_GROUP application: seata-tc-server #seata服务名称 username: nacos password: nacos tx-service-group: seata-demo #事务组名称 service: vgroup-mapping: #事务组与cluster的映射关系 seata-demo: TJ
每个配置文件修改后启动 测试:如下图就代表配置就没有问题了
- 理解并使用Seata的XA模式
- 理解并使用Seata的AT模式【重点】
- 理解并使用Seata的TCC模式
- 了解Seata的SAGA模式
下面我们就一起学习下Seata中的四种不同的事务模式。
1. XA模式
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
两阶段提交
XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交:
一阶段:
事务协调者通知每个事务参与者执行其本地事务
本地事务执行后暂不提交,继续持有数据库锁;向事务协调者报告事务的执行状态
二阶段:
事务协调者基于一阶段的报告来决定下一步操作
如果一阶段所有事务都成功:则通知所有事务参与者都提交事务
如果一阶段有事务执行失败:则通知所有事务参与者都回滚事务
如图:
如果所有事务都正常:
如果有事务出现异常:
![]()
Seata的XA模型
Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型
一阶段-RM工作:
注册分支事务到TC
执行分支业务的SQL但不提交
向TC报告事务执行状态
二阶段-TC工作
TC检测各分支事务的执行状态
如果都成功:TC通知所有RM提交事务
如果有失败:TC通知所有RM回滚事务
二阶段-RM工作
根据TC的通知指定,提交或回滚事务
Seata的XA基本架构如图:
![]()
XA模式的优缺点
XA模式的优点:
事务的强一致性,满足ACID原则。
常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点:
性能较差,因为一阶段需要锁定数据库资源,等待二阶段结束才释放,所以性能较差
依赖关系型数据库实现事务。NoSQL参与不进来
使用示例
Seata的依赖启动器已经完成了XA模式的自动装配,使用起来非常简单,步骤:
设置XA模式 修改微服务配置文件:
修改配置文件,开启XA模式
修改全局事务入口方法,添加注解
@GlobalTransactional
注解:TM全局事务每个分支事务的方法上,添加注解
@Transactional
注解:RM分支事务重启测试
1) 开启XA模式
修改每个参与事务的微服务的配置文件,开启XA模式:
seata: data-source-proxy-mode: XA2) 添加注解@GlobalTransactional
在发起全局事务的入口方法上添加注解
@GlobalTransactional
在本例中是
OrderServiceImpl
中的create
方法
@Override @GlobalTransactional public Long create(Order order) { // 创建订单 orderMapper.insert(order); try { // 扣用户余额 accountClient.deduct(order.getUserId(), order.getMoney()); // 扣库存 storageClient.deduct(order.getCommodityCode(), order.getCount()); } catch (FeignException e) { log.error("下单失败,原因:{}", e.contentUTF8(), e); throw new RuntimeException(e.contentUTF8(), e); } return order.getId(); }3) 重启测试
重启所有微服务,再次测试
2. AT模式【重点】
Seata的AT模型
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
一阶段-RM工作:
向TC注册事务分支
记录undo-log(数据变更之前的快照)
执行业务SQL并提交
向TC报告事务状态
二阶段-TC工作:
基于一阶段的报告来决定下一步操作
如果所有分支事务都成功:通知所有RM提交事务
如果任一分支事务失败了:通知所有RM回滚事务
二阶段-RM工作:
如果收到提交通知指令:删除undo-log
如果收到回滚通知指令:根据undo-log恢复到更新前的数据
AT与XA的区别
XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
XA模式依赖数据库机制实现回滚; AT模式利用数据快照实现数据回滚。
XA模式强一致; AT模式最终一致
脏写问题
多线程并发访问AT模式的分布式事务时,可能出现脏写问题。如图:
解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
![]()
AT模式的优缺点
AT模式的优点:
一阶段完成直接提交事务,释放数据库资源,性能比较好
利用全局锁实现读写隔离
没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
两阶段之间属于软状态,属于最终一致
框架的快照功能会影响性能,但比XA模式要好很多
使用示例【掌握】
T模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。
只不过,AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。
步骤:
准备数据快照表
开启AT模式
重启并测试
1) 准备数据库表
执行脚本《undo_log表.sql》,把
undo_log
表导入到微服务的库。我们这里是seata-demo
库这张表用于存储一阶段的undo日志,二阶段回滚时会使用这些日志进行数据恢复;二阶段提交时则直接清除日志
2) 开启AT模式
修改参与分布式事务的所有微服务的配置文件,将事务模式修改为AT模式
可以不配置,因为Seata默认使用的就是AT模式
seata: data-source-proxy-mode: AT3) 添加注解
在全局事务的入口方法上添加注解
@GlobalTransactional
我们这里在
OrderServiceImpl
的create
方法上添加
4) 重启测试
重启所有微服务
使用Postman测试
3. TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
Try:资源的检测和预留;
Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
Cancel:预留资源释放,可以理解为try的反向操作。
TCC模式的实现流程
举例说明:减扣余额的业务。
假设帐户A原本余额是100,需要减扣30元。
要提前准备一个位置存储冻结金额,例如:创建一张数据库表,存储冻结的金额
一阶段(Try)
检查余额是否充足。如果余额充足,则扣除余额30元,在冻结金额里增加30元。
此时总金额 = 余额 + 冻结金额,总数仍然是100元不变,分支事务可以直接提交,无需等待其它事务
![]()
二阶段(Confirm)
如果TC通知要提交,则冻结金额-30,直接提交; 用户的余额不变
此时总金额 = 余额 + 冻结金额,总数是70
![]()
二阶段(Cancel)
如果TC通知要回滚,则释放冻结金额,恢复用户余额,即:冻结金额-30,用户余额+30
此时总金额 = 余额 + 冻结金额,总数是100
![]()
Seata的TCC模式
Seata中的TCC模型依然延续之前的事务架构,如图:
优缺点
TCC模式的每个阶段做什么:
Try:资源检查和预留
Confirm:业务执行和提交
Cancel:预留资源的释放
TCC的优点:
一阶段完成直接提交事务,释放数据库资源,性能好
相比AT模型,无需生成快照,无需使用全局锁,性能最强
不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点:
有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
软状态,事务是最终一致
需要考虑Confirm和Cancel的失败情况,做好幂等处理
TCC的几个问题
空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就要允许空回滚。
业务悬挂
对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。
执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂
幂等性
当TC通知RM提交或回滚时,如果RM明明已经提交或回滚,但是因为某些原因(例如网络拥堵)导致没有给TC返回结果,TC会重复通知RM提交或回滚,直到收到结果为止。
为了避免Try或Confirm业务的重复执行,Try和Confirm需要实现幂等:判断一下事务的状态,如果已经处理过,就直接返回成功,结束即可。
使用示例
我们使用AT和TCC混合使用的方式进行演示:
用户余额处理适合使用TCC,就使用TCC模式
库存处理也适合使用TCC,但是我们TCC比较麻烦,就不处理了,仍然使用AT
创建订单不适合使用TCC,还使用AT模式
解决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel。
步骤:
定义一张表,用于存储冻结金额和事务状态
定义Try业务、Confirm业务和Cancel业务,并处理业务悬挂和空回滚问题
1) 创建表存储事务状态和冻结数据
在微服务的数据库(我们这里是
seata-demo
库)里创建表,如下:
CREATE TABLE `account_freeze_tbl` ( `xid` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `user_id` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `freeze_money` INT(11) UNSIGNED NULL DEFAULT 0, `state` INT(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel', PRIMARY KEY (`xid`) USING BTREE ) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;其中:
xid:全局事务的id
user_id:用户id,即 哪个用户的数据
freeze_money:冻结金额
state:事务状态
2) 实现Try、Confirm和Cancel业务
Try业务:
先根据xid查询
account_freeze_tbl
表数据,如果找到了说明Cancel已执行,拒绝执行Try业务如果找不到:
把冻结金额和事务状态保存到
account_freeze_tbl
表里减扣帐户表的余额
Confirm业务:
根据xid,删除记录(冻结金额就删除掉了)
Cancel业务:
根据xid先查询
account_freeze_tbl
表数据,如果找不到说明try还没有做,需要空回滚如果找到了:
修改
account_freeze_tbl
表:冻结金额为0,state为2(cancel)修改帐户表,恢复余额
声明TCC接口
TCC的Try、Confirm、Cancel方法都需要在接口中基于注解进行声明
修改帐户服务account-service,利用TCC实现余额扣减功能:
在
com.itheima.order.service
包里创建接口:注意在接口上添加
@LocalTCC
注解
/* * 这个接口,是用于让Seata扫描,告诉Seata一些TCC相关的配置。需要 * 1.接口上添加@LocalTCC * 2.在接口里定义好Try Confirm Cancel三个方法,方法名称随意 * 需要我们使用注解@TwoPhaseBusinessAction配置一下,告诉Seata哪里是Try、哪个是Confirm、哪个是Cancel * 注解在Try方法上,注解要求配置 * name:唯一标识 * commitMethod:提交方法是哪个(Confirm方法是哪个) * rollbackMethod:回滚方法是哪个(Cancel方法是哪个) */ @LocalTCC public interface AccountTccService { /** *Try,方法,尝试执行业务功能。扣除余额 * @param userId 用户id * @param money 要扣除的金额 */ @TwoPhaseBusinessAction(name = "deduct",commitMethod = "confirm",rollbackMethod = "cancel") void deduct(String userId, int money); /** * Confirm确认方法,相关于提交事务方法 */ void confirm(); /** * Cancel取消方法,相当于回滚事务方法 */ void cancel(); }编写实现业务
空回滚
业务悬挂
代码
修改:TCC接口为
/* * 这个接口,是用于让Seata扫描,告诉Seata一些TCC相关的配置。需要 * 1.接口上添加@LocalTCC * 2.在接口里定义好Try Confirm Cancel三个方法,方法名称随意 * 需要我们使用注解@TwoPhaseBusinessAction配置一下, * 告诉Seata哪里是Try、哪个是Confirm、哪个是Cancel。 * * 注解在Try方法上,注解要求配置 * name:唯一标识 * commitMethod:提交方法是哪个(Confirm方法是哪个) * rollbackMethod:回滚方法是哪个(Cancel方法是哪个) */ @LocalTCC public interface AccountTccService { /* *Try,方法,尝试执行业务功能。扣除余额 * @param userId 用户id * @param money 要扣除的金额 */ @TwoPhaseBusinessAction(name = "deduct",commitMethod = "confirm",rollbackMethod = "cancel") //参数上添加@BusinessActionContextParameter注解,表示把这个参数放到Context对象里进行共享 void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money") int money); /* * Confirm确认方法,相关于提交事务方法 * @param context Seata提供的一个上下文对象,用于在一次全局事务整个过程中,共享数据的 * 如果把一项数据放到这个context里,在整个事务里就能够共享着项数据 * 这个对象里,本身就有当前事务的id */ void confirm(BusinessActionContext context); /* * Cancel取消方法,相当于回滚事务方法 */ void cancel(BusinessActionContext context); }编写实现业务+@Transactional:实现幂等,要成功都成功要么反之
/* * 允许空回滚:Cancel方法里实现 * 如果回滚的时候,发现冻结表里没有任何冻结信息,说明没有执行过Try * 这时候要回滚,就要做空回滚:记录一条冻结信息,金额是0,状态是Cancel * * 防止业务悬挂:Try方法里实现,只要执行Try成功,就一定会增加一条冻结信息 * 已经执行Cancel回滚了,又执行Try,会造成业务悬挂 * 我们要避免业务悬挂;如果已经有冻结记录,并且状态是Cancel说明已经回滚过了 */ @Service public class AccountTccServiceImpl implements AccountTccService { @Autowired private AccountMapper accountMapper; @Autowired private AccountFreezeMapper accountFreezeMapper; @Override @Transactional public void deduct(String userId, int money) { //Seata提供一个工具类,用于在Try方法里获取当前全局事务id String xid = RootContext.getXID(); //要避免业务悬挂,如果已经执行Cancel方法,就不再执行Try的代码了 AccountFreeze freeze = accountFreezeMapper.selectById(xid); if (freeze != null && freeze.getState() == AccountFreeze.State.CANCEL) { return; } //1.扣除余额 accountMapper.deduct(userId, money); //2.把扣除的金额冻结起来 freeze = new AccountFreeze(); freeze.setXid(xid); freeze.setUserId(userId); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); accountFreezeMapper.insert(freeze); } @Override public void confirm(BusinessActionContext context) { //获取当前全局事务的id String xid = context.getXid(); accountFreezeMapper.deleteById(xid); } @Override @Transactional public void cancel(BusinessActionContext context) { String xid = context.getXid(); //1.先查询当前事务冻结的信息 AccountFreeze freeze = accountFreezeMapper.selectById(xid); if (freeze == null) { //是没有执行try,要允许空回滚;插入一条冻结金额为0,状态是Cancel的回滚信息 freeze = new AccountFreeze(); freeze.setXid(xid); //用户id:在try方法里,使用@BusinessActionContextParameter, //把这个参数放到了context对象里。现在就可以从context对象里取出这个userId freeze.setUserId(context.getActionContext("userId").toString()); freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); accountFreezeMapper.insert(freeze); return; } //2.把冻结的金额加回到当前用户的余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); //3.清除冻结的数据,把状态修改为Cancel,把金额设置成0 freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); accountFreezeMapper.updateById(freeze); } }
修改Controller
让Controller调用
AccountTCCService
的deduct
方法
@RestController @RequestMapping("account") public class AccountController { //@Autowired //private AccountService accountService; @Autowired private AccountTccService accountTccService; @PutMapping("/{userId}/{money}") public ResponseEntity<Void> deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money) { accountTccService.deduct(userId, money); return ResponseEntity.noContent().build(); } }在全局事务入口方法上加@GlobalTransactional
在OrderServiceImpl的
create
方法上添加注解@GlobalTransactional
@Override @GlobalTransactional public Long create(Order order) { ......; }
重启测试
使用Postman重新发请求进行下单,结果会下单失败;查看数据库里,订单、余额、库存数据都没变
4. SAGA模式
Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。
其理论基础是Hector & Kenneth 在1987年发表的论文Sagas。
Seata官网对于Saga的指南:Seata Saga 模式
原理说明
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
![]()
Saga也分为两个阶段:
一阶段:直接提交本地事务
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
优缺点
优点:
事务参与者可以基于事件驱动实现异步调用,吞吐高
一阶段直接提交事务,无锁,性能好
不用编写TCC中的三个阶段,实现简单
缺点:
软状态持续时间不确定,时效性差
没有锁,没有事务隔离,会有脏写
适用场景:
业务流程长、业务流程多
参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
5. 四种模式对比
-->XA一致性最强,但性能弱
-->AT 性能好一些,但NoSQL不能参与
-->TCC 性能更好,NoSQL能参与, 但第三方系统不能参与
-->SAGA 性能更好,NoSQL能参与,第三方系统也能参与
6. 小结
1. XA模式-使用步骤
修改所有微服务的配置文件,使用XA模式 seata.data-source-proxy-mode: XA
修改全局事务的入口方法,添加注解@GlobalTransactional
所有分支事务的方法,添加注解@Transactional
2. XA模式-两阶段提交过程
一阶段:注册并执行
开启全局事务,告诉TC
所有分支事务注册到TC
所有分支事务执行SQL但不提交事务
所有分支事务向TC汇报状态
二阶段:最终的决策结果
TC根据所有分支事务汇报的状态做决策
如果所有分支事务都成功:就通知所有分支执行提交操作。分支事务接收指令,执行提交
如果任意分支事务失败了:就通知所有分支执行回滚操作。分支事务接收指令,执行回滚
3. XA模式-优缺点
优点:
强一致性,所有节点的数据要么一起提交,要么一起回滚
实现简单,没有代码入侵
缺点:
可用性弱,一阶段长时间持有数据库锁不释放,到二阶段才会释放锁,影响性能
依赖于关系数据库本身的事务机制,NoSQL参与不进来
4. AT模式-使用步骤
先给微服务的库里,执行SQL脚本,创建一张表,用于存储备份数据
修改所有微服务的配置文件,设置事务模式为AT。如果不设置,默认就是AT
修改全局事务的入口方法,添加注解@GlobalTransactional
所有分支事务的方法,添加注解@Transactional
5. AT模式-两阶段提交
一阶段:注册与执行
开启全局事务告诉TC
各分支事务注册到TC
各分支事务执行SQL并直接提交、备份数据
各分支事务向TC汇报状态
二阶段:最终的决策与处理
TC根据所有分支事务的状态做决策
如果任意一个分支事务失败了:会通知所有分支事务执行回滚操作。分支事务会拿备份的数据恢复过来
如果所有分支事务都成功了:会通知所有分支事务执行提交操作。分支事务会删除备份数据
5. AT模式-脏写问题
一个事务在一阶段直接释放DB锁,二阶段才会对数据做提交或回滚
在一阶段和二阶段中间,其它事务可能对这条数据做修改
使用“全局锁”可以解决脏写问题:
一阶段在释放DB锁之前,先加全局锁
二阶段在最终操作的时候,释放全局锁,添加DB锁,操作完成释放DB锁
在一阶段和二阶段全过程,都对这条数据有锁,避免了其它事务的干扰,避免了脏写问题
6. AT模式-优缺点
优点:
没有代码入侵
性能比XA模式要好:一阶段直接释放DB锁
缺点:
是最终一致,存储数据的临时不一致状态
利用了框架的快照功能,会对性能造成一些影响。但是比XA模式性能要好
依赖于关系数据库本身的事务机制,NoSQL参与不进来
7. TCC模式-两阶段提交过程
一阶段:注册与执行
开启全局事务告诉TC
各分支事务注册到TC
各分支事务执行Try方法:执行资源的预留
各分支事务向TC汇报状态
二阶段:最终的决策
TC会根据所有分支事务的状态,做最终的决策
如果所有分支事务都成功了,就通知所有分支事务执行提交:分支事务执行Confirm方法
如果任意分支事务失败了,就通知所有分支事务执行回滚:分支事务执行Cancel方法
8. TCC模式-业务悬挂与空回滚
业务悬挂:已经执行过Cancel了,再执行Try。这样的话Try预留的资源将永远不会提交或回滚。要防止悬挂
空回滚:还没有执行Try,就Cancel了。要允许空回滚
防止业务悬挂,允许空回滚
前提:
只要执行了Try,就一定有一条冻结记录,状态是Try
只要执行了Cancel,就一定有一条冻结记录,状态是Cancel
实现:
执行Try的时候,如果发现已经有冻结记录并且是Cancel,说明已经回滚过了,不再执行Try
执行Cancel的时候,如果发现还没有冻结记录,说明还没有执行过Try,就空回滚,插入一条冻结0的状态Cancel的记录
9. TCC模式-优缺点
优点:
一阶段直接提交事务,不需要长时间持有DB锁,性能好
不需要使用快照备份,而是基于资源预留的补偿操作,不需要全局锁,性能更好
不依赖于关系数据库本身的事务机制,NoSQL也能参与进来
缺点:
代码入侵比较严重,需要编写Try、Confirm、Cancel方法,还要注意防悬挂、空回滚、幂等性
是最终一致状态,存在数据的临时不一致
不方便让老旧系统或第三方系统参与到事务里
Seata的TC服务作为分布式事务核心,一定要保证seata集群的高可用性。
搭建TC服务集群非常简单,启动多个TC服务,注册到nacos即可,但集群并不能确保100%安全。例如集群所在机房故障了怎么办?
所以如果可用性要求较高,一般都会做异地多机房容灾。比如一个TC集群在上海,另一个TC集群在杭州:
微服务基于事务组(tx-service-group)与TC集群的映射关系,来查找当前应该使用哪个TC集群。当SH集群故障时,只需要将vgroup-mapping中的映射关系改成HZ。则所有微服务就会切换到HZ的TC集群了。
参考资料里《Seata高可用\seata的部署和集成.md》中的第三章节
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。