Seata 分布式事务:从入门到精通的完整指南
本文将带你深入理解 Seata 分布式事务解决方案,从基础概念到生产实践,循序渐进地掌握分布式事务的核心原理与实战技巧。
适用版本:Seata 1.7+ / 2.x | Spring Boot 2.7+ / 3.x | Spring Cloud Alibaba 2021+
第一部分:基础入门
1.1 Seata 是什么?
在单体架构时代,事务管理很简单——Spring 的 @Transactional 注解就能搞定。数据库的 ACID 特性让一切看起来都很美好。
但在微服务架构下,问题来了:
订单服务 → 扣减库存 → 扣减余额
↓ ↓ ↓
订单DB 库存DB 账户DB
一个业务操作跨越了三个数据库,如何保证要么全部成功,要么全部失败?这就是分布式事务要解决的问题。
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。它前身是 Fescar(Fast & Easy Commit And Rollback),2019 年更名为 Seata。
核心价值:
- ✅ 对业务零侵入(AT 模式下)
- ✅ 支持多种事务模式(AT、TCC、SAGA、XA)
- ✅ 高性能(单机支持万级 TPS)
- ✅ 生态丰富(支持 Spring Cloud、Dubbo 等)
1.2 分布式事务的痛点
在没有分布式事务框架之前,常见的解决方案有:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 2PC(两阶段提交) | 强一致性 | 性能差、阻塞、单点故障 |
| TCC(Try-Confirm-Cancel) | 性能好 | 代码侵入性强,需要写补偿逻辑 |
| 本地消息表 | 简单可靠 | 不实时,需要定时任务轮询 |
| 事务消息 | 解耦 | 只能保证最终一致性 |
痛点总结:
- 实现复杂:需要手动处理各种异常场景
- 代码侵入:业务代码与事务代码耦合
- 难以维护:分布式事务的边界不清晰
- 缺乏工具:没有统一的事务协调器
Seata 的出现,就是为了解决这些痛点。
1.3 Seata 的核心概念
Seata 的架构非常清晰,包含三个核心角色:
graph LR
TM[TM 事务管理器<br/>Transaction Manager] -->|开启/提交/回滚| TC[TC 事务协调器<br/>Transaction Coordinator]
RM1[RM 资源管理器<br/>Resource Manager] -->|注册分支事务| TC
RM2[RM 资源管理器<br/>Resource Manager] -->|注册分支事务| TC
TC -->|协调| RM1
TC -->|协调| RM2
style TC fill:#ff9999,stroke:#333,stroke-width:3px
style TM fill:#99ff99,stroke:#333
style RM1 fill:#9999ff,stroke:#333
style RM2 fill:#9999ff,stroke:#333
角色职责
TC (Transaction Coordinator) - 事务协调器
- 维护全局事务的状态
- 协调全局事务的提交或回滚
- 通常是一个独立的服务(Seata Server)
TM (Transaction Manager) - 事务管理器
- 开启全局事务
- 提交或回滚全局事务
- 通常在业务发起方(微服务中的调用方)
RM (Resource Manager) - 资源管理器
- 管理分支事务
- 向 TC 注册分支事务
- 汇报分支事务状态
- 通常在业务参与方(微服务中的被调用方)
一个典型的事务流程:
sequenceDiagram
participant TM as TM(订单服务)
participant TC as TC(Seata Server)
participant RM1 as RM(库存服务)
participant RM2 as RM(账户服务)
TM->>TC: 1. 开启全局事务
TC-->>TM: XID (全局事务ID)
TM->>RM1: 2. 调用库存服务(XID传播)
RM1->>TC: 3. 注册分支事务
TC-->>RM1: 分支事务ID
RM1->>RM1: 执行本地事务
RM1->>TC: 4. 汇报分支状态
TM->>RM2: 5. 调用账户服务(XID传播)
RM2->>TC: 6. 注册分支事务
TC-->>RM2: 分支事务ID
RM2->>RM2: 执行本地事务
RM2->>TC: 7. 汇报分支状态
alt 所有分支成功
TM->>TC: 8. 提交全局事务
TC->>RM1: 提交分支事务
TC->>RM2: 提交分支事务
else 有分支失败
TM->>TC: 8. 回滚全局事务
TC->>RM1: 回滚分支事务
TC->>RM2: 回滚分支事务
end
1.4 四种事务模式对比
Seata 提供了四种事务模式,每种都有其适用场景:
AT 模式(Auto Transaction)
核心思想:基于支持本地 ACID 事务的关系型数据库,自动记录前后镜像,实现无侵入的分布式事务。
// 业务代码完全无感知
@GlobalTransactional
public void placeOrder(Order order) {
orderMapper.insert(order); // 本地事务
storageService.deduct(order.getItemId(), order.getCount()); // 远程调用
accountService.debit(order.getUserId(), order.getAmount()); // 远程调用
}
优点:
- 零侵入,业务代码无需改动
- 一阶段提交,性能好
- 学习成本低
缺点:
- 只支持关系型数据库
- 存在脏写风险(需要全局锁)
适用场景:大多数基于关系型数据库的微服务系统
TCC 模式(Try-Confirm-Cancel)
核心思想:将业务逻辑拆分为三个阶段——Try(预留资源)、Confirm(确认提交)、Cancel(取消回滚)。
public interface StorageTccService {
// Try: 预留库存
boolean tryDeduct(String itemId, int count);
// Confirm: 确认扣减
boolean confirmDeduct(String itemId, int count);
// Cancel: 释放预留的库存
boolean cancelDeduct(String itemId, int count);
}
实现示例:
@Service
public class StorageTccServiceImpl implements StorageTccService {
@Autowired
private StorageMapper storageMapper;
@Override
public boolean tryDeduct(String itemId, int count) {
// 预留库存:冻结库存
int rows = storageMapper.freezeStock(itemId, count);
return rows > 0;
}
@Override
public boolean confirmDeduct(String itemId, int count) {
// 确认扣减:减少冻结库存
int rows = storageMapper.deductFrozenStock(itemId, count);
return rows > 0;
}
@Override
public boolean cancelDeduct(String itemId, int count) {
// 取消:释放冻结库存
int rows = storageMapper.releaseFrozenStock(itemId, count);
return rows > 0;
}
}
优点:
- 性能最优(锁粒度最小)
- 支持非关系型数据库
缺点:
- 代码侵入性强
- 需要实现三个接口
- 业务逻辑复杂
适用场景:对性能要求高、数据库类型多样、业务允许幂等
SAGA 模式
核心思想:将长事务拆分为多个本地短事务,每个短事务都有对应的补偿事务。如果某个环节失败,则反向执行补偿事务。
graph LR
T1[事务T1<br/>创建订单] --> T2[事务T2<br/>扣减库存]
T2 --> T3[事务T3<br/>扣减余额]
T3 --> T4[事务T4<br/>发送通知]
T1 -.-> C1[补偿C1<br/>取消订单]
T2 -.-> C2[补偿C2<br/>恢复库存]
T3 -.-> C3[补偿C3<br/>恢复余额]
T4 -.-> C4[补偿C4<br/>撤回通知]
style T1 fill:#99ff99
style T2 fill:#99ff99
style T3 fill:#99ff99
style T4 fill:#99ff99
style C1 fill:#ff9999
style C2 fill:#ff9999
style C3 fill:#ff9999
style C4 fill:#ff9999
状态机定义(JSON):
{
"Name": "placeOrder",
"Comment": "下单流程",
"StartState": "CreateOrder",
"States": [
{
"Name": "CreateOrder",
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "create",
"Next": "DeductStock"
},
{
"Name": "DeductStock",
"Type": "ServiceTask",
"ServiceName": "storageService",
"ServiceMethod": "deduct",
"Next": "DeductBalance",
"CompensateState": "CompensateStock"
},
{
"Name": "DeductBalance",
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "debit",
"CompensateState": "CompensateBalance"
}
]
}
优点:
- 适合长事务
- 可视化流程编排
- 支持异步
缺点:
- 缺乏隔离性(中间状态可见)
- 补偿逻辑复杂
适用场景:业务流程长、跨系统、需要人工干预的场景
XA 模式
核心思想:基于数据库的 XA 协议实现的两阶段提交。
@GlobalTransactional
public void placeOrder(Order order) {
// XA 协议:数据库层面支持
orderMapper.insert(order); // XA START → XA END → XA PREPARE
storageService.deduct(itemId, count); // 同上
accountService.debit(userId, amount); // 同上
// 全局提交时:XA COMMIT
}
优点:
- 强一致性
- 标准协议,兼容性好
缺点:
- 性能差(两阶段阻塞)
- 数据库支持有限(MySQL 5.7+、PostgreSQL 等)
适用场景:对一致性要求极高、并发量不大的系统
四种模式对比表
| 特性 | AT | TCC | SAGA | XA |
|---|---|---|---|---|
| 一致性 | 最终一致 | 最终一致 | 最终一致 | 强一致 |
| 隔离性 | 全局锁 | 业务锁 | 无隔离 | 全局锁 |
| 性能 | 高 | 最高 | 中 | 低 |
| 代码侵入 | 无 | 高 | 中 | 无 |
| 数据库支持 | 关系型 | 任意 | 任意 | XA 支持 |
| 适用场景 | 常规业务 | 高性能/非关系型 | 长事务 | 强一致性 |
第二部分:快速上手
2.1 Spring Boot 集成 Seata
环境准备
| 组件 | 版本 |
|---|---|
| Spring Boot | 2.7.18 |
| Spring Cloud Alibaba | 2021.0.8.0 |
| Seata | 1.7.1 |
| MySQL | 8.0+ |
第一步:下载 Seata Server
# 下载 Seata Server(推荐 1.7.x 或 2.x)
wget https://github.com/seata/seata/releases/download/v1.7.1/seata-server-1.7.1.zip
# 解压
unzip seata-server-1.7.1.zip
cd seata-server-1.7.1
# 启动(默认端口 8091)
sh bin/seata-server.sh
第二步:创建项目
Maven 依赖:
<!-- 父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<!-- Seata 依赖 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
<!-- 如果使用 Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.0.8.0</version>
</dependency>
第三步:配置文件
application.yml:
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
grouplist:
default: 127.0.0.1:8091
config:
type: file
registry:
type: file
# AT 模式配置
data-source-proxy-mode: AT
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root123
配置说明:
| 配置项 | 说明 |
|---|---|
application-id |
应用唯一标识 |
tx-service-group |
事务分组(逻辑分组) |
vgroup-mapping |
事务分组到集群的映射 |
grouplist |
Seata Server 地址 |
data-source-proxy-mode |
事务模式(AT、XA) |
2.2 AT 模式实战
场景说明
实现一个经典的下单扣库存扣余额流程:
订单服务 → 库存服务 → 账户服务
第一步:准备数据库
每个微服务都需要一个 undo_log 表(AT 模式必需):
-- 每个数据库都需要这张表
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 订单表
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`item_id` bigint NOT NULL,
`count` int NOT NULL,
`amount` decimal(10,2) NOT NULL,
`status` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 库存表
CREATE TABLE `t_storage` (
`id` bigint NOT NULL AUTO_INCREMENT,
`item_id` bigint NOT NULL,
`count` int NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 账户表
CREATE TABLE `t_account` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`balance` decimal(10,2) NOT NULL DEFAULT '0.00',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第二步:编写业务代码
订单服务(TM - 事务发起方):
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StorageClient storageClient;
@Autowired
private AccountClient accountClient;
/**
* 下单:开启全局事务
*/
@GlobalTransactional(name = "place-order", rollbackFor = Exception.class)
@Override
public void placeOrder(Order order) {
// 1. 创建订单(本地事务)
orderMapper.insert(order);
// 2. 扣减库存(远程调用)
storageClient.deduct(order.getItemId(), order.getCount());
// 3. 扣减余额(远程调用)
accountClient.debit(order.getUserId(), order.getAmount());
// 4. 模拟异常
if (order.getAmount().compareTo(new BigDecimal("1000")) > 0) {
throw new RuntimeException("金额超过限额,触发回滚");
}
}
}
库存服务(RM - 事务参与方):
@Service
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageMapper storageMapper;
@Override
public void deduct(String itemId, int count) {
// 本地事务,自动注册为分支事务
int rows = storageMapper.deduct(itemId, count);
if (rows == 0) {
throw new RuntimeException("库存不足");
}
}
}
账户服务(RM - 事务参与方):
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public void debit(String userId, BigDecimal amount) {
// 本地事务,自动注册为分支事务
int rows = accountMapper.debit(userId, amount);
if (rows == 0) {
throw new RuntimeException("余额不足");
}
}
}
第三步:配置数据源代理
Seata 需要代理数据源才能拦截 SQL:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
/**
* Seata 数据源代理
* AT 模式:DataSourceProxy
* XA 模式:DataSourceProxyXA
*/
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
}
注意:Spring Boot 2.x + MyBatis 需要手动配置 SqlSessionFactory:
@Configuration
public class MyBatisConfig {
@Autowired
private DataSourceProxy dataSourceProxy;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSourceProxy); // 使用代理数据源
factory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml"));
return factory.getObject();
}
}
第四步:测试验证
@SpringBootTest
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
public void testPlaceOrder_Success() {
Order order = new Order();
order.setUserId(1L);
order.setItemId(1L);
order.setCount(2);
order.setAmount(new BigDecimal("200"));
orderService.placeOrder(order); // 正常提交
}
@Test
public void testPlaceOrder_Rollback() {
Order order = new Order();
order.setUserId(1L);
order.setItemId(1L);
order.setCount(2);
order.setAmount(new BigDecimal("1500")); // 超过限额
orderService.placeOrder(order); // 触发回滚
}
}
2.3 配置详解
Seata 有两个核心配置文件:file.conf 和 registry.conf。
file.conf(事务配置)
## 事务配置
service {
# 事务分组映射
vgroup_mapping.my_tx_group = "default"
# Seata Server 地址(仅 file 模式需要)
grouplist.default = "127.0.0.1:8091"
# 全局事务开关
enableDegrade = false
disableGlobalTransaction = false
}
## 客户端配置
client {
## 异步提交缓冲区大小
async.commit.buffer.limit = 10000
## 报告重试次数
report.retry.count = 5
## 是否上报成功
report.success.enable = false
## 是否上报成功状态
saga.retry.persist.mode.update = false
## 全局事务超时时间(毫秒)
default.global.transaction.timeout = 60000
## 是否解除数据源代理支持
undo.data.validation = true
## undo_log 表名
undo.log.table = undo_log
## 只读事务是否传播全局事务
rm.async.commit.buffer.limit = 10000
rm.report.retry.count = 5
rm.table.meta.check.enable = true
rm.report.success.enable = false
## TM 配置
tm.commit.retry.count = 1
tm.rollback.retry.count = 1
## undo 配置
undo.only.care.update.columns = true
}
## 存储模式(Server 端)
store {
mode = "file" # file、db、redis
file {
dir = "sessionStore"
max.branch.session.size = 16384
max.global.session.size = 512
file.write.buffer.size = 16384
}
db {
datasource = "druid"
dbType = "mysql"
url = "jdbc:mysql://localhost:3306/seata"
user = "root"
password = "root"
}
}
registry.conf(注册中心配置)
## 注册中心配置
registry {
type = "file" # file、nacos、eureka、redis、zk、consul、etcd3、sofa
nacos {
application = "seata-server"
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
group = "SEATA_GROUP"
username = ""
password = ""
}
file {
name = "file.conf"
}
}
## 配置中心
config {
type = "file" # file、nacos、apollo、zk、consul、etcd3
nacos {
serverAddr = "localhost:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
file {
name = "file.conf"
}
}
2.4 数据库脚本准备
Seata Server 需要的表
如果是生产环境,建议使用数据库存储事务日志:
-- 全局事务表
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint DEFAULT NULL,
`status` tinyint NOT NULL,
`application_id` varchar(32) DEFAULT NULL,
`transaction_service_group` varchar(32) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int DEFAULT NULL,
`begin_time` bigint DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status`,`gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 分支事务表
CREATE TABLE `branch_table` (
`branch_id` bigint NOT NULL,
`xid` varchar(128) NOT NULL,
`transaction_id` bigint DEFAULT NULL,
`resource_group_id` varchar(32) DEFAULT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`branch_type` varchar(8) DEFAULT NULL,
`status` tinyint DEFAULT NULL,
`client_id` varchar(64) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 全局锁表
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(128) DEFAULT NULL,
`transaction_id` bigint DEFAULT NULL,
`branch_id` bigint NOT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT '0',
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第三部分:进阶实战
3.1 TCC 模式详解与实战
核心概念
TCC 模式将业务逻辑分为三个阶段:
graph LR
A[Try 尝试] --> B{成功?}
B -->|是| C[Confirm 确认]
B -->|否| D[Cancel 取消]
style A fill:#ffff99
style C fill:#99ff99
style D fill:#ff9999
- Try:预留资源(如:冻结库存)
- Confirm:确认提交(如:扣减冻结库存)
- Cancel:取消回滚(如:释放冻结库存)
实战案例:库存扣减
第一步:设计数据库表
-- 库存表(需要增加冻结字段)
CREATE TABLE `t_storage` (
`id` bigint NOT NULL AUTO_INCREMENT,
`item_id` bigint NOT NULL,
`count` int NOT NULL DEFAULT '0',
`frozen_count` int NOT NULL DEFAULT '0', -- 冻结数量
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第二步:定义 TCC 接口
@LocalTCC
public interface StorageTccService {
/**
* Try: 预留库存
* @BusinessActionContextParameter 注解的参数会存入上下文
*/
@TwoPhaseBusinessAction(
name = "storageTcc",
commitMethod = "commit",
rollbackMethod = "rollback"
)
boolean prepareDeduct(
@BusinessActionContextParameter(paramName = "itemId") String itemId,
@BusinessActionContextParameter(paramName = "count") int count
);
/**
* Confirm: 提交扣减
*/
boolean commit(BusinessActionContext context);
/**
* Cancel: 回滚
*/
boolean rollback(BusinessActionContext context);
}
第三步:实现 TCC 接口
@Service
@Slf4j
public class StorageTccServiceImpl implements StorageTccService {
@Autowired
private StorageMapper storageMapper;
/**
* Try 阶段:冻结库存
*/
@Override
public boolean prepareDeduct(String itemId, int count) {
log.info("Try: 预留库存, itemId={}, count={}", itemId, count);
// 检查库存是否充足
Storage storage = storageMapper.selectByItemId(itemId);
if (storage.getCount() < count) {
throw new RuntimeException("库存不足");
}
// 冻结库存
int rows = storageMapper.freezeStock(itemId, count);
return rows > 0;
}
/**
* Confirm 阶段:真正扣减
*/
@Override
public boolean commit(BusinessActionContext context) {
String itemId = context.getActionContext("itemId", String.class);
Integer count = context.getActionContext("count", Integer.class);
log.info("Confirm: 扣减库存, itemId={}, count={}", itemId, count);
// 减少冻结库存
int rows = storageMapper.deductFrozenStock(itemId, count);
return rows > 0;
}
/**
* Cancel 阶段:释放冻结库存
*/
@Override
public boolean rollback(BusinessActionContext context) {
String itemId = context.getActionContext("itemId", String.class);
Integer count = context.getActionContext("count", Integer.class);
log.info("Cancel: 释放库存, itemId={}, count={}", itemId, count);
// 释放冻结库存
int rows = storageMapper.releaseFrozenStock(itemId, count);
return rows > 0;
}
}
Mapper SQL:
<mapper namespace="com.example.mapper.StorageMapper">
<!-- 冻结库存 -->
<update id="freezeStock">
UPDATE t_storage
SET count = count - #{count},
frozen_count = frozen_count + #{count}
WHERE item_id = #{itemId} AND count >= #{count}
</update>
<!-- 扣减冻结库存 -->
<update id="deductFrozenStock">
UPDATE t_storage
SET frozen_count = frozen_count - #{count}
WHERE item_id = #{itemId} AND frozen_count >= #{count}
</update>
<!-- 释放冻结库存 -->
<update id="releaseFrozenStock">
UPDATE t_storage
SET count = count + #{count},
frozen_count = frozen_count - #{count}
WHERE item_id = #{itemId} AND frozen_count >= #{count}
</update>
</mapper>
第四步:调用 TCC 服务
@Service
public class OrderServiceImpl {
@Autowired
private StorageTccService storageTccService;
@GlobalTransactional
public void placeOrder(Order order) {
// 创建订单
orderMapper.insert(order);
// TCC 方式扣减库存
boolean success = storageTccService.prepareDeduct(
order.getItemId(),
order.getCount()
);
if (!success) {
throw new RuntimeException("预留库存失败");
}
// 其他业务逻辑...
}
}
TCC 关键问题
1. 空回滚
问题:Try 未执行成功,但 Cancel 被调用了。
解决方案:在 Cancel 方法中判断 Try 是否执行过。
@Override
public boolean rollback(BusinessActionContext context) {
String itemId = context.getActionContext("itemId", String.class);
Integer count = context.getActionContext("count", Integer.class);
// 检查是否有冻结记录(判断 Try 是否执行过)
Storage storage = storageMapper.selectByItemId(itemId);
if (storage.getFrozenCount() < count) {
log.warn("空回滚: itemId={}, 没有冻结记录", itemId);
return true; // 直接返回成功
}
// 正常回滚逻辑
return storageMapper.releaseFrozenStock(itemId, count) > 0;
}
2. 悬挂
问题:Cancel 比 Try 先执行,导致 Try 预留的资源无法释放。
解决方案:记录事务状态,防止悬挂。
// 增加事务状态表
CREATE TABLE `tcc_transaction` (
`xid` varchar(128) NOT NULL,
`branch_id` bigint NOT NULL,
`status` varchar(10) DEFAULT NULL, -- TRY、CONFIRM、CANCEL
PRIMARY KEY (`xid`, `branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@Override
public boolean prepareDeduct(String itemId, int count) {
String xid = RootContext.getXID();
String branchId = String.valueOf(RootContext.getBranchId());
// 检查是否已经 Cancel 过
TccTransaction tx = tccMapper.selectByXidAndBranchId(xid, branchId);
if (tx != null && "CANCEL".equals(tx.getStatus())) {
log.warn("悬挂: xid={}, branchId={}, 已经 Cancel 过", xid, branchId);
return false;
}
// 记录 Try 状态
tccMapper.insert(xid, branchId, "TRY");
// 正常 Try 逻辑
// ...
}
@Override
public boolean rollback(BusinessActionContext context) {
String xid = context.getXid();
String branchId = String.valueOf(context.getBranchId());
// 记录 Cancel 状态
tccMapper.insert(xid, branchId, "CANCEL");
// 正常 Cancel 逻辑
// ...
}
3. 幂等性
问题:Confirm 或 Cancel 可能被重复调用。
解决方案:使用事务状态判断。
@Override
public boolean commit(BusinessActionContext context) {
String xid = context.getXid();
String branchId = String.valueOf(context.getBranchId());
// 检查是否已经 Confirm 过
TccTransaction tx = tccMapper.selectByXidAndBranchId(xid, branchId);
if (tx != null && "CONFIRM".equals(tx.getStatus())) {
log.info("幂等: xid={}, branchId={}, 已经 Confirm 过", xid, branchId);
return true;
}
// 正常 Confirm 逻辑
// ...
// 更新状态
tccMapper.updateStatus(xid, branchId, "CONFIRM");
return true;
}
3.2 SAGA 模式适用场景
适用场景
- 长事务:业务流程长,不适合 2PC
- 跨系统:涉及第三方系统,无法保证一致性
- 人工干预:某些环节需要人工审核
- 异步流程:需要异步执行的业务
实战示例
状态机定义(JSON):
{
"Name": "orderProcess",
"Comment": "订单处理流程",
"StartState": "CreateOrder",
"Version": "1.0.0",
"States": [
{
"Name": "CreateOrder",
"Type": "ServiceTask",
"Comment": "创建订单",
"ServiceName": "orderService",
"ServiceMethod": "create",
"Next": "DeductStock",
"Input": [
"$.[0].orderId",
"$.[0].userId",
"$.[0].amount"
],
"Output": {
"createOrderResult": "$"
}
},
{
"Name": "DeductStock",
"Type": "ServiceTask",
"Comment": "扣减库存",
"ServiceName": "stockService",
"ServiceMethod": "deduct",
"Next": "DeductBalance",
"CompensateState": "CompensateStock",
"Input": [
"$.[0].itemId",
"$.[0].count"
]
},
{
"Name": "DeductBalance",
"Type": "ServiceTask",
"Comment": "扣减余额",
"ServiceName": "balanceService",
"ServiceMethod": "deduct",
"CompensateState": "CompensateBalance",
"Input": [
"$.[0].userId",
"$.[0].amount"
]
},
{
"Name": "CompensateStock",
"Type": "CompensateSubMachine",
"Comment": "补偿库存",
"ServiceName": "stockService",
"ServiceMethod": "compensate"
},
{
"Name": "CompensateBalance",
"Type": "CompensateSubMachine",
"Comment": "补偿余额",
"ServiceName": "balanceService",
"ServiceMethod": "compensate"
}
]
}
Spring Boot 配置:
@Configuration
public class SagaConfig {
@Bean
public StateMachineEngine stateMachineEngine(DataSource dataSource) {
DbStateMachineConfig config = new DbStateMachineConfig();
config.setDataSource(dataSource);
config.setApplicationId("order-service");
config.setTxServiceGroup("my_tx_group");
config.setTablePrefix("seata_");
return new ProcessCtrlStateMachineEngine(config);
}
}
3.3 多数据源事务处理
场景说明
一个微服务中访问多个数据库,需要保证事务一致性。
实现方式
@Configuration
public class MultiDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return new DruidDataSource();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return new DruidDataSource();
}
/**
* 为每个数据源创建代理
*/
@Bean
public DataSourceProxy primaryDataSourceProxy(DataSource primaryDataSource) {
return new DataSourceProxy(primaryDataSource);
}
@Bean
public DataSourceProxy secondaryDataSourceProxy(DataSource secondaryDataSource) {
return new DataSourceProxy(secondaryDataSource);
}
/**
* 多数据源路由
*/
@Bean
@Primary
public DataSource routingDataSource(
DataSourceProxy primaryDataSourceProxy,
DataSourceProxy secondaryDataSourceProxy) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("primary", primaryDataSourceProxy);
targetDataSources.put("secondary", secondaryDataSourceProxy);
RoutingDataSource routing = new RoutingDataSource();
routing.setDefaultTargetDataSource(primaryDataSourceProxy);
routing.setTargetDataSources(targetDataSources);
return routing;
}
}
3.4 事务分组与高可用部署
事务分组
概念:事务分组是逻辑上的分组,映射到实际的 Seata Server 集群。
seata:
tx-service-group: my_tx_group # 逻辑分组
service:
vgroup-mapping:
my_tx_group: cluster1 # 映射到集群 cluster1
优势:
- 灵活切换 Seata Server 集群
- 支持多租户隔离
- 方便灰度发布
高可用部署
graph TB
Client1[微服务A] --> LB[Nginx/LB]
Client2[微服务B] --> LB
Client3[微服务C] --> LB
LB --> Server1[Seata Server 1]
LB --> Server2[Seata Server 2]
LB --> Server3[Seata Server 3]
Server1 --> DB[(MySQL 集群)]
Server2 --> DB
Server3 --> DB
style LB fill:#ff9999
style Server1 fill:#99ff99
style Server2 fill:#99ff99
style Server3 fill:#99ff99
部署要点:
- 注册中心:使用 Nacos/ZooKeeper 管理 Seata Server
- 数据库:使用 MySQL 集群存储事务日志
- 负载均衡:Nginx 或客户端负载均衡
配置示例(Nacos):
seata:
registry:
type: nacos
nacos:
application: seata-server
server-addr: nacos-cluster:8848
namespace: ""
group: SEATA_GROUP
cluster: default
config:
type: nacos
nacos:
server-addr: nacos-cluster:8848
namespace: ""
group: SEATA_GROUP
第四部分:原理深入
4.1 AT 模式的实现原理
AT 模式是 Seata 的核心创新,实现了无侵入的分布式事务。
核心机制:前后镜像
sequenceDiagram
participant App as 应用程序
participant Proxy as DataSourceProxy
participant DB as 数据库
participant Undo as undo_log
App->>Proxy: UPDATE t_stock SET count=90 WHERE id=1
Proxy->>DB: SELECT * FROM t_stock WHERE id=1 (前镜像)
DB-->>Proxy: {id:1, count:100}
Proxy->>DB: UPDATE t_stock SET count=90 WHERE id=1
Proxy->>DB: SELECT * FROM t_stock WHERE id=1 (后镜像)
DB-->>Proxy: {id:1, count:90}
Proxy->>Undo: INSERT INTO undo_log (前后镜像)
Note over Proxy,Undo: 全局提交时删除 undo_log<br/>全局回滚时根据 undo_log 回滚
详细流程:
一阶段(业务 SQL 执行):
- 解析 SQL,得到表名、条件
- 查询前镜像(SELECT * FROM table WHERE condition)
- 执行业务 SQL
- 查询后镜像(SELECT * FROM table WHERE condition)
- 生成回滚 SQL
- 插入 undo_log
undo_log 结构:
{
"branchId": 123456,
"xid": "192.168.1.1:8091:12345",
"beforeImage": {
"tableName": "t_stock",
"rows": [
{
"fields": [
{"name": "id", "type": "BIGINT", "value": 1},
{"name": "count", "type": "INT", "value": 100}
]
}
]
},
"afterImage": {
"tableName": "t_stock",
"rows": [
{
"fields": [
{"name": "id", "type": "BIGINT", "value": 1},
{"name": "count", "type": "INT", "value": 90}
]
}
]
}
}
二阶段(提交/回滚):
提交:
- 异步删除 undo_log
- 释放全局锁
回滚:
- 根据 undo_log 生成反向 SQL
- 执行反向 SQL(恢复数据)
- 删除 undo_log
4.2 全局锁机制
为什么需要全局锁?
在 AT 模式下,一阶段本地事务已提交,数据可能被其他事务修改。全局锁确保隔离性。
全局锁原理
sequenceDiagram
participant Tx1 as 全局事务1
participant TC as TC
participant DB as 数据库
participant Tx2 as 全局事务2
Tx1->>DB: UPDATE t_stock SET count=90 WHERE id=1
Tx1->>TC: 申请全局锁 (id=1)
TC-->>Tx1: 获取成功
Tx1->>DB: 提交本地事务
Tx2->>DB: UPDATE t_stock SET count=80 WHERE id=1
Tx2->>TC: 申请全局锁 (id=1)
TC-->>Tx2: 锁冲突,等待
Tx1->>TC: 全局提交,释放全局锁
Tx2->>TC: 再次申请全局锁
TC-->>Tx2: 获取成功
Tx2->>DB: 提交本地事务
全局锁表结构:
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL, -- lock_key = table_name + pk
`xid` varchar(128) NOT NULL,
`branch_id` bigint NOT NULL,
PRIMARY KEY (`row_key`)
);
全局锁获取流程:
// 伪代码
public boolean tryLock(String lockKey, String xid) {
// 1. 查询是否已被锁定
Lock lock = lockMapper.selectByLockKey(lockKey);
if (lock == null) {
// 2. 未锁定,插入锁记录
return lockMapper.insert(lockKey, xid) > 0;
} else if (lock.getXid().equals(xid)) {
// 3. 已被自己锁定
return true;
} else {
// 4. 被其他事务锁定
return false;
}
}
4.3 事务传播与隔离级别
XID 传播
全局事务 ID(XID)需要在微服务间传播:
graph LR
A[订单服务<br/>XID: 123] -->|HTTP Header| B[库存服务<br/>XID: 123]
B -->|HTTP Header| C[账户服务<br/>XID: 123]
style A fill:#99ff99
style B fill:#99ff99
style C fill:#99ff99
传播方式:
- HTTP:通过 Header 传递
- RPC:通过 Attachment 传递(Dubbo)
- MQ:通过 Message Property 传递
Spring Cloud OpenFeign 示例:
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor seataInterceptor() {
return template -> {
String xid = RootContext.getXID();
if (xid != null) {
template.header("TX_XID", xid);
}
};
}
}
// 接收方
@Component
public class SeataFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest req = (HttpServletRequest) request;
String xid = req.getHeader("TX_XID");
if (xid != null) {
RootContext.bind(xid);
}
try {
chain.doFilter(request, response);
} finally {
RootContext.unbind();
}
}
}
隔离级别
Seata AT 模式的隔离级别:
| 隔离级别 | 说明 |
|---|---|
| 读未提交 | 默认,可能读到中间状态 |
| 读已提交 | 需要配置全局锁 |
配置读已提交:
@Service
public class OrderService {
@GlobalTransactional(isolation = Isolation.READ_COMMITTED)
public void placeOrder(Order order) {
// 业务逻辑
}
}
4.4 事务日志与恢复机制
事务日志存储
Seata Server 使用事务日志记录全局事务状态:
erDiagram
GLOBAL_TABLE {
varchar xid PK
bigint transaction_id
tinyint status
varchar application_id
datetime gmt_create
}
BRANCH_TABLE {
bigint branch_id PK
varchar xid FK
varchar resource_id
varchar branch_type
tinyint status
}
GLOBAL_TABLE ||--o{ BRANCH_TABLE : contains
事务状态流转:
stateDiagram-v2
[*] --> Begin: 开启全局事务
Begin --> Committing: 所有分支成功
Begin --> Rollbacking: 有分支失败
Committing --> Committed: 提交成功
Rollbacking --> Rollbacked: 回滚成功
Committed --> [*]
Rollbacked --> [*]
恢复机制
场景 1:TM 崩溃
- TC 超时后自动回滚
场景 2:RM 崩溃
- TC 记录分支事务状态
- RM 重启后根据 undo_log 回滚
场景 3:TC 崩溃
- 事务日志持久化到数据库
- TC 重启后恢复事务状态
第五部分:生产实践
5.1 性能优化建议
1. 数据库优化
-- undo_log 表优化
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`),
KEY `idx_log_created` (`log_created`) -- 定期清理用
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 定期清理已提交的 undo_log
DELETE FROM undo_log
WHERE log_created < DATE_SUB(NOW(), INTERVAL 7 DAY)
AND log_status = 0;
2. 连接池优化
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 10
max-active: 50
max-wait: 60000
# 关键配置:关闭自动提交
default-auto-commit: false
3. Seata Server 优化
## server 配置
server {
## 最大并发全局事务
max.commit.retry.timeout = 100
max.rollback.retry.timeout = 100
## 重试次数
retry.commit.times = 3
retry.rollback.times = 3
}
## 存储(建议使用 DB 或 Redis)
store {
mode = "db"
db {
datasource = "druid"
dbType = "mysql"
url = "jdbc:mysql://mysql-cluster:3306/seata"
user = "root"
password = "root"
## 连接池配置
min-conn = 10
max-conn = 100
}
}
4. 异步提交
// 开启异步提交(默认)
@GlobalTransactional(rollbackFor = Exception.class)
public void placeOrder(Order order) {
// 业务逻辑
}
5.2 常见问题与解决方案
问题 1:全局锁获取失败
错误信息:
io.seata.rm.datasource.exec.LockConflictException: get global lock fail
原因:
- 多个全局事务同时修改同一行数据
- 全局锁超时
解决方案:
// 增加重试
@GlobalTransactional(
rollbackFor = Exception.class,
lockRetryInternal = 10, // 重试间隔(毫秒)
lockRetryTimes = 30 // 重试次数
)
public void placeOrder(Order order) {
// 业务逻辑
}
问题 2:undo_log 表不存在
错误信息:
Table 'order_db.undo_log' doesn't exist
解决方案:每个数据库都需要创建 undo_log 表(见上文)。
问题 3:事务超时
错误信息:
Transaction timeout
解决方案:
// 增加超时时间(默认 60 秒)
@GlobalTransactional(
timeoutMills = 300000 // 5 分钟
)
public void longRunningTransaction() {
// 长事务逻辑
}
问题 4:XID 未传播
现象:下游服务没有加入全局事务
解决方案:
// 检查拦截器配置
@Configuration
public class SeataConfig {
@Bean
public SeataFeignInterceptor seataFeignInterceptor() {
return new SeataFeignInterceptor();
}
@Bean
public SeataHandlerInterceptor seataHandlerInterceptor() {
return new SeataHandlerInterceptor();
}
}
5.3 监控与运维
Seata Console
Seata 提供了 Web 控制台,用于监控全局事务:
# 下载 Seata Console
wget https://github.com/seata/seata/releases/download/v1.7.1/seata-console-1.7.1.zip
# 启动
unzip seata-console-1.7.1.zip
cd seata-console-1.7.1
sh bin/seata-console.sh
# 访问
http://localhost:7091
功能特性:
- 全局事务列表
- 事务详情查看
- 强制回滚
- 事务日志查询
Prometheus + Grafana
监控指标:
# application.yml
seata:
metrics:
enabled: true
registry-type: compact
exporter-list: prometheus
exporter-prometheus-port: 9898
关键指标:
| 指标 | 说明 |
|---|---|
seata_transaction_total |
全局事务总数 |
seata_transaction_committed_total |
已提交事务数 |
seata_transaction_rollbacked_total |
已回滚事务数 |
seata_transaction_active_count |
活跃事务数 |
seata_resource_lock_total |
锁竞争次数 |
5.4 与 Spring Cloud Alibaba 整合
完整配置示例
父工程:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.8.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
微服务模块:
<dependencies>
<!-- Spring Cloud Alibaba -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>
配置文件:
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: nacos-cluster:8848
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
registry:
type: nacos
nacos:
application: seata-server
server-addr: nacos-cluster:8848
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: nacos-cluster:8848
group: SEATA_GROUP
总结
Seata 作为一款优秀的分布式事务框架,极大地简化了分布式事务的开发难度。本文从基础概念、快速上手、进阶实战、原理深入、生产实践五个维度进行了全面讲解。
核心要点回顾
-
四种模式选择:
- AT 模式:零侵入,适合大多数场景
- TCC 模式:高性能,需要代码侵入
- SAGA 模式:长事务,流程编排
- XA 模式:强一致,性能较低
-
AT 模式原理:
- 一阶段:记录前后镜像(undo_log)
- 二阶段:异步删除或反向回滚
-
生产实践:
- 高可用部署(集群 + 数据库存储)
- 性能优化(连接池、异步提交)
- 监控运维(Console + Prometheus)
学习路径建议
入门:AT 模式 → 理解全局事务概念
↓
进阶:TCC 模式 → 掌握补偿事务
↓
深入:源码分析 → 理解事务协调原理
↓
实践:生产部署 → 高可用与性能优化
参考资料
作者提示:本文基于 Seata 1.7.x / 2.x 版本编写,部分配置在不同版本可能有所差异,请以官方文档为准。如有疑问,欢迎在评论区留言讨论。