分布式事务 Seata
在微服务架构中,一个业务操作可能涉及多个服务,每个服务都有自己的数据库。如何保证这些跨服务的操作要么全部成功,要么全部失败?这就是分布式事务要解决的核心问题。
为什么需要分布式事务?
单体应用的事务管理
在传统的单体应用中,所有业务逻辑都在一个应用中,共享同一个数据库。我们只需要使用 Spring 的 @Transactional 注解,就能轻松实现事务管理:
@Service
public class OrderService {
@Transactional // 本地事务,简单可靠
public void placeOrder(Order order) {
// 扣减库存
inventoryRepository.deduct(order.getProductId(), order.getQuantity());
// 创建订单
orderRepository.save(order);
// 扣减余额
accountRepository.deduct(order.getUserId(), order.getAmount());
}
}
数据库通过 ACID 特性保证事务的原子性、一致性、隔离性和持久性。
微服务架构的挑战
当我们将单体应用拆分为微服务后,情况变得复杂。订单服务、库存服务、账户服务各自独立部署,拥有独立的数据库:
┌─────────────────────────────────────────────────────────────────────┐
│ 下单业务流程 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 订单服务 │ ──→ │ 库存服务 │ ──→ │ 账户服务 │ │
│ │ Order DB │ │ Inventory DB │ │ Account DB │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 订单表 库存表 账户表 │
│ │
│ 问题:三个服务使用不同的数据库,无法使用本地事务! │
└─────────────────────────────────────────────────────────────────────┘
核心问题:每个服务只能管理自己数据库的事务,无法跨数据库保证 ACID。
分布式事务的场景
| 场景 | 描述 | 一致性要求 |
|---|---|---|
| 电商下单 | 订单、库存、账户三个服务协调 | 最终一致 |
| 银行转账 | 转出方和转入方可能在不同银行系统 | 强一致或最终一致 |
| 订单取消 | 取消订单后需恢复库存、退还余额 | 最终一致 |
| 跨系统数据同步 | 主系统变更需同步到多个从系统 | 最终一致 |
分布式事务理论基础
在深入 Seata 之前,我们需要理解分布式事务的理论基础。
CAP 定理
CAP 定理指出,在分布式系统中,以下三个特性最多只能同时满足两个:
- Consistency(一致性):所有节点在同一时间看到的数据是一致的
- Availability(可用性):每个请求都能在合理时间内得到响应
- Partition Tolerance(分区容错):系统在网络分区发生时仍能继续运行
在分布式系统中,网络分区是不可避免的,因此我们实际上只能在 C 和 A 之间做选择。
BASE 理论
BASE 理论是对 CAP 定理的补充,提出了一种折中方案:
- Basically Available(基本可用):系统在出现故障时,允许损失部分可用性
- Soft State(软状态):允许系统存在中间状态,该状态不影响系统整体可用性
- Eventually Consistent(最终一致):经过一段时间后,所有副本最终达到一致
大多数分布式事务解决方案采用最终一致性,通过补偿机制保证数据最终一致。
两阶段提交(2PC)
两阶段提交是分布式事务的经典协议:
┌─────────────────────────────────────────────────────────────────┐
│ 两阶段提交流程 │
│ │
│ 阶段一:准备阶段 │
│ ┌─────────┐ Prepare ┌─────────┐ │
│ │ 协调者 │ ───────────→ │ 参与者A │ 锁定资源,写 undo/redo │
│ │ (TM) │ Prepare │ 参与者B │ 锁定资源,写 undo/redo │
│ └─────────┘ ───────────→ └─────────┘ │
│ │
│ 阶段二:提交阶段 │
│ ┌─────────┐ Commit ┌─────────┐ │
│ │ 协调者 │ ───────────→ │ 参与者A │ 释放锁,提交事务 │
│ │ (TM) │ Commit │ 参与者B │ 释放锁,提交事务 │
│ └─────────┘ ───────────→ └─────────┘ │
│ │
│ 如果任一参与者返回失败,协调者发送 Rollback 命令 │
└─────────────────────────────────────────────────────────────────┘
优点:强一致性,原理简单
缺点:
- 同步阻塞:参与者在等待协调者指令期间一直锁定资源
- 单点故障:协调者宕机导致所有参与者阻塞
- 数据不一致:部分参与者收到提交指令后宕机,导致数据不一致
Seata 简介
Seata(Simple Extensible Autonomous Transaction Architecture)是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 原属阿里巴巴,后捐赠给 Apache 基金会。
核心概念
Seata 将分布式事务抽象为三个核心角色:
| 角色 | 英文 | 职责 |
|---|---|---|
| 事务协调者 | Transaction Coordinator (TC) | 维护全局事务状态,驱动全局事务提交或回滚。独立部署的 Server |
| 事务管理器 | Transaction Manager (TM) | 开启全局事务、提交或回滚全局事务。通常是发起方应用 |
| 资源管理器 | Resource Manager (RM) | 管理分支事务资源,与 TC 交互注册分支事务并汇报状态。通常是参与方应用 |
事务执行流程
┌──────────────────────────────────────────────────────────────────────┐
│ Seata 事务执行流程 │
│ │
│ ┌──────────────────┐ │
│ │ TM │ 1. 开启全局事务 │
│ │ (订单服务) │ ────────────────────────┐ │
│ └────────┬─────────┘ │ │
│ │ ▼ │
│ │ ┌──────────────────┐ │
│ │ │ TC │ │
│ │ │ (Seata Server) │ │
│ │ └────────┬─────────┘ │
│ │ │ │
│ ┌────────┴─────────┐ │ │
│ │ │ 2. 注册分支事务 │ │
│ ▼ ▼ │ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ RM │ │ RM │ │ RM │ │ │
│ │(库存服务) │ │(账户服务) │ │(订单服务) │ ◄──┘ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 3. TM 通知 TC 提交/回滚全局事务 │
│ 4. TC 驱动所有 RM 提交/回滚分支事务 │
└──────────────────────────────────────────────────────────────────────┘
详细步骤:
- TM 向 TC 申请开启全局事务,TC 返回全局唯一的 XID
- XID 在服务调用链中传播,RM 向 TC 注册分支事务,纳入 XID 对应的全局事务管辖
- TM 向 TC 发起全局提交或回滚
- TC 调度 XID 下管辖的所有分支事务完成提交或回滚
Seata 事务模式
Seata 提供四种事务模式,适应不同的业务场景:
模式对比
| 模式 | 一致性 | 性能 | 侵入性 | 隔离性 | 适用场景 |
|---|---|---|---|---|---|
| AT | 最终一致 | 高 | 无侵入 | 全局锁 | 大多数业务场景 |
| TCC | 最终一致 | 高 | 高侵入 | 资源预留 | 核心交易、资金类 |
| Saga | 最终一致 | 中 | 低侵入 | 无隔离 | 长事务、跨企业 |
| XA | 强一致 | 低 | 无侵入 | 全局锁 | 金融核心系统 |
AT 模式
AT(Auto Transaction)模式是 Seata 最推荐的模式,对业务代码无侵入,基于前后镜像自动生成反向 SQL 实现回滚。
工作原理
┌─────────────────────────────────────────────────────────────────┐
│ AT 模式一阶段 │
│ │
│ 1. 解析 SQL → 得到数据类型(更新)、表名、条件 │
│ 2. 查询前镜像 → SELECT * FROM table WHERE id = ? │
│ 3. 执行业务 SQL → UPDATE table SET field = ? WHERE id = ? │
│ 4. 查询后镜像 → SELECT * FROM table WHERE id = ? │
│ 5. 生成锁记录 → 向 TC 注册全局锁 │
│ 6. 生成 undo_log → 记录前后镜像 │
│ 7. 提交本地事务 → 业务 SQL 和 undo_log 一起提交 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ AT 模式二阶段 │
│ │
│ 提交:异步删除 undo_log │
│ │
│ 回滚: │
│ 1. 根据 XID 和分支ID查找 undo_log │
│ 2. 校验脏写:比较后镜像与当前数据 │
│ 3. 根据前镜像生成反向 SQL → 恢复数据 │
│ 4. 删除 undo_log │
└─────────────────────────────────────────────────────────────────┘
全局锁
AT 模式通过全局锁解决脏写问题:
- 分支事务提交前,需要向 TC 申请全局锁
- 如果其他全局事务持有该数据的全局锁,当前事务需要等待
- 获取全局锁后才能提交本地事务
这确保了同一数据不会被两个全局事务同时修改。
undo_log 表结构
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL COMMENT '分支事务ID',
`xid` varchar(100) NOT NULL COMMENT '全局事务ID',
`context` varchar(128) NOT NULL COMMENT '上下文',
`rollback_info` longblob NOT NULL COMMENT '回滚信息(前后镜像)',
`log_status` int NOT NULL COMMENT '日志状态',
`log_created` datetime NOT NULL COMMENT '创建时间',
`log_modified` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB COMMENT='AT 模式回滚日志表';
示例
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StorageService storageService;
@Autowired
private AccountService accountService;
/**
* 下单:创建订单 → 扣减库存 → 扣减余额
* 使用 @GlobalTransactional 开启全局事务
*/
@Override
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public void createOrder(Order order) {
// 一:创建订单
orderMapper.insert(order);
// 二:扣减库存(远程调用库存服务)
storageService.deduct(order.getProductId(), order.getCount());
// 三:扣减余额(远程调用账户服务)
accountService.debit(order.getUserId(), order.getMoney());
log.info("订单创建完成:{}", order);
}
}
关键点:
- 只需要在发起方添加
@GlobalTransactional注解 - 参与方无需任何改动,自动纳入全局事务
- 每个参与方的数据库需要创建
undo_log表
TCC 模式
TCC(Try-Confirm-Cancel)模式将业务逻辑拆分为三个阶段,需要业务代码显式实现。
三个阶段
┌─────────────────────────────────────────────────────────────────┐
│ TCC 模式三阶段 │
│ │
│ Try:尝试执行业务 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 检查业务可行性,预留必要的业务资源 │ │
│ │ 例:账户扣款时,先冻结金额而非直接扣减 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Confirm:确认执行业务 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 真正执行业务,使用 Try 阶段预留的资源 │ │
│ │ 例:将冻结金额真正扣减 │ │
│ │ 注意:Confirm 不会失败,只要执行就认为成功 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Cancel:取消执行业务 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 释放 Try 阶段预留的资源 │ │
│ │ 例:释放冻结的金额 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
账户扣款示例
public interface AccountTccService {
/**
* Try:冻结金额
* @param userId 用户ID
* @param money 金额
*/
@TwoPhaseBusinessAction(name = "prepareDeduct", commitMethod = "commit", rollbackMethod = "rollback")
void prepareDeduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
/**
* Confirm:真正扣减
*/
boolean commit(BusinessActionContext context);
/**
* Cancel:释放冻结
*/
boolean rollback(BusinessActionContext context);
}
@Service
public class AccountTccServiceImpl implements AccountTccService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private FreezeMapper freezeMapper;
@Override
@Transactional
public void prepareDeduct(String userId, BigDecimal money) {
// 1. 检查余额是否充足
Account account = accountMapper.selectByUserId(userId);
if (account.getBalance().compareTo(money) < 0) {
throw new RuntimeException("余额不足");
}
// 2. 冻结金额(而非直接扣减)
accountMapper.freeze(userId, money);
// 3. 记录冻结明细(用于后续 Confirm/Cancel)
FreezeRecord record = new FreezeRecord();
record.setUserId(userId);
record.setMoney(money);
record.setState("FREEZE");
freezeMapper.insert(record);
}
@Override
@Transactional
public boolean commit(BusinessActionContext context) {
String userId = context.getActionContext("userId", String.class);
BigDecimal money = context.getActionContext("money", BigDecimal.class);
// 1. 真正扣减余额
accountMapper.deduct(userId, money);
// 2. 清除冻结记录
freezeMapper.deleteByUserId(userId);
return true;
}
@Override
@Transactional
public boolean rollback(BusinessActionContext context) {
String userId = context.getActionContext("userId", String.class);
BigDecimal money = context.getActionContext("money", BigDecimal.class);
// 1. 释放冻结金额
accountMapper.unfreeze(userId, money);
// 2. 清除冻结记录
freezeMapper.deleteByUserId(userId);
return true;
}
}
TCC 三大问题
| 问题 | 描述 | 解决方案 |
|---|---|---|
| 空回滚 | Try 未执行但收到 Cancel | 记录事务状态,Cancel 时检查是否执行过 Try |
| 悬挂 | Cancel 先于 Try 执行 | Cancel 执行后标记,Try 检查标记则拒绝执行 |
| 幂等 | Confirm/Cancel 可能重复执行 | 记录执行状态,已执行则直接返回 |
@Service
public class AccountTccServiceImpl implements AccountTccService {
// 解决空回滚和悬挂
@Override
@Transactional
public void prepareDeduct(String userId, BigDecimal money) {
// 获取事务 XID
String xid = RootContext.getXID();
// 检查是否已有 Cancel 记录(防止悬挂)
if (freezeMapper.existsCancelRecord(xid)) {
return; // Cancel 已执行,拒绝 Try
}
// 检查是否已执行过 Try(幂等)
if (freezeMapper.existsFreezeRecord(xid)) {
return; // 已执行,直接返回
}
// 正常执行 Try 逻辑...
// 并记录 xid
}
@Override
@Transactional
public boolean rollback(BusinessActionContext context) {
String xid = context.getXid();
String userId = context.getActionContext("userId", String.class);
// 检查是否执行过 Try(防止空回滚)
FreezeRecord record = freezeMapper.selectByXid(xid);
if (record == null) {
// 记录 Cancel 已执行(防止悬挂)
freezeMapper.insertCancelRecord(xid, userId);
return true;
}
// 检查是否已执行过 Cancel(幂等)
if ("CANCEL".equals(record.getState())) {
return true;
}
// 正常执行 Cancel 逻辑...
}
}
Saga 模式
Saga 模式将长事务拆分为多个本地短事务,每个本地事务有对应的补偿事务。适合跨企业、跨系统的长事务场景。
实现方式
状态机编排:通过 JSON 定义状态图
{
"Name": "placeOrder",
"Comment": "下单流程",
"StartState": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "createOrder",
"CompensateState": "CancelOrder",
"Next": "DeductInventory",
"Input": ["$.order"]
},
"DeductInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "deduct",
"CompensateState": "RestoreInventory",
"Next": "DeductAccount",
"Input": ["$.order.productId", "$.order.count"]
},
"DeductAccount": {
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "debit",
"CompensateState": "RestoreAccount",
"Input": ["$.order.userId", "$.order.money"]
},
"CancelOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "cancelOrder"
},
"RestoreInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "restore"
},
"RestoreAccount": {
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "restore"
}
}
}
执行流程
┌─────────────────────────────────────────────────────────────────┐
│ Saga 模式执行流程 │
│ │
│ 正向流程: │
│ CreateOrder → DeductInventory → DeductAccount → 成功 │
│ │
│ 补偿流程(任一步骤失败): │
│ CreateOrder → DeductInventory → DeductAccount (失败) │
│ ↓ │
│ RestoreInventory ← CancelOrder │
│ │
│ 补偿顺序:按正向流程逆序执行 │
└─────────────────────────────────────────────────────────────────┘
XA 模式
XA 模式利用数据库原生的 XA 协议实现两阶段提交,提供强一致性保证。
工作原理
-- 一阶段:Prepare
XA START 'xid';
UPDATE account SET balance = balance - 100 WHERE user_id = 'A';
XA END 'xid';
XA PREPARE 'xid'; -- 不提交,只准备
-- 二阶段:Commit 或 Rollback
XA COMMIT 'xid'; -- 提交
-- 或
XA ROLLBACK 'xid'; -- 回滚
配置
seata:
data-source-proxy-mode: XA # 使用 XA 模式
适用场景
- 对数据一致性要求极高的场景(如金融核心系统)
- 可以接受性能损耗
- 数据库支持 XA 协议(MySQL、PostgreSQL、Oracle 等主流数据库都支持)
Seata Server 部署
下载安装
# 下载
wget https://github.com/apache/incubator-seata/releases/download/v2.0.0/seata-server-2.0.0.zip
# 解压
unzip seata-server-2.0.0.zip
cd seata-server-2.0.0
配置文件
application.yml(Seata Server 2.x 使用 Spring Boot 配置风格):
server:
port: 7091
spring:
application:
name: seata-server
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
username: nacos
password: nacos
store:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true
user: root
password: root
建表(DB 存储模式)
-- 全局事务表
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
-- 分支事务表
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
-- 全局锁表
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
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;
启动
# Linux/Mac
sh bin/seata-server.sh
# Windows
bin\seata-server.bat
# 指定参数启动
sh bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db
客户端集成
添加依赖
<!-- Seata Starter -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 如果使用 OpenFeign,需要添加 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2022.0.0.0</version>
</dependency>
配置文件
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group # 事务分组
# 事务分组与集群的映射
service:
vgroup-mapping:
my_tx_group: default # my_tx_group 对应 default 集群
# 注册中心配置(与 Server 保持一致)
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
# 配置中心
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
# AT 模式数据源代理
enable-auto-data-source-proxy: true
data-source-proxy-mode: AT
数据源代理(AT 模式)
AT 模式需要使用 Seata 的 DataSourceProxy 包装原始数据源:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
/**
* 使用 DataSourceProxy 包装原始数据源
* Seata 2.x 可以通过配置 enable-auto-data-source-proxy: true 自动代理
*/
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
/**
* MyBatis 使用代理数据源
*/
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSourceProxy);
// 其他配置...
return factoryBean.getObject();
}
}
XID 传播
当使用 OpenFeign 或 Dubbo 进行服务间调用时,需要将 XID 传播到下游服务:
OpenFeign 方式(Spring Cloud Alibaba Seata 自动支持):
@Configuration
public class FeignConfig {
/**
* Seata 提供的 RequestInterceptor,自动传播 XID
*/
@Bean
public RequestInterceptor seataFeignRequestInterceptor() {
return new SeataFeignClientInterceptor();
}
}
手动传播:
// 在发起远程调用前
String xid = RootContext.getXID();
// 将 xid 通过 HTTP Header 或 RPC 参数传递给下游
// 下游服务收到后设置
RootContext.bind(xid);
完整示例:电商下单
业务场景
用户下单,需要协调三个服务:
- 订单服务:创建订单
- 库存服务:扣减库存
- 账户服务:扣减余额
项目结构
seata-demo/
├── seata-order-service/ # 订单服务(TM + RM)
├── seata-storage-service/ # 库存服务(RM)
├── seata-account-service/ # 账户服务(RM)
└── seata-common/ # 公共模块
订单服务(发起方)
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StorageFeignClient storageFeignClient;
@Autowired
private AccountFeignClient accountFeignClient;
/**
* 下单业务
* @GlobalTransactional 开启全局事务
*/
@Override
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public CommonResult<Void> createOrder(OrderDTO orderDTO) {
log.info("-------> 开始全局事务,XID: {}", RootContext.getXID());
// 1. 创建订单
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setCount(orderDTO.getCount());
order.setMoney(orderDTO.getMoney());
order.setStatus(0); // 待支付
orderMapper.create(order);
log.info("-------> 订单创建完成");
// 2. 扣减库存(远程调用)
storageFeignClient.decrement(orderDTO.getProductId(), orderDTO.getCount());
log.info("-------> 库存扣减完成");
// 3. 扣减余额(远程调用)
accountFeignClient.decrement(orderDTO.getUserId(), orderDTO.getMoney());
log.info("-------> 余额扣减完成");
// 4. 更新订单状态
order.setStatus(1); // 已支付
orderMapper.update(order);
log.info("-------> 全局事务提交");
return CommonResult.success("下单成功");
}
}
库存服务(参与方)
@Service
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageMapper storageMapper;
/**
* 参与方无需 @GlobalTransactional
* 通过 XID 自动纳入全局事务
*/
@Override
public void decrement(String productId, Integer count) {
log.info("-------> 库存服务开始扣减库存,XID: {}", RootContext.getXID());
storageMapper.decrement(productId, count);
log.info("-------> 库存服务扣减库存完成");
}
}
账户服务(参与方)
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public void decrement(String userId, BigDecimal money) {
log.info("-------> 账户服务开始扣减余额,XID: {}", RootContext.getXID());
accountMapper.decrement(userId, money);
log.info("-------> 账户服务扣减余额完成");
}
}
测试
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
void testCreateOrder() {
OrderDTO orderDTO = new OrderDTO();
orderDTO.setUserId("U100001");
orderDTO.setProductId("P100001");
orderDTO.setCount(2);
orderDTO.setMoney(new BigDecimal("100"));
CommonResult<Void> result = orderService.createOrder(orderDTO);
System.out.println("下单结果: " + result);
}
@Test
void testCreateOrderRollback() {
// 模拟余额不足的场景,触发回滚
OrderDTO orderDTO = new OrderDTO();
orderDTO.setUserId("U100002"); // 余额不足的用户
orderDTO.setProductId("P100001");
orderDTO.setCount(2);
orderDTO.setMoney(new BigDecimal("100000")); // 金额过大
try {
orderService.createOrder(orderDTO);
} catch (Exception e) {
System.out.println("下单失败,事务已回滚: " + e.getMessage());
}
}
}
最佳实践
模式选择
| 业务场景 | 推荐模式 | 理由 |
|---|---|---|
| 普通业务、无特殊要求 | AT | 无侵入、开发成本低 |
| 核心交易、资金类 | TCC | 资源预留、隔离性好 |
| 跨企业、跨系统 | Saga | 长事务、补偿机制 |
| 金融核心、强一致 | XA | 强一致性保证 |
性能优化
- 全局事务范围最小化:只在必要的业务操作上加
@GlobalTransactional - 避免大事务:将大事务拆分为小事务,减少锁持有时间
- 合理设置超时:根据业务特点设置合理的全局事务超时时间
- 异步提交:AT 模式下,二阶段提交异步执行,不影响性能
注意事项
- 每个数据库都要建 undo_log 表(AT 模式)
- 确保 XID 正确传播:使用 OpenFeign 时引入
spring-cloud-starter-alibaba-seata - 数据源代理:AT 模式必须使用
DataSourceProxy - 避免在全局事务中执行耗时操作:如文件上传、外部 API 调用等
小结
本章我们学习了:
- 分布式事务的必要性:微服务架构下跨数据库事务的挑战
- 理论基础:CAP 定理、BASE 理论、两阶段提交
- Seata 核心概念:TC、TM、RM 三个角色及其协作
- 四种事务模式:AT、TCC、Saga、XA 的原理和适用场景
- 部署与集成:Seata Server 部署和客户端配置
- 实战示例:电商下单场景的完整实现
参考资料
练习
- 搭建 Seata Server 并启动,查看控制台日志理解启动过程
- 实现一个简单的分布式事务示例,包含两个服务
- 模拟一个失败场景,观察事务回滚过程
- 尝试使用 TCC 模式实现账户转账功能