Spring Cloud Seata

是什么

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

官方文档 下载地址

术语

这里的TC、TM的概念和LCN的是不一样的。

TC - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务,是事务的发起者。

RM - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

一次分布式事务处理过程是由一个XID和三个组件模型(TCTMRM)组成。

处理过程

  1. TMTC申请开启一个全局事务,全局事务创建成功并声称一个全局唯一的XID;

  2. XID在微服务调用链路的上下文中传播;

  3. RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;

  4. TM向TC发起针对XID的全局提交或回滚决议;

  5. TC调度XID下管辖的全部分支事务完成提交或回滚请求。

seata-01

安装

下载seata推荐使用带GA标签的版本,表示是官方推荐的稳定版,截止2020年7月,下载的是v1.0.0-GA版本。

然后备份并修改file.conf配置文件,主要修改:自定义事务组名字、事务日志存储模式(DB)、数据库连接信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
service {
vgroup_mapping.my_test_tx_group = "midkuro_tx_group"
}

store {
mode = "db"
}

db {
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "账号"
password = "密码"
}

创建seata库,建表语句db_store.sql安装目录\seata\conf里。

修改registry.conf配置文件,指明注册中心是nacos,并修改其配置信息:

1
2
3
4
5
6
7
8
9
registry {
type = "nacos"

nacos {
serveAddr = "localhost"
namespace = ""
cluster = "default"
}
}

通过执行bin目录下的seata-server.bat启动程序。

用例

用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:

  • 仓储服务:对给定的商品扣除仓储数量。
  • 订单服务:根据采购需求创建订单。
  • 帐户服务:从用户帐户中扣除余额。

架构图

seata-02

seata-03

spring中使用@Transactional注解标记本地事务,而在seata中使用的是@GolbalTransactional注解,我们只需要使用一个 @GlobalTransactional 注解在业务方法上。

写隔离

1
2
3
1.一阶段本地事务提交前,需要确保先拿到全局锁。
2.拿不到全局锁,不能提交本地事务。
3.拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。

tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁

seata

x1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

seata

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

seata

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

怎么使用Seata框架,来保证事务的隔离性?

1
2
3
4
5
6
7
8
1.因seata一阶段本地事务已提交,为防止其他事务脏读脏写需要加强隔离。
2.脏读 select语句加 for update,代理方法增加 @GlobalLock + @Transactional 或 @GlobalTransaction
3.脏写 必须使用 @GlobalTransaction

注:如果你查询的业务的接口没有GlobalTransactional 包裹,也就是这个方法上压根没有分布式事务的需求,这时你可以在方法上标注 @GlobalLock + @Transactional 注解,并且在查询语句上加 for update。
如果你查询的接口在事务链路上外层有GlobalTransactional注解,那么你查询的语句只要加for update就行。
设计这个注解的原因是在没有这个注解之前,需要查询分布式事务读已提交的数据,但业务本身不需要分布式事务。
若使用GlobalTransactional注解就会增加一些没用的额外的rpc开销比如begin 返回xid,提交事务等。GlobalLock简化了rpc过程,使其做到更高的性能。

原理

分布式事务的执行流程

  1. TM开启分布式事务(TMTC注册全局事务记录);
  1. 按业务场景编排数据库,服务等事务内资源(RMTC汇报资源准备状态);

  2. TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务);

  3. TC汇总事务信息,决定分布式事务是提交还是回滚;

  4. TC通知所有RM提交/回滚资源,事务二阶段结束。

Seata提供了 ATTCCSAGAXA 事务模式,默认使用AT模式。

AT模式

AT:Automatic Transaction两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

Seata的AT模式实现了对业务的无入侵,它是怎么做到的呢?

一阶段

在一阶段,Seata会拦截业务SQL

  1. 解析SQL语义,找到业务SQL要更新的业务数据,在业务数据被更新前,将其保存成before image
  2. 执行业务SQL更新业务数据,在业务数据更新后
  3. 将其保存成after image,最后生成行锁

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

seata-04

二阶段提交

因为业务SQL在一阶段已经提交至数据库,所以二阶段如果是顺利提交的话,seata框架只需异步得将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

1
2
3
步骤:
1.收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
2.异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

seata-05

二阶段回滚

当二阶段是回滚的话,seata就需要回滚一阶段已经执行的业务SQL,还原业务数据。

回滚方式是用before image还原业务数据,但是在还原前要首先校验脏写,对比数据库当前业务数据after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

1
2
3
4
5
6
步骤:
1.收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
2.通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
3.数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。
4.根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句
5.提交本地事务

seata-06

编码

假设有三个系统,订单系统、库存系统、支付系统,操作流程是【下订单->减库存->扣余额->改订单状态】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--pom.xml-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!--如果客户端和pom依赖版本不一致,需要排除依赖自行引入-->
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.0.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
#application.yml
server:
port: 2001

spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定义事务组名称需要和seata-server中对应
tx-server-group: midkuro-tx-group
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;

@GlobalTransactional(name = "midkuro-create-order",rollbackFor = Exception.class)
@Override
public void create(Order order) {
log.info("-------->开始创建新订单");
orderDao.create(order);
log.info("--------订单微服务开始调用库存,做扣减");
storageService.decrease(order.getProductId(),order.getCount());
log.info("-------订单微服务开始调用库存,做扣减end");

log.info("-------订单微服务开始调用账户,做扣减");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("-------订单微服务开始调用账户,做扣减end");
log.info("-------修改订单状态");
orderDao.update(order.getUserId(),0);
log.info("-------修改订单状态结束");

log.info("--------下订单结束了,哈哈哈哈");
}
}

通过在微服务的调用链上增加注解实现分布式事务全局回滚操作。

TCC模式

Seata的TCC模式和LCN的TCC模式基本是相同的,也是不依赖于底层数据资源的事务支持。

TCC没有全局锁的概念,也不使用undo.log表。

seata

编码

1
2
3
4
5
6
7
8
9
10
11
12
//定义TCC注解
@LocalTCC
public interface RmOneInterface {

//设定回滚和提交的方法
@TwoPhaseBusinessAction(name = "rm1TccAction" , commitMethod = "rm1Commit" ,rollbackMethod = "rm1Rollback")
public String rm1(BusinessActionContext businessActionContext);

public boolean rm1Commit(BusinessActionContext businessActionContext);

public boolean rm1Rollback(BusinessActionContext businessActionContext);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
public class RmOneInterfaceImpl implements RmOneInterface {

@Override
@Transactional
public String rm1(BusinessActionContext businessActionContext) {
// 查询事务记录表,xxxx
System.out.println("rm1 try");

//调用远程服务
rm2();
rm3();
return null;
}

@Override
@Transactional
public boolean rm1Commit(BusinessActionContext businessActionContext) {
System.out.println("rm1 confirm");
return true;
}

@Override
@Transactional
public boolean rm1Rollback(BusinessActionContext businessActionContext) {
System.out.println("rm1 rollback");
return true;
}
}

总结

seata-07

三种异常

在使用TCC时需要兼容以下可能产生的异常场景

幂等处理

因为网络抖动等原因,分布式事务框架可能会重复调用同一个分布式事务中的一个分支事务的二阶段接口

所以分支事务的二阶段接口Confirm/Cancel需要能够保证幂等性。如果二阶段接口不能保证幂等性,则会产生严重的问题,造成资源的重复使用或者重复释放,进而导致业务故障。

seata

从上图中红色部分可以看到:如果当TC调用参与者的二阶段方法时,发生了异常(TC本身异常或者网络异常丢失结果)。此时TC无法感知到调用的结果。为了保证分布式事务能够走到终态,此时TC会按照一定的规则重复调用参与者的二阶段方法。

应对策略:增加一张事务状态控制表来实现

1
2
3
4
这个表的关键字段有以下几个:
1.主事务ID
2.分支事务ID
3.分支事务状态(1.初始化状态 2.已提交 3.已回滚)

seata

幂等记录的插入时机是参与者的Try方法,此时的分支事务状态会被初始化为INIT。然后当二阶段的Confirm/Cancel执行时会将其状态置为CONFIRMED/ROLLBACKED。

当TC重复调用二阶段接口时,参与者会先获取事务状态控制表的对应记录查看其事务状态。如果状态已经为CONFIRMED/ROLLBACKED,那么表示参与者已经处理完其分内之事,不需要再次执行,可以直接返回幂等成功的结果给TC,帮助其推进分布式事务。

空回滚

当没有调用参与方Try方法的情况下,就调用了二阶段的Cancel方法,Cancel方法需要有办法识别出此时Try有没有执行。如果Try还没执行,表示这个Cancel操作是无效的,即本次Cancel属于空回滚;如果Try已经执行,那么执行的是正常的回滚逻辑。

seata

首先发起方在调用参与者之前,会向TC申请开始一笔分布式事务。然后发起方调用参与者的一阶段方法,在调用实际发生之前,一般会有切面拦截器感知到此次Try调用,然后写入一条分支事务记录。紧接着,在实际调用参与者的Try方法时发生了异常。异常原因可以是发起方宕机,网络抖动等。

1
2
3
有两种情况会触发分布式事务的回滚:
1.发起方认为当前分布式事务无法成功,主动通知TC回滚
2.TC发现分布式事务超时,被动触发回滚

触发回滚操作后,TC会对该分布式事务关联的分支事务调用其二阶段Cancel。在执行Cancel时,Try还未执行成功,触发空回滚。如果不对空回滚加以防范的话,可能会造成资源的无效释放。即在没有预留资源的情况下就释放资源,造成故障。

应对策略:

当Try方法被成功执行后,事务控制表会插入一条记录,标识该分支事务处于INIT状态。所以后续当二阶段的Cancel方法被调用时,可以通过查询控制表的对应记录进行判断。如果记录存在且状态为INIT,就表示一阶段已成功执行,可以正常执行回滚操作,释放预留的资源;如果记录不存在则表示一阶段未执行,本次为空回滚,不释放任何资源。

seata

资源悬挂

事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况;

然而此时分布式事务已经走到终态,后续再没有任何手段能够处理这些try执行后的二阶段操作,至此,就形成了资源悬挂。

seata

悬挂的产生背景是一阶段try方法未执行,产生了空回滚,并且要拒绝空回滚之后的try请求的执行。

应对策略:

在判断为空回滚的场景下(体现在对应一阶段事务控制表中不存在数据),二阶段的Cancel运行时在事务状态表中插入一条状态为已回滚的控制记录。

当try后执行时,先去事务控制表中插入数据,发现已存在数据时,则执行空try,否则(非资源悬挂场景)正常执行。

seata

总结

seata


TCC的异常场景及应对机制内容转载自文章

最后更新: 2021年01月07日 12:19

原始链接: https://midkuro.gitee.io/2020/06/29/springcloud-seata/

× 请我吃糖~
打赏二维码