分布式事务基础

<<分布式事务基础理论>>

<<分布式事务解决方案>>

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

Seata 全局框架

Seata 的设计思路是将一个分布式事务理解成一个全局事务下面挂了多个分支事务,而一个分支事务是一个满足 ACID 的本地事务,因此我们可以操作分布式事务像操作本地事务一样。

在 Seata 内部定义了三个模块来处理全局事务和分支事务:

  • Transaction Coordinator(TC) - 事务协调者: 维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
  • Transaction Manager (TM)- 事务管理器: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
  • Resource Manager (RM) - 资源管理器: 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

Seata 提供的 AT、TCC、SAGA 和 XA 事务模式,都是基于这三个模块进行的。Seata 整体的执行步骤为:

  1. TM 向 TC 申请开启一个全局事务,TC 创建全局事务并返回一个唯一的 XID,XID 会在全局事务的上下文中传播。
  2. RM 向 TC 注册分支事务,该分支事务归属于拥有相同 XID 的全局事务。
  3. TM 向 TC 发起全局的提交或回滚。
  4. TC 调度 XID 下的所有分支事务提交或回滚。

AT 模式

《分布式事务解决方案》中介绍了常见的几种方案,总的来说主要分为两类:对业务无入侵和有入侵的方案。无入侵方案主要有基于数据库 XA 协议,虽然 XA 协议与业务代码解耦,但是它必须要求数据库对 XA 协议的支持,且 XA 协议会造成事务资源长时间得不到释放,锁定周期长,性能很差。有入侵的方案都需要通过在应用层做手脚,比如很出名的 TCC 方案,基于 TCC 也有很多成熟的框架,如 ByteTCC、tcc-transaction 等。

针对以上事务解决方案的痛点,Seata 提出了 AT 模式,也是Seata默认的事务模式

AT 模式的实现原理是在数据源做了一层代理(DataSourceProxy),在代理层中 Seata 加入了一些额外的逻辑,包括解析 SQL,把业务数据在更新前后的数据镜像组织成回滚日志,并将 undo log 日志插入 undo_log 表中,保证每条更新数据的业务 sql 都有对应的回滚日志存在。

AT 模式的执行过程

  • 一阶段:

Seata 拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成before image,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。最后生成的before imageafter image会保存到 undo log 表中

  • 二阶段:

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

如果是回滚,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用before image还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写业务 SQL,便能轻松使用分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

环境搭建

Seata 分 TC、TM 和 RM 三个角色,TC(Server 端)为单独服务端部署,TM 和 RM(Client 端)由业务系统集成。

服务端部署

  1. 下载启动包:https://github.com/seata/seata/releases
  2. 建表,主要的表有三个:
  • 全局事务:global_table

  • 分支事务:branch_table

  • 全局锁:lock_table

    在 MySQL 中,创建一个名为 seata 的数据库实例。创建相关表的脚本在 seata-->script-->server-->db目录下

  1. 设置配置中心和注册中心
  • 搭建 nacos,具体的搭建过程自行查资料

  • 配置中心: seata-->conf-->application.yml 修改 seata.config.type=”nacos”,在 seata-->conf-->application.example.yml 中 seata.config.nacos 下有相关的 nacos 配置,将其复制到 application.yml 下,并将 nacos 相关的数据配置完整。

    设置配置中心可以参考官网:https://seata.io/zh-cn/docs/user/configuration/nacos.html

  • 注册中心: seata-->conf-->application.yml 修改 seata.registry.type=”nacos”,在 seata-->conf-->application.example.yml 中 seata.registry.nacos 下有相关的 nacos 配置,将其复制到 application.yml 下,并将 nacos 相关的数据配置完整。

  1. 修改存储模式 store.mode
    Server 端存储模式(store.mode)现有 file、db、redis 三种,file 模式无需改动,直接启动即可,下面专门讲下 db,因为 db 模式为高可用模式,全局事务会话信息通过 db 共享。
    seata-->conf-->application.yml,修改 store.mode=”db”

  2. 修改数据库连接
    seata-->conf-->application.example.yml 中附带额外配置,将其 db 相关配置复制至 application.yml,修改 store.db 相关属性。

  3. 启动

1
seata-server.sh -h 127.0.0.1 -p 8091 -m db

业务系统集成

  1. 添加依赖,Seata 提供了不同的依赖包。可以根据项目自行选择,建议单选。
  • 依赖 seata-all
  • 依赖 seata-spring-boot-starter,支持 yml、properties 配置(.conf 可删除),内部已依赖 seata-all
  • 依赖 spring-cloud-alibaba-seata,内部集成了 seata,并实现了 xid 传递
  1. 在涉及到的服务的数据库中创建undo_log
1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
  1. 初始化 GlobalTransactionScanner,如果引入seata-spring-boot-starterspring-cloud-starter-alibaba-seata等 jar 会自动初始化,否则需要手动初始化。
1
2
3
4
5
6
7
8
9
10
11
@Bean
public GlobalTransactionScanner globalTransactionScanner() {
String applicationName = this.applicationContext.getEnvironment().getProperty("spring.application.name");
String txServiceGroup = this.seataProperties.getTxServiceGroup();
if (StringUtils.isEmpty(txServiceGroup)) {
txServiceGroup = applicationName + "-fescar-service-group";
this.seataProperties.setTxServiceGroup(txServiceGroup);
}

return new GlobalTransactionScanner(applicationName, txServiceGroup);
}
  1. 实现 xid 跨服务传递,如果是 Spring Cloud 项目,并引用了spring-cloud-starter-alibaba-seatajar,则已经自动实现了,否则需要参考源码 integration 文件夹下的各种 rpc 实现 module

业务使用

1、以一个 Spring Cloud 项目为例,项目中有两个服务:订单服务和 库存服务。业务场景为创建订单的同时减库存。

在两个服务中添加依赖

1
2
3
4
5
6
7
8
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</dependency>

2、在每个业务服务下的数据库里添加undo_log表。

3、在每个业务服务下的配置文件中添加 seata 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
seata:
tx-service-group: default_tx_group
registry:
type: nacos
nacos:
application: seata-server # seata server 的服务名seata-server ,如果没有修改可以不配
server-addr: 127.0.0.1:8848 # seata server 所在的nacos服务地址
group : DEFAULT_GROUP # seata server 所在的组,默认就是SEATA_GROUP,没有改也可以不配
namespace: 0d876b7d-4cfd-4860-bf81-8e5266c9375c # 自己seata注册中心namespace
username: nacos
password: nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848 # seata server 所在的nacos服务地址
username: nacos
password: nacos
group: DEFAULT_GROUP
namespace: 0d876b7d-4cfd-4860-bf81-8e5266c9375c # 自己seata注册中心namespace
application-id: seata-demo #
enabled: true

注意

  1. 这里的 group 要与 server 端配置的保持一致
  2. tx-service-group 为事务群组,要部署同一套分布式事务的微服务要求事务群组要一致。可以在 nacos 的配置中查询 :service.vgroupMapping.xxx。

  1. 库存服务 StockController
1
2
3
4
5
6
7
8
@PostMapping(value = "/reduct")
public void reduct(String productId) {
//去库存
stockService.reduct(order.getProductId());
// 异常
int a=1/0;
return order;
}

在减库存的方法中模拟了一个业务异常int a=1/0,表示服务调用发生异常。

  1. 订单服务中创建调用库存服务的 Feign
1
2
3
4
5
6
@FeignClient(value = "stock-service")
public interface StockApi {
@PostMapping(value = "/reduct")
void reduct(String productId);
}

订单服务OrderService,在需要开启全局事务的方法上添加@GlobalTransactional注解

1
2
3
4
5
6
7
8
9
10
11
Autowired
private StockApi stockApi;

@GlobalTransactional
public Order create(Order order) {
// 插入
orderMapper.insert(order);
// 减库存
stockApi.reduct(order.getProductId());
return order;
}

当调用订单服务时,库存服务发生异常,可以判断发生异常后两个数据库中的数据是否回滚。

参考: https://seata.io/zh-cn/blog/seata-at-tcc-saga.html