第 14 章:分布式事务
学习目标
- 理解分布式事务的 CAP/BASE 理论
- 掌握 2PC、TCC、Saga、可靠消息四种方案
- 学会根据业务场景选择方案
- 避免分布式事务中的常见坑
一、CAP 定理
text
C - Consistency(一致性)
A - Availability(可用性)
P - Partition tolerance(分区容错)
分布式系统 P 必然存在,只能在 C 和 A 间取舍java
// 经典场景:下单 + 扣库存
@Transactional
public void placeOrder(OrderDto dto) {
// 1. 订单库 INSERT
orderDao.insert(dto);
// 2. 库存库 UPDATE
inventoryDao.deduct(dto.skuId, dto.quantity);
}
// 网络抖动:订单成功,库存失败 = 数据不一致二、BASE 理论
基本可用 + 软状态 + 最终一致,放弃强一致,换可用性。
text
强一致:任何时候查,所有人看到的数据一致
最终一致:经过一段时间后,数据一致适用场景:大多数业务(订单、库存、积分)
三、2PC(两阶段提交)
text
协调者(TC)
├─ prepare ──→ 参与者 1
├─ prepare ──→ 参与者 2
├─ prepare ──→ 参与者 3
│
├─ commit ──→ 参与者 1
├─ commit ──→ 参与者 2
└─ commit ──→ 参与者 3java
// Atomikos / Narayana 实现
@Atomikos
public void placeOrder(OrderDto dto) {
orderDao.insert(dto);
inventoryDao.deduct(dto.skuId, dto.quantity);
}text
优点:实现简单(声明式注解)
缺点:同步阻塞,协调者单点,数据不一致窗口⚠️ 坑 1:2PC 协调者挂了,参与者永远等不到 commit,锁死资源。高并发场景慎用。
四、TCC(补偿事务)
三个阶段:Try → Confirm → Cancel
java
// Try:预留资源
public class OrderTccAction {
@TwoPhaseBusinessAction(name = "orderTry")
public boolean tryOrder(OrderDto dto) {
// 订单状态 PENDING
orderDao.insertPending(dto);
return true;
}
@TwoPhaseBusinessAction(name = "orderConfirm")
public boolean confirmOrder(OrderDto dto) {
// 订单状态 CONFIRMED
orderDao.updateStatus(dto.id, "CONFIRMED");
return true;
}
@TwoPhaseBusinessAction(name = "orderCancel")
public boolean cancelOrder(OrderDto dto) {
// 订单状态 CANCELLED
orderDao.delete(dto.id);
return true;
}
}java
// 库存 TCC
public class InventoryTccAction {
public boolean tryFreeze(OrderDto dto) {
// 冻结库存:stock - frozenStock
inventoryDao.freeze(dto.skuId, dto.quantity);
return true;
}
public boolean confirmFreeze(OrderDto dto) {
// 真实扣减
inventoryDao.commitFreeze(dto.skuId, dto.quantity);
return true;
}
public boolean cancelFreeze(OrderDto dto) {
// 解冻
inventoryDao.unfreeze(dto.skuId, dto.quantity);
return true;
}
}text
优点:最终一致,无锁
缺点:业务侵入大,每个操作都要写三个方法⚠️ 坑 2:TCC 的 cancel 写错逻辑 = 数据不一致。幂等 + 防悬挂 必须做好。
五、Saga 模式
text
[订单] ──→ [支付] ──→ [库存] ──→ [物流]
│ │ │ │
↓ ↓ ↓ ↓
[取消订单] [退款] [恢复库存] [取消发货]java
// Saga 编排
public class OrderSaga {
public void placeOrder(OrderDto dto) {
// 1. 创建订单
orderService.create(dto);
try {
// 2. 支付
paymentService.pay(dto);
} catch (Exception e) {
// 补偿:取消订单
orderService.cancel(dto);
throw e;
}
try {
// 3. 扣库存
inventoryService.deduct(dto);
} catch (Exception e) {
// 补偿:退款 + 取消订单
paymentService.refund(dto);
orderService.cancel(dto);
throw e;
}
}
}text
优点:实现简单,业务侵入小
缺点:补偿链长,中间状态难排查六、可靠消息(本地消息表)
java
@Transactional
public void placeOrder(OrderDto dto) {
// 1. 业务表 + 消息表,在同一事务
orderDao.insert(dto);
messageDao.insert(new OutboxMessage("stock.deduct", dto));
// 2. 定时任务扫描消息表发送
}
// 定时任务
@Scheduled(fixedDelay = 1000)
public void sendPendingMessages() {
List<OutboxMessage> messages = messageDao.findByStatus("PENDING");
for (OutboxMessage msg : messages) {
try {
// 3. 发送到 MQ
rocketMQ.send(msg.getTopic(), msg.getPayload());
messageDao.updateStatus(msg.getId(), "SENT");
} catch (Exception e) {
// 重试
}
}
}text
优点:可靠,不丢消息
缺点:有延迟(取决于扫描间隔)七、各种方案对比
| 方案 | 一致性 | 性能 | 侵入 | 适用 |
|---|---|---|---|---|
| 2PC | 强 | 低 | 小 | 银行核心 |
| TCC | 最终 | 中 | 大 | 金融支付 |
| Saga | 最终 | 中 | 中 | 长事务 |
| 可靠消息 | 最终 | 高 | 小 | 异步解耦 |
| 最大努力 | 弱 | 极高 | 极小 | 通知类 |
八、避坑指南
java
// 1. 幂等:任何操作都要可重试
// 业务表加唯一索引,或者用 Redis SETNX
// 2. 防悬挂:TCC cancel 比 try 先到
// 取消时判断"是否有对应 try 记录"
// 3. 空回滚:TCC try 没执行就 cancel
// 记录 try 状态,如果没 try 的 cancel 失败,要记录异常
// 4. 资源预留超时:TCC try 后 30 分钟不 confirm
// 定时任务扫描 Prepare 状态,超时 cancel⚠️ 坑 3:为了"完全一致"上 2PC,导致订单接口 TPS 从 1000 跌到 100。互联网业务选最终一致,补偿 + 对账。
本章小结
| 场景 | 推荐方案 |
|---|---|
| 跨订单 + 库存 | Saga / 可靠消息 |
| 支付 + 退款 | TCC |
| 异步通知 | 可靠消息 |
| 简单业务 | 单库事务 |
动手练习
- TCC 实现:用
byteTCC框架实现订单 + 库存两个操作 - Saga 编排:写一个
OrderSaga长流程,任意一步失败触发补偿 - 本地消息表:实现订单 + 消息同事务,定时任务发送
- 幂等设计:任何一个接口加幂等校验,防止重复提交
下一章:第 15 章:Seata 实战 →