⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 苏三说技术 「苏三」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

分布式事务问题,无论在面试,还是工作中经常会遇到。

分布式系统下,数据一致性不再是数据库事务那么简单的。

分布式事务作为其中最复杂的挑战之一,曾让无数团队深夜加班、焦头烂额。

今天这篇文章就跟大家一起聊聊分布式事务问题的7种常见解决方案,希望对你会有所帮助。

1.为什么分布式事务如此棘手?

在单体应用时代,数据库的ACID事务保证了数据一致性。

但在微服务架构下,一个业务操作需要跨多个服务、多个数据库,传统事务模型不再适用。

想象一下电商下单场景:

  1. 订单服务创建订单(订单数据库)
  2. 库存服务扣减库存(库存数据库)
  3. 支付服务处理支付(支付数据库)
  4. 积分服务增加积分(积分数据库)

这四个操作要么全部成功,要么全部失败。

这就是分布式事务要解决的核心问题。

那么,如何解决问题呢?

2. 常见的解决方案

2.1 2PC(两阶段提交)

该方案是强一致性方案。

2PC是最经典的分布式事务协议,通过协调者(Coordinator) 统一调度参与者(Participant) 的执行。

分为两个阶段:

第一阶段:准备阶段协调者询问所有参与者:“能否提交事务?”

参与者执行本地事务但不提交,锁定资源并回复YES/NO。

// 参与者伪代码
public boolean prepare() {
try {
startTransaction();
executeSql("UPDATE account SET frozen = 100 WHERE id = 1"); // 预留资源
return true; // 返回YES
} catch (Exception e) {
rollback();
return false; // 返回NO
}
}

第二阶段:提交/回滚阶段

  • 若所有参与者返回YES,协调者发送commit命令,参与者提交事务
  • 若有任一参与者返回NO,协调者发送rollback命令,参与者回滚事务

致命缺陷:

  • 同步阻塞:所有参与者在prepare后锁定资源,直到收到commit/rollback(高并发下吞吐量骤降)
  • 单点故障:协调者宕机导致参与者永久阻塞
  • 数据不一致:网络分区时部分参与者可能提交成功

2.2 3PC(三阶段提交)

该方案也是强一致性方案。

3PC可以解决2PC阻塞问题。

3PC在2PC基础上增加预提交阶段,并引入超时机制:

  1. CanCommit阶段:协调者询问参与者状态(不锁定资源)
  2. PreCommit阶段:参与者锁定资源并执行SQL(不提交)
  3. DoCommit阶段:正式提交

改进点:

  • 参与者超时未收到命令自动提交(降低阻塞风险)
  • 预提交阶段发现异常可提前终止

但依然存在问题:

  • 网络分区时仍可能数据不一致
  • 实现复杂度显著增加

2.3 TCC(Try-Confirm-Cancel)

该方案是最终一致性方案。

它是业务层面的2PC。

TCC将业务逻辑拆分为三个阶段:

  • Try:预留资源(如冻结库存)
  • Confirm:确认操作(正式扣减库存)
  • Cancel:释放资源(解冻库存)
// 积分服务TCC实现
publicclass PointsService {

@Transactional
public boolean tryDeductPoints(Long userId, int points) {
// 检查用户积分是否充足
UserPoints user = userPointsDao.selectForUpdate(userId);
if (user.getAvailable() < points) {
thrownew InsufficientPointsException();
}
// 冻结积分
userPointsDao.freeze(userId, points);
}

public boolean confirmDeductPoints(Long userId, int points) {
// 实际扣减冻结积分
userPointsDao.confirmDeduct(userId, points);
}

public boolean cancelDeductPoints(Long userId, int points) {
// 释放冻结积分
userPointsDao.unfreeze(userId, points);
}
}

执行流程:

  1. 主业务调用所有服务的try方法
  2. 全部try成功则调用confirm;任一try失败则调用cancel

优势:

  • 无全局锁:只在try阶段锁定局部资源
  • 高可用:协调者可集群部署

挑战:

  • 需手动实现回滚逻辑(业务侵入性强)
  • 所有服务需提供三种接口

金融核心系统首选:某银行跨境支付系统采用TCC方案,日均处理200万笔交易,跨5个服务的事务成功率99.99%

2.4 可靠消息最终一致性

该方案也是最终一致性方案。

可以使用RocketMQ的事务消息。

RocketMQ的事务消息完美解决本地操作与消息发送的一致性问题:

关键步骤:

  1. 发送half消息(对消费者不可见)
  2. 执行本地事务
  3. 根据本地事务结果commit/rollback
  4. MQ定时回查未决事务

示例代码:

// 订单服务使用事务消息
publicclass OrderService {

@Autowired
private RocketMQTemplate rocketMQTemplate;

public void createOrder(Order order) {
// 1. 发送half消息
Message msg = MessageBuilder.withPayload(order).build();
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"order_topic", msg, null);

// 2. 执行本地事务(在TransactionListener中实现)
}
}

// 事务监听器
@RocketMQTransactionListener
class OrderTransactionListener implements RocketMQLocalTransactionListener {

@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
Order order = (Order) msg.getPayload();
orderDao.save(order); // 本地事务
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}

@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 回查逻辑
return checkOrderStatus(msg);
}
}

2.5 最大努力通知

该方案是弱一致性方案。

适用于对实时性要求低的场景(如短信通知):

  1. 业务主流程完成后发送通知
  2. 失败后按策略重试(如间隔1min、5min、10min)
  3. 达到阈值后人工干预
// 最大努力通知服务
publicclass BestEffortNotifier {

privatestaticfinalint[] RETRY_INTERVALS = {1, 5, 10, 30, 60}; // 分钟

public void notify(String event) {
int retryCount = 0;
while (retryCount < RETRY_INTERVALS.length) {
try {
if (sendNotification(event)) {
return; // 通知成功
}
} catch (Exception e) {
// 记录日志
}
Thread.sleep(RETRY_INTERVALS[retryCount] * 60 * 1000);
retryCount++;
}
alertManualIntervention(event); // 人工介入
}
}

实战经验:支付回调采用此方案,重试8次跨12小时,99.5%的通知在30分钟内成功

2.6 Seata AT模式

该方案是自动化的TCC。

Seata的AT(Auto Transaction)模式在不侵入业务代码的前提下实现分布式事务:

核心机制:

  1. 全局锁:TC(事务协调器)管理内存级全局锁(替代数据库行锁)
  2. SQL代理:解析业务SQL自动生成回滚日志
  3. 二阶段异步提交:极大提升吞吐量
/* 原始SQL */
UPDATE product SET stock = stock - 10 WHERE id = 1001;

/* Seata自动记录回滚日志 */
INSERT INTO undo_log (branch_id, xid,
before_image, after_image)
VALUES (?, ?,
'{"stock":100}', -- 更新前值
'{"stock":90}'); -- 更新后值

性能对比:

方案 锁持有时间 锁冲突检测耗时 适用场景
传统2PC 500~2000ms 5~20ms 低并发强一致性
Seata AT 1~10ms 0.01ms 高并发最终一致性

局限:

  • 不支持嵌套事务
  • 热点数据更新冲突率高

2.7 eBay事件队列

该方案是基于本地事务的最终一致性方案。

eBay提出的经典方案:

  1. 将分布式操作拆分为本地事务+异步事件
  2. 使用事件表确保事件不丢失
  3. 通过补偿机制解决失败场景
-- 订单服务数据库
BEGIN TRANSACTION;
-- 1. 创建订单
INSERT INTO orders (...) VALUES (...);
-- 2. 记录事件(与订单在同一个事务)
INSERT INTO event_queue (event_type, payload, status)
VALUES ('ORDER_CREATED', '{"orderId":1001}', 'PENDING');
COMMIT;

-- 定时任务扫描事件表并发布

该方案在早期eBay系统中每天处理1亿+事件,保证核心交易链路最终一致

3.方案的选型指南

根据业务场景选择合适方案:

方案 一致性级别 性能 复杂度 适用场景
2PC/3PC 强一致性 低 中 银行核心系统
TCC 最终一致 高 高 电商交易、积分体系
RocketMQ事务消息 最终一致 高 中 订单创建、物流通知
最大努力通知 弱一致 高 低 短信提醒、运营通知
Seata AT 最终一致 高 低 微服务架构的常规业务
eBay事件队列 最终一致 高 中 内部状态同步

黄金法则:

  • 强一致性需求:选择2PC/ZooKeeper(牺牲性能)
  • 高并发场景:选择可靠消息/Seata AT(最终一致)
  • 弱一致性场景:最大努力通知(成本最低)

总结

经过十年演进,分布式事务解决方案已从强一致性向高性能最终一致性发展。

技术没有绝对的好坏,只有适合与否。

我曾见过团队为了追求理论上的强一致性,把系统搞得复杂不堪;也见过过度追求性能导致资金损失的血泪教训。

分布式事务的本质,是在业务需求与技术可行性之间找到平衡点。

致开发者:不必追求完美的分布式事务解决方案,适合业务场景的才是最好的。

在设计时多问自己:

  1. 业务能容忍多长时间不一致?
  2. 事务失败后如何补偿?
  3. 是否有完善的监控和人工介入机制?

愿你在分布式系统的海洋中,乘风破浪,游刃有余。

文章目录
  1. 1. 前言
  2. 2. 1.为什么分布式事务如此棘手?
  3. 3. 2. 常见的解决方案
    1. 3.1. 2.1 2PC(两阶段提交)
    2. 3.2. 2.2 3PC(三阶段提交)
    3. 3.3. 2.3 TCC(Try-Confirm-Cancel)
    4. 3.4. 2.4 可靠消息最终一致性
    5. 3.5. 2.5 最大努力通知
    6. 3.6. 2.6 Seata AT模式
    7. 3.7. 2.7 eBay事件队列
  4. 4. 3.方案的选型指南
  5. 5. 总结