Seata 分布式事务原理与实战
·
Seata 分布式事务原理与实战
作者:薪火铺子(薪火铺子)
核心要点
分布式事务是微服务架构中的难点:
- 需要理解 CAP 定理和 BASE 理论
- 需要掌握常见的分布式事务解决方案(2PC、TCC、Saga)
- 需要了解 Seata AT 模式的工作机制
本文将深入剖析分布式事务的原理和 Seata 的实现,帮助你理解如何保证跨服务的数据一致性。
一、核心问题:本地事务为什么不够用了?
先看一个经典场景:
本地事务的局限:
// 本地事务:只能保证同一个数据库内的操作
@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 故障场景
2.2 根因分析
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 定理
分布式系统中的选择:
| 选择 | 说明 | 框架 |
|---|---|---|
| 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 三大模式详解
| 模式 | 原理 | 侵入性 | 适用场景 |
|---|---|---|---|
| AT | 自动生成 undo log | 无 | 关系型数据库 |
| TCC | Try-Confirm-Cancel | 有 | Redis/MongoDB |
| Saga | 正向 + 补偿 | 有 | 长流程、审批流 |
五、AT 模式:零侵入的分布式事务
5.1 AT 模式原理
5.2 AT 模式执行流程
5.3 全局锁机制
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 三阶段原理
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 模式原理
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 选择
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 账户转账 | 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; // 已处理,直接返回
}
十二、核心要点总结
更多推荐




所有评论(0)