分布式事务Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。定义全
一、基础概念
1.Seata 是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)
官网:Apache Seata
源码: https://github.com/seata/seata
官方Demo: https://github.com/seata/seata-samples
seata版本:v1.4.0
2.Seata AT模式设计特点
1.优点
相比与其它分布式事务框架,Seata架构的亮点主要有几个:
-
应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
-
将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚;
-
通过全局锁实现了写隔离与读隔离。
2.不足
1.性能损耗
一条Update的SQL,则需要全局事务xid获取(与TC通讯)、before image(解析SQL,查询一次数据库)、after image(查询一次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要一次远程通讯RPC,而且是同步的。另外undo log写入时blob字段的插入性能也是不高的。每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间。
2.性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?按照二八原则预估,为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?
3.全局锁
1.热点数据
相比XA,Seata 虽然在一阶段成功后会释放数据库锁,但一阶段在commit前全局锁的判定也拉长了对数据锁的占有时间,这个开销比XA的prepare低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重。
2.回滚锁释放时间
Seata在回滚时,需要先删除各节点的undo log,然后才能释放TC内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长。
3.死锁问题
Seata的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。
二、架构设计
1. Seata的三大角色
1.角色定义
在 Seata 的架构中,一共有三个角色:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。
架构关系如下:

2.角色关联

1.TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
2.RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
3.TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
4.TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。
2.Seata执行流程
AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如下
-
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
-
二阶段:
-
提交异步化,非常快速地完成。
-
回滚通过一阶段的回滚日志进行反向补偿。
1.第一阶段
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql进行解析,转换成undolog,并同时入库,这是怎么做的呢?先抛出一个概念DataSourceProxy代理数据源,通过名字大家大概也能基本猜到是什么个操作,后面做具体分析
参考官方文档: Seata AT 模式 | Apache Seata

-
解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息。
-
查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
-
执行业务 SQL:更新这条记录的 name 为 'GTS'。
-
查询后镜像:根据前镜像的结果,通过 主键 定位数据。
-
插入回滚日志:把前后镜像数据以及业务 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"
}
-
提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
-
本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
-
将本地事务提交的结果上报给 TC。
2.第二阶段
1.分布式事务操作成功.
则TC通知RM异步删除undolog

-
收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
-
异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
2.分布式事务操作失败.
TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

-
收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
-
通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
-
数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
-
根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
-
提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
然后整体执行流程如下

global_table、lock_table、branch_table
表中的变化过程参考:
分布式事务框架 Seata 入门案例 - 废物大师兄 - 博客园
3.Seata并发隔离方案
思考一个问题:
比如商品增加库存,它不是一个分布式事务,既然没有分布式事务去管理它,那就不会被全局锁锁住。
于是 AT 还支持管理这种单次操作(加一个注解@GlobalLock),让它也注册到 AT(虽然不是分布式事务,但可以使用里面的锁)
所以它在操作库存时,也会到 TC 里找所要操作的记录是否被锁住,这就搞定了隔离性
不会出现下单操作还没回滚呢,库存就被修改了,这就保证了不会脏写。
1.写隔离
1.分布式事务-脏写的问题
假设你的业务代码是这样的:
-
updateAll()用来同时更新A和B表记录,updateA() updateB()则分别更新A、B表记录
-
updateAll()已经加上了@GlobalTransactional

回滚时,发现A记录已被修改,会造成无法全局回滚,产生如下信息:
2.全局锁-解决脏写的问题
-
一阶段本地事务提交前,需要确保先拿到 全局锁 。
-
拿不到 全局锁 ,不能提交本地事务。
-
拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。

此时二阶段也分为成功和失败两种情况
-
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
-
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
2.读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
所以如果某业务先调用updateA,本地事务已经执行完成但全局事务未执行完成,另一业务后调用queryA(),那么就可能会出现脏读的情况(因为可能会出现全局事务回滚):
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
思考
1.GlobalLock使用限制条件
目前使用全局锁@GlobalLock时,限制条件过于苛刻——必须要添加@transaction 注解、同时必须是for update语句。
如果不使用for update ,那么不会触发全局锁检查.
GlobalLock与for update 必须一起使用?
“这里为什么要加上select for update? 只用@GlobalLock能不能防止脏写?” 能。但请再回看下上面的图,select for update能带来这么几个好处:
-
锁冲突更“温柔”些。如果只有@GlobalLock,检查到全局锁,则立刻抛出异常,也许再“坚持”那么一下,全局锁就释放了,抛出异常岂不可惜了。
-
在updateA()中可以通过select for update获得最新的A,接着再做更新。
三、部署实战
1.部署
1.部署架构
1.Server端存储模式
Server端存储模式(store.mode)支持三种:
-
file:单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高
-
db:高可用模式,全局事务会话信息通过db共享,相应性能差些
-
redis:Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置适合当前场景的redis持久化配置
2.资源目录
资源目录:https://github.com/seata/seata/tree/1.4.0/script
-
client
存放client端sql脚本,参数配置
-
config-center
各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件
-
server
server端数据库脚本及各个容器配置
部署集群 Seata TC Server,实现高可用,生产环境下必备。在集群时,多个 Seata TC Server 通过 db 数据库,实现全局事务会话信息的共享。
同时,每个 Seata TC Server 可以注册自己到注册中心上,方便应用从注册中心获得到他们。最终我们部署 集群 TC Server 如下图所示:

Seata TC Server 对主流的注册中心都提供了集成,具体可见 discovery 目录。考虑到国内使用 Nacos 作为注册中心越来越流行,这里我们就采用它。
2.服务配置和启动
按照以下步骤进行操作
步骤一:下载安装包
见官网
步骤二:建表(仅db模式)
见官网
步骤三:修改store.mode
见官网
步骤四:修改数据库连接
见官网
步骤五:配置Nacos注册中心
见官网
步骤六:配置Nacos配置中心
注意:如果配置了seata server使用nacos作为配置中心,则配置信息会从nacos读取,file.conf可以不用配置。 客户端配置registry.conf使用nacos时也要注意group要和seata server中的group一致,默认group是"DEFAULT_GROUP"
获取脚本/seata/script/config-center/config.txt,修改配置信息

执行脚本将配置信息设置到nacos配置中心
sh nacos-config.sh -h localhost
在nacos配置中心可以看到配置信息

步骤七:启动Seata Server
在bin目录下执行下面命令启动seata
sh seata-server.sh
2.代码demo
https://gitee.com/daiwei-dave/spring-cloud-alibaba-demo.git
参考文档
1.Seata官网
2.Seata AT模式的全局锁GlobalLock
更多推荐
所有评论(0)