微服务时代,分布式事务是每个后端工程师的必修课!本文带你彻底搞懂分布式事务的所有解决方案!
开篇先问:你遇到过这些问题吗?
场景1:电商下单
- 订单创建成功了
- 库存也扣减了
- 但支付失败了...
现在订单和库存怎么办?
场景2:转账操作
- A账户扣款成功
- 但B账户加款失败
- 钱去哪了?
如果你遇到过这些问题,恭喜你,已经踏入了分布式事务的世界!
第1章:什么是事务?从ACID说起
本地事务
1.1 回顾经典的ACID特性
还记得数据库课上学的ACID吗?
- A - 原子性:要么全成功,要么全失败
- C - 一致性:数据前后状态一致
- I - 隔离性:事务之间互不干扰
- D - 持久性:提交后永久保存
1.2 单体应用的本地事务
在传统单体应用中,事务非常简单:
@Transactional // 一个注解搞定!
public void createOrder(Order order) {
// 1. 插入订单
orderDao.insert(order);
// 2. 插入订单明细
orderItemDao.insertBatch(order.getItems());
// 3. 扣减库存
stockDao.deduct(order.getProductId(), order.getQuantity());
// 任何一步失败 → 全部回滚 ✅
}
优点显而易见: ✅ 实现简单 - 一个注解解决 ✅ 性能极高 - 数据库原生支持 ✅ 强一致性 - ACID完全保证
但局限也很明显: ❌ 只能单库使用 ❌ 无法跨服务调用
第2章:分布式场景下的"灾难"
分布式事务场景图
2.1 微服务架构下的电商下单
现在我们把系统拆成了微服务:
public void placeOrder(OrderRequest req) {
// 1. 订单服务 - 创建订单(数据库A)
Order order = orderService.create(req);
// 2. 库存服务 - 扣减库存(数据库B)
stockService.deduct(req.getProductId(), req.getQuantity());
// 3. 账户服务 - 扣款(数据库C)
accountService.deduct(req.getUserId(), req.getAmount());
// ❓ 如果第3步失败,前两步已经提交了,怎么办?
}
2.2 面临的核心问题
网络不可靠 - 服务调用可能超时、失败 部分成功 - 有的服务成功,有的失败 数据不一致 - 各服务数据库状态不同步 回滚困难 - 每个服务的事务独立提交
CAP定理告诉我们: 在分布式系统中,一致性(C)、可用性(A)、分区容错性(P),最多只能同时满足两个!
第3章:刚性事务 - 2PC/3PC强一致性方案
2PC两阶段提交
3.1 2PC(两阶段提交)
执行流程:
阶段1:准备阶段
- 协调者问:大家能提交事务吗?
- 参与者:执行事务但不提交,回答YES/NO
阶段2:提交阶段
-
如果都是YES → 协调者:大家提交!
-
如果有NO → 协调者:大家回滚!
@Transactional public void placeOrder(OrderRequest req) { UserTransaction tx = getUserTransaction();
try { tx.begin(); // 开启全局事务 // 操作订单库 Connection conn1 = orderDataSource.getConnection(); conn1.execute("INSERT INTO orders ..."); // 操作库存库 Connection conn2 = stockDataSource.getConnection(); conn2.execute("UPDATE stock ..."); tx.commit(); // 全局提交 } catch (Exception e) { tx.rollback(); // 全局回滚 }}
优点: ✅ 强一致性,符合ACID ✅ 实现相对简单
缺点: ❌ 同步阻塞 - 所有参与者等待期间会阻塞 ❌ 单点故障 - 协调者挂了,整个系统不可用 ❌ 数据不一致 - 部分参与者可能收不到指令 ❌ 性能极差 - 锁定资源时间长
3.2 3PC(三阶段提交)
3PC三阶段提交
3PC在2PC基础上增加了预提交阶段:
阶段1:CanCommit(询问)
- 只做预检查,不锁资源
阶段2:PreCommit(预提交)
- 执行事务并锁资源,但不提交
阶段3:DoCommit(提交)
- 执行最终提交或回滚
改进: ✅ 引入超时机制,减少阻塞 ✅ 提前发现问题
仍然存在: ❌ 实现更复杂 ❌ 性能开销更大 ❌ 仍有数据不一致风险
第4章:柔性事务之TCC模式 ⚡
TCC事务补偿模式
4.1 TCC核心思想
TCC将事务分为三个阶段:
- Try(尝试) - 预留资源,检查并锁定
- Confirm(确认) - 执行真正的业务逻辑
- Cancel(取消) - 释放资源,回滚操作
4.2 实战代码
// 库存服务 - TCC接口实现
@Service
public class StockTccService {
/**
* Try阶段:冻结库存
*/
public boolean tryDeduct(Long productId, Integer quantity, String orderId) {
Stock stock = stockMapper.selectById(productId);
// 检查库存是否充足
if (stock.getAvailable() < quantity) {
return false;
}
// 冻结库存:available - quantity, frozen + quantity
stockMapper.freezeStock(productId, quantity);
// 记录冻结日志(重要!用于回滚)
stockMapper.insertFreezeLog(orderId, productId, quantity, "TRY");
return true;
}
/**
* Confirm阶段:确认扣减
*/
public boolean confirmDeduct(String orderId) {
FreezeLog log = stockMapper.selectFreezeLog(orderId);
// 扣减冻结的库存:frozen - quantity
stockMapper.deductFrozenStock(log.getProductId(), log.getQuantity());
// 更新日志状态
stockMapper.updateFreezeLog(orderId, "CONFIRM");
return true;
}
/**
* Cancel阶段:释放库存
*/
public boolean cancelDeduct(String orderId) {
FreezeLog log = stockMapper.selectFreezeLog(orderId);
// 解冻库存:available + quantity, frozen - quantity
stockMapper.unfreezeStock(log.getProductId(), log.getQuantity());
// 更新日志状态
stockMapper.updateFreezeLog(orderId, "CANCEL");
return true;
}
}
// 订单服务 - TCC事务协调
@Service
public class OrderTccCoordinator {
public void placeOrder(OrderRequest req) {
String orderId = UUID.randomUUID().toString();
try {
// Try阶段 - 预留资源
boolean stockOk = stockTccService.tryDeduct(
req.getProductId(), req.getQuantity(), orderId
);
boolean accountOk = accountTccService.tryDeduct(
req.getUserId(), req.getAmount(), orderId
);
if (stockOk && accountOk) {
// Confirm阶段 - 确认执行
stockTccService.confirmDeduct(orderId);
accountTccService.confirmDeduct(orderId);
} else {
// Cancel阶段 - 回滚
if (stockOk) stockTccService.cancelDeduct(orderId);
if (accountOk) accountTccService.cancelDeduct(orderId);
}
} catch (Exception e) {
// 异常时Cancel
stockTccService.cancelDeduct(orderId);
accountTccService.cancelDeduct(orderId);
}
}
}
4.3 TCC的优缺点
优点: ✅ 不依赖数据库事务,性能更好 ✅ 可以跨数据库、跨服务 ✅ 业务逻辑清晰
缺点: ❌ 代码侵入性强,需要实现三个方法 ❌ 开发成本高 ❌ 需要考虑幂等性和悬挂问题
⚠️ 关键点:
- 必须实现幂等性(重复调用结果一致)
- 防止资源悬挂(Try成功但Confirm/Cancel失败)
- 需要完善的日志记录
第5章:柔性事务之Saga模式
Saga长事务模式
5.1 Saga核心思想
Saga将长事务拆分为多个本地短事务,每个事务都有对应的补偿事务。
5.2 实战代码
@Service
public class OrderSagaService {
public void placeOrder(OrderRequest req) {
List compensations = new ArrayList<>();
try {
// 步骤1:创建订单
Order order = orderService.createOrder(req);
compensations.add(() -> orderService.cancelOrder(order.getId()));
// 步骤2:扣减库存
stockService.deductStock(req.getProductId(), req.getQuantity());
compensations.add(() ->
stockService.addStock(req.getProductId(), req.getQuantity())
);
// 步骤3:扣减余额
accountService.deductBalance(req.getUserId(), req.getAmount());
compensations.add(() ->
accountService.addBalance(req.getUserId(), req.getAmount())
);
// 所有步骤成功
orderService.confirmOrder(order.getId());
} catch (Exception e) {
// 逆序执行补偿操作
for (int i = compensations.size() - 1; i >= 0; i--) {
try {
compensations.get(i).compensate();
} catch (Exception ex) {
// 记录补偿失败,人工介入
log.error("补偿失败: {}", ex.getMessage());
}
}
throw new BusinessException("下单失败");
}
}
}
@FunctionalInterface
interface Compensation {
void compensate();
}
5.3 Saga的优缺点
优点: ✅ 长事务拆分,不会长时间锁定资源 ✅ 业务流程灵活 ✅ 适合复杂业务场景
缺点: ❌ 不保证隔离性,可能出现脏读 ❌ 补偿逻辑复杂 ❌ 需要处理补偿失败的情况
第6章:可靠消息最终一致性
可靠消息最终一致性
6.1 核心思想
通过消息队列实现异步事务,保证最终一致性。
6.2 实战代码
// 订单服务 - 发送可靠消息
@Service
public class OrderMessageService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Transactional
public void createOrderWithMessage(OrderRequest req) {
// 1. 创建订单(本地事务)
Order order = new Order();
order.setUserId(req.getUserId());
order.setAmount(req.getAmount());
order.setStatus("PENDING");
orderMapper.insert(order);
// 2. 发送事务消息(确保消息和订单在同一事务中)
rocketMQTemplate.sendMessageInTransaction(
"order-topic",
MessageBuilder.withPayload(order).build(),
null
);
}
// 事务消息监听器
@RocketMQTransactionListener
class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 本地事务已在createOrderWithMessage中执行
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 回查本地事务状态
Order order = JSON.parseObject(msg.getPayload(), Order.class);
Order dbOrder = orderMapper.selectById(order.getId());
return dbOrder != null ?
RocketMQLocalTransactionState.COMMIT :
RocketMQLocalTransactionState.ROLLBACK;
}
}
}
// 库存服务 - 消费消息
@Service
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "stock-consumer")
public class StockMessageConsumer implements RocketMQListener {
@Override
public void onMessage(Order order) {
try {
// 执行扣减库存(需要保证幂等性!)
stockService.deductStock(order.getProductId(), order.getQuantity());
} catch (Exception e) {
// 消费失败,消息会重试
log.error("扣减库存失败: {}", e.getMessage());
throw new RuntimeException(e);
}
}
}
6.3 关键要点
三大核心机制: 事务消息 - 确保消息发送和本地事务绑定 消息回查 - MQ定期回查本地事务状态 幂等性 - 消费者需要实现幂等,防止重复消费
优点: ✅ 性能高,异步处理 ✅ 解耦服务间依赖 ✅ 吞吐量大
缺点: ❌ 最终一致性,存在延迟 ❌ 消息可能丢失或重复 ❌ 需要额外的消息中间件
第7章:最大努力通知
最大努力通知
7.1 适用场景
适用于对一致性要求不高的场景:
- 支付结果通知
- 订单状态通知
- 对账场景
7.2 实战代码
@Service
public class NotificationService {
/**
* 发送通知,失败后定时重试
*/
public void sendNotification(String targetUrl, Object data) {
// 记录通知日志
NotificationLog log = new NotificationLog();
log.setTargetUrl(targetUrl);
log.setData(JSON.toJSONString(data));
log.setRetryCount(0);
log.setStatus("PENDING");
logMapper.insert(log);
// 异步发送
executeNotification(log);
}
private void executeNotification(NotificationLog log) {
try {
// 发送HTTP请求
ResponseEntity response = restTemplate.postForEntity(
log.getTargetUrl(),
log.getData(),
String.class
);
if (response.getStatusCode().is2xxSuccessful()) {
// 成功
log.setStatus("SUCCESS");
logMapper.updateById(log);
} else {
// 失败,等待重试
scheduleRetry(log);
}
} catch (Exception e) {
// 异常,等待重试
scheduleRetry(log);
}
}
/**
* 定时重试任务
*/
@Scheduled(fixedDelay = 60000) // 每分钟执行一次
public void retryFailedNotifications() {
List pendingLogs = logMapper.selectPendingLogs();
for (NotificationLog log : pendingLogs) {
if (log.getRetryCount() >= 5) {
// 超过最大重试次数,标记为失败
log.setStatus("FAILED");
logMapper.updateById(log);
// 发送告警,人工介入
alertService.sendAlert("通知失败: " + log.getTargetUrl());
} else {
// 增加重试次数
log.setRetryCount(log.getRetryCount() + 1);
logMapper.updateById(log);
// 重新发送
executeNotification(log);
}
}
}
}
第8章:Seata - 阿里开源的分布式事务神器
Seata AT模式
8.1 Seata架构
Seata包含三大角色:
- TC(事务协调器) - 维护全局事务和分支事务的状态
- TM(事务管理器) - 定义全局事务的范围
- RM(资源管理器) - 管理分支事务的资源
8.2 AT模式实战
AT模式是Seata的默认模式,对业务代码几乎无侵入!
// 1. 引入依赖
/*
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
*/
// 2. 配置文件
/*
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
*/
// 3. 业务代码 - 订单服务
@Service
public class OrderServiceImpl {
@Autowired
private StockServiceClient stockService;
@Autowired
private AccountServiceClient accountService;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(OrderRequest req) {
// 1. 创建订单
Order order = new Order();
order.setUserId(req.getUserId());
order.setAmount(req.getAmount());
orderMapper.insert(order);
// 2. 远程调用库存服务
stockService.deduct(req.getProductId(), req.getQuantity());
// 3. 远程调用账户服务
accountService.deduct(req.getUserId(), req.getAmount());
// 任何一步失败,Seata会自动回滚所有操作 ✅
}
}
// 4. 库存服务(只需要本地事务)
@Service
public class StockServiceImpl {
@Transactional
public void deduct(Long productId, Integer quantity) {
stockMapper.deduct(productId, quantity);
}
}
8.3 AT模式原理
一阶段:
- 解析SQL,生成前后镜像(before image & after image)
- 执行业务SQL
- 提交本地事务,释放本地锁
- 向TC注册分支事务
二阶段:
- 如果成功:删除undo log,完成
- 如果失败:根据undo log生成反向SQL,回滚数据
优势: ✅ 无代码侵入 ✅ 性能高,一阶段即提交 ✅ 自动生成回滚SQL
第9章:生产实践与选型建议
9.1 方案对比表
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 2PC/3PC | 强一致 | ⭐ | ⭐ | 金融核心系统 |
| TCC | 最终一致 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 资金交易 |
| Saga | 最终一致 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 长流程业务 |
| 可靠消息 | 最终一致 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 高并发场景 |
| 最大努力通知 | 弱一致 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 通知类场景 |
| Seata AT | 最终一致 | ⭐⭐⭐⭐ | ⭐⭐ | 通用场景 |
9.2 选型建议
根据一致性要求选择:
- 强一致性要求 → 选择2PC或Seata XA模式
- 最终一致性可接受 → 选择柔性事务
根据业务特点选择:
- 高性能要求 → 可靠消息或Saga
- 快速落地 → Seata AT模式
- 资金类业务 → TCC
- 长流程业务 → Saga
- 通知场景 → 最大努力通知
根据团队能力选择:
- 新手团队 → Seata AT(简单易用)
- 资深团队 → TCC(灵活可控)
总结:分布式事务的本质
核心要点
1. 没有银弹
- 不同场景需要不同方案
- 一致性、可用性、性能需要权衡
2. 优先本地事务
- 不要为了分布式而分布式
- 能用本地事务就不要用分布式事务
3. 柔性事务是主流
- 互联网场景更适合柔性事务
- 追求最终一致性,提升性能
4. Seata是快速方案
- 低侵入,易落地
- 适合大部分业务场景