概述
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
分布式事务(Distributed Transaction)特指多个服务同时访问多个数据源的事务处理机制,分布式是相对于服务而言的,更严谨地说,应该被称为在分布式服务环境下的事务处理机制。一般的单体应用,如果使用多个数据源,才需要面临分布式事务的问题(要考虑多个数据源的一致性)。
分布式计算领域中有个著名的 CAP 定理(Consistency、Availability、Partition Tolerance Theorem),Seata 就是为了解决这问题而诞生的。
四种事务模式
Seata 是一站式的分布事务的解决方案,提供四种事务模式,可参考 Seata 官方文档:
- AT 模式
- TCC 模式
- Saga 模式
- XA 模式
目前使用的流行度情况是:AT > TCC > Saga > XA。因此使用 Seata 的时候,可以花更多精力在 AT 模式上,要搞懂背后的实现原理,毕竟分布式事务涉及到数据的正确性,出问题需要快速排查定位并解决。
Seata 的三大角色
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围,开始全局事务、提交或回滚全局事务。
- RM ( Resource Manager ) - 资源管理器:管理分支事务处理的资源( Resource ),与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。如何安装 TC,可参考 Docker Compose 部署
由于 Seata 也是 SpringCloud Alibaba 的一员,安装的时候使用 Nacos 作为注册中心,如果对 Nacos 不熟悉,可参考 Nacos 注册中心与配置中心
一个分布式事务的生命周期如下:
- TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号(全局锁)。
XID,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
-
RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
-
TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
-
TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。
Seata 支持
- Dubbo:seata-dubbo
- SOFA-RPC:seata-sofa-rpc
- Motan:seata-motan
- gRPC:seata-grpc
- Apache HttpClient:seata-http
- Spring Cloud OpenFeign:spring-cloud-starter-alibaba-seata
- Spring RestTemplate:spring-cloud-starter-alibaba-seata
由此可见, Seata 内置对 Dubbo 和 Feign 这两种方式都有很好的集成,提供分布式事务的功能。
因为 Seata 是基于 DataSource 数据源进行代理来拓展,所以天然对主流的 ORM 框架提供了非常好的支持:MyBatis、MyBatis-Plus、JPA、Hibernate
安装
事务模式
AT 模式
整体机制:
AT 模式分为两阶段提交:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。
写隔离
一阶段本地事务提交前,需要确保先拿到 全局锁 。
拿不到 全局锁 ,不能提交本地事务。
拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
AT 演示
这里以 Feign 为主,Seata 提供了 spring-cloud-starter-alibaba-seata
项目,对 Spring Cloud 进行集成。实现原理是:
服务消费者,使用 Seata 封装的 SeataFeignClient 过滤器,在使用 Feign 发起 HTTP 调用时,将 Seata 全局事务 XID 通过 Header 传递。
服务提供者,使用 Seata 提供的 SpringMVC SeataHandlerInterceptor 拦截器,将 Header 中的 Seata 全局事务 XID 解析出来,设置到 Seata 上下文 中。
如此,我们便实现了多个 Spring Cloud 应用的 Seata 全局事务的传播。
以经典的用户购买商品的业务逻辑,来作为具体示例,一共会有三个服务:Order 订单服务、Product 商品服务、Account 账户服务,分别对应自己的数据库。
Product 与 Account 作为服务提供者,提供 Restful API,Order 作为服务消费者通过 OpenFeign 进行 HTTP 调用。具体如下:
代码仓库,基本结构是
初始化数据库
在 数据库表 中,我们可以看到,每个表中,都有相对应的 undo_log
表,这是 Seata AT 模式必须创建的表,主要用于分支事务的回滚。
在 order-service 项目中,作为订单服务。它主要提供 /order/create
接口,实现创建订单服务。
引入依赖
基本上所有的依赖文件都差不多
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.rcrime.cloud</groupId>
<artifactId>seata-feign-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<!-- 统一依赖管理 -->
<spring.boot.version>3.3.4</spring.boot.version>
<spring.cloud.version>2023.0.3</spring.cloud.version>
<spring.cloud.alibaba.version>2023.0.1.2</spring.cloud.alibaba.version>
<lombok.version>1.18.34</lombok.version>
</properties>
<artifactId>order-service</artifactId>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- DB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- Spring Cloud Alibaba Seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
配置文件
server:
port: 8081 # 端口
spring:
application:
name: order-service
datasource:
url: jdbc:mysql://127.0.0.1:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
cloud:
# Nacos 作为注册中心的配置项
nacos:
discovery:
server-addr: 127.0.0.1:8848
# Seata 配置项,对应 SeataProperties 类
seata:
application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
tx-service-group: ${spring.application.name}-group # Seata 事务组编号,用于 TC 集群名
# Seata 服务配置项,对应 ServiceProperties 类
service:
# 虚拟组和分组的映射
vgroup-mapping:
order-service-group: default
# Seata 注册中心配置项,对应 RegistryProperties 类
registry:
type: nacos # 注册中心类型,默认为 file
nacos:
cluster: default # 使用的 Seata 分组
namespace: dev # Nacos 命名空间
serverAddr: localhost # Nacos 服务地址
上面的事务分组是什么呢?
- 事务分组:seata 的资源逻辑,可以按微服务的需要,在客户端对自行定义事务分组,每组取一个名字。
- 集群:seata-server 服务端一个或多个节点组成的集群 cluster。 客户端使用时需要指定事务逻辑分组与 Seata 服务端集群的映射关系。
简单来说,就是多了一层虚拟映射。这里不设置 seata.service.grouplist
配置项,因为从注册中心加载 Seata TC Server 的地址。
如果不使用 Nacos 注册中心,就需要设置此配置项,不过也可以少了
seata.registry
配置项
seata.registry
配置项,设置 Seata 注册中心配置项,对应 RegistryProperties
类。
type
配置项,设置注册中心的类型,默认为 false
。这里,我们设置为 nacos
来使用 Nacos 作为注册中心。
nacos
配置项就是设置 Nacos 注册中心的配置项。
工程
OrderController
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderService orderService;
@PostMapping("/create")
public Integer createOrder(@RequestParam("userId") Long userId,
@RequestParam("productId") Long productId,
@RequestParam("price") Integer price) throws Exception {
log.info("[createOrder] 收到下单请求,用户:{}, 商品:{}, 价格:{}", userId, productId, price);
return orderService.createOrder(userId, productId, price);
}
}
ProductController
@Slf4j
@RestController
@RequestMapping("/product")
public class ProductController {
@Resource
private ProductService productService;
@PostMapping("/reduce-stock")
public void reduceStock(@RequestBody ProductReduceStockDTO productReduceStockDTO)
throws Exception {
log.info("[reduceStock] 收到减少库存请求, 商品:{}, 价格:{}", productReduceStockDTO.getProductId(),
productReduceStockDTO.getAmount());
productService.reduceStock(productReduceStockDTO.getProductId(), productReduceStockDTO.getAmount());
}
}
AccountController
@Slf4j
@RestController
@RequestMapping("/account")
public class AccountController {
@Resource
private AccountService accountService;
@PostMapping("/reduce-balance")
public void reduceBalance(@RequestBody AccountReduceBalanceDTO accountReduceBalanceDTO) throws Exception {
log.info("[reduceBalance] 收到减少余额请求, 用户:{}, 金额:{}", accountReduceBalanceDTO.getUserId(),
accountReduceBalanceDTO.getPrice());
accountService.reduceBalance(accountReduceBalanceDTO.getUserId(), accountReduceBalanceDTO.getPrice());
}
}
在这里,OrderController
会调用 OrderService
进行下单,而 OrderService
会通过 OpenFeign 远程调用 Account 和 Product 服务进行扣减。
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private AccountServiceFeignClient accountService;
@Resource
private ProductServiceFeignClient productService;
@Override
@GlobalTransactional // 声明全局事务
public Integer createOrder(Long userId, Long productId, Integer price) {
Integer amount = 1; // 购买数量
log.info("[createOrder] 当前 XID: {}", RootContext.getXID());
// 远程调用扣减库存
productService.reduceStock(new ProductReduceStockDTO().setProductId(productId).setAmount(amount));
// 远程调用扣减余额
accountService.reduceBalance(new AccountReduceBalanceDTO().setUserId(userId).setPrice(price));
// 保存订单
OrderDO order = new OrderDO().setUserId(userId).setProductId(productId).setPayAmount(amount * price);
orderDao.saveOrder(order);
log.info("[createOrder] 保存订单: {}", order.getId());
// 返回订单编号
return order.getId();
}
}
这里通过 OpenFeign 远程调用了 productService
和 accountService
进行减库存和减余额,如果这时候扣除余额远程调用失败,又或是扣除库存失败,都会抛出 Exception 异常,从而回滚 Seata 全局事务
远程调用 API
/**
* `account-service` 服务的 Feign 客户端
*/
@FeignClient(name = "account-service")
public interface AccountServiceFeignClient {
@PostMapping("/account/reduce-balance")
void reduceBalance(@RequestBody AccountReduceBalanceDTO accountReduceBalanceDTO);
}
/**
* `product-service` 服务的 Feign 客户端
*/
@FeignClient(name = "product-service")
public interface ProductServiceFeignClient {
@PostMapping("/product/reduce-stock")
void reduceStock(@RequestBody ProductReduceStockDTO productReduceStockDTO);
}
在全部调用成功后,调用 OrderDao 保存订单。至此,整个流程也就完成了。
简单测试
正常流程测试就不用说了,而现在主要是测试异常情况。可以将订单的金额给设置大一点,再进行测试。会发现库存进行了回滚。
TCC 模式
根据 AT 模式我们知道,一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
一阶段 prepare 行为
二阶段 commit 或 rollback 行为
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode
AT 模式基于 支持本地 ACID 事务 的关系型数据库:
一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
二阶段 commit 行为:调用 自定义 的 commit 逻辑。
二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
Saga 模式
Saga 模式是 Seata 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
Saga的实现:
基于状态机引擎的 Saga 实现:
目前 Seata 提供的 Saga 模式是基于状态机引擎来实现的,机制是:
- 通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
- 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
- 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
注意: 异常发生时是否进行补偿也可由用户自定义决定
可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
具体参考 官方文档
XA 模式
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。
- 执行阶段:
- 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
- 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
- 完成阶段:
- 分支提交:执行 XA 分支的 commit
- 分支回滚:执行 XA 分支的 rollback
工作机制
XA 模式 运行在 Seata 定义的事务框架内:
- 执行阶段(E xecute):XA start/XA end/XA prepare + SQL + 注册分支
- 完成阶段(F inish):XA commit/XA rollback
具体参考 官方文档