目录
一、分布式事务的问题
1. 本地事务
事务概念:即传统的单机事务,是数据库的概念,表示由一个或多个操作组成的一个业务。比如:银行转账
事务作用:组成事务的多个操作单元,在操作数据库时,要成功都成功,要失败都失败
事务特性:ACID
-
多个数据库操作如果想要属于同一个事务:必须使用同一个数据库连接
-
如果开启了事务,在数据库底层会对数据加锁:如果一个事务长时间不提交,一定会影响性能
2. 分布式事务
1 介绍
在分布式环境上同样需要事务来保证数据的一致性。而因为跨数据源或跨服务环境所导致的传统事务不可用,形成的新的事务需求,这样的事务叫分布式事务。
传统事务中,要想让多个操作属于同一事务,就需要使用同一个数据库连接Connection
对象。但是在分布式环境下,通常是做不到这一点的,必须使用分布式事务。比如:
-
跨数据源的分布式事务:程序要操作不同服务器上的数据库
-
跨服务的分布式事务:程序要调用多个服务,每个服务都要操作数据库
-
综合情况
2 示例场景
电商行业中比较常见的下单付款案例,包括下面几个行为:
-
创建新订单
-
扣减商品库存
-
从用户账户余额扣除金额
要完成上面的操作,需要访问三个不同的微服务和三个不同的数据库,如图:
订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。
但是当我们把三件事情看做一个"业务",要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务要解决的问题了
3. 分布式事务的问题演示
1 准备数据库
使用SQLyog执行SQL脚本《seata-demo.sql》,初始化数据库
2 导入演示项目
今天的资料里提供了《seata-demo》项目,把这个项目拷贝到你的工作空间目录里
用idea打开这个项目
3 启动服务
-
启动nacos
-
启动所有微服务
4 测试下单功能
使用Postman发请求下单
请求路径是:http://localhost:8082/order
请求方式是:POST
表单参数是:
-
userId:user202103032042012
-
commodityCode:100202003032041
-
count:20
-
money:200
最终结果是:因为库存量不够,导致扣减库存失败,但是用户的帐户余额已经扣减了。已经出现了分布式事务问题
二、CAP定理和BASE理论【面试】
分布式事务问题的处理,其实就是在数据的一致性与服务的可用性之间做一个权衡:
-
如果要保证所有子事务的数据一致性:就要舍弃一些服务的可用性。因为数据库事务会对数据行加锁
-
如果要保证所有服务的可用性:就要考虑一下数据的一致性如何处理
解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导
1. CAP定理
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。 Eric Brewer 说,这三个指标不可能同时做到,这个结论就叫做 CAP 定理。
1 CAP三指标介绍
1 C一致性
一致性Consitency,即 用户访问分布式系统中的任意节点,得到的数据必须一致(业务上的一致)。这就要求节点之间必须要及时同步数据。
比如:集群中有两个节点,初始数据是一致的;当修改了其中一个节点的数据时,要把数据变更立即同步到另外一个节点,保证所有节点的数据是一致的。
2 A可用性
可用性Availability,即 用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
3 P分区容错性
分区Partition:因为网络故障或者其它原因,导致分布式系统中的部分节点与其它节点失联,形成独立分区
分区容错性Partition Tolerance,即 集群出现分区时,整个系统也要持续对外提供服务
2 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理论就提出了完善和弥补的方案
2. BASE理论
BASE理论,是对CAP定理中的CA矛盾进行权衡之后,提供的一种解决思路。这是指:
-
采用CP模式,追求一致性,舍弃一定的可用性:
-
BA (Basically Available),基本可用:分布式系统在出现故障时,允许损失部分可用性,要保证核心可用
响应时间的损失:比如原本要求0.5秒内响应,现在允许5秒内响应
系统功能的损失:出现某些故障时,核心功能保证可用,部分非核心功能允许不可用
-
-
采用AP模式,追求可用性,对一致性采用一些补偿措施
-
S (Soft State),软状态:在一定时间内,允许出现中间状态,比如 数据临时不一致
-
E (Eventually Consistent),最终一致性:虽然无法保证强一致性,但是在软状态之后,最终达到数据一致
-
3. 分布式事务的解决思路
1 解决思路
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论。有两种解决思路:
-
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
-
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
2 几个概念
-
分支事务RM:ResourceManager 在整个业务中,每一个子系统的事务,称为一个分支事务。
对应@Transactional
-
全局事务TM:TransactionManager 一个完整的业务,需要众多分支事务共同组成一个全局事务。
对应@GlobalTransactional
-
事务协调者TC:TransactionCoordinator 用于在整个全局事务里,管理、协调各个分支事务的状态。
对应Seata软件
4. 小结
CAP定理
CAP分别是:
C:一致性。表示分布式系统各节点之间的数据或状态 应该保持一致。 需要花费一定的时间进行数据同步
A:可用性。表示访问分布式系统的任意一个节点,都必须立即响应。如果要求立即响应就不能保证数据已经同步完成
P:分区容错性。表示当系统出现问题形成多个分区时,应当正常提供服务
CAP定理:
一个分布式系统最多只能满足CAP里的2项,不可能全部满足
要么AP:追求可用性,就不能保证一致性
要么CP:追求一致性,就不能保证可用性
BASE理论:对CAP定理的一种权衡后的处理方案
BA:Basically Available,基本可用
不要求绝对可用,只要做到基本可用即可。
追求一致性的时候,允许响应时间略有延迟、允许非核心功能暂不可用
SE:S软状态 + E最终一致
不要求绝对的一致性,只要达到最终一致即可,允许存在临时的数据不一致状态
三、Seata入门与整合
1. Seata简介
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:Apache Seata,其中的文档、博客中提供了大量的使用说明、源码分析。
2. Seata架构
Seata事务管理中有三个重要的角色:
-
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
是Seata本身
-
TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开启全局事务、提交或回滚全局事务。
负责事务的边界。@GlobalTransactional
-
RM (Resource Manager) - 资源管理器:处理分支事务的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。@Transactional
分支事务
Seata基于上述架构提供了四种不同的分布式事务解决方案:
-
XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
-
AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
-
TCC模式:最终一致的分阶段事务模式,有业务侵入
-
SAGA模式:长事务模式,有业务侵入
无论哪种方案,都离不开TC,也就是事务的协调者。
3. Seata部署
seata-server中分布式事务中充当了TC的角色
1 下载与安装
1 下载
下载seata-server包,地址 http://seata.io/zh-cn/blog/download.html
也可以直接使用资料里提供好的程序:《seata-server-1.4.2.zip》
2 安装
seata-server免安装,直接解压到一个不含中文、空格、特殊字符的目录里即可
2 准备数据库
tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,包括全局事务、分支事务、全局锁等信息,因此要提前创建好这些表:
用Navicat或其它工具连接本机MySQL,执行脚本:《seata-tc-server.sql》
3 配置
1 在nacos里添加配置
注意,为了让tc服务的集群可以共享配置,我们选择了nacos作为统一配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好
-
在nacos中新建配置:
2. 配置的内容如下:
注意:其中的数据库地址、帐号、密码都要修改成自己的
# 数据存储方式,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?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=9898
2 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"
}
}
4 启动TC服务
-
先启动nacos
-
启动seata:
进入seata的bin目录,运行其中的
seata-server.bat
3.验证是否启动:
如果启动成功了,seata-server应该已经注册到nacos注册中心了
我们打开nacos,看一下有没有seata服务
4. 微服务集成Seata
每个需要分布式事务的微服务,都要按照下面的步骤进行配置。
我们以订单服务order-service
为例进行说明;其它微服务也要做相同配置
1 添加依赖
修改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>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</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>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.9</version>
</dependency>
2 配置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
5. 小结
安装部署Seata的步骤:
-
执行SQL脚本:创建数据库,这个库给seata使用的,seata用来存储事务信息
-
在Nacos里准备配置文件:配置文件给seata使用的,seata要拉取这个配置文件,根据配置信息连接数据库
-
解压Seata,修改Seata的配置文件:conf/registry.conf
-
config的配置:从哪拉取配置文件。以下配置的参数是
使用了type为nacos的配置中心
从 serverAddr对应配置中心里拉取配置文件
-
serverAddr要配置Nacos的地址,username和password是nacos的帐号密码
-
要从namespace为""的名称空间里,从group为SEATA_GROUP的组里,拉取名称为 seataServer.properties的配置文件
-
-
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
registry配置:要把当前seata服务注册到哪
要使用type为nacos的注册中心,把自己(当前seata服务)上报到注册中心nacos里
-
自己的应用名称是:使用application配置
-
nacos的地址信息:serverAddr,username,password
-
把当前seata服务注册到:namespace为""的名称空间、group为DEFAULT_GROUP的组、集群名称为BJ
registry {
type = "nacos"nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "BJ"
username = "nacos"
password = "nacos"
}
}
4.启动seata
微服务要整合seata:
-
所有微服务添加依赖坐标
-
所有微服务修改配置文件:主要是要设置 从哪才能拉取得到seata的地址
seata:
registry:
type: nacos #要从nacos里拉取seata的信息
nacos:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: ""
group: DEFAULT_GROUP
application: seata-server
tx-service-group: seata-demo #微服务所属的组名,自定义一个组名
service:
vgroup-mapping:
seata-demo: BJ #设置微服务组关联的seata集群。seata-demo服务组,要使用Bj集群的seata服务
四、Seata的事务模式
1. XA模式
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
1 两阶段提交
XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交:
-
一阶段:
事务协调者通知每个事务参与者执行其本地事务
本地事务执行后暂不提交,继续持有数据库锁;向事务协调者报告事务的执行状态
-
二阶段:
事务协调者基于一阶段的报告来决定下一步操作
如果一阶段所有事务都成功:则通知所有事务参与者都提交事务
如果一阶段有事务执行失败:则通知所有事务参与者都回滚事务
如图:
-
如果所有事务都正常:
如果有事务出现异常:
2 Seata的XA模型
Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型
-
一阶段-RM工作:
注册分支事务到TC
执行分支业务的SQL但不提交
向TC报告事务执行状态
-
二阶段-TC工作
TC检测各分支事务的执行状态
如果都成功:TC通知所有RM提交事务
如果有失败:TC通知所有RM回滚事务
-
二阶段-RM工作
根据TC的通知指定,提交或回滚事务
Seata的XA基本架构如图:
3 XA模式的优缺点
XA模式的优点:
-
事务的强一致性,满足ACID原则。
-
多事务(即多分布式事务之间)之间是完全隔离。多事务并发完全不受影响
-
常用RDBMS数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点:
-
性能较差,因为一阶段需要锁定数据库资源,等待二阶段结束才释放,所以性能较差
-
依赖关系型数据库实现事务。NoSQL参与不进来
4 使用示例
Seata的依赖启动器已经完成了XA模式的自动装配,使用起来非常简单,步骤:
-
修改配置文件,开启XA模式
-
修改全局事务入口方法,添加注解
@GlobalTransactional
注解:TM全局事务每个分支事务的方法上,添加注解
@Transactional
注解:RM分支事务 -
重启测试
1) 开启XA模式
修改每个参与事务的微服务的配置文件,开启XA模式:
seata:
data-source-proxy-mode: XA
2) 添加注解@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模式【重点】
1 Seata的AT模型
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
-
一阶段-RM工作:
向TC注册事务分支
记录undo-log(数据变更之前的快照)
执行业务SQL并提交
向TC报告事务状态
-
二阶段-TC工作:
基于一阶段的报告来决定下一步操作
如果所有分支事务都成功:通知所有RM提交事务
如果任一分支事务失败了:通知所有RM回滚事务
-
二阶段-RM工作:
如果收到提交通知指令:删除undo-log
如果收到回滚通知指令:根据undo-log恢复到更新前的数据
2 AT与XA的区别
-
XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
-
XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
-
XA模式强一致;AT模式最终一致
3 脏写问题
多线程并发访问AT模式的分布式事务时,可能出现脏写问题。如图:
解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
5 AT模式的优缺点
AT模式的优点:
-
一阶段完成直接提交事务,释放数据库资源,性能比较好
-
利用全局锁实现事务之间的隔离
-
没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
-
两阶段之间属于软状态,属于最终一致
-
框架的快照功能会影响性能,但比XA模式要好很多
6 使用示例【掌握】
T模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。
只不过,AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。
步骤:
-
准备数据快照表
-
开启AT模式
-
重启并测试
1) 准备数据库表
执行脚本《undo_log表.sql》,把undo_log
表导入到微服务的库。我们这里是seata-demo
库
这张表用于存储一阶段的undo日志,二阶段回滚时会使用这些日志进行数据恢复;二阶段提交时则直接清除日志
2) 开启AT模式
修改参与分布式事务的所有微服务的配置文件,将事务模式修改为AT模式
可以不配置,因为Seata默认使用的就是AT模式
seata:
data-source-proxy-mode: AT
3) 添加注解
在全局事务的入口方法上添加注解@GlobalTransactional
我们这里在OrderServiceImpl
的create
方法上添加
4) 重启测试
-
重启所有微服务
-
使用Postman测试
3. TCC模式【了解】
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
-
Try:资源的检测和预留;
-
Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
-
Cancel:预留资源释放,可以理解为try的反向操作。
1 TCC模式的实现流程
举例说明:减扣余额的业务。
-
假设帐户A原本余额是100,需要减扣30元。
-
要提前准备一个位置存储冻结金额,例如:创建一张数据库表,存储冻结的金额
一阶段(Try)
检查余额是否充足。如果余额充足,则扣除余额30元,在冻结金额里增加30元。
此时总金额 = 余额 + 冻结金额,总数仍然是100元不变,分支事务可以直接提交,无需等待其它事务
二阶段(Confirm)
如果TC通知要提交,则冻结金额-30,直接提交; 用户的余额不变
此时总金额 = 余额 + 冻结金额,总数是70
三阶段(Cancel)
如果TC通知要回滚,则释放冻结金额,恢复用户余额,即:冻结金额-30,用户余额+30
此时总金额 = 余额 + 冻结金额,总数是100
2 Seata的TCC模式
Seata中的TCC模型依然延续之前的事务架构,如图:
3 优缺点
TCC模式的每个阶段做什么:
-
Try:资源检查和预留
-
Confirm:业务执行和提交
-
Cancel:预留资源的释放
TCC的优点:
-
一阶段完成直接提交事务,释放数据库资源,性能好
-
基于资源预留实现数据隔离;相比AT模型,无需生成快照,无需使用全局锁,性能更强
-
不依赖数据库事务,而是依赖补偿操作,可以用于非关系型数据库
TCC的缺点:
-
有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
-
软状态,事务是最终一致
-
需要考虑Confirm和Cancel的失败情况,做好幂等处理
4 TCC的几个问题
1 空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时就要允许空回滚。
2 业务悬挂
对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。
执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂
3 幂等性
当TC通知RM提交或回滚时,如果RM明明已经提交或回滚,但是因为某些原因(例如网络拥堵)导致没有给TC返回结果,TC会重复通知RM提交或回滚,直到收到结果为止。
为了避免Try或Confirm业务的重复执行,Try和Confirm需要实现幂等:判断一下事务的状态,如果已经处理过,就直接返回成功,结束即可。
5 使用示例
我们使用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) -
修改帐户表,恢复余额
-
1. 声明TCC接口
TCC的Try、Confirm、Cancel方法都需要在接口中基于注解进行声明
修改帐户服务account-service,利用TCC实现余额扣减功能:
-
在
com.itheima.order.service
包里创建接口: -
注意在接口上添加
@LocalTCC
注解
package com.itheima.account.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
@LocalTCC
public interface AccountTCCService {
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money") int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
2. 编写实现业务
package com.itheima.account.service.impl;
import com.itheima.account.mapper.AccountFreezeMapper;
import com.itheima.account.mapper.AccountMapper;
import com.itheima.account.pojo.AccountFreeze;
import com.itheima.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper accountFreezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
//1. 获取全局事务id
String xid = RootContext.getXID();
//2. 防止业务悬挂:如果已经Cancel了,就拒绝执行Try
AccountFreeze oldFreeze = accountFreezeMapper.selectById(xid);
if (oldFreeze != null) {
return;
}
//3. 扣减余额
accountMapper.deduct(userId, money);
//4. 把冻结金额和事务状态存储起来
AccountFreeze freeze = new AccountFreeze();
freeze.setXid(xid);
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
accountFreezeMapper.insert(freeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
//获取全局事务id
String xid = ctx.getXid()();
//删除冻结信息
int count = accountFreezeMapper.deleteById(xid);
return count == 1;
}
@Override
@Transactional
public boolean cancel(BusinessActionContext ctx) {
//1. 获取全局事务id
String xid = ctx.getXid();
//2. 空回滚判断
AccountFreeze freeze = accountFreezeMapper.selectById(xid);
if (freeze == null) {
freeze = new AccountFreeze();
freeze.setXid(xid);
String userId = ctx.getActionContext("userId").toString();
freeze.setUserId(userId);
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
accountFreezeMapper.insert(freeze);
return true;
}
//3. 如果已经Cancel过了,就不需要重新Cancel
if (freeze.getState() == AccountFreeze.State.CANCEL) {
return true;
}
//4. 恢复用户余额
accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
//5. 清零冻结金额,修改事务状态
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
int count = accountFreezeMapper.updateById(freeze);
return count == 1;
}
}
3. 修改Controller
让Controller调用AccountTCCService
的deduct
方法
@RestController
@RequestMapping("account")
public class AccountController {
@Autowired
// private AccountService accountService;
private AccountTCCService accountService;
@PutMapping("/{userId}/{money}")
public ResponseEntity<Void> deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money) {
accountService.deduct(userId, money);
return ResponseEntity.noContent().build();
}
}
3) 在全局事务入口方法上加@GlobalTransactional
在OrderServiceImpl的create
方法上添加注解@GlobalTransactional
@Override
@GlobalTransactional
public Long create(Order order) {
......;
}
4) 重启测试
使用Postman重新发请求进行下单,结果会下单失败;查看数据库里,订单、余额、库存数据都没变
4. SAGA模式【拓展】
Saga 模式是 Seata 开源的长事务解决方案,将由蚂蚁金服主要贡献。
其理论基础是Hector & Kenneth 在1987年发表的论文Sagas。
Seata官网对于Saga的指南:https://seata.io/zh-cn/docs/user/saga.html
1 原理说明
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga也分为两个阶段:
-
一阶段:直接提交本地事务
-
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
2 优缺点
优点:
-
事务参与者可以基于事件驱动实现异步调用,吞吐高
-
一阶段直接提交事务,无锁,性能好
-
不用编写TCC中的三个阶段,实现简单
缺点:
-
软状态持续时间不确定,时效性差
-
没有锁,没有事务隔离,会有脏写。
某一个环节给你转账过去了,后边的环节出错要补偿撤消。但是转给你的钱,已经被你花掉了
做法:
-
宁可长款,不可短款:商家宁可多收你钱,如果出错最后退给你;也不能先给你钱,出错后找你要
-
具体实现:扣你钱的操作放到前边,给你加钱的操作放到最后的环节
-
适用场景:
-
业务流程长、业务流程多
-
参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
5. 四种模式对比
6. 小结
XA模式
XA模式的用法:
1. 修改所有微服务的配置文件,设置事务模式为XA。 seata.data-source-proxy-mode=XA
2. 全局事务入口方法上加 @GlobalTransactional;所有分支事务方法上加@Transactional
XA两阶段提交过程:
一阶段:执行、汇报但不提交
开启全局事务,各分支事务注册到TC
各分支事务执行SQL但不提交
各分支事务向TC汇报状态
二阶段:最终的提交或回滚
全局事务要结束,通知TC做最终决策
如果所有分支事务都成功,就通知所有分支事务一起提交
如果任意分支事务失败了,就通知所有分支事务一起回滚
各分支事务:执行TC的最终决策,提交或回滚
XA模式的优缺点:
优点:
强一致性,各个分支事务之间是完全一致的,一起提交,一起回滚
隔离性好:借助于单机事务实现多全局事务的隔离。多全局事务并发不会受影响
实现简单:没有代码入侵,只要添加注解就能实现XA模式的事务
缺点:
可用性弱:因为一阶段开启事务执行SQL但不提交,数据库底层会对数据长时间加锁
NoSQL参与不进来:依赖于关系型数据库的事务机制实现
AT模式
AT模式的用法:
1. 在微服务的库里准备一张undo_log表。给seata用的,我们的代码用不上
2. 修改所有微服务的配置文件,设置事务模式为AT。 seata.data-source-proxy-mode=AT
3. 全局事务入口方法上加 @GlobalTransactional;所有分支事务方法上加@Transactional
AT的两阶段提交过程:
一阶段:
各分支事务注册到TC
各分支事务执行SQL提交,并备份数据(数据变更前后的数据)
各分支事务向TC汇报状态
二阶段:
TC根据各分支事务状态做最终的决策,通知给所有分支事务
各分支事务根据TC的通知:
如果要提交:直接清除undo备份
如果要回滚:就拿undo备份的数据进行恢复,然后删除undo备份
AT模式优缺点:
优点:
实现简单,无代码入侵
可用性比XA模式要强:因为一阶段直接提交事务
事务并发时的隔离性:使用全局锁实现,处理脏写问题
缺点:
存在脏读脏写问题,隔离性不如XA强
只支持RDBMS数据库,依赖于关系型数据库本身的事务备份机制
AT模式脏写问题:
一个全局事务在一阶段提交之后,到二阶段之前 要防止数据被其他人修改掉:加全局锁
具体的过程是:
一阶段:全程持有数据的DB锁
开启事务:抢DB锁,锁定要修改的数据
执行SQL并备份:备份的是变更前后的数据。比如 余额之前100,之后90
加全局锁:对数据加全局锁===> 事务xx对xx数据持有全局锁
提交事务:释放DB锁
一和二阶段之间:持有数据的全局锁
在这个阶段,其它全局事务不可能抢到数据的全局锁,不可能对数据进行修改
但是其它 非Seata事务仍然可以修改数据
二阶段:如果要回滚,全程持有数据的DB锁
开启事务:抢DB锁,锁定要修改的数据
释放全局锁
恢复数据:拿备份的数据进行恢复
先判断:数据库里当前数据,和自己之前修改后的数据是否相同
如果不同:说明在我一阶段到二阶段之间,数据被其他人修改了,Seata会报错,等待人工干预
如果相同:说明在我一阶段一二阶段之间,数据没有被别人修改,直接恢复数据即可
提交事务:释放DB锁
TCC模式
TCC:
T:Try,是在一阶段执行的,尝试进行资源的预留预扣(冻结)。我们写,我们调
C:Confirm,是在二阶段执行,相当于事务的提交方法。我们写,由Seata调用
直接清除冻结的数据
C:Cancel,是在二阶段执行,相当于事务的回滚方法。我们写,由Seata调用
把冻结的数据,加回到原始数据里
TCC模式两阶段提交:
一阶段:
各分支事务注册到TC
各分支事务执行Try方法,直接提交
向TC上报自己的状态
二阶段:
TC根据各分支事务的状态做最终的决策,然后通知给所有的分支事务
各分支事务根据TC决策:
如果要提交:就执行Confirm方法
如果要回滚:就执行Cancel方法
TCC的使用示例:
1. 准备一张表,用于存储冻结的数据;准备这张表的实体类、Mapper等等
2. 编写TCC接口:Service层的接口
类上加@LocalTCC
类里要编写3个抽象方法:
Try方法:方法名随意,执行数据的扣除与冻结
方法上加@TwoPhaseBusinessAction(name="唯一标识",commitMethod="confirm方法",rollbackMethod="cancel方法")
告诉Seata,哪个方法是Confirm,哪个方法是Cancel
方法的形参加@BusinessActionContextParameter(paramName="参数名")
告诉Seata,把这个形参的值维护起来。后续在Confirm和Cancel方法里通过BusinessActionContext对象可以获取到
Confirm方法:方法名随意,相当于提交事务,清除冻结数据
Cancel方法:方法名随意,相当于回滚事务,把冻结的数据加回到原始数据里
3. 编写TCC接口的实现类,实现Try、Confirm、Cancel方法
Try方法:
要防止业务悬挂:防止Try没成功,Cancel之后Try方法执行造成的业务悬挂
扣除资源
冻结资源
Confirm方法:
相当于提交事务,直接清除冻结的数据
Cancel方法:
相当于回滚事务
要允许空回滚:如果Try还没有执行成功,就执行Cancel了,要允许空回滚
要实现幂等性:如果Cancel方法被多次重复调用,不应该产生不良后果。如果已经回滚过,就直接结束
把冻结的数据进行恢复,设置状态为已回滚
TCC模式的优缺点:
优点:
可用性强。不需要全局锁了,并且一阶段直接提交事务
NoSQL也能参与进来,因为两阶段的代码需要我们自己实现
数据隔离:通过资源预留进行隔离的
缺点:
有代码入侵,实现麻烦
是最终一致,存在数据的临时不一致状态
五、Seata高可用【拓展】
Seata的TC服务作为分布式事务核心,一定要保证seata集群的高可用性。
1. Seata高可用架构模型
搭建TC服务集群非常简单,启动多个TC服务,注册到nacos即可,但集群并不能确保100%安全。例如集群所在机房故障了怎么办?
所以如果可用性要求较高,一般都会做异地多机房容灾。比如一个TC集群在上海,另一个TC集群在杭州:
微服务基于事务组(tx-service-group)与TC集群的映射关系,来查找当前应该使用哪个TC集群。当SH集群故障时,只需要将vgroup-mapping中的映射关系改成HZ。则所有微服务就会切换到HZ的TC集群了。
2. 实现高可用
参考资料里《Seata高可用\seata的部署和集成.md》中的第三章节
大总结
CAP定理
-
C:一致性。分布式系统的多个节点数据和状态要保持一致。靠数据同步实现
-
A:可用性。访问分布式系统的任意节点,都要立即给出响应,不能阻塞、不能拒绝
-
P:分区容错性。分布式系统出现问题形成多分区,整个系统应该继续正常提供服务
-
CAP矛盾:首先分布式系统必须要满足P分区容错性,A和C矛盾
BASE理论:
-
BA:基本可用。只要做到基本可用即可,允许舍弃一定的可用性,比如响应时间稍有延长,非核心功能允许暂不可用
-
SE:软状态 + 最终一致。允许节点之间存在数据的临时不一致,只要最终一致即可
分布式事务的实现技术:Seata
-
TM:全局事务管理器,在代码里指@GlobalTransactional,决定了事务的边界与开启、结束
-
RM:资源管理器,在代码里指@Transactional,是分支事务
-
TC:事务协调者,指Seata软件,用于协调各分支事务的状态,决定提交还是回滚
Seata的事务模式:
-
XA:强一致、弱可用的
用法:
-
修改微服务的配置文件设置为XA模式;
-
全局事务入口方法加@GlobalTransactional,分支事务方法加@Transactional
两阶段提交:
-
一阶段:各分支事务注册到TC,执行语句但不提交,向TC汇报状态
-
二阶段:
TC根据各分支事务的状态,决定提交还是回滚并通知给所有分支事务
分支事务根据TC的通知,执行提交或回滚
优缺点:
-
优点:
强一致性。所有节点一起提交或一起回滚
实现简单。无代码入侵
事务并发时完全隔离。通过数据库本地事务实现隔离
-
缺点:
可用性弱。一阶段不提交事务,会对数据长时间加DB锁
依赖于RDBMS本身的事务机制,所以NoSQL不能参与进来
-
-
AT:弱一致,可用性较强
用法:
-
在微服务的库里创建一张undo-log表。给Seata使用的,用来存储备份数据
-
修改微服务的配置文件设置为AT模式;
-
全局事务入口方法加@GlobalTransactional,分支事务方法加@Transactional
两阶段提交:
-
一阶段:
各分支事务注册到TC,执行语句备份数据
先对数据加全局锁,然后再提交事务
向TC汇报状态
-
二阶段:
TC根据各分支事务的状态,决定提交还是回滚并通知给所有分支事务
分支事务根据TC的通知,执行提交或回滚
-
如果 要提交:就直接清除备份数据
-
如果 要回滚:就使用备份数据进行恢复
获取DB锁,释放全局锁
执行数据恢复
释放DB锁
-
AT脏写问题的处理:使用了全局锁
优缺点:
-
优点:
可用性较强。因为一阶段直接提升事务
使用了全局锁实现事务并发时的隔离
实现简单。没有代码入侵
-
缺点:
存在数据的临时不一致状态
使用了数据库的快照机制进行备份,并有全局锁,对性能有一定影响
-
-
TCC:
用法:和AT模式混用
-
创建TCC接口:加@LocalTCC;写3个方法
Try方法上加@TwoPhaseBusinessAction(name="", commitMethod="",rollbackMethod="")
Try方法形参加@BusinessActionContextParameter(paramName="参数名")
Confirm和Cancel方法:添加BusinessActionContext形参
-
实现接口
Try方法:防止业务悬挂,扣除资源,冻结资源
Confirm方法:清除冻结数据
Cancel:允许空回滚,实现幂等性,把冻结的数据进行恢复
两阶段提交:
-
一阶段:
各分支事务注册到TC,执行Try方法并提交,向TC汇报状态
-
二阶段:
TC根据各分支事务的状态,决定提交还是回滚并通知给所有分支事务
分支事务根据TC的通知,执行提交或回滚
-
如果要提交:就执行Confirm方法
-
如果要回滚:就执行Cancel方法
-
优缺点:
-
优点:
可用性强。一阶段直接提交事务,不需要全局锁
基于资源预留实现事务并发的隔离
NoSQL也能参与进来
-
缺点:
实现麻烦,需要编写TCC三个方法
数据临时不一致的状态
-
-
SAGA:长事务模型
两阶段提交:
-
一阶段:直接执行正向业务
-
二阶段:如果一切正常,就直接结束;如果一阶段出错,就要逆向执行所有业务的补充操作
优缺点:
-
优点:
性能强,完全不做隔离
适用于长事务,可以让第三方系统和旧系统加入全局事务
-
缺点:
有脏写问题
数据不一致,是最终一致
-