跳到主要内容

分布式事务 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 提交/回滚分支事务 │
└──────────────────────────────────────────────────────────────────────┘

详细步骤

  1. TM 向 TC 申请开启全局事务,TC 返回全局唯一的 XID
  2. XID 在服务调用链中传播,RM 向 TC 注册分支事务,纳入 XID 对应的全局事务管辖
  3. TM 向 TC 发起全局提交或回滚
  4. 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);

完整示例:电商下单

业务场景

用户下单,需要协调三个服务:

  1. 订单服务:创建订单
  2. 库存服务:扣减库存
  3. 账户服务:扣减余额

项目结构

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强一致性保证

性能优化

  1. 全局事务范围最小化:只在必要的业务操作上加 @GlobalTransactional
  2. 避免大事务:将大事务拆分为小事务,减少锁持有时间
  3. 合理设置超时:根据业务特点设置合理的全局事务超时时间
  4. 异步提交:AT 模式下,二阶段提交异步执行,不影响性能

注意事项

  1. 每个数据库都要建 undo_log 表(AT 模式)
  2. 确保 XID 正确传播:使用 OpenFeign 时引入 spring-cloud-starter-alibaba-seata
  3. 数据源代理:AT 模式必须使用 DataSourceProxy
  4. 避免在全局事务中执行耗时操作:如文件上传、外部 API 调用等

小结

本章我们学习了:

  1. 分布式事务的必要性:微服务架构下跨数据库事务的挑战
  2. 理论基础:CAP 定理、BASE 理论、两阶段提交
  3. Seata 核心概念:TC、TM、RM 三个角色及其协作
  4. 四种事务模式:AT、TCC、Saga、XA 的原理和适用场景
  5. 部署与集成:Seata Server 部署和客户端配置
  6. 实战示例:电商下单场景的完整实现

参考资料

练习

  1. 搭建 Seata Server 并启动,查看控制台日志理解启动过程
  2. 实现一个简单的分布式事务示例,包含两个服务
  3. 模拟一个失败场景,观察事务回滚过程
  4. 尝试使用 TCC 模式实现账户转账功能