Seata 分布式事务原理与实战

作者:薪火铺子(薪火铺子)


核心要点

分布式事务是微服务架构中的难点:

  • 需要理解 CAP 定理和 BASE 理论
  • 需要掌握常见的分布式事务解决方案(2PC、TCC、Saga)
  • 需要了解 Seata AT 模式的工作机制

本文将深入剖析分布式事务的原理和 Seata 的实现,帮助你理解如何保证跨服务的数据一致性。


一、核心问题:本地事务为什么不够用了?

先看一个经典场景:

问题

订单服务

账号服务

BUT

跨库调用

谁来回滚?

扣减余额

UPDATE account
SET balance = balance - 100

创建订单

INSERT INTO orders
VALUES (...)

账号扣款成功

订单创建失败

数据不一致!

本地事务的局限:

// 本地事务:只能保证同一个数据库内的操作
@Transactional
public void transfer(String from, String to, int amount) {
    // ✓ 同一数据库,可以回滚
    accountMapper.decrease(from, amount);
    orderMapper.createOrder(...);
}

// 分布式事务:涉及多个数据库/服务
@Transactional
public void transfer(String from, String to, int amount) {
    // ✓ 同一数据库
    accountMapper.decrease(from, amount);

    // ✗ 不同数据库/服务,无法回滚!
    orderService.createOrder(...);
}

二、典型场景:分布式事务问题

2.1 故障场景

数据库 账号服务 订单服务 用户 数据库 账号服务 订单服务 用户 此时账号已扣款,但订单失败! 问题:账号已扣款,如何回滚? 1. 创建订单 2. 扣减余额 3. UPDATE account SET balance = balance - 100 4. 成功 5. 扣款成功 6. INSERT INTO orders 7. 失败:库存不足! 8. 返回错误

2.2 根因分析

无事务

本地事务

传统分布式事务

用户扣款成功
订单创建失败

为什么没有回滚?

使用什么事务?

根本没用事务

本地事务只作用单个库

TCC/Saga 太复杂,没用

加上 @Transactional

需要分布式事务框架

使用 Seata

2.3 解决方案

// 使用 Seata 的 @GlobalTransactional
@GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
public void createOrder(String userId, Long productId, BigDecimal amount) {
    // 1. 扣减余额(自动回滚)
    accountService.decrease(userId, amount);

    // 2. 创建订单(如果失败,自动回滚步骤1)
    orderService.createOrder(userId, productId, amount);

    // 3. 扣减库存
    stockService.decreaseStock(productId, 1);
}

三、CAP 定理与 BASE 理论

3.1 CAP 定理

所有节点同一时刻
看到相同数据

每个请求都能得到
响应

系统可以容忍
网络分区

CAP 定理

C: Consistency
一致性

A: Availability
可用性

P: Partition Tolerance
分区容忍性

三选二

分布式系统中的选择:

选择 说明 框架
CA 放弃分区容忍(单机数据库) MySQL
CP 放弃可用性 Zookeeper, etcd
AP 放弃强一致性 Eureka, Nacos
BASE 最终一致性 Seata, 绝大多数业务系统

3.2 BASE 理论

BASE = Basically Available + Soft State + Eventually Consistent

┌─────────────────────────────────────────────────────────────────┐
│  基本可用:允许系统在故障时降级,但不是完全不可用                  │
│  软状态:允许数据在不同节点间存在中间状态                          │
│  最终一致:经过一段时间后,数据会达到一致                          │
└─────────────────────────────────────────────────────────────────┘

分布式事务的选择:
- 强一致性:金融交易、银行系统(Seata XA)
- 最终一致性:大多数互联网业务(Seata AT/TCC/Saga)

四、Seata 三大模式详解

关系型数据库
追求简单

非关系型数据库
追求性能

长流程
多参与者

Seata 模式选择

业务场景?

AT 模式

TCC 模式

Saga 模式

自动处理
无侵入

Try-Confirm-Cancel
三阶段

正向操作
补偿操作

模式 原理 侵入性 适用场景
AT 自动生成 undo log 关系型数据库
TCC Try-Confirm-Cancel Redis/MongoDB
Saga 正向 + 补偿 长流程、审批流

五、AT 模式:零侵入的分布式事务

5.1 AT 模式原理

开启全局事务
获取 XID

返回 XID

执行本地事务
自动生成 undo log

注册分支
上报状态

全局提交/回滚

RM (Resource Manager)
各服务

账号服务

订单服务

库存服务

TM (Transaction Manager)
业务应用

@GlobalTransactional

发起全局事务

TC (Transaction Coordinator)
Seata Server

管理全局事务状态

协调分支提交/回滚

维护全局锁

5.2 AT 模式执行流程

订单服务 账号服务 Seata Server 事务发起方 订单服务 账号服务 Seata Server 事务发起方 阶段一:执行分支事务 阶段二:全局提交 1. 开启全局事务,返回 XID 2. 执行分支:扣减余额 3. 生成 undo log 4. 注册分支:branch-001 5. 分支成功 6. 执行分支:创建订单 7. 生成 undo log 8. 注册分支:branch-002 9. 分支成功 10. 全局提交 11. 提交分支 12. 提交分支 13. 删除 undo log 14. 删除 undo log

5.3 全局锁机制

业务表(RM 维护)

全局锁(TC 维护)

XID

BranchID

Table:PK

id=1001

锁定

global_table

事务 ID

分支 ID

表:主键

account 表

行锁

5.4 undo log 原理

-- 执行前:生成 undo log
-- UPDATE account SET balance = 900 WHERE id = 1001 AND balance = 1000

-- undo log 内容:
{
  "afterImage": {"balance": 900},
  "beforeImage": {"balance": 1000},
  "tableName": "account",
  "pk": {"id": 1001}
}

-- 如果需要回滚,执行反向操作:
-- UPDATE account SET balance = 1000 WHERE id = 1001

六、TCC 模式:高性能分布式事务

6.1 TCC 三阶段原理

成功

失败

执行结果

全部成功 → Confirm

任一失败 → Cancel

Cancel 阶段(取消操作)

释放冻结

恢复可用数量

Confirm 阶段(确认执行)

真正扣减金额

释放冻结

Try 阶段(预留资源)

检查前置条件

冻结金额/库存

记录预扣状态

6.2 TCC 代码实现

// TCC 接口定义
public interface AccountTccService {

    @TwoPhaseBusinessAction(
        name = "accountTccAction",
        commitMethod = "confirm",      // 确认方法
        rollbackMethod = "cancel"       // 回滚方法
    )
    void tryDecrease(
        @BusinessActionContextParameter(paramName = "userId") String userId,
        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount
    );

    boolean confirm(BusinessActionContext context);
    boolean cancel(BusinessActionContext context);
}

// TCC 实现
@Service
public class AccountTccServiceImpl implements AccountTccService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    @Transactional
    public void tryDecrease(String userId, BigDecimal amount) {
        // 1. 检查余额是否充足
        Account account = accountMapper.selectByUserId(userId);
        if (account.getAvailableBalance().compareTo(amount) < 0) {
            throw new RuntimeException("余额不足");
        }

        // 2. 冻结金额(预扣)
        accountMapper.freezeBalance(userId, amount);

        // 3. 扣减可用余额
        accountMapper.decreaseAvailableBalance(userId, amount);
    }

    @Override
    public boolean confirm(BusinessActionContext context) {
        // Confirm 阶段:释放冻结金额(真正扣减完成)
        String userId = context.getActionContext("userId", String.class);
        BigDecimal amount = context.getActionContext("amount", BigDecimal.class);

        // 冻结金额已经扣减,只需要删除冻结记录
        accountMapper.deleteFreezeRecord(userId, amount);
        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        // Cancel 阶段:回滚冻结金额
        String userId = context.getActionContext("userId", String.class);
        BigDecimal amount = context.getActionContext("amount", BigDecimal.class);

        // 1. 恢复可用余额
        accountMapper.increaseAvailableBalance(userId, amount);

        // 2. 释放冻结金额
        accountMapper.unfreezeBalance(userId, amount);

        return true;
    }
}

七、Saga 模式:长流程分布式事务

7.1 Saga 模式原理

补偿操作(Undo)

正向操作(Compensation)

如果失败
执行补偿

如果失败
执行补偿

如果失败
执行补偿

创建订单

扣减库存

扣减余额

发送通知

取消订单

恢复库存

退还余额

取消通知

7.2 Saga 状态机配置

# saga-state.json
{
  "Name": "orderSaga",
  "Comment": "订单创建 saga",
  "StartState": "CreateOrder",
  "States": {
    "CreateOrder": {
      "Type": "ServiceTask",
      "ServiceName": "order-service",
      "ServiceMethod": "createOrder",
      "CompensateState": "CancelOrder",
      "Next": "DecreaseStock"
    },
    "DecreaseStock": {
      "Type": "ServiceTask",
      "ServiceName": "stock-service",
      "ServiceMethod": "decreaseStock",
      "CompensateState": "RestoreStock",
      "Next": "DecreaseBalance"
    },
    "CancelOrder": {
      "Type": "ServiceTask",
      "ServiceName": "order-service",
      "ServiceMethod": "cancelOrder"
    },
    "RestoreStock": {
      "Type": "ServiceTask",
      "ServiceName": "stock-service",
      "ServiceMethod": "restoreStock"
    }
  }
}

八、⚠️ 避坑指南:我踩过的 5 个坑

坑 1:AT 模式全局锁导致性能差

// ❌ 错误:大事务,长时间锁定
@GlobalTransactional
public void batchTransfer(List<TransferRequest> requests) {
    for (TransferRequest req : requests) {
        accountService.decrease(req.getUserId(), req.getAmount());
    }
}
// ✅ 正确:拆分小事务
@GlobalTransactional
public void batchTransfer(List<TransferRequest> requests) {
    for (TransferRequest req : requests) {
        // 每个请求单独开启事务
        transferService.transfer(req.getUserId(), req.getAmount());
    }
}

坑 2:TCC 空回滚

// ❌ 错误:Try 失败后,Cancel 被调用但找不到记录
@Override
public boolean cancel(BusinessActionContext context) {
    // Try 失败了,Cancel 执行时数据可能已经不存在
    accountMapper.unfreezeBalance(userId, amount);  // 可能 NPE!
}
// ✅ 正确:检查状态后再操作
@Override
public boolean cancel(BusinessActionContext context) {
    // 1. 检查是否已执行过 Try
    if (context.isTrendAttempt()) {
        return true;  // 幂等,防止重复取消
    }

    // 2. 使用补偿逻辑,而不是直接删除
    FreezeRecord record = freezeMapper.selectByXidAndUserId(
        context.getXid(), userId
    );
    if (record == null) {
        // Try 没执行过,需要补偿创建
        // ...
    } else if (record.getStatus() == FREEZED) {
        // 正常补偿
        accountMapper.unfreezeBalance(userId, amount);
    }

    return true;
}

坑 3:悬挂问题

// 问题:Cancel 比 Try 先执行
// 1. 网络问题,Try 请求没到
// 2. TC 触发超时,执行 Cancel
// 3. Try 请求到达,执行成功
// 结果:余额扣了,但订单没创建
// ✅ 解决方案:使用防悬挂检查
@Override
public void tryDecrease(String userId, BigDecimal amount) {
    // 1. 检查是否有 Cancel 记录
    CancelRecord cancelRecord = cancelMapper.selectByXid(context.getXid());
    if (cancelRecord != null) {
        throw new RuntimeException("事务已取消,禁止 Try");
    }

    // 2. 插入 Try 记录
    tryRecordMapper.insert(context.getXid(), userId, amount, STATUS_Trying);
}

坑 4:@GlobalTransactional 方法内部调用不生效

// ❌ 错误:内部调用不走代理
@Service
public class OrderServiceImpl implements OrderService {

    @GlobalTransactional
    @Override
    public void createOrder(...) {
        // OK,走代理
        accountService.decrease(userId, amount);
    }

    public void createOrderWithSavepoint(...) {
        // 问题:这个方法内部调用 createOrder()
        this.createOrder(...);  // 不走代理!
    }
}
// ✅ 正确:注入自己或使用 AopContext
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderService self;  // 注入自己

    @GlobalTransactional
    @Override
    public void createOrder(...) {
        accountService.decrease(userId, amount);
        orderMapper.insert(...);
    }

    public void createOrderWithSavepoint(...) {
        // 使用代理调用
        self.createOrder(...);
    }
}

坑 5:Seata Server 单点故障

# ❌ 错误:单节点 Seata
seata:
  server:
    vgroup-mapping:
      my-tx-group: default  # 只有一个节点
# ✅ 正确:集群部署
seata:
  server:
    vgroup-mapping:
      my-tx-group: seata-cluster  # 集群名称

# 配置多个 TC 节点
seata:
  tx-group-config:
    my-tx-group:
      grouplist: 192.168.1.1:8091,192.168.1.2:8091,192.168.1.3:8091

九、实战:Seata AT 模式快速入门

9.1 Seata Server 部署

# docker-compose.yml
version: '3'
services:
  seata-server:
    image: seataio/seata-server:1.7.0
    ports:
      - "8091:8091"
    environment:
      - STORE_MODE=db
      - SEATA_CONFIG_NAME=file:/root/seata-config/registry
    volumes:
      - ./registry.conf:/root/seata-config/registry

9.2 数据库初始化

-- 1. 创建 UNDO_LOG 表(必须!)
CREATE TABLE `undo_log` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `branch_id` bigint NOT NULL,
  `xid` varchar(100) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int NOT NULL COMMENT '0:init,1:committed,2:rollbacked',
  `log_created` datetime DEFAULT CURRENT_TIMESTAMP,
  `log_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
);

-- 2. 创建业务表
CREATE TABLE `account` (
  `id` bigint PRIMARY KEY AUTO_INCREMENT,
  `user_id` varchar(64) NOT NULL,
  `balance` decimal(10,2) NOT NULL DEFAULT 0,
  `frozen_balance` decimal(10,2) NOT NULL DEFAULT 0 COMMENT '冻结金额',
  UNIQUE KEY `uk_user_id` (`user_id`)
);

CREATE TABLE `orders` (
  `id` bigint PRIMARY KEY AUTO_INCREMENT,
  `order_no` varchar(64) NOT NULL,
  `user_id` varchar(64) NOT NULL,
  `product_id` bigint NOT NULL,
  `amount` decimal(10,2) NOT NULL,
  `status` varchar(32) DEFAULT 'CREATED',
  UNIQUE KEY `uk_order_no` (`order_no`)
);

9.3 业务代码实现

// 订单服务
@Service
public class OrderServiceImpl implements OrderService {

    @GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
    @Override
    public void createOrder(String userId, Long productId, BigDecimal amount) {
        // 1. 扣减账号余额
        accountFeignClient.decrease(userId, amount);

        // 2. 创建订单
        Order order = new Order();
        order.setOrderNo(UUID.randomUUID().toString());
        order.setUserId(userId);
        order.setProductId(productId);
        order.setAmount(amount);
        order.setStatus("CREATED");
        orderMapper.insert(order);

        // 3. 模拟失败(测试回滚)
        if (Math.random() < 0.1) {
            throw new RuntimeException("随机失败,用于测试回滚");
        }
    }
}

十、AT vs TCC vs Saga 选择

MySQL/Oracle
PostgreSQL

Redis/MongoDB
其他

长流程
10+ 步骤

如何选择模式?

数据库类型?

AT 模式

TCC 模式

Saga 模式

优点:零侵入
缺点:全局锁

优点:高性能
缺点:有侵入

优点:长流程友好
缺点:补偿复杂

场景 推荐模式 原因
账户转账 AT 简单,关系型数据库
库存扣减 TCC 高并发,无全局锁
订单流程 AT 步骤少,< 5 步
审批流程 Saga 步骤多,10+
金融交易 XA 强一致性要求

十一、面试高频问题

Q1:分布式事务和本地事务的区别?

本地事务:
- 依赖数据库的 ACID 特性
- 在单个数据库连接中执行
- 自动回滚

分布式事务:
- 跨多个数据库/服务
- 需要协调器(TC)协调
- 需要考虑网络问题
- 只能保证最终一致性(BASE)

实际区别:
┌─────────────────────────────────────────────────────────────────┐
│ 本地事务:ACID → 强一致性                                        │
│ 分布式事务:CAP → 最终一致性                                      │
└─────────────────────────────────────────────────────────────────┘

加分回答:

分布式事务的挑战:
1. 网络不可靠:消息可能丢失/延迟
2. 节点可能故障:需要高可用设计
3. 性能 vs 一致性:需要权衡

解决方案:
- 2PC/3PC:强一致,但性能差
- TCC/Saga:最终一致,性能好
- 本地消息表 + 定时任务:最终一致,简化实现

Q2:Seata 的隔离级别?

隔离级别 脏写 脏读 说明
READ UNCOMMITTED 不支持
READ COMMITTED 不支持
REPEATABLE READ 默认(全局锁)
SERIALIZABLE 性能最差
Seata AT 模式默认是 REPEATABLE READ:
- 全局锁保证不脏写
- 但不能防止脏读(其他事务可能读到未提交的数据)

如果需要更高隔离级别:
- 使用 TCC 模式
- 或使用 SELECT FOR UPDATE

Q3:TCC 的空回滚和悬挂如何处理?

// TCC 接口实现
public class AccountTccServiceImpl {

    @Override
    public boolean cancel(BusinessActionContext context) {
        // 1. 空回滚检查
        // Try 方法没有执行,Cancel 也执行了
        if (!context.isTryExists()) {
            // 记录空回滚日志
            log.warn("空回滚: xid={}", context.getXid());
            return true;
        }

        // 2. 幂等检查
        // 防止重复取消
        if (context.isRollbacked()) {
            return true;
        }

        // 3. 正常取消
        String userId = context.getActionContext("userId", String.class);
        BigDecimal amount = context.getActionContext("amount", BigDecimal.class);
        accountMapper.unfreezeBalance(userId, amount);

        return true;
    }
}

Q4:如何保证 TCC 的幂等性?

幂等性问题:
- 网络超时导致重试
- TC 重试已执行过的操作

解决方案:
1. 状态表 + 主键唯一
2. 分布式锁
3. 请求 ID + 缓存

示例:
@TwoPhaseBusinessAction
void tryDecrease(
    @BusinessActionContextParameter(paramName = "xid") String xid,
    @BusinessActionContextParameter(paramName = "userId") String userId
);

// 在 Try/Confirm/Cancel 中检查状态
if (statusService.isProcessed(xid, actionName)) {
    return;  // 已处理,直接返回
}

十二、核心要点总结

Seata
分布式事务

CAP & BASE

CAP: 三选二

BASE: 最终一致性

互联网业务选 AP

AT 模式

自动生成 undo log

无侵入

全局锁保证隔离性

适用于关系型数据库

TCC 模式

Try 预留资源

Confirm 确认执行

Cancel 释放资源

高性能,无全局锁

Saga 模式

正向操作

补偿操作

适用于长流程

避坑指南

大事务拆分

空回滚处理

幂等性保证

悬挂问题


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐