面试官:你用过Seata吗?AT、TCC、Saga模式有什么区别?
候选人:用过AT模式...
面试官:为什么选AT而不是TCC?
候选人:😰💦(因为...简单?)
别慌!今天我们深入剖析Seata的三大模式,让你做选择时胸有成竹!
🎬 开篇:Seata是什么?
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务解决方案。
Seata = 一站式分布式事务解决方案
支持模式:
- AT模式(自动补偿)⭐⭐⭐⭐⭐ 最常用
- TCC模式(手动补偿)⭐⭐⭐⭐
- Saga模式(长事务)⭐⭐⭐
- XA模式(传统2PC)⭐⭐
架构图
客户端应用
│
┌─────────┼─────────┐
│ │ │
服务A 服务B 服务C
(RM) (RM) (RM)
│ │ │
└─────────┼─────────┘
│
Seata Server
(TC)
TC:Transaction Coordinator(事务协调者)
TM:Transaction Manager(事务管理器)
RM:Resource Manager(资源管理器)
🌟 第一章:AT模式 - 自动挡汽车(最简单!)
核心理念:自动补偿,业务无侵入
AT模式流程
阶段1:业务数据和回滚日志记录在同一个本地事务中提交
(自动记录前后快照)
↓
阶段2:提交异步化,非常快
成功:删除快照
失败:用快照自动回滚
🎭 生活比喻:自动驾驶汽车
你:我要去超市买东西(开启全局事务)
自动驾驶系统:
1. 记录你的出发点(前镜像)
2. 开车去超市买东西(执行业务)
3. 记录购物后的状态(后镜像)
4. 如果出问题,自动开回原点(自动回滚)
你完全不用关心怎么回滚,系统自动帮你搞定!✨
💻 代码示例
// 订单服务
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockFeignClient stockClient;
@Autowired
private AccountFeignClient accountClient;
/**
* AT模式:只需要加一个注解!
*/
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 1. 创建订单(本地事务)
Order order = new Order();
order.setUserId(dto.getUserId());
order.setAmount(dto.getAmount());
orderMapper.insert(order); // Seata自动记录快照
// 2. 远程调用:扣减库存
stockClient.deduct(dto.getProductId(), dto.getQuantity());
// 3. 远程调用:扣减余额
accountClient.deduct(dto.getUserId(), dto.getAmount());
// 如果任何一步失败,Seata自动回滚所有操作!
}
}
// 库存服务(被调用方也不需要特殊代码)
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
@Transactional // 普通的本地事务
public void deduct(Long productId, Integer quantity) {
Stock stock = stockMapper.selectById(productId);
if (stock.getQuantity() < quantity) {
throw new StockNotEnoughException(); // 异常会触发全局回滚
}
stock.setQuantity(stock.getQuantity() - quantity);
stockMapper.updateById(stock); // Seata自动记录快照
}
}
🔍 AT模式的工作原理
第一阶段:执行
-- 原始业务SQL
UPDATE stock SET quantity = quantity - 1 WHERE id = 1;
-- Seata在背后做的事:
-- 1. 查询前镜像(Before Image)
SELECT id, quantity FROM stock WHERE id = 1;
-- 结果:{id: 1, quantity: 100}
-- 2. 执行业务SQL
UPDATE stock SET quantity = quantity - 1 WHERE id = 1;
-- 3. 查询后镜像(After Image)
SELECT id, quantity FROM stock WHERE id = 1;
-- 结果:{id: 1, quantity: 99}
-- 4. 插入回滚日志
INSERT INTO undo_log (
branch_id, xid,
before_image, after_image,
rollback_info
) VALUES (
'2001', 'global-tx-001',
'{id: 1, quantity: 100}', -- 前镜像
'{id: 1, quantity: 99}', -- 后镜像
'...'
);
-- 5. 提交本地事务(业务数据和undo_log一起提交)
COMMIT;
第二阶段:提交/回滚
// 成功场景:
TC向所有RM发送commit请求
→ RM删除undo_log
→ 完成
// 失败场景:
TC向所有RM发送rollback请求
→ RM根据undo_log的before_image恢复数据
→ 删除undo_log
→ 回滚完成
⚖️ AT模式的优缺点
✅ 优点
-
业务无侵入 👍👍👍
// 只需要加一个注解,不用改业务代码! @GlobalTransactional public void business() { // 原有的业务代码 } -
自动补偿
- 不需要写回滚逻辑
- Seata自动生成反向SQL
-
性能较好
- 第一阶段直接提交,释放锁
- 第二阶段异步化
❌ 缺点
-
只支持关系型数据库(MySQL、Oracle等)
- 不支持NoSQL(Redis、MongoDB)
-
有脏读风险
时间线: T1: 事务A修改库存100→99(本地提交) T2: 事务B读到库存99(脏读!) T3: 事务A全局回滚,库存恢复100 T4: 事务B基于99做决策(错误!) -
对SQL有一定要求
- WHERE条件必须包含主键或唯一索引
- 不支持多表JOIN的UPDATE
📊 适用场景
✅ 适合:
- 基于关系型数据库的微服务
- 业务代码不想大改的项目
- 对性能有一定要求
- 能接受脏读风险
❌ 不适合:
- 使用NoSQL数据库
- 必须避免脏读
- SQL太复杂(大量JOIN)
💪 第二章:TCC模式 - 手动挡汽车(可控但复杂)
核心理念:手动补偿,完全可控
TCC = Try - Confirm - Cancel
Try: 尝试执行,预留资源(冻结)
Confirm:确认执行,使用资源(扣减)
Cancel: 取消执行,释放资源(解冻)
🎭 生活比喻:网购下单
Try阶段:
你:下单买手机
商家:给你保留一台(库存100→99,冻结1)
Confirm阶段(支付成功):
商家:手机发货给你(真正扣减冻结的库存)
Cancel阶段(支付失败):
商家:释放保留的手机(解冻,库存恢复100)
💻 代码示例
// 库存服务的TCC实现
@LocalTCC
public interface StockTccService {
/**
* Try:冻结库存
* @param productId 商品ID
* @param quantity 数量
* @param businessKey 业务键(用于幂等)
*/
@TwoPhaseBusinessAction(
name = "stockTccService",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
boolean freeze(
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity,
@BusinessActionContextParameter(paramName = "businessKey") String businessKey
);
/**
* Confirm:确认扣减库存
*/
boolean confirm(BusinessActionContext context);
/**
* Cancel:取消,释放冻结的库存
*/
boolean cancel(BusinessActionContext context);
}
// TCC实现类
@Service
public class StockTccServiceImpl implements StockTccService {
@Autowired
private StockMapper stockMapper;
@Autowired
private FreezeLockMapper freezeLockMapper;
/**
* Try阶段:冻结库存
*/
@Override
@Transactional
public boolean freeze(Long productId, Integer quantity, String businessKey) {
// 幂等判断:检查是否已经冻结过
FreezeLock existLock = freezeLockMapper.selectByBusinessKey(businessKey);
if (existLock != null) {
return true; // 已经冻结过,返回成功
}
// 1. 检查库存是否充足
Stock stock = stockMapper.selectById(productId);
int available = stock.getQuantity() - stock.getFrozen();
if (available < quantity) {
throw new StockNotEnoughException("库存不足");
}
// 2. 冻结库存(不减总库存,只增加冻结数)
stock.setFrozen(stock.getFrozen() + quantity);
stockMapper.updateById(stock);
// 3. 记录冻结记录(用于Confirm和Cancel)
FreezeLock lock = new FreezeLock();
lock.setBusinessKey(businessKey);
lock.setProductId(productId);
lock.setQuantity(quantity);
lock.setStatus("FROZEN");
freezeLockMapper.insert(lock);
return true;
}
/**
* Confirm阶段:真正扣减库存
*/
@Override
@Transactional
public boolean confirm(BusinessActionContext context) {
String businessKey = context.getActionContext("businessKey").toString();
// 幂等判断
FreezeLock lock = freezeLockMapper.selectByBusinessKey(businessKey);
if (lock == null) {
return true; // 已经处理过
}
if ("CONFIRMED".equals(lock.getStatus())) {
return true; // 已经confirm过
}
// 1. 真正扣减库存
Long productId = lock.getProductId();
Integer quantity = lock.getQuantity();
Stock stock = stockMapper.selectById(productId);
stock.setQuantity(stock.getQuantity() - quantity); // 减总库存
stock.setFrozen(stock.getFrozen() - quantity); // 减冻结数
stockMapper.updateById(stock);
// 2. 更新冻结记录状态
lock.setStatus("CONFIRMED");
freezeLockMapper.updateById(lock);
return true;
}
/**
* Cancel阶段:释放冻结的库存
*/
@Override
@Transactional
public boolean cancel(BusinessActionContext context) {
String businessKey = context.getActionContext("businessKey").toString();
// 幂等判断
FreezeLock lock = freezeLockMapper.selectByBusinessKey(businessKey);
if (lock == null) {
return true; // 空回滚:Try还没执行,Cancel先来了
}
if ("CANCELLED".equals(lock.getStatus())) {
return true; // 已经cancel过
}
// 1. 释放冻结库存
Long productId = lock.getProductId();
Integer quantity = lock.getQuantity();
Stock stock = stockMapper.selectById(productId);
stock.setFrozen(stock.getFrozen() - quantity); // 减冻结数(总库存不变)
stockMapper.updateById(stock);
// 2. 更新冻结记录状态
lock.setStatus("CANCELLED");
freezeLockMapper.updateById(lock);
return true;
}
}
// 业务调用
@Service
public class OrderService {
@Autowired
private StockTccService stockTccService;
@Autowired
private AccountTccService accountTccService;
@GlobalTransactional
public void createOrder(OrderDTO dto) {
String businessKey = UUID.randomUUID().toString();
// 调用TCC的Try方法
stockTccService.freeze(dto.getProductId(), dto.getQuantity(), businessKey);
accountTccService.freeze(dto.getUserId(), dto.getAmount(), businessKey);
// Seata会自动调用Confirm或Cancel
}
}
🛡️ TCC三大难题及解决方案
1️⃣ 空回滚问题
问题:Cancel在Try之前到达
时间线:
T1: 网络延迟,Try请求未到达
T2: 全局事务超时,TC发送Cancel请求
T3: Cancel执行(但Try还没执行!)
T4: Try请求终于到达
→ 结果:资源被冻结,永远释放不了!
解决:
@Override
public boolean cancel(BusinessActionContext context) {
String businessKey = context.getActionContext("businessKey").toString();
FreezeLock lock = freezeLockMapper.selectByBusinessKey(businessKey);
if (lock == null) {
// 空回滚:记录一条CANCELLED状态的记录,防止后续Try执行
FreezeLock emptyLock = new FreezeLock();
emptyLock.setBusinessKey(businessKey);
emptyLock.setStatus("CANCELLED");
freezeLockMapper.insert(emptyLock);
return true;
}
// 正常回滚逻辑...
}
2️⃣ 幂等问题
问题:网络重试导致重复调用
T1: Try执行成功,冻结库存
T2: 网络超时,Seata重试
T3: Try又执行一次(重复冻结!)
解决:
@Override
public boolean freeze(Long productId, Integer quantity, String businessKey) {
// 检查businessKey是否已存在
FreezeLock existLock = freezeLockMapper.selectByBusinessKey(businessKey);
if (existLock != null) {
return true; // 已经执行过,直接返回成功
}
// 正常冻结逻辑...
}
3️⃣ 悬挂问题
问题:Try在Cancel之后到达
时间线:
T1: Try请求网络延迟
T2: 全局事务超时,Cancel执行(空回滚)
T3: Try请求终于到达,执行成功
→ 结果:资源被冻结,但事务已经结束,永远无法释放!
解决:
@Override
public boolean freeze(Long productId, Integer quantity, String businessKey) {
FreezeLock lock = freezeLockMapper.selectByBusinessKey(businessKey);
// 检查是否已经Cancel过(悬挂检测)
if (lock != null && "CANCELLED".equals(lock.getStatus())) {
return false; // 拒绝执行Try
}
// 正常冻结逻辑...
}
⚖️ TCC模式的优缺点
✅ 优点
-
性能最好
- Try阶段提交,不长时间锁定
- 无需记录回滚日志
-
完全可控
- 自己写补偿逻辑
- 可以支持NoSQL
-
无脏读问题
- 其他事务读到的是冻结前的数据
❌ 缺点
-
开发成本高 👎👎👎
- 每个操作都要写Try、Confirm、Cancel
- 要处理空回滚、幂等、悬挂
-
数据库设计复杂
- 需要冻结字段
- 需要中间状态表
-
业务侵入性强
- 必须修改业务代码
📊 适用场景
✅ 适合:
- 对性能要求极高
- 涉及NoSQL数据库
- 必须避免脏读
- 愿意付出高开发成本
❌ 不适合:
- 快速开发的项目
- 团队技术能力一般
- 业务逻辑频繁变化
🌊 第三章:Saga模式 - 长途旅行(适合长流程)
核心理念:正向服务+补偿服务
Saga = 一系列本地事务 + 补偿事务
正向:T1 → T2 → T3 → 成功
失败:T1 → T2 → T3失败 → C3 → C2 → C1
💻 代码示例(状态机模式)
// 定义Saga状态机(JSON格式)
{
"Name": "OrderSaga",
"StartState": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "create",
"CompensateState": "CancelOrder",
"Next": "DeductStock"
},
"DeductStock": {
"Type": "ServiceTask",
"ServiceName": "stockService",
"ServiceMethod": "deduct",
"CompensateState": "RestoreStock",
"Next": "DeductAccount"
},
"DeductAccount": {
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "deduct",
"CompensateState": "RestoreAccount",
"Next": "Succeed"
},
"Succeed": {
"Type": "Succeed"
},
"CancelOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "cancel"
},
"RestoreStock": {
"Type": "ServiceTask",
"ServiceName": "stockService",
"ServiceMethod": "restore"
},
"RestoreAccount": {
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "restore"
}
}
}
// 订单服务
@Service
public class OrderService {
/**
* 正向服务:创建订单
*/
@Transactional
public boolean create(BusinessActionContext context) {
OrderDTO dto = (OrderDTO) context.getActionContext("dto");
Order order = new Order();
order.setUserId(dto.getUserId());
order.setAmount(dto.getAmount());
order.setStatus(OrderStatus.CREATED);
orderMapper.insert(order);
return true;
}
/**
* 补偿服务:取消订单
*/
@Transactional
public boolean cancel(BusinessActionContext context) {
String orderId = context.getActionContext("orderId").toString();
Order order = orderMapper.selectById(orderId);
order.setStatus(OrderStatus.CANCELLED);
orderMapper.updateById(order);
return true;
}
}
// 业务调用
@Service
public class BusinessService {
@Autowired
private StateMachineEngine stateMachineEngine;
public void createOrder(OrderDTO dto) {
Map<String, Object> context = new HashMap<>();
context.put("dto", dto);
// 启动Saga状态机
StateMachineInstance instance = stateMachineEngine.start(
"OrderSaga",
null,
context
);
if (ExecutionStatus.SU.equals(instance.getStatus())) {
System.out.println("订单创建成功!");
} else {
System.out.println("订单创建失败,已补偿!");
}
}
}
⚖️ Saga模式的优缺点
✅ 优点
-
适合长流程
- 可以跨越很长时间
- 不长时间锁定资源
-
业务侵入性中等
- 只需要写补偿方法
- 比TCC简单很多
❌ 缺点
-
只能保证最终一致性
- 中间状态可能被看到
-
补偿逻辑可能很复杂
- 某些操作难以补偿(如发送短信)
📊 适用场景
✅ 适合:
- 长流程业务(订单流程、工单流程)
- 对一致性要求不那么严格
- 需要异步处理
❌ 不适合:
- 短流程
- 需要强一致性
🎯 第四章:三种模式全方位对比
| 维度 | AT模式 | TCC模式 | Saga模式 |
|---|---|---|---|
| 一致性 | 最终一致 | 强一致 | 最终一致 |
| 隔离性 | 读未提交(脏读) | 读已提交 | 读未提交 |
| 性能 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 业务侵入 | ⭐(很小) | ⭐⭐⭐⭐⭐(很大) | ⭐⭐⭐(中等) |
| 开发难度 | ⭐(很简单) | ⭐⭐⭐⭐⭐(很难) | ⭐⭐⭐(中等) |
| 适用数据库 | 仅关系型 | 全部 | 全部 |
| 锁定资源 | 短时间 | 短时间 | 不锁定 |
| 适用场景 | 常规微服务 | 金融核心 | 长流程 |
🎮 选型决策树
graph TD
A[开始选型] --> B{使用NoSQL吗?}
B -->|是| C{长流程?}
B -->|否| D{能接受脏读吗?}
C -->|是| E[选择Saga]
C -->|否| F[选择TCC]
D -->|是| G{追求开发效率?}
D -->|否| H[选择TCC]
G -->|是| I[选择AT]
G -->|否| J[选择TCC]
🎯 推荐选择
| 场景 | 推荐模式 | 理由 |
|---|---|---|
| 电商下单 | AT | 流程短,用MySQL,追求开发效率 |
| 银行转账 | TCC | 必须强一致,不能脏读 |
| 订单审批流 | Saga | 长流程,涉及多个审批节点 |
| 积分系统 | AT | 允许最终一致,快速开发 |
| 库存扣减(Redis) | TCC | 使用NoSQL |
| 工单系统 | Saga | 长流程,多状态流转 |
💼 第五章:实际项目经验分享
案例1:电商项目的混合使用
@Service
public class OrderBusinessService {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService; // MySQL,使用AT
@Autowired
private AccountService accountService; // MySQL,使用AT
@Autowired
private RedisStockService redisStock; // Redis,使用TCC
@GlobalTransactional
public void createOrder(OrderDTO dto) {
// 1. 创建订单(AT模式,自动补偿)
orderService.create(dto);
// 2. 扣减MySQL库存(AT模式)
stockService.deduct(dto.getProductId(), dto.getQuantity());
// 3. 扣减Redis缓存库存(TCC模式)
redisStock.tryFreeze(dto.getProductId(), dto.getQuantity());
// 4. 扣减账户余额(AT模式)
accountService.deduct(dto.getUserId(), dto.getAmount());
// 混合使用AT和TCC,发挥各自优势!
}
}
案例2:性能优化
// 优化前:全部使用AT模式
@GlobalTransactional
public void createOrder(OrderDTO dto) {
orderService.create(dto); // 需要锁行
stockService.deduct(dto); // 需要锁行
accountService.deduct(dto); // 需要锁行
pointsService.add(dto); // 需要锁行
// 全部在一个分布式事务中,性能差
}
// 优化后:核心用AT,非核心用消息
@GlobalTransactional
public void createOrder(OrderDTO dto) {
// 核心流程:订单、库存、支付(AT模式)
orderService.create(dto);
stockService.deduct(dto);
accountService.deduct(dto);
// 非核心流程:异步处理(消息队列)
rabbitTemplate.send("points.add", dto); // 积分
rabbitTemplate.send("coupon.send", dto); // 优惠券
rabbitTemplate.send("notify.send", dto); // 通知
}
🎓 第六章:面试高分回答
问题:Seata的AT、TCC、Saga怎么选?
标准回答:
"这三种模式各有优劣,需要根据实际场景选择:
AT模式是我们项目中用得最多的,因为:
- 业务代码几乎不用改,只需加@GlobalTransactional
- Seata自动生成回滚日志,自动补偿
- 性能也不错,因为第二阶段是异步的
但AT模式只支持关系型数据库,而且有脏读风险。
TCC模式我们在Redis库存扣减时使用,因为:
- AT不支持NoSQL
- TCC可以自己控制冻结逻辑
- 性能最好,无脏读问题
缺点是开发成本高,需要写Try、Confirm、Cancel,还要处理空回滚、幂等、悬挂。
Saga模式适合长流程,比如工单审批系统:
- 流程可能跨越几天
- 不能长时间锁定资源
- 通过状态机管理流程
总结:常规场景用AT,NoSQL用TCC,长流程用Saga。"
常见追问
Q1:AT模式的脏读怎么解决?
A:有几种方案:
1. @GlobalLock + SELECT FOR UPDATE:在查询时加全局锁
2. 业务设计上避免:比如库存扣减后立即释放,其他事务读到的就是准确的
3. 使用TCC模式替代
Q2:TCC的空回滚、幂等、悬挂具体怎么实现?
A:通过中间状态表:
1. 空回滚:Cancel时检查Try是否执行过,没执行就插入CANCELLED状态
2. 幂等:Try/Confirm/Cancel都先检查businessKey是否已处理过
3. 悬挂:Try时检查是否已经Cancel过,如果是就拒绝执行
🎁 总结:一句话记住
- AT模式:自动挡,省心但只能走平路(MySQL)
- TCC模式:手动挡,累但能走各种路(NoSQL)
- Saga模式:长途客车,适合长途旅行(长流程)
📚 扩展资源
- 官方文档:seata.io/zh-cn/
- 源码地址:github.com/seata/seata
- 最佳实践:Seata官方示例项目
记住:没有最好的模式,只有最合适的选择!🎯
祝你面试顺利!💪✨