Spring Cloud Seata
是什么
Seata
是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
术语
这里的TC、TM的概念和LCN的是不一样的。
TC - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务,是事务的发起者。
RM - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
一次分布式事务处理过程是由一个XID
和三个组件模型(TC
、TM
、RM
)组成。
处理过程
TM
向TC
申请开启一个全局事务,全局事务创建成功并声称一个全局唯一的XID;XID在微服务调用链路的上下文中传播;
RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
TM向TC发起针对XID的全局提交或回滚决议;
TC调度XID下管辖的全部分支事务完成提交或回滚请求。
安装
下载seata
推荐使用带GA
标签的版本,表示是官方推荐的稳定版,截止2020年7月,下载的是v1.0.0-GA
版本。
然后备份并修改file.conf
配置文件,主要修改:自定义事务组名字、事务日志存储模式(DB)、数据库连接信息。
1 | service { |
创建seata
库,建表语句db_store.sql
在安装目录\seata\conf
里。
修改registry.conf
配置文件,指明注册中心是nacos
,并修改其配置信息:
1 | registry { |
通过执行bin
目录下的seata-server.bat
启动程序。
用例
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
- 仓储服务:对给定的商品扣除仓储数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
架构图
在spring
中使用@Transactional
注解标记本地事务,而在seata
中使用的是@GolbalTransactional
注解,我们只需要使用一个 @GlobalTransactional
注解在业务方法上。
写隔离
1 | 1.一阶段本地事务提交前,需要确保先拿到全局锁。 |
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。
tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
x1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 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 语句。
怎么使用Seata框架,来保证事务的隔离性?
1 | 1.因seata一阶段本地事务已提交,为防止其他事务脏读脏写需要加强隔离。 |
原理
分布式事务的执行流程:
TM
开启分布式事务(TM
向TC
注册全局事务记录);
按业务场景编排数据库,服务等事务内资源(
RM
向TC
汇报资源准备状态);TM
结束分布式事务,事务一阶段结束(TM
通知TC
提交/回滚分布式事务);TC
汇总事务信息,决定分布式事务是提交还是回滚;TC
通知所有RM
提交/回滚资源,事务二阶段结束。
Seata
提供了 AT
、TCC
、SAGA
和 XA
事务模式,默认使用AT
模式。
AT模式
AT:Automatic Transaction两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
Seata
的AT模式实现了对业务的无入侵,它是怎么做到的呢?
一阶段
在一阶段,Seata
会拦截业务SQL:
- 解析
SQL
语义,找到业务SQL要更新的业务数据,在业务数据被更新前,将其保存成before image
- 执行业务SQL更新业务数据,在业务数据更新后
- 将其保存成
after image
,最后生成行锁
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交
因为业务SQL在一阶段已经提交至数据库,所以二阶段如果是顺利提交的话,seata
框架只需异步得将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
1 | 步骤: |
二阶段回滚
当二阶段是回滚的话,seata
就需要回滚一阶段已经执行的业务SQL,还原业务数据。
回滚方式是用before image
还原业务数据,但是在还原前要首先校验脏写,对比数据库当前业务数据和after image
,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
1 | 步骤: |
编码
假设有三个系统,订单系统、库存系统、支付系统,操作流程是【下订单->减库存->扣余额->改订单状态】
1 | <!--pom.xml--> |
1 | #application.yml |
1 |
|
通过在微服务的调用链上增加注解实现分布式事务全局回滚操作。
TCC模式
Seata的TCC模式和LCN的TCC模式基本是相同的,也是不依赖于底层数据资源的事务支持。
TCC没有全局锁的概念,也不使用undo.log表。
编码
1 | //定义TCC注解 |
1 |
|
总结
三种异常
在使用TCC时需要兼容以下可能产生的异常场景
幂等处理
因为网络抖动等原因,分布式事务框架可能会重复调用同一个分布式事务中的一个分支事务的二阶段接口。
所以分支事务的二阶段接口Confirm/Cancel需要能够保证幂等性。如果二阶段接口不能保证幂等性,则会产生严重的问题,造成资源的重复使用或者重复释放,进而导致业务故障。
从上图中红色部分可以看到:如果当TC调用参与者的二阶段方法时,发生了异常(TC本身异常或者网络异常丢失结果)。此时TC无法感知到调用的结果。为了保证分布式事务能够走到终态,此时TC会按照一定的规则重复调用参与者的二阶段方法。
应对策略:增加一张事务状态控制表来实现
1 | 这个表的关键字段有以下几个: |
幂等记录的插入时机是参与者的Try方法,此时的分支事务状态会被初始化为INIT。然后当二阶段的Confirm/Cancel执行时会将其状态置为CONFIRMED/ROLLBACKED。
当TC重复调用二阶段接口时,参与者会先获取事务状态控制表的对应记录查看其事务状态。如果状态已经为CONFIRMED/ROLLBACKED,那么表示参与者已经处理完其分内之事,不需要再次执行,可以直接返回幂等成功的结果给TC,帮助其推进分布式事务。
空回滚
当没有调用参与方Try方法的情况下,就调用了二阶段的Cancel方法,Cancel方法需要有办法识别出此时Try有没有执行。如果Try还没执行,表示这个Cancel操作是无效的,即本次Cancel属于空回滚;如果Try已经执行,那么执行的是正常的回滚逻辑。
首先发起方在调用参与者之前,会向TC申请开始一笔分布式事务。然后发起方调用参与者的一阶段方法,在调用实际发生之前,一般会有切面拦截器感知到此次Try调用,然后写入一条分支事务记录。紧接着,在实际调用参与者的Try方法时发生了异常。异常原因可以是发起方宕机,网络抖动等。
1 | 有两种情况会触发分布式事务的回滚: |
触发回滚操作后,TC会对该分布式事务关联的分支事务调用其二阶段Cancel。在执行Cancel时,Try还未执行成功,触发空回滚。如果不对空回滚加以防范的话,可能会造成资源的无效释放。即在没有预留资源的情况下就释放资源,造成故障。
应对策略:
当Try方法被成功执行后,事务控制表会插入一条记录,标识该分支事务处于INIT状态。所以后续当二阶段的Cancel方法被调用时,可以通过查询控制表的对应记录进行判断。如果记录存在且状态为INIT,就表示一阶段已成功执行,可以正常执行回滚操作,释放预留的资源;如果记录不存在则表示一阶段未执行,本次为空回滚,不释放任何资源。
资源悬挂
事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况;
然而此时分布式事务已经走到终态,后续再没有任何手段能够处理这些try执行后的二阶段操作,至此,就形成了资源悬挂。
悬挂的产生背景是一阶段try方法未执行,产生了空回滚,并且要拒绝空回滚之后的try请求的执行。
应对策略:
在判断为空回滚的场景下(体现在对应一阶段事务控制表中不存在数据),二阶段的Cancel运行时在事务状态表中插入一条状态为已回滚的控制记录。
当try后执行时,先去事务控制表中插入数据,发现已存在数据时,则执行空try,否则(非资源悬挂场景)正常执行。
总结
TCC的异常场景及应对机制内容转载自文章
最后更新: 2021年01月07日 12:19
原始链接: https://midkuro.gitee.io/2020/06/29/springcloud-seata/