Spring Cloud 分布式事务完全指南
"在分布式系统中,数据一致性是核心挑战,掌握分布式事务是微服务架构的必备技能"
📚 目录
- 分布式事务基础 - 理解分布式事务的必要性和挑战
- 理论基础 - CAP定理、BASE理论和一致性模型
- 分布式事务解决方案 - 主流方案对比分析
- Seata框架深度解析 - 阿里开源分布式事务框架
- Seata AT模式实战 - 最常用的分布式事务模式
- Seata TCC模式实战 - 高性能事务模式
- Seata SAGA模式实战 - 长事务解决方案
- 实战场景应用 - 电商订单等真实场景
- 面试核心问题 - 高频面试题汇总
- 最佳实践与避坑指南 - 生产环境经验总结
分布式事务基础
什么是分布式事务?
分布式事务是指涉及多个独立数据库或服务的事务操作,这些操作需要要么全部成功,要么全部失败,保持数据的一致性。
为什么需要分布式事务?
在单体应用中,我们使用本地事务(@Transactional)就能保证数据一致性:
// ❌ 单体应用中的本地事务 - 在微服务中失效
@Service
public class OrderService {
@Transactional
public void placeOrder(Order order) {
// 1. 保存订单
orderMapper.insert(order);
// 2. 扣减库存
inventoryMapper.deduct(order.getProductId(), order.getCount());
// 3. 扣减余额
accountMapper.deduct(order.getUserId(), order.getAmount());
// 本地事务可以保证这三个操作要么全成功,要么全失败
}
}
但在微服务架构中,这些操作分散在不同的服务中:
// ✅ 微服务架构中需要分布式事务
@Service
public class OrderService {
@Autowired
private InventoryService inventoryService; // 库存服务
@Autowired
private AccountService accountService; // 账户服务
@GlobalTransactional // Seata分布式事务注解
public void placeOrder(Order order) {
// 1. 保存订单
orderMapper.insert(order);
// 2. 调用库存服务扣减库存 (远程调用)
inventoryService.deduct(order.getProductId(), order.getCount());
// 3. 调用账户服务扣减余额 (远程调用)
accountService.deduct(order.getUserId(), order.getAmount());
// 需要分布式事务保证跨服务的操作一致性
}
}
分布式事务的核心挑战
分布式事务面临的挑战:
├── 网络不确定性
│ ├── 请求可能丢失
│ ├── 响应可能延迟
│ └── 服务可能不可用
├── 数据隔离
│ ├── 不同数据库之间无法直接加锁
│ ├── 跨服务的并发控制困难
│ └── 死锁问题复杂化
├── 性能与一致性权衡
│ ├── 强一致性严重影响性能
│ ├── 最终一致性设计复杂
│ └── 补偿机制难以完善
└── 故障恢复
├── 服务宕机后的状态恢复
├── 数据回滚的复杂性
└── 重试机制的幂等性保证
理论基础
CAP定理
CAP定理指出,一个分布式系统不可能同时满足以下三点:
| 特性 | 说明 | 示例 |
|---|---|---|
| C - Consistency 一致性 | 所有节点在同一时间看到相同的数据 | 写入后,所有读取都返回最新值 |
| A - Availability 可用性 | 每个请求都能得到响应(不保证是最新数据) | 服务始终能响应,不因故障而拒绝 |
| P - Partition Tolerance 分区容错性 | 系统在网络分区时仍能继续运行 | 节点间网络断开时,系统仍能运行 |
核心结论: 在分布式系统中,网络分区(P)是必然存在的,因此只能在CA之间做权衡:
CAP权衡选择:
├── CP - 保证一致性和分区容错
│ ├── 代表: Seata的XA模式、Zookeeper
│ ├── 特点: 强一致性,但可能拒绝服务
│ └── 场景: 金融系统、支付系统
│
├── AP - 保证可用性和分区容错
│ ├── 代表: Seata的TCC/SAGA模式、Cassandra
│ ├── 特点: 高可用,但数据可能短期不一致
│ └── 场景: 社交媒体、内容分发
│
└── CA - 保证一致性和可用性
├── 代表: 传统单机数据库(RDBMS)
├── 特点: 无法应对网络分区
└── 场景: 单体应用
BASE理论
BASE理论是对CAP理论的补充,提倡采用最终一致性:
| 理论 | 含义 | 实现方式 |
|---|---|---|
| BA - Basically Available 基本可用 | 允许损失部分可用性 | 响应时间增加、功能降级 |
| S - Soft State 软状态 | 允许数据存在中间状态 | 数据同步过程中存在延迟 |
| E - Eventually Consistent 最终一致性 | 数据最终会达到一致状态 | 通过补偿机制、重试等 |
BASE vs ACID:
// ACID: 强一致性,刚性事务
@Transactional(isolation = Isolation.SERIALIZABLE)
public void traditionalTransaction() {
// 所有操作必须同时成功或失败
// 任何一步失败都会完全回滚
// 性能较差,但数据绝对一致
}
// BASE: 最终一致性,柔性事务
@GlobalTransactional
public void flexibleTransaction() {
// 允许中间状态的存在
// 通过补偿机制保证最终一致
// 性能较好,但实现复杂
}
分布式事务一致性模型
一致性模型层次:
强一致性 ◄─────────────────────────────────────► 弱一致性
│ │
├─ 顺序一致性 ├─ 因果一致性
│ │
├─ 因果一致性 └─ 最终一致性
│
└─ 最终一致性
Spring Cloud中常用的一致性级别:
- 强一致性 (CP): XA模式
- 最终一致性 (AP): TCC、SAGA、AT模式
分布式事务解决方案
方案对比总览
分布式事务方案全景:
├── 2PC/3PC 两阶段提交
│ ├── 优点: 强一致性,实现简单
│ ├── 缺点: 性能差,存在阻塞,单点故障
│ └── 代表: XA协议、Seata XA模式
│
├── TCC (Try-Confirm-Cancel)
│ ├── 优点: 性能好,无锁,灵活
│ ├── 缺点: 代码侵入性强,开发成本高
│ └── 代表: Seata TCC模式、Hmily
│
├── SAGA (长事务解决方案)
│ ├── 优点: 适合长事务,可观测性好
│ ├── 缺点: 状态机复杂,开发成本高
│ └── 代表: Seata SAGA模式、ServiceComb
│
├── 本地消息表
│ ├── 优点: 实现简单,可靠性高
│ ├── 缺点: 需要定时任务扫表
│ └── 代表: RocketMQ事务消息
│
└── AT (自动事务模式)
├── 优点: 无侵入,使用简单
├── 缺点: 需要解析SQL,存在锁竞争
└── 代表: Seata AT模式
详细对比表
| 方案 | 一致性 | 性能 | 复杂度 | 代码侵入 | 适用场景 |
|---|---|---|---|---|---|
| XA (2PC) | 强一致 | ⭐⭐ | ⭐ | 无 | 金融交易、支付系统 |
| AT | 最终一致 | ⭐⭐⭐⭐ | ⭐⭐ | 无 | 一般业务、电商订单 |
| TCC | 最终一致 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 高 | 高并发、核心业务 |
| SAGA | 最终一致 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 高 | 长流程、业务链长 |
| 本地消息表 | 最终一致 | ⭐⭐⭐ | ⭐⭐⭐ | 中 | 异步场景、解耦需求 |
Seata框架深度解析
Seata是什么?
Seata (Simple Extensible Autonomous Transaction Architecture) 是阿里开源的一站式分布式事务解决方案,提供了AT、TCC、SAGA、XA四种事务模式。
Seata架构组件:
├── TC (Transaction Coordinator) - 事务协调器
│ ├── 维护全局事务的状态
│ ├── 驱动全局提交或回滚
│ └── 是一个独立部署的服务
│
├── TM (Transaction Manager) - 事务管理器
│ ├── 定义全局事务的范围
│ ├── 发起全局事务
│ ├── 提交或回滚全局事务
│ └── 由应用程序中发起事务的方担任
│
└── RM (Resource Manager) - 资源管理器
├── 管理分支事务的资源
├── 向TC注册分支事务
├── 汇报分支事务的状态
├── 驱动分支事务的提交或回滚
└── 每个参与事务的数据库都是RM
Seata事务流程
// 典型的Seata分布式事务流程
@GlobalTransactional // TM: 开启全局事务
public void purchaseOrder() {
// 1. TM向TC申请开启全局事务,TC生成XID
String xid = RootContext.getXID();
// 2. RM1: 订单服务 - 创建订单
orderService.createOrder(order);
// 订单数据库作为RM,向TC注册分支事务
// 3. RM2: 库存服务 - 扣减库存
inventoryService.deductStock(productId, count);
// 库存数据库作为RM,向TC注册分支事务
// 4. RM3: 账户服务 - 扣减余额
accountService.deductBalance(userId, amount);
// 账户数据库作为RM,向TC注册分支事务
// 5. TM: 如果所有分支事务都成功,TC通知所有RM提交
// 如果有任何一个失败,TC通知所有RM回滚
}
Seata四种模式对比
Seata模式选择指南:
├── AT模式 (推荐入门)
│ ├── 特点: 无侵入,自动补偿
│ ├── 原理: 自动解析SQL,记录undo_log
│ ├── 性能: 中等
│ └── 场景: 普通业务,性能要求不高
│
├── TCC模式 (高性能场景)
│ ├── 特点: 需要编写三个接口
│ ├── 原理: 应用层自定义补偿逻辑
│ ├── 性能: 高
│ └── 场景: 核心业务,高并发要求
│
├── SAGA模式 (长事务场景)
│ ├── 特点: 状态机定义业务流程
│ ├── 原理: 编排+补偿
│ ├── 性能: 中高
│ └── 场景: 业务流程长、服务多
│
└── XA模式 (强一致性场景)
├── 特点: 数据库XA协议支持
├── 原理: 两阶段提交(2PC)
├── 性能: 低
└── 场景: 金融交易、强一致性要求
Seata AT模式实战
AT模式原理
AT模式 (Automatic Transaction) 是Seata默认的模式,对业务代码零侵入,通过自动解析SQL实现事务控制。
AT模式两阶段提交:
├── 第一阶段: 业务数据和回滚日志记录在同一个本地事务中提交
│ ├── 1. 解析SQL语义,找到要修改的数据(before image)
│ ├── 2. 执行业务SQL
│ ├── 3. 查询修改后的数据(after image)
│ ├── 4. 保存undo_log(包含before和after镜像)
│ └── 5. 提交本地事务,释放本地锁
│
└── 第二阶段: 异步提交或回滚
├── 提交场景:
│ └── 异步删除undo_log,释放全局锁
│
└── 回滚场景:
├── 读取undo_log
├── 生成反向SQL
├── 执行回滚操作
└── 删除undo_log
快速开始
1. 安装Seata Server
# 下载Seata Server
wget https://github.com/seata/seata/releases/download/v2.0.0/seata-server-2.0.0.zip
# 解压
unzip seata-server-2.0.0.zip
# 修改配置文件 conf/file.conf
# 修改配置文件 conf/registry.conf
# 启动Seata Server (默认端口8091)
cd seata/bin
sh seata-server.sh -p 8091
2. 添加依赖
<!-- pom.xml -->
<dependencies>
<!-- Seata Spring Boot Starter -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 需要使用分布式事务的微服务都要添加 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
</dependencies>
3. 配置文件
# application.yml
seata:
enabled: true
application-id: order-service
tx-service-group: my_test_tx_group # 事务组名
service:
vgroup-mapping:
my_test_tx_group: default # 映射到Seata Server的集群名
grouplist:
default: 127.0.0.1:8091 # Seata Server地址
registry:
type: file
config:
type: file
# AT模式配置
enable-auto-data-source-proxy: true # 自动代理数据源
data-source-proxy-mode: AT # 代理模式
4. 创建undo_log表
每个需要参与分布式事务的数据库都要创建undo_log表:
-- 在业务数据库中创建undo_log表
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT '分支事务ID',
`xid` VARCHAR(128) NOT NULL COMMENT '全局事务ID',
`context` VARCHAR(128) NOT NULL COMMENT '上下文',
`rollback_info` LONGBLOB NOT NULL COMMENT '回滚日志',
`log_status` INT(11) NOT NULL COMMENT '状态',
`log_created` DATETIME NOT NULL COMMENT '创建时间',
`log_modified` DATETIME NOT NULL COMMENT '修改时间',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT事务回滚日志表';
业务代码实现
订单服务 (TM)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryService inventoryService;
@Autowired
private AccountService accountService;
/**
* 下单业务 - 使用AT模式实现分布式事务
* @GlobalTransactional: Seata的全局事务注解
*/
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void placeOrder(String userId, String productId, Integer count, BigDecimal amount) {
log.info("开始下单, 用户:{}, 商品:{}, 数量:{}, 金额:{}", userId, productId, count, amount);
// 1. 创建订单 (本地事务1)
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setAmount(amount);
order.setStatus(OrderStatus.INIT.getValue());
orderMapper.insert(order);
log.info("订单创建成功, 订单ID:{}", order.getId());
// 2. 扣减库存 (远程调用 - 库存服务)
inventoryService.deduct(productId, count);
log.info("库存扣减成功");
// 3. 扣减余额 (远程调用 - 账户服务)
accountService.deduct(userId, amount);
log.info("余额扣减成功");
// 4. 更新订单状态
order.setStatus(OrderStatus.SUCCESS.getValue());
orderMapper.updateById(order);
log.info("订单状态更新成功");
log.info("下单完成!");
}
/**
* 测试回滚场景
*/
@GlobalTransactional(rollbackFor = Exception.class)
public void placeOrderWithRollback(String userId, String productId,
Integer count, BigDecimal amount) {
// 1. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setAmount(amount);
orderMapper.insert(order);
// 2. 扣减库存
inventoryService.deduct(productId, count);
// 3. 扣减余额
accountService.deduct(userId, amount);
// 4. 模拟异常 - 触发回滚
if (amount.compareTo(new BigDecimal("100")) > 0) {
throw new RuntimeException("金额超过100,测试回滚");
}
// 如果抛出异常,Seata会自动回滚:
// - 删除订单记录
// - 恢复库存
// - 恢复余额
}
}
库存服务 (RM)
@Service
public class InventoryService {
@Autowired
private InventoryMapper inventoryMapper;
/**
* 扣减库存 - 作为分布式事务的分支事务
* 不需要任何特殊注解,Seata会自动拦截
*/
public void deduct(String productId, Integer count) {
log.info("开始扣减库存, 商品:{}, 数量:{}", productId, count);
// 查询库存
Inventory inventory = inventoryMapper.selectById(productId);
if (inventory == null) {
throw new RuntimeException("商品不存在");
}
// 检查库存是否充足
if (inventory.getCount() < count) {
throw new RuntimeException("库存不足");
}
// 扣减库存
inventory.setCount(inventory.getCount() - count);
inventoryMapper.updateById(inventory);
log.info("库存扣减成功, 剩余:{}", inventory.getCount());
}
}
账户服务 (RM)
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
/**
* 扣减余额 - 作为分布式事务的分支事务
*/
public void deduct(String userId, BigDecimal amount) {
log.info("开始扣减余额, 用户:{}, 金额:{}", userId, amount);
// 查询账户
Account account = accountMapper.selectById(userId);
if (account == null) {
throw new RuntimeException("账户不存在");
}
// 检查余额是否充足
if (account.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
// 扣减余额
account.setBalance(account.getBalance().subtract(amount));
accountMapper.updateById(account);
log.info("余额扣减成功, 剩余:{}", account.getBalance());
}
}
AT模式的核心机制
1. 自动代理数据源
// Seata自动代理数据源,拦截SQL执行
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource")
public DataSource dataSource() {
// 原始数据源
DataSource originalDataSource = DataSourceBuilder.create().build();
// Seata代理数据源 - 自动拦截SQL,记录undo_log
return new DataSourceProxy(originalDataSource);
}
}
2. undo_log日志示例
-- 业务SQL执行前,记录before image
SELECT id, user_id, product_id, count, amount
FROM t_order
WHERE id = 1;
-- 执行业务SQL
UPDATE t_order
SET count = count - 1
WHERE id = 1;
-- 业务SQL执行后,记录after image
SELECT id, user_id, product_id, count, amount
FROM t_order
WHERE id = 1;
-- 插入undo_log (JSON格式存储)
INSERT INTO undo_log (xid, branch_id, rollback_info, ...)
VALUES ('xid...', 'branchId...', '{
"beforeImage": {
"rows": [{"fields": {"id": 1, "count": 10, ...}}]
},
"afterImage": {
"rows": [{"fields": {"id": 1, "count": 9, ...}}]
}
}', ...);
3. 回滚时生成反向SQL
// 如果需要回滚,Seata自动生成反向SQL
// beforeImage: count = 10
// afterImage: count = 9
// 回滚SQL:
UPDATE t_order
SET count = 10 -- 恢复为before image的值
WHERE id = 1;
AT模式的优缺点
优点:
├── ✅ 无侵入: 业务代码不需要改动
├── ✅ 使用简单: 只需添加@GlobalTransactional注解
├── ✅ 自动补偿: 自动记录和执行回滚
└── ✅ 性能可接受: 第一阶段释放本地锁
缺点:
├── ❌ 需要undo_log表: 每个数据库都要创建
├── ❌ 存在脏写风险: 两阶段之间存在时间窗口
├── ❌ 性能不如TCC: 需要解析SQL,记录镜像
└── ❌ 不适合复杂SQL: 复杂查询可能解析失败
Seata TCC模式实战
TCC模式原理
TCC (Try-Confirm-Cancel) 是一种应用层补偿型的分布式事务模式,需要业务方实现三个接口:
TCC两阶段提交:
├── 第一阶段: Try - 资源预留
│ ├── 完成业务检查
│ ├── 预留必须资源
│ └── 返回成功,表示可以进入第二阶段
│
└── 第二阶段:
├── Confirm: 确认执行 - 调用Try成功后执行
│ ├── 使用Try阶段预留的资源
│ ├── 真正执行业务操作
│ └── 幂等设计,支持重复调用
│
└── Cancel: 取消执行 - 调用Try失败后执行
├── 释放Try阶段预留的资源
├── 回滚业务操作
└── 幂等设计,支持重复调用
TCC模式实现
1. 定义TCC接口
/**
* 库存服务TCC接口
*/
@LocalTCC // 标记为TCC接口
public interface InventoryTccService {
/**
* Try阶段: 预留库存
* @BusinessActionContextParameter: 参数传递到Confirm/Cancel
*/
@TwoPhaseBusinessAction(
name = "inventoryDeductTcc", // 行为名称
commitMethod = "confirm", // Confirm方法名
rollbackMethod = "cancel" // Cancel方法名
)
boolean deduct(
BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "productId") String productId,
@BusinessActionContextParameter(paramName = "count") Integer count
);
/**
* Confirm阶段: 确认扣减库存
* 使用Try阶段预留的库存
*/
boolean confirm(BusinessActionContext businessActionContext);
/**
* Cancel阶段: 取消扣减库存
* 释放Try阶段预留的库存
*/
boolean cancel(BusinessActionContext businessActionContext);
}
2. 实现TCC接口
@Service
public class InventoryTccServiceImpl implements InventoryTccService {
@Autowired
private InventoryMapper inventoryMapper;
@Autowired
private InventoryFreezeMapper inventoryFreezeMapper;
/**
* Try阶段: 预留库存
*/
@Transactional
@Override
public boolean deduct(BusinessActionContext businessActionContext,
String productId, Integer count) {
String xid = businessActionContext.getXid();
log.info("Try阶段开始, xid:{}, 商品:{}, 数量:{}", xid, productId, count);
// 1. 检查库存是否充足
Inventory inventory = inventoryMapper.selectById(productId);
if (inventory == null || inventory.getCount() < count) {
throw new RuntimeException("库存不足");
}
// 2. 冻结库存 - 从可用库存中扣减,转入冻结库存
int freezeResult = inventoryMapper.freezeInventory(productId, count);
if (freezeResult == 0) {
throw new RuntimeException("冻结库存失败");
}
// 3. 记录冻结日志 - 用于Confirm和Cancel
InventoryFreeze freeze = new InventoryFreeze();
freeze.setXid(xid);
freeze.setProductId(productId);
freeze.setCount(count);
freeze.setStatus(FreezeStatus.TRY.getValue());
inventoryFreezeMapper.insert(freeze);
log.info("Try阶段成功, xid:{}", xid);
return true;
}
/**
* Confirm阶段: 确认扣减库存
* 真正从冻结库存中扣减
*/
@Transactional
@Override
public boolean confirm(BusinessActionContext businessActionContext) {
String xid = businessActionContext.getXid();
String productId = (String) businessActionContext.getActionContext("productId");
Integer count = (Integer) businessActionContext.getActionContext("count");
log.info("Confirm阶段开始, xid:{}, 商品:{}, 数量:{}", xid, productId, count);
// 1. 幂等性检查 - 避免重复Confirm
InventoryFreeze freeze = inventoryFreezeMapper.selectByXid(xid);
if (freeze == null) {
log.info("冻结记录不存在,可能已Confirm, xid:{}", xid);
return true;
}
if (freeze.getStatus() == FreezeStatus.CONFIRM.getValue()) {
log.info("已经Confirm过,无需重复执行, xid:{}", xid);
return true;
}
// 2. 真正扣减库存 - 从冻结库存中扣减
inventoryMapper.deductFrozenInventory(productId, count);
// 3. 更新冻结状态
freeze.setStatus(FreezeStatus.CONFIRM.getValue());
inventoryFreezeMapper.updateById(freeze);
log.info("Confirm阶段成功, xid:{}", xid);
return true;
}
/**
* Cancel阶段: 取消扣减库存
* 释放冻结的库存
*/
@Transactional
@Override
public boolean cancel(BusinessActionContext businessActionContext) {
String xid = businessActionContext.getXid();
String productId = (String) businessActionContext.getActionContext("productId");
Integer count = (Integer) businessActionContext.getActionContext("count");
log.info("Cancel阶段开始, xid:{}, 商品:{}, 数量:{}", xid, productId, count);
// 1. 幂等性检查 - 避免重复Cancel
InventoryFreeze freeze = inventoryFreezeMapper.selectByXid(xid);
if (freeze == null) {
log.info("冻结记录不存在,可能已Cancel, xid:{}", xid);
return true;
}
if (freeze.getStatus() == FreezeStatus.CANCEL.getValue()) {
log.info("已经Cancel过,无需重复执行, xid:{}", xid);
return true;
}
// 2. 释放冻结库存 - 恢复到可用库存
inventoryMapper.releaseFrozenInventory(productId, count);
// 3. 更新冻结状态
freeze.setStatus(FreezeStatus.CANCEL.getValue());
inventoryFreezeMapper.updateById(freeze);
log.info("Cancel阶段成功, xid:{}", xid);
return true;
}
}
3. 数据库表设计
-- 库存表
CREATE TABLE `t_inventory` (
`id` varchar(64) NOT NULL COMMENT '商品ID',
`count` int(11) NOT NULL COMMENT '可用库存',
`freeze_count` int(11) NOT NULL DEFAULT '0' COMMENT '冻结库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 库存冻结记录表
CREATE TABLE `t_inventory_freeze` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`xid` varchar(128) NOT NULL COMMENT '全局事务ID',
`product_id` varchar(64) NOT NULL COMMENT '商品ID',
`count` int(11) NOT NULL COMMENT '冻结数量',
`status` tinyint(4) NOT NULL COMMENT '状态: 1-Try, 2-Confirm, 3-Cancel',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4. Mapper接口
@Mapper
public interface InventoryMapper {
/**
* 冻结库存 - Try阶段
* 从可用库存转移到冻结库存
*/
@Update("UPDATE t_inventory " +
"SET count = count - #{count}, " +
" freeze_count = freeze_count + #{count} " +
"WHERE id = #{productId} " +
"AND count >= #{count}") -- 乐观锁
int freezeInventory(@Param("productId") String productId,
@Param("count") Integer count);
/**
* 扣减冻结库存 - Confirm阶段
* 从冻结库存中真正扣减
*/
@Update("UPDATE t_inventory " +
"SET freeze_count = freeze_count - #{count} " +
"WHERE id = #{productId} " +
"AND freeze_count >= #{count}")
int deductFrozenInventory(@Param("productId") String productId,
@Param("count") Integer count);
/**
* 释放冻结库存 - Cancel阶段
* 从冻结库存恢复到可用库存
*/
@Update("UPDATE t_inventory " +
"SET count = count + #{count}, " +
" freeze_count = freeze_count - #{count} " +
"WHERE id = #{productId} " +
"AND freeze_count >= #{count}")
int releaseFrozenInventory(@Param("productId") String productId,
@Param("count") Integer count);
}
TCC模式使用
@Service
public class OrderService {
@Autowired
private InventoryTccService inventoryTccService;
@Autowired
private AccountTccService accountTccService;
/**
* 使用TCC模式下单
*/
@GlobalTransactional(name = "create-order-tcc", rollbackFor = Exception.class)
public void placeOrderWithTcc(String userId, String productId,
Integer count, BigDecimal amount) {
log.info("开始TCC下单");
// 1. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setAmount(amount);
orderMapper.insert(order);
// 2. Try阶段 - 预留资源
inventoryTccService.deduct(null, productId, count); // 预留库存
accountTccService.deduct(null, userId, amount); // 预留余额
// 3. 如果上述Try都成功,Seata会自动调用Confirm
// 如果任何一个Try失败,Seata会自动调用Cancel
log.info("TCC下单完成");
}
}
TCC模式的优缺点
优点:
├── ✅ 性能高: 第一阶段就释放资源
├── ✅ 无锁: 不需要数据库锁
├── ✅ 灵活: 业务方自己控制补偿逻辑
└── ✅ 适用范围广: 适合复杂业务
缺点:
├── ❌ 代码侵入性强: 需要编写三个方法
├── ❌ 开发成本高: 需要考虑幂等性、悬挂等
├── ❌ 容易出错: 补偿逻辑需要仔细设计
└── ❌ 维护成本高: 业务变更需要同步修改TCC
TCC模式注意事项
1. 幂等性设计
// Confirm/Cancel必须支持幂等
public boolean confirm(BusinessActionContext context) {
// 先检查是否已处理
if (isAlreadyProcessed(context.getXid())) {
return true; // 已处理过,直接返回成功
}
// 执行业务逻辑
doConfirm();
// 标记为已处理
markAsProcessed(context.getXid());
return true;
}
2. 空回滚处理
// Try还没执行,Cancel先到了(网络延迟等原因)
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
// 检查是否有Try记录
if (freezeMapper.selectByXid(xid) == null) {
// 没有Try记录,插入一条Cancel记录,避免空回滚
InventoryFreeze freeze = new InventoryFreeze();
freeze.setXid(xid);
freeze.setStatus(FreezeStatus.CANCEL.getValue());
freezeMapper.insert(freeze);
return true; // 空回滚成功
}
// 正常Cancel逻辑
doCancel(context);
return true;
}
3. 悬挂处理
// Cancel先执行了,Try后执行
public boolean deduct(BusinessActionContext context, String productId, Integer count) {
String xid = context.getXid();
// 检查是否已经Cancel
InventoryFreeze freeze = freezeMapper.selectByXid(xid);
if (freeze != null && freeze.getStatus() == FreezeStatus.CANCEL.getValue()) {
// 已经Cancel,不允许Try
throw new RuntimeException("已取消,不允许Try");
}
// 正常Try逻辑
doTry(productId, count);
return true;
}
Seata SAGA模式实战
SAGA模式原理
SAGA模式是一种长事务解决方案,通过定义状态机来编排业务流程,每个步骤都有对应的补偿操作。
SAGA模式核心概念:
├── 状态机(State Machine): 定义业务流程
├── 状态(State): 每个业务步骤
├── 转移(Transition): 状态之间的流转
├── 补偿(Compensation): 失败时的回滚操作
└── 事件(Event): 触发状态转移的事件
SAGA状态机定义
{
"Name": "purchaseOrderFlow",
"Comment": "下单流程",
"StartState": "createOrder",
"States": {
"createOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "create",
"CompensateState": "compensateCreateOrder",
"Next": "deductInventory",
"Input": ["$input.userId", "$input.productId", "$input.count"],
"Output": {
"orderId": "$.orderId"
}
},
"deductInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "deduct",
"CompensateState": "compensateDeductInventory",
"Next": "deductBalance",
"Input": ["$input.productId", "$input.count"],
"Output": {
"inventory": "$.inventory"
}
},
"deductBalance": {
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "deduct",
"CompensateState": "compensateDeductBalance",
"Next": "updateOrderStatus",
"Input": ["$input.userId", "$input.amount"],
"Output": {
"balance": "$.balance"
}
},
"updateOrderStatus": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "updateStatus",
"CompensateState": "compensateUpdateOrderStatus",
"Input": ["$.orderId"],
"IsEnd": true
},
"compensateCreateOrder": {
"Type": "Compensation",
"ServiceName": "orderService",
"ServiceMethod": "delete"
},
"compensateDeductInventory": {
"Type": "Compensation",
"ServiceName": "inventoryService",
"ServiceMethod": "addBack"
},
"compensateDeductBalance": {
"Type": "Compensation",
"ServiceName": "accountService",
"ServiceMethod": "refund"
},
"compensateUpdateOrderStatus": {
"Type": "Compensation",
"ServiceName": "orderService",
"ServiceMethod": "revertStatus"
}
}
}
SAGA业务实现
@Service
public class OrderService {
/**
* 正向操作 - 创建订单
*/
public Order create(String userId, String productId, Integer count) {
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setStatus(OrderStatus.INIT.getValue());
orderMapper.insert(order);
return order;
}
/**
* 补偿操作 - 删除订单
*/
public void delete(Order order) {
orderMapper.deleteById(order.getId());
}
/**
* 正向操作 - 更新订单状态
*/
public void updateStatus(Long orderId) {
Order order = new Order();
order.setId(orderId);
order.setStatus(OrderStatus.SUCCESS.getValue());
orderMapper.updateById(order);
}
/**
* 补偿操作 - 恢复订单状态
*/
public void revertStatus(Long orderId) {
Order order = new Order();
order.setId(orderId);
order.setStatus(OrderStatus.CANCEL.getValue());
orderMapper.updateById(order);
}
}
@Service
public class InventoryService {
/**
* 正向操作 - 扣减库存
*/
public Inventory deduct(String productId, Integer count) {
Inventory inventory = inventoryMapper.selectById(productId);
inventory.setCount(inventory.getCount() - count);
inventoryMapper.updateById(inventory);
return inventory;
}
/**
* 补偿操作 - 恢复库存
*/
public void addBack(String productId, Integer count) {
Inventory inventory = inventoryMapper.selectById(productId);
inventory.setCount(inventory.getCount() + count);
inventoryMapper.updateById(inventory);
}
}
@Service
public class AccountService {
/**
* 正向操作 - 扣减余额
*/
public Account deduct(String userId, BigDecimal amount) {
Account account = accountMapper.selectById(userId);
account.setBalance(account.getBalance().subtract(amount));
accountMapper.updateById(account);
return account;
}
/**
* 补偿操作 - 退款
*/
public void refund(String userId, BigDecimal amount) {
Account account = accountMapper.selectById(userId);
account.setBalance(account.getBalance().add(amount));
accountMapper.updateById(account);
}
}
SAGA模式使用
@Service
public class OrderBusinessService {
@Autowired
private StateMachineEngine stateMachineEngine;
/**
* 使用SAGA模式下单
*/
public void placeOrderWithSaga(String userId, String productId,
Integer count, BigDecimal amount) {
// 构建输入参数
Map<String, Object> startParams = new HashMap<>();
startParams.put("userId", userId);
startParams.put("productId", productId);
startParams.put("count", count);
startParams.put("amount", amount);
// 启动状态机
StateMachineInstance instance = stateMachineEngine.start(
"purchaseOrderFlow", // 状态机名称
null, // 业务键
startParams // 输入参数
);
// 检查执行结果
if (instance.getStatus() == ExecutionStatus.SU) {
log.info("下单成功");
} else {
log.error("下单失败, 状态:{}", instance.getStatus());
throw new RuntimeException("下单失败");
}
}
}
SAGA模式的优缺点
优点:
├── ✅ 适合长事务: 可包含几十个步骤
├── ✅ 可观测性好: 状态机可视化
├── ✅ 灵活: 支持串行、并行、子流程
└── ✅ 无锁: 不占用数据库锁
缺点:
├── ❌ 开发成本高: 需要定义状态机
├── ❌ 学习曲线陡: 需要理解状态机
├── ❌ 补偿复杂: 每个步骤都需要补偿
└── ❌ 不支持隔离: 无法实现可串行化
实战场景应用
场景1: 电商下单
@Service
public class EcommerceOrderService {
/**
* 电商下单 - 典型的分布式事务场景
* 涉及: 订单服务、库存服务、账户服务、优惠券服务、积分服务
*/
@GlobalTransactional(name = "ecommerce-order", rollbackFor = Exception.class)
public void placeOrder(OrderRequest request) {
// 1. 校验优惠券
couponService.useCoupon(request.getUserId(), request.getCouponId());
// 2. 创建订单
Order order = createOrder(request);
// 3. 扣减库存
inventoryService.deduct(order.getProductId(), order.getCount());
// 4. 扣减余额
accountService.deduct(order.getUserId(), order.getPayAmount());
// 5. 增加积分
pointService.add(order.getUserId(), order.getPayAmount());
// 6. 发送通知
notificationService.sendOrderMessage(order);
}
}
场景2: 跨行转账
@Service
public class TransferService {
/**
* 跨行转账 - 强一致性要求
* 建议: 使用XA模式或TCC模式
*/
@GlobalTransactional(name = "bank-transfer", rollbackFor = Exception.class)
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
// 1. 转出账户扣款
Account from = accountService.query(fromAccount);
if (from.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
accountService.deduct(fromAccount, amount);
// 2. 转入账户收款
accountService.add(toAccount, amount);
// 3. 记录转账流水
TransferRecord record = new TransferRecord();
record.setFromAccount(fromAccount);
record.setToAccount(toAccount);
record.setAmount(amount);
record.setStatus(TransferStatus.SUCCESS);
transferRecordMapper.insert(record);
}
}
场景3: 订单超时取消
@Service
public class OrderTimeoutService {
/**
* 订单超时取消 - SAGA模式
* 涉及: 取消订单、恢复库存、退款、恢复优惠券、扣减积分
*/
public void cancelOrder(Long orderId) {
Order order = orderMapper.selectById(orderId);
// 使用SAGA模式执行复杂的取消流程
Map<String, Object> params = new HashMap<>();
params.put("orderId", orderId);
params.put("userId", order.getUserId());
params.put("productId", order.getProductId());
params.put("count", order.getCount());
params.put("amount", order.getAmount());
params.put("couponId", order.getCouponId());
StateMachineInstance instance = stateMachineEngine.start(
"cancelOrderFlow",
orderId.toString(),
params
);
if (instance.getStatus() != ExecutionStatus.SU) {
log.error("订单取消失败, orderId:{}", orderId);
throw new RuntimeException("订单取消失败");
}
}
}
场景4: 旅游预订
@Service
public class TravelBookingService {
/**
* 旅游预订 - 涉及多个外部服务
* 航班、酒店、景点、保险、接送机
*/
@GlobalTransactional(name = "travel-booking", rollbackFor = Exception.class)
public void bookTravel(TravelBookingRequest request) {
// 1. 预订航班
flightService.book(request.getFlightId(), request.getPassengers());
// 2. 预订酒店
hotelService.book(request.getHotelId(), request.getCheckInDate(),
request.getCheckOutDate(), request.getRooms());
// 3. 预订景点门票
attractionService.book(request.getAttractionIds(),
request.getVisitDate());
// 4. 购买保险
insuranceService.buy(request.getInsurancePlan(),
request.getPassengers());
// 5. 预订接送机
transferService.book(request.getTransferId(),
request.getFlightTime());
// 6. 创建行程单
itineraryService.create(request);
}
}
面试核心问题
Q1: 什么是分布式事务?为什么需要分布式事务?
参考答案:
分布式事务是指涉及多个独立数据库或服务的事务操作,需要保证这些操作的原子性,要么全部成功,要么全部失败。
在微服务架构中,业务被拆分成多个独立的服务,每个服务有独立的数据库。一个业务操作可能需要调用多个服务,如果不使用分布式事务,就可能出现数据不一致的问题。
举例: 下单时,订单创建成功,但库存扣减失败,就会出现超卖问题。
Q2: 分布式事务有哪些解决方案?各有什么优缺点?
参考答案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 2PC/XA | 强一致,实现简单 | 性能差,阻塞,单点故障 | 金融系统 |
| TCC | 性能好,灵活 | 代码侵入,开发成本高 | 高并发核心业务 |
| SAGA | 适合长事务 | 状态机复杂,补偿难 | 长流程业务 |
| 本地消息表 | 可靠,简单 | 需要定时任务 | 异步解耦 |
| AT模式 | 无侵入,简单 | 需要undo_log,脏写风险 | 一般业务 |
Q3: Seata的AT模式是如何实现的?
参考答案:
AT模式分为两个阶段:
第一阶段:
- 解析SQL,找到要修改的行,记录before image
- 执行业务SQL
- 查询修改后的数据,记录after image
- 将before和after镜像存入undo_log表
- 提交本地事务,释放本地锁
第二阶段:
- 提交: 异步删除undo_log,释放全局锁
- 回滚: 读取undo_log,生成反向SQL,执行回滚,删除undo_log
核心: 通过undo_log实现自动补偿,对业务代码零侵入。
Q4: Seata的TCC模式如何实现幂等性?
参考答案:
TCC的Confirm和Cancel需要支持幂等性,因为网络重试可能导致多次调用。
实现方法:
- 数据库唯一约束: 在业务表中增加xid字段,设置唯一索引
- 状态机记录: 记录每个分支事务的执行状态
- 先检查再执行: 每次执行前先检查是否已执行过
public boolean confirm(BusinessActionContext context) {
String xid = context.getXid();
// 幂等检查
Record record = mapper.selectByXid(xid);
if (record != null && record.getStatus() == Status.CONFIRMED) {
return true; // 已执行,直接返回
}
// 执行业务
doConfirm();
// 更新状态
record.setStatus(Status.CONFIRMED);
mapper.updateById(record);
return true;
}
Q5: 什么是CAP定理?分布式系统如何权衡?
参考答案:
CAP定理指出,分布式系统不可能同时满足一致性(C)、可用性(A)、分区容错性(P):
-
CP: 放弃可用性,保证一致性
- 代表: XA、Seata XA模式
- 场景: 金融交易、支付系统
-
AP: 放弃强一致性,保证可用性
- 代表: TCC、SAGA、最终一致性
- 场景: 社交媒体、内容分发
-
CA: 放弃分区容错
- 代表: 单体应用、RDBMS
- 场景: 传统应用
在分布式系统中,网络分区是必然的,所以只能在CP和AP之间权衡。
Q6: 什么是BASE理论?
参考答案:
BASE理论是对CAP理论的补充,提倡采用最终一致性:
- BA (Basically Available): 基本可用,允许损失部分可用性
- S (Soft State): 软状态,允许数据存在中间状态
- E (Eventually Consistent): 最终一致性,数据最终会达到一致
与ACID的区别:
- ACID追求强一致性,刚性事务
- BASE接受最终一致性,柔性事务
Q7: 分布式事务如何处理超时和重试?
参考答案:
-
超时处理:
- 设置合理的超时时间
- 超时后触发回滚或补偿
- Seata通过全局锁检测超时
-
重试机制:
- 最大重试次数限制
- 指数退避策略
- 幂等性保证
@GlobalTransactional(
timeoutMills = 60000, // 超时时间60秒
rollbackFor = Exception.class
)
public void business() {
// 业务逻辑
}
Q8: 如何保证分布式事务的可靠性?
参考答案:
- 幂等性: 所有接口都要支持幂等
- 超时控制: 合理设置超时时间
- 重试机制: 失败自动重试
- 事务日志: 记录完整的执行过程
- 监控告警: 监控事务成功率、耗时等
- 降级策略: 失败时的降级方案
- 数据校验: 定期对账,发现不一致
Q9: TCC模式的空回滚和悬挂问题如何解决?
参考答案:
空回滚: Cancel先执行,Try还没执行
- 解决: Cancel时检查是否有Try记录,没有则插入一条Cancel记录
悬挂: Cancel先执行了,Try后执行
- 解决: Try时检查是否已Cancel,已Cancel则拒绝Try
// 空回滚处理
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
if (mapper.selectByXid(xid) == null) {
// 没有Try记录,插入Cancel记录
insertCancelRecord(xid);
return true;
}
// 正常Cancel逻辑
}
// 悬挂处理
public boolean try(BusinessActionContext context) {
String xid = context.getXid();
Record record = mapper.selectByXid(xid);
if (record != null && record.getStatus() == Status.CANCELED) {
throw new RuntimeException("已Cancel,不允许Try");
}
// 正常Try逻辑
}
Q10: 分布式事务的性能如何优化?
参考答案:
- 选择合适的模式: 普通业务用AT,高性能用TCC
- 减少事务范围: 只将必要操作纳入事务
- 异步提交: 使用Seata的异步提交
- 优化SQL: 减少锁的持有时间
- 合并请求: 批量处理减少RPC调用
- 缓存优化: 减少数据库访问
// 减少事务范围
public void placeOrder(OrderRequest request) {
// 不在事务中的操作
validateRequest(request);
calculatePrice(request);
// 在事务中的核心操作
doPlaceOrder(request);
// 不在事务中的操作
sendNotification(request);
}
最佳实践与避坑指南
最佳实践
1. 选择合适的事务模式
模式选择决策树:
是否需要强一致性?
├── 是 → 使用XA模式
└── 否 → 是否高并发场景?
├── 是 → 使用TCC模式
└── 否 → 是否长事务?
├── 是 → 使用SAGA模式
└── 否 → 使用AT模式(推荐)
2. 合理设置超时时间
// ❌ 不合理: 超时时间太长
@GlobalTransactional(timeoutMills = 300000) // 5分钟
// ✅ 合理: 根据业务设置
@GlobalTransactional(timeoutMills = 60000) // 1分钟
3. 幂等性设计
// ✅ 所有分布式事务接口都要支持幂等
@Service
public class InventoryService {
public void deduct(String productId, Integer count) {
// 使用数据库唯一索引保证幂等
// 或使用Redis分布式锁
// 或使用状态机记录
}
}
4. 异常处理
@GlobalTransactional(rollbackFor = Exception.class)
public void placeOrder(OrderRequest request) {
try {
// 业务逻辑
} catch (BusinessException e) {
// 业务异常,回滚
throw e;
} catch (Exception e) {
// 系统异常,回滚
log.error("下单失败", e);
throw e;
}
}
5. 监控告警
@Component
public class TransactionMonitor {
/**
* 监控分布式事务的执行情况
*/
public void monitor(GlobalTransactionContext context) {
// 记录事务执行时间
// 记录事务成功率
// 记录回滚原因
// 异常情况告警
}
}
避坑指南
1. 避免大事务
// ❌ 错误: 事务中包含太多操作
@GlobalTransactional
public void badBigTransaction() {
operation1();
operation2();
operation3();
// ... 几十个操作
operation20();
}
// ✅ 正确: 拆分成多个小事务
public void goodSmallTransaction() {
subTransaction1();
subTransaction2();
subTransaction3();
}
2. 避免跨库JOIN
// ❌ 错误: 在分布式事务中跨库查询
@GlobalTransactional
public void badQuery() {
List<Data> data = repository.crossDbJoin();
}
// ✅ 正确: 在应用层组装数据
public void goodQuery() {
Data1 data1 = service1.query();
Data2 data2 = service2.query();
// 在应用层组装
}
3. 避免在事务中调用外部服务
// ❌ 错误: 在事务中调用第三方API
@GlobalTransactional
public void badExternalCall() {
orderMapper.insert(order);
thirdPartyPaymentService.pay(); // 可能很慢或超时
}
// ✅ 正确: 将外部调用放在事务外
public void goodExternalCall() {
createOrder();
paymentService.pay(); // 事务外调用
}
4. 避免忽略回滚日志
// ❌ 错误: 没有创建undo_log表
// AT模式会失败
// ✅ 正确: 每个数据库都创建undo_log表
CREATE TABLE `undo_log` (...);
5. 避免TCC实现不完整
// ❌ 错误: TCC只实现了Try,没有实现Confirm和Cancel
@LocalTCC
public interface BadTccService {
@TwoPhaseBusinessAction(
commitMethod = "confirm",
rollbackMethod = "cancel"
)
boolean tryMethod();
// 缺少confirm和cancel的实现
}
// ✅ 正确: 完整实现Try、Confirm、Cancel
@LocalTCC
public interface GoodTccService {
@TwoPhaseBusinessAction(
commitMethod = "confirm",
rollbackMethod = "cancel"
)
boolean tryMethod();
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
总结
分布式事务是微服务架构中的核心技术,Seata提供了AT、TCC、SAGA、XA四种模式,可以满足不同场景的需求:
学习路线:
├── 1. 理解分布式事务的基础理论
│ ├── CAP定理
│ ├── BASE理论
│ └── 一致性模型
│
├── 2. 掌握Seata的基本使用
│ ├── 安装部署Seata Server
│ ├── 配置数据源代理
│ └── 创建undo_log表
│
├── 3. 深入AT模式
│ ├── 理解两阶段提交原理
│ ├── 掌握undo_log机制
│ └── 实际项目应用
│
├── 4. 学习TCC模式(进阶)
│ ├── 理解Try-Confirm-Cancel
│ ├── 掌握幂等性设计
│ ├── 解决空回滚和悬挂
│ └── 高并发场景应用
│
├── 5. 了解SAGA模式(高级)
│ ├── 理解状态机
│ ├── 定义业务流程
│ └── 长事务场景应用
│
└── 6. 生产实践
├── 性能优化
├── 监控告警
└── 问题排查
核心要点:
- 根据场景选择合适的模式: AT最简单,TCC性能最好,SAGA适合长事务
- 所有接口都要幂等: 分布式环境中重试是常态
- 合理设置超时: 避免长时间占用锁
- 完善的监控: 及时发现和处理问题
- 充分的测试: 各种异常场景都要测试
掌握分布式事务,能够帮助你在微服务架构中构建可靠、一致的业务系统!