面试官:分布式事务怎么解决?
候选人:用2PC啊!
面试官:2PC有什么问题?TCC、Saga了解吗?
候选人:😰💦(内心OS:完了,只知道个名字...)
别慌!今天我们用最生动的方式,把分布式事务的四大门派讲得明明白白!
🎬 开篇:为什么需要分布式事务?
单体应用时代(一个人说了算)
下单 → 减库存 → 扣款 → 增积分
↓
一个数据库,一个事务,简单!
@Transactional 搞定! ✅
微服务时代(众人拾柴火焰高,但也容易扯皮)
订单服务(DB1) → 创建订单
库存服务(DB2) → 减库存
支付服务(DB3) → 扣款
积分服务(DB4) → 加积分
问题:如果扣款成功,但加积分失败了怎么办?😱
核心痛点:不同服务使用不同数据库,无法用传统的ACID事务保证一致性!
🏛️ 第一章:2PC(两阶段提交)- 严肃的包办婚姻
原理:分成"准备"和"提交"两个阶段
协调者(婚姻介绍所)
│
┌─────────────┼─────────────┐
│ │ │
服务A 服务B 服务C
(新郎家) (新娘家) (酒店)
🎭 生活比喻:包办婚姻的流程
阶段一:准备阶段(Prepare - 订婚)
婚姻介绍所:各位,咱们准备办婚礼了,你们准备好了吗?
新郎家:✅ 彩礼准备好了,可以!
新娘家:✅ 嫁妆准备好了,可以!
酒店:✅ 婚宴场地订好了,可以!
→ 所有人都准备好了,但还没真正行动
阶段二:提交阶段(Commit - 正式结婚)
婚姻介绍所:好!大家都准备好了,正式开始吧!
新郎家:💰 交彩礼
新娘家:📦 送嫁妆
酒店:🍽️ 开席
→ 所有人同时行动,婚礼成功!✨
💻 代码示例
// 协调者
@Service
public class TransactionCoordinator {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
@Autowired
private PaymentService paymentService;
public void execute() {
String txId = UUID.randomUUID().toString();
try {
// 阶段一:准备阶段(向所有参与者发送prepare请求)
boolean orderPrepared = orderService.prepare(txId);
boolean stockPrepared = stockService.prepare(txId);
boolean paymentPrepared = paymentService.prepare(txId);
// 如果所有服务都准备好了
if (orderPrepared && stockPrepared && paymentPrepared) {
// 阶段二:提交阶段
orderService.commit(txId);
stockService.commit(txId);
paymentService.commit(txId);
System.out.println("事务提交成功!✅");
} else {
// 有任何一个服务准备失败,全部回滚
orderService.rollback(txId);
stockService.rollback(txId);
paymentService.rollback(txId);
System.out.println("事务回滚!❌");
}
} catch (Exception e) {
// 异常时全部回滚
orderService.rollback(txId);
stockService.rollback(txId);
paymentService.rollback(txId);
}
}
}
// 参与者
@Service
public class OrderService {
private Map<String, Order> preparedOrders = new ConcurrentHashMap<>();
public boolean prepare(String txId) {
try {
// 准备创建订单,但不真正提交到数据库
Order order = new Order();
preparedOrders.put(txId, order);
return true; // 准备成功
} catch (Exception e) {
return false; // 准备失败
}
}
public void commit(String txId) {
Order order = preparedOrders.get(txId);
orderRepository.save(order); // 真正提交
preparedOrders.remove(txId);
}
public void rollback(String txId) {
preparedOrders.remove(txId); // 清除准备的数据
}
}
⚖️ 优缺点
✅ 优点
- 强一致性:要么全成功,要么全失败
- 实现相对简单:逻辑清晰,容易理解
❌ 缺点
- 同步阻塞:准备阶段所有参与者都要等待,性能差
- 单点故障:协调者挂了,整个系统瘫痪
- 数据不一致风险:如果第二阶段网络分区,部分提交部分未提交
- 资源锁定时间长:prepare到commit期间,资源一直被锁定
性能评分:⭐⭐ (2/5)
一致性评分:⭐⭐⭐⭐ (4/5)
适用场景:低并发、强一致性要求
🎯 第二章:3PC(三阶段提交)- 改进版包办婚姻
原理:在2PC基础上增加"预询问"阶段,引入超时机制
阶段1:CanCommit (能不能办?)
阶段2:PreCommit (准备办!)
阶段3:DoCommit (正式办!)
🎭 生活比喻:更谨慎的婚礼筹备
阶段一:CanCommit(预询问)
婚姻介绍所:各位,咱们打算办婚礼,你们有没有档期?
新郎家:有档期,可以考虑 ✅
新娘家:有档期,可以考虑 ✅
酒店:有档期,可以考虑 ✅
→ 只是询问意向,不锁定资源
阶段二:PreCommit(预提交)
婚姻介绍所:好,那大家开始准备吧!
新郎家:✅ 彩礼准备好了,锁定!
新娘家:✅ 嫁妆准备好了,锁定!
酒店:✅ 场地预定了,锁定!
→ 锁定资源,但还没真正执行
阶段三:DoCommit(提交)
婚姻介绍所:正式开始!
(后续流程与2PC的commit阶段相同)
⚖️ 相比2PC的改进
- 增加了预询问阶段:减少不必要的资源锁定
- 引入超时机制:
- 协调者超时:参与者自动提交(optimistic)
- 参与者超时:自动中断事务
❌ 但仍然存在的问题
- 复杂度增加:多了一个阶段
- 网络分区时仍可能不一致:超时自动提交可能导致问题
性能评分:⭐⭐⭐ (3/5)
一致性评分:⭐⭐⭐ (3/5)
适用场景:中等并发、相对较强一致性要求
结论:实际应用很少,因为更复杂但问题没完全解决!
💪 第三章:TCC(Try-Confirm-Cancel)- 自由恋爱模式
原理:业务层面的补偿型事务
- Try:尝试执行,预留资源
- Confirm:确认执行,使用预留资源
- Cancel:取消执行,释放预留资源
🎭 生活比喻:租房的故事
Try阶段(看房预定)
你:老板,这房子我想租,先给我留着!
房东:好的,给你保留3天,但要交500元定金
银行:冻结你的账户里500元(不扣,只是冻结)
→ 预留资源,但没真正扣款
Confirm阶段(正式租房)
你:好,我决定租了!
房东:好的,合同签了,房子给你!
银行:把之前冻结的500元扣掉,转给房东
→ 真正执行业务,扣除预留资源
Cancel阶段(不租了)
你:不好意思,我不租了
房东:好吧,房子重新出租
银行:解冻你的500元,归还给你
→ 释放预留资源,回滚
💻 代码示例
// TCC订单服务
@Service
public class OrderTccService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private AccountTccService accountService;
@Autowired
private StockTccService stockService;
/**
* Try阶段:创建订单,冻结库存和余额
*/
@Transactional
public boolean tryCreateOrder(OrderDTO dto) {
try {
// 1. 创建订单,状态为"进行中"
Order order = new Order();
order.setStatus(OrderStatus.TRY);
order.setAmount(dto.getAmount());
orderRepository.save(order);
// 2. 冻结库存(不减库存,只是标记冻结)
boolean stockFrozen = stockService.tryFreeze(
dto.getProductId(),
dto.getQuantity(),
order.getId()
);
// 3. 冻结账户余额(不扣款,只是冻结)
boolean balanceFrozen = accountService.tryFreeze(
dto.getUserId(),
dto.getAmount(),
order.getId()
);
return stockFrozen && balanceFrozen;
} catch (Exception e) {
return false;
}
}
/**
* Confirm阶段:确认订单,真正扣减库存和余额
*/
@Transactional
public void confirmCreateOrder(String orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
// 1. 更新订单状态为"成功"
order.setStatus(OrderStatus.SUCCESS);
orderRepository.save(order);
// 2. 真正扣减库存(将冻结的库存扣掉)
stockService.confirm(orderId);
// 3. 真正扣款(将冻结的余额扣掉)
accountService.confirm(orderId);
}
/**
* Cancel阶段:取消订单,释放冻结的资源
*/
@Transactional
public void cancelCreateOrder(String orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
// 1. 更新订单状态为"取消"
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
// 2. 释放冻结的库存
stockService.cancel(orderId);
// 3. 释放冻结的余额
accountService.cancel(orderId);
}
}
// 账户TCC服务
@Service
public class AccountTccService {
@Autowired
private AccountRepository accountRepository;
@Autowired
private FreezeRecordRepository freezeRecordRepository;
/**
* Try:冻结金额
*/
@Transactional
public boolean tryFreeze(String userId, BigDecimal amount, String orderId) {
Account account = accountRepository.findByUserId(userId);
// 检查余额是否足够
if (account.getBalance().compareTo(amount) < 0) {
return false;
}
// 增加冻结金额
account.setFrozenAmount(account.getFrozenAmount().add(amount));
accountRepository.save(account);
// 记录冻结记录(用于Confirm和Cancel)
FreezeRecord record = new FreezeRecord();
record.setOrderId(orderId);
record.setUserId(userId);
record.setAmount(amount);
record.setStatus("FROZEN");
freezeRecordRepository.save(record);
return true;
}
/**
* Confirm:真正扣款
*/
@Transactional
public void confirm(String orderId) {
FreezeRecord record = freezeRecordRepository.findByOrderId(orderId);
Account account = accountRepository.findByUserId(record.getUserId());
// 减少余额
account.setBalance(account.getBalance().subtract(record.getAmount()));
// 减少冻结金额
account.setFrozenAmount(account.getFrozenAmount().subtract(record.getAmount()));
accountRepository.save(account);
// 更新记录状态
record.setStatus("CONFIRMED");
freezeRecordRepository.save(record);
}
/**
* Cancel:解冻金额
*/
@Transactional
public void cancel(String orderId) {
FreezeRecord record = freezeRecordRepository.findByOrderId(orderId);
Account account = accountRepository.findByUserId(record.getUserId());
// 减少冻结金额(归还)
account.setFrozenAmount(account.getFrozenAmount().subtract(record.getAmount()));
accountRepository.save(account);
// 更新记录状态
record.setStatus("CANCELLED");
freezeRecordRepository.save(record);
}
}
⚖️ 优缺点
✅ 优点
- 性能较好:不长时间锁定资源
- 不依赖资源管理器:业务层面实现,灵活性高
- 强一致性:通过补偿机制保证
❌ 缺点
- 业务侵入性强:需要为每个操作实现Try、Confirm、Cancel
- 开发成本高:要处理很多边界情况
- 数据库设计复杂:需要冻结字段、状态字段等
性能评分:⭐⭐⭐⭐ (4/5)
一致性评分:⭐⭐⭐⭐ (4/5)
开发难度:⭐⭐⭐⭐⭐ (5/5 - 很难!)
适用场景:金融支付、核心交易系统
典型应用:阿里巴巴Seata的TCC模式
🌊 第四章:Saga模式 - 长征式的分步提交
原理:将分布式事务拆分成多个本地事务,每个事务都有对应的补偿操作
正向流程:T1 → T2 → T3 → T4 → 成功!✅
失败回滚:T1 → T2 → T3失败 → C3 → C2 → C1 → 回滚完成
(做) (做) (失败) (补偿)(补偿)(补偿)
🎭 生活比喻:自助旅行
正向流程(一切顺利)
第1步:订机票 ✅
第2步:订酒店 ✅
第3步:订景点门票 ✅
第4步:订餐厅 ✅
→ 旅行愉快!🎉
补偿流程(中途出问题)
第1步:订机票 ✅
第2步:订酒店 ✅
第3步:订景点门票 ❌(门票卖完了!)
开始补偿:
第2步补偿:取消酒店预订 ✅
第1步补偿:退机票 ✅
→ 旅行取消,全部退款
💻 代码示例(基于消息队列)
// Saga协调器
@Service
public class OrderSagaOrchestrator {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
@Autowired
private PaymentService paymentService;
@Autowired
private PointsService pointsService;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 编排式Saga:由协调器控制流程
*/
public void executeOrderSaga(OrderDTO dto) {
String sagaId = UUID.randomUUID().toString();
try {
// Step 1: 创建订单
String orderId = orderService.createOrder(dto, sagaId);
// Step 2: 扣减库存
stockService.deductStock(dto.getProductId(), dto.getQuantity(), sagaId);
// Step 3: 扣款
paymentService.pay(dto.getUserId(), dto.getAmount(), sagaId);
// Step 4: 增加积分
pointsService.addPoints(dto.getUserId(), dto.getPoints(), sagaId);
// 全部成功
log.info("Saga事务成功!sagaId: {}", sagaId);
} catch (StockNotEnoughException e) {
// Step 2失败,补偿Step 1
log.error("库存不足,开始补偿...");
orderService.cancelOrder(orderId, sagaId);
} catch (PaymentFailedException e) {
// Step 3失败,补偿Step 2和Step 1
log.error("支付失败,开始补偿...");
stockService.returnStock(dto.getProductId(), dto.getQuantity(), sagaId);
orderService.cancelOrder(orderId, sagaId);
} catch (PointsException e) {
// Step 4失败,补偿Step 3、Step 2、Step 1
log.error("积分增加失败,开始补偿...");
paymentService.refund(dto.getUserId(), dto.getAmount(), sagaId);
stockService.returnStock(dto.getProductId(), dto.getQuantity(), sagaId);
orderService.cancelOrder(orderId, sagaId);
}
}
}
// 订单服务
@Service
public class OrderService {
/**
* 正向操作:创建订单
*/
@Transactional
public String createOrder(OrderDTO dto, String sagaId) {
Order order = new Order();
order.setSagaId(sagaId);
order.setStatus(OrderStatus.CREATED);
order.setAmount(dto.getAmount());
orderRepository.save(order);
log.info("订单创建成功:{}", order.getId());
return order.getId();
}
/**
* 补偿操作:取消订单
*/
@Transactional
public void cancelOrder(String orderId, String sagaId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
log.info("订单已取消(补偿):{}", orderId);
}
}
📊 Saga的两种实现方式
1️⃣ 编排式Saga (Orchestration)
Saga协调器
│
┌───────────┼───────────┐
↓ ↓ ↓
订单服务 库存服务 支付服务
协调器控制全流程,类似导演指挥演员
优点:流程集中管理,容易理解和监控
缺点:协调器是单点,耦合度高
2️⃣ 编舞式Saga (Choreography)
订单服务 → (发消息) → 库存服务 → (发消息) → 支付服务
↑ ↓
└──────────── (失败消息) ─────────────────┘
每个服务监听消息,自主决定下一步,类似接力赛
优点:服务解耦,没有单点
缺点:流程分散,难以追踪和调试
⚖️ 优缺点
✅ 优点
- 适合长事务:可以跨越很长时间(几分钟甚至几小时)
- 业务侵入性低:相比TCC简单很多
- 性能好:不长时间锁定资源
❌ 缺点
- 只能保证最终一致性:中间状态可能被看到
- 补偿逻辑复杂:需要为每个操作写补偿
- 难以调试:分布式流程追踪困难
性能评分:⭐⭐⭐⭐⭐ (5/5)
一致性评分:⭐⭐⭐ (3/5 - 最终一致)
开发难度:⭐⭐⭐ (3/5)
适用场景:长流程、对一致性要求不那么严格的场景
典型应用:微服务架构、复杂业务流程
📊 第五章:四种方案全方位对比
| 维度 | 2PC | 3PC | TCC | Saga |
|---|---|---|---|---|
| 一致性 | 强一致 | 强一致 | 强一致 | 最终一致 |
| 性能 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 复杂度 | 简单 | 复杂 | 非常复杂 | 中等 |
| 业务侵入 | 低 | 低 | 高 | 中 |
| 适用场景 | 低并发 | 很少用 | 金融交易 | 长流程 |
| 锁定资源 | 长时间 | 较长时间 | 短时间 | 不锁定 |
| 实现难度 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
🎯 选型决策树
开始选型
│
是否需要强一致性?
┌─────┴─────┐
是│ │否
│ │
并发量高吗? 选择Saga
┌───┴───┐ (最终一致)
高│ │低
│ │
选择TCC 选择2PC
(强一致 (强一致
高性能) 简单)
💼 第六章:实际项目选型建议
场景1:电商下单
/**
* 推荐方案:Saga(编排式)
*
* 理由:
* 1. 流程长(订单→库存→支付→积分→通知)
* 2. 允许最终一致性(用户可以接受积分延迟到账)
* 3. 高并发场景
*/
@Service
public class OrderSagaService {
public void createOrder(OrderDTO dto) {
// 1. 创建订单(立即成功)
Order order = orderService.create(dto);
// 2. 发送消息到下游服务(异步)
sagaOrchestrator.execute(order);
}
}
场景2:银行转账
/**
* 推荐方案:TCC
*
* 理由:
* 1. 强一致性要求(钱不能少也不能多)
* 2. 流程短
* 3. 可以接受较高开发成本
*/
@Service
public class TransferTccService {
public void transfer(String from, String to, BigDecimal amount) {
try {
// Try:冻结转出账户金额
accountService.tryFreeze(from, amount);
// Try:预增加转入账户金额
accountService.tryIncrease(to, amount);
// Confirm:真正扣款和加款
accountService.confirm(from, to, amount);
} catch (Exception e) {
// Cancel:回滚
accountService.cancel(from, to, amount);
}
}
}
场景3:配置管理系统(小流量)
/**
* 推荐方案:2PC
*
* 理由:
* 1. 并发量低
* 2. 需要强一致性
* 3. 实现简单,开发成本低
*/
@Service
public class ConfigSyncService {
@GlobalTransactional // 使用Seata的2PC
public void syncConfig(Config config) {
configService.save(config);
cacheService.update(config);
auditService.log(config);
}
}
🎓 第七章:面试高分回答模板
问题:你们项目中分布式事务怎么解决的?
标准回答(STAR法则):
S(背景):"我们是一个电商系统,下单流程涉及订单、库存、支付、积分四个微服务,每个服务独立数据库。"
T(任务):"需要保证这四个操作要么全成功,要么全失败,否则会出现扣款了但订单没创建的问题。"
A(行动):"我们采用了Saga模式:
- 对于核心的订单创建和支付,使用Seata的TCC模式,保证强一致性
- 对于非核心的积分增加、消息通知,使用消息队列实现最终一致性
- 设计了补偿机制:支付失败自动取消订单,库存不足自动退款"
R(结果):"上线后,99.9%的订单能在3秒内完成,异常情况通过补偿机制在1分钟内恢复一致性,大促期间支撑了100万+订单。"
常见追问
Q1:TCC的空回滚和悬挂问题怎么处理?
A:空回滚:Cancel在Try之前到达
→ 解决:记录Try是否执行过,Cancel时检查
悬挂:Try在Cancel之后到达
→ 解决:Cancel时记录状态,Try时检查Cancel是否已执行
Q2:Saga如何保证幂等性?
A:
1. 业务主键去重(订单号全局唯一)
2. 版本号机制(乐观锁)
3. 状态机(只允许特定状态转换)
4. 分布式锁(关键操作加锁)
🎁 总结:一句话记住四种方案
- 2PC:严肃的包办婚姻(强一致但慢)
- 3PC:改进版包办婚姻(多一个阶段,少用)
- TCC:自由恋爱需要预定(预留资源,性能好,代码多)
- Saga:长征式分步走(适合长流程,最终一致)
📚 实战框架推荐
- Seata(阿里开源):支持AT、TCC、Saga、XA四种模式
- ByteTCC:基于TCC的分布式事务框架
- Hmily:高性能TCC框架
- ServiceComb Pack:华为开源的Saga框架
记住:没有银弹,选择最适合业务场景的方案!🎯
面试加油!下一个offer就是你的!💪✨