问题
分布式事务方案:两阶段提交、三阶段提交、TCC、基于消息补偿的最终一致性
答案
1. 方案概览
分布式事务解决方案可以按照一致性强度分为两大类:
刚性事务(强一致性) 柔性事务(最终一致性)
↓ ↓
┌───┴───┐ ┌─────┴─────┐
│ │ │ │
2PC 3PC TCC 消息补偿方案
XA Saga ┌──────┴──────┐
│ │
本地消息表 事务消息
(RocketMQ)
2. 两阶段提交(2PC)
原理回顾
阶段划分:
阶段1:Prepare(准备)
- 协调者询问所有参与者是否可以提交
- 参与者执行事务操作(但不提交)
- 参与者锁定资源,返回Yes/No
阶段2:Commit/Rollback(提交/回滚)
- 所有参与者返回Yes → 协调者发送Commit
- 任一参与者返回No → 协调者发送Rollback
执行时序:
// 协调者代码示例
public boolean execute2PC(Transaction tx) {
List<Participant> participants = tx.getParticipants();
// ===== 阶段1:Prepare =====
for (Participant p : participants) {
boolean prepared = p.prepare(tx);
if (!prepared) {
// 有参与者准备失败,执行回滚
rollback(participants);
return false;
}
}
// ===== 阶段2:Commit =====
for (Participant p : participants) {
p.commit(tx);
}
return true;
}
核心问题
问题1:同步阻塞
时间线:
0ms ────→ 协调者发送Prepare
10ms ────→ 参与者锁定资源
↓
[阻塞期:资源被锁定]
↓
100ms ───→ 协调者发送Commit
110ms ───→ 参与者释放资源
在10ms-110ms期间,资源被锁定,其他事务无法访问
问题2:单点故障
场景:协调者在Commit前崩溃
协调者 [崩溃] ✗
|
|─Prepare→ 参与者1 [资源锁定,等待...]
|─Prepare→ 参与者2 [资源锁定,等待...]
|─Prepare→ 参与者3 [资源锁定,等待...]
结果:参与者无限期等待,资源无法释放
问题3:数据不一致
场景:部分参与者收到Commit,部分未收到
协调者
|─Commit→ 参与者1 [已提交] ✓
|─Commit✗ 参与者2 [网络故障,未收到]
|─Commit✗ 参与者3 [网络故障,未收到]
结果:参与者1已提交,参与者2、3不知所措
优缺点总结
优点:
- ✅ 强一致性保证
- ✅ 实现相对简单
- ✅ 有成熟的实现(XA协议)
缺点:
- ❌ 同步阻塞,性能差
- ❌ 单点故障风险
- ❌ 可能数据不一致
- ❌ 不适合高并发场景
适用场景:
- 对一致性要求极高的场景(金融交易)
- 并发量不大的系统
- 短事务场景
3. 三阶段提交(3PC)
原理回顾
阶段划分:
阶段1:CanCommit(询问)
- 协调者询问参与者是否可以执行事务
- 参与者只做检查,不执行事务,不锁资源
阶段2:PreCommit(预提交)
- 参与者执行事务操作(但不提交)
- 参与者锁定资源
- 引入超时机制
阶段3:DoCommit(提交)
- 参与者提交或回滚事务
- 参与者超时后自动提交
与2PC对比:
2PC:
Prepare(执行+锁定) → Commit/Rollback
3PC:
CanCommit(只检查) → PreCommit(执行+锁定) → DoCommit
改进点:
1. 拆分Prepare阶段,减少锁定时间
2. 引入超时机制,降低阻塞风险
超时机制
// 参与者超时处理
public void preCommit(Transaction tx) {
// 执行事务
executeTransaction(tx);
// 启动超时定时器
ScheduledFuture<?> timeout = scheduler.schedule(() -> {
// 超时后自动提交(假设PreCommit成功说明大概率会提交)
commit(tx);
log.info("Auto commit after timeout");
}, TIMEOUT_SECONDS, TimeUnit.SECONDS);
// 等待DoCommit命令
waitForDoCommit(tx, timeout);
}
核心问题
问题:仍无法完全避免数据不一致
场景:协调者发送DoCommit时网络分区
协调者本应发送Abort,但网络分区了:
|─DoCommit✗ 参与者1 [超时,自动提交] ✗错误
|─DoCommit✗ 参与者2 [超时,自动提交] ✗错误
结果:参与者误以为应该提交,实际应该回滚
优缺点总结
优点:
- ✅ 降低阻塞时间
- ✅ 降低单点故障影响
缺点:
- ❌ 实现复杂度更高
- ❌ 性能更差(多一次网络往返)
- ❌ 仍可能数据不一致
- ❌ 实际应用很少
适用场景:
- 理论意义大于实际意义
- 生产环境几乎不用
4. TCC(Try-Confirm-Cancel)
原理回顾
阶段划分:
Try(尝试):
- 完成业务检查
- 预留必要资源
- 不执行实际业务
Confirm(确认):
- 使用Try阶段预留的资源
- 真正执行业务
Cancel(取消):
- 释放Try阶段预留的资源
- 执行补偿逻辑
完整案例
电商下单场景:
// 主事务服务
@Service
public class OrderTccService {
@Autowired
private AccountTccService accountService;
@Autowired
private InventoryTccService inventoryService;
public void createOrder(Order order) {
String txId = generateTxId();
try {
// ===== Try阶段 =====
accountService.tryDeduct(order.getAccountId(), order.getAmount(), txId);
inventoryService.tryDeduct(order.getProductId(), order.getQuantity(), txId);
orderService.tryCreate(order, txId);
// ===== Confirm阶段 =====
accountService.confirmDeduct(order.getAccountId(), order.getAmount(), txId);
inventoryService.confirmDeduct(order.getProductId(), order.getQuantity(), txId);
orderService.confirmCreate(order.getOrderId(), txId);
} catch (Exception e) {
// ===== Cancel阶段 =====
accountService.cancelDeduct(order.getAccountId(), order.getAmount(), txId);
inventoryService.cancelDeduct(order.getProductId(), order.getQuantity(), txId);
orderService.cancelCreate(order.getOrderId(), txId);
}
}
}
// 账户服务TCC实现
@Service
public class AccountTccService {
// Try:冻结金额
@Transactional
public void tryDeduct(String accountId, BigDecimal amount, String txId) {
// 检查余额
Account account = accountMapper.selectById(accountId);
if (account.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}
// 冻结金额
accountMapper.freezeAmount(accountId, amount);
// 记录TCC日志(幂等)
tccLogMapper.insert(txId, "account", TccStatus.TRYING);
}
// Confirm:扣减冻结金额
@Transactional
public void confirmDeduct(String accountId, BigDecimal amount, String txId) {
// 幂等性检查
if (isAlreadyConfirmed(txId)) {
return;
}
// 扣减冻结金额
accountMapper.deductFrozenAmount(accountId, amount);
// 更新状态
tccLogMapper.updateStatus(txId, "account", TccStatus.CONFIRMED);
}
// Cancel:解冻金额
@Transactional
public void cancelDeduct(String accountId, BigDecimal amount, String txId) {
// 幂等性检查
if (isAlreadyCanceled(txId)) {
return;
}
// 解冻金额
accountMapper.unfreezeAmount(accountId, amount);
// 更新状态
tccLogMapper.updateStatus(txId, "account", TccStatus.CANCELED);
}
}
数据库设计:
-- 账户表(支持资源预留)
CREATE TABLE account (
id VARCHAR(64) PRIMARY KEY,
balance DECIMAL(20, 2) NOT NULL, -- 可用余额
frozen_amount DECIMAL(20, 2) NOT NULL -- 冻结金额
);
-- TCC事务日志表
CREATE TABLE tcc_transaction (
tx_id VARCHAR(64) PRIMARY KEY,
service_name VARCHAR(64) NOT NULL,
status VARCHAR(20) NOT NULL, -- TRYING, CONFIRMED, CANCELED
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
);
关键设计点
设计点1:幂等性
// 通过唯一键保证幂等
public void tryDeduct(String accountId, BigDecimal amount, String txId) {
// 查询是否已执行
if (tccLogMapper.exists(txId)) {
return; // 已执行,直接返回
}
// 首次执行
// ...
}
设计点2:空回滚
// Cancel时Try可能还没执行
public void cancelDeduct(String accountId, BigDecimal amount, String txId) {
TccLog log = tccLogMapper.selectByTxId(txId);
if (log == null) {
// Try未执行,记录Cancel状态,防止后续Try
tccLogMapper.insert(txId, TccStatus.CANCELED);
return;
}
// 正常Cancel流程
// ...
}
设计点3:防悬挂
// Try时检查是否已经Cancel
public void tryDeduct(String accountId, BigDecimal amount, String txId) {
TccLog log = tccLogMapper.selectByTxId(txId);
if (log != null && log.getStatus() == TccStatus.CANCELED) {
// 已经Cancel过,不能再Try
throw new BusinessException("Transaction already canceled");
}
// 正常Try流程
// ...
}
优缺点总结
优点:
- ✅ 性能好(不长时间锁定资源)
- ✅ 数据一致性强
- ✅ 无单点故障
缺点:
- ❌ 实现复杂(需实现Try、Confirm、Cancel)
- ❌ 对业务侵入大
- ❌ 需要设计资源预留机制
适用场景:
- 对一致性和性能都有要求
- 能够设计资源预留机制
- 典型应用:电商下单、支付
5. 基于消息补偿的最终一致性
方案1:本地消息表
原理:利用本地事务保证业务操作和消息发送的原子性。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LocalMessageMapper messageMapper;
/**
* 创建订单
*/
@Transactional
public void createOrder(Order order) {
// 1. 创建订单(业务操作)
orderMapper.insert(order);
// 2. 插入本地消息表(同一个本地事务)
LocalMessage message = new LocalMessage();
message.setId(UUID.randomUUID().toString());
message.setContent(JSON.toJSONString(order));
message.setStatus(MessageStatus.PENDING);
messageMapper.insert(message);
// 本地事务保证以上两步的原子性
}
}
// 定时任务发送待发送的消息
@Component
public class MessageSender {
@Scheduled(fixedDelay = 1000)
public void sendPendingMessages() {
// 查询待发送的消息
List<LocalMessage> messages = messageMapper.selectPending();
for (LocalMessage message : messages) {
try {
// 发送到MQ
mqProducer.send("order-topic", message.getContent());
// 更新消息状态
message.setStatus(MessageStatus.SENT);
messageMapper.updateById(message);
} catch (Exception e) {
// 发送失败,下次重试
log.error("Failed to send message", e);
}
}
}
}
数据库设计:
-- 本地消息表
CREATE TABLE local_message (
id VARCHAR(64) PRIMARY KEY,
content TEXT NOT NULL,
status VARCHAR(20) NOT NULL, -- PENDING, SENT, CONSUMED
retry_count INT DEFAULT 0,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_status (status)
);
流程图:
订单服务 MQ 库存服务
| | |
|--[本地事务]------------------- | |
| 1. 创建订单 | |
| 2. 插入消息表 | |
|--[本地事务结束]--------------- | |
| | |
|--[定时任务扫描]--------------- | |
|--发送消息-------------------> | |
| |--消费消息-------------> |
| | [扣减库存]
优点:
- ✅ 实现简单
- ✅ 利用本地事务保证可靠性
- ✅ 最终一致性
缺点:
- ❌ 与业务耦合(需要消息表)
- ❌ 消息可能重复(需要下游幂等)
方案2:RocketMQ事务消息
原理:RocketMQ提供的半消息机制。
@Service
public class OrderService {
@Autowired
private TransactionMQProducer producer;
/**
* 创建订单
*/
public void createOrder(Order order) {
// 发送事务消息
producer.sendMessageInTransaction(
new Message("order-topic", JSON.toJSONString(order).getBytes()),
order
);
}
}
// 事务监听器
@Component
class OrderTransactionListener implements TransactionListener {
/**
* 执行本地事务
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
Order order = (Order) arg;
// 执行本地事务:创建订单
orderMapper.insert(order);
// 本地事务成功,提交消息
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
// 本地事务失败,回滚消息
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
/**
* 回查本地事务状态
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// MQ回查:本地事务是否执行成功?
String orderId = extractOrderId(msg);
Order order = orderMapper.selectById(orderId);
if (order != null) {
// 订单存在,提交消息
return LocalTransactionState.COMMIT_MESSAGE;
} else {
// 订单不存在,回滚消息
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
}
流程图:
订单服务 RocketMQ Broker 库存服务
| | |
|--1.发送半消息--------> | |
|<--2.半消息已存储-------| |
| | |
|--3.执行本地事务 | |
| (创建订单) | |
| | |
|--4.提交/回滚消息-----> | |
| | |
| |--5.可见消息---------> |
| | [扣减库存]
| | |
|<--6.回查本地事务-------| |
|--7.返回本地事务状态--> | |
优点:
- ✅ RocketMQ原生支持
- ✅ 实现相对简单
- ✅ 可靠性高
缺点:
- ❌ 依赖RocketMQ
- ❌ 只能保证最终一致性
方案3:最大努力通知
原理:主动通知+定期对账。
@Service
public class OrderNotificationService {
/**
* 创建订单后通知下游
*/
@Transactional
public void createOrder(Order order) {
// 1. 创建订单
orderMapper.insert(order);
// 2. 异步通知下游(最大努力)
asyncNotify(order);
}
/**
* 异步通知(重试机制)
*/
private void asyncNotify(Order order) {
executor.execute(() -> {
int maxRetry = 3;
for (int i = 0; i < maxRetry; i++) {
try {
// 调用下游接口
restTemplate.postForObject(
"http://inventory-service/api/deduct",
order,
String.class
);
return; // 成功则返回
} catch (Exception e) {
log.error("Notify failed, retry {}/{}", i + 1, maxRetry, e);
sleep(1000 * (i + 1)); // 递增延迟
}
}
// 重试失败,记录到失败表
notificationFailureMapper.insert(order.getId());
});
}
/**
* 定期对账
*/
@Scheduled(cron = "0 0 2 * * ?")
public void reconcile() {
// 查询昨天的订单
List<Order> orders = orderMapper.selectYesterday();
// 查询下游的库存扣减记录
List<String> deductedOrderIds = inventoryService.getDeductedOrders();
// 找出差异
Set<String> diff = orders.stream()
.map(Order::getId)
.filter(id -> !deductedOrderIds.contains(id))
.collect(Collectors.toSet());
// 补偿通知
for (String orderId : diff) {
Order order = orderMapper.selectById(orderId);
asyncNotify(order);
}
}
}
优点:
- ✅ 实现简单
- ✅ 对下游系统要求低
缺点:
- ❌ 可靠性最低
- ❌ 需要定期对账
6. 方案对比总结
对比表格
| 方案 | 一致性 | 性能 | 复杂度 | 业务侵入 | 适用场景 |
|---|---|---|---|---|---|
| 2PC/XA | 强一致 | ★☆☆ | ★★☆ | ★☆☆ | 金融交易、对一致性要求极高 |
| 3PC | 强一致 | ★☆☆ | ★★★ | ★☆☆ | 理论研究,实际几乎不用 |
| TCC | 最终一致 | ★★★ | ★★★ | ★★★ | 电商下单、对性能和一致性都有要求 |
| 本地消息表 | 最终一致 | ★★★ | ★★☆ | ★★☆ | 简单场景、可接受最终一致 |
| 事务消息 | 最终一致 | ★★★ | ★★☆ | ★☆☆ | 使用RocketMQ的系统 |
| 最大努力通知 | 弱一致 | ★★★ | ★☆☆ | ★☆☆ | 通知类场景、对一致性要求低 |
选型决策树
是否需要强一致性?
├─ 是 → 是否能接受性能损失?
│ ├─ 是 → 2PC/XA
│ └─ 否 → 考虑TCC或优化业务逻辑
│
└─ 否 → 可以接受最终一致性
├─ 是否使用RocketMQ?
│ ├─ 是 → 事务消息
│ └─ 否 → 继续
│
├─ 是否需要业务补偿?
│ ├─ 是 → TCC
│ └─ 否 → 继续
│
├─ 对可靠性要求高?
│ ├─ 是 → 本地消息表
│ └─ 否 → 最大努力通知
7. 实际应用建议
场景1:金融支付
选择:2PC/XA
原因:对一致性要求极高,可接受性能损失
场景2:电商下单
选择:TCC 或 Seata AT模式
原因:对性能和一致性都有要求
场景3:订单通知
选择:事务消息 或 本地消息表
原因:可接受最终一致性,实现简单
场景4:日志同步
选择:最大努力通知
原因:对一致性要求不高,允许丢失
8. 总结
核心要点:
- 没有完美的方案,只有合适的方案
- 刚性事务保证强一致性,但性能差
- 柔性事务保证最终一致性,性能好
- 根据业务特性选择合适的一致性级别
最佳实践:
- 优先考虑避免分布式事务(业务拆分、异步化)
- 能用消息解决的,不用TCC
- 能用TCC解决的,不用2PC
- 互联网系统优先选择柔性事务
面试要点:
- 能清晰对比各方案的优缺点
- 理解不同方案的适用场景
- 掌握方案选型的决策依据
- 了解实际项目中的应用