从零开始学3PC:分布式事务的进阶方案
一、什么是3PC?—— 解决2PC的“堵车”问题
3PC(Three-Phase Commit) 是2PC的升级版,核心目标是通过减少同步阻塞时间和避免单点故障,提升分布式事务的可用性与性能。其名字中的“3”代表三个阶段:CanCommit(预提交)、PreCommit(预提交确认)、Commit(最终提交)。
核心改进:
• 解决2PC的痛点:
• 协调者单点故障(2PC中协调者宕机会导致参与者永久阻塞)。
• 长时间同步阻塞资源(如银行转账时账户被锁)。
• 核心角色:
• 协调者(Coordinator):发起全局事务。
• 参与者(Participant):执行本地事务。
• 预提交阶段:让参与者在最终决定前“表态”,减少阻塞时间。
通俗比喻:
想象你组织一个多人聚餐,2PC就像指挥大家同时举手表决(同步阻塞),而3PC则分成三步:
- 问大家“能不能参加?”(CanCommit)。
- 大家回复“可以”,你再确认“真的可以吗?”(PreCommit)。
- 最后通知“开始吃吧!”(Commit)。
这样避免所有人在等待中“干瞪眼”。
二、3PC原理:三步走策略
1. 第一阶段:CanCommit(预提交询问)
• 协调者行动:
• 向所有参与者广播 CanCommit
请求,询问是否愿意参与事务。
• 参与者行动:
• 执行本地事务的预检查(如库存是否充足、账户余额是否足够)。
• 回复Yes/No:
◦ Yes:表示“如果我被选中,我能完成”。
◦ No:直接拒绝参与(如库存不足)。
• 关键点:
• 不执行实际操作:仅检查可行性,不扣减库存或转账。
• 快速响应:参与者几乎不阻塞资源。
2. 第二阶段:PreCommit(预提交确认)
• 协调者行动:
• 如果所有参与者均回复 Yes
,广播 PreCommit
指令。
• 否则,广播 Abort
,直接终止事务。
• 参与者行动:
• 执行本地事务(如扣减库存、转账)。
• 记录 Undo Log(回滚日志)。
• 广播 PreCommitted
状态给协调者。
• 关键点:
• 实际执行事务:但未提交,仍处于“临时状态”。
• 参与者可能宕机:需通过超时机制处理。
3. 第三阶段:Commit(最终提交)
• 协调者行动:
• 收集所有参与者的 PreCommitted
状态。
• 如果所有参与者均确认,广播 Commit
指令。
• 否则,广播 Abort
。
• 参与者行动:
• 提交事务:正式提交本地操作,释放资源。
• 清理日志:删除 Undo Log
。
• 关键点:
• 异步提交:参与者收到 Commit
后才提交,减少同步阻塞。
三、3PC适用场景:高并发与高性能需求
以下场景适合使用3PC:
- 电商秒杀:
• 库存扣减、订单生成、支付扣款需原子性完成,但需高并发处理(如618、双11)。
• 3PC通过缩短同步时间提升吞吐量。 - 金融交易:
• 跨行转账需强一致性,但需避免长时间锁表。 - 分布式数据库:
• 跨库事务需高可用性(如MySQL Cluster)。
反例:
• 日志记录:允许异步写入,无需强一致。
• 数据分析:结果允许滞后几分钟。
四、实战:手把手实现3PC
4.1 最简代码示例
// 协调者类
public class Coordinator {
private List<Participant> participants = new ArrayList<>();
private Map<String, Boolean> preCommitStatus = new ConcurrentHashMap<>();
public void addParticipant(Participant p) {
participants.add(p);
}
// 第一阶段:CanCommit
public boolean canCommit() {
for (Participant p : participants) {
if (!p.canCommit()) {
return false;
}
}
return true;
}
// 第二阶段:PreCommit
public void preCommit() {
for (Participant p : participants) {
p.preCommit();
preCommitStatus.put(p.getName(), true);
}
}
// 第三阶段:Commit
public void commit() {
for (Participant p : participants) {
p.commit();
}
}
// 回滚
public void abort() {
for (Participant p : participants) {
p.abort();
}
}
}
// 参与者类
public class Participant {
private String name;
private boolean isPreCommitted = false;
public Participant(String name) {
this.name = name;
}
// 第一阶段:CanCommit(预检查)
public boolean canCommit() {
System.out.println(name + " 执行预检查...");
// 模拟库存是否充足
return Math.random() > 0.3; // 70%成功率
}
// 第二阶段:PreCommit(执行本地事务)
public void preCommit() {
System.out.println(name + " 执行本地操作(扣减库存/转账)...");
isPreCommitted = true;
// 模拟可能的失败
if (Math.random() > 0.7) {
throw new RuntimeException("本地事务执行失败!");
}
}
// 提交
public void commit() {
if (isPreCommitted) {
System.out.println(name + " 提交成功!");
isPreCommitted = false;
}
}
// 回滚
public void abort() {
if (isPreCommitted) {
System.out.println(name + " 执行回滚...");
isPreCommitted = false;
}
}
}
// 测试类
public class Main {
public static void main(String[] args) {
Coordinator coord = new Coordinator();
coord.addParticipant(new Participant("库存服务"));
coord.addParticipant(new Participant("支付服务"));
try {
if (coord.canCommit()) {
coord.preCommit();
coord.commit();
System.out.println("全局事务提交成功!");
} else {
coord.abort();
System.out.println("全局事务回滚!");
}
} catch (Exception e) {
coord.abort();
System.out.println("发生异常,强制回滚!");
}
}
}
输出示例:
库存服务 执行预检查...
支付服务 执行预检查...
库存服务 执行本地操作(扣减库存)...
支付服务 执行本地操作(扣减库存)...
库存服务 提交成功!
支付服务 提交成功!
全局事务提交成功!
4.2 使用Seata框架(进阶实战)
Seata支持3PC模式,通过 TC(Transaction Coordinator)、TM(Transaction Manager)、RM(Resource Manager) 实现。代码与2PC类似,但内部自动处理三阶段逻辑。
// 业务代码(仅需加注解)
@Service
public class OrderService {
@GlobalTransactional(timeout=30000, name="createOrder", transactionMode=TransactionMode.TCC)
public void createOrder(Order order) {
inventoryService.deduct(order.getSkuId()); // 扣库存
paymentService.charge(order.getUserId(), order.getAmount()); // 扣款
orderDAO.insert(order); // 生成订单
}
}
流程说明:
- TM发起全局事务,请求TC进入CanCommit阶段。
- 各RM预检查资源,回复CanCommit结果。
- TC广播PreCommit,RM执行本地事务并记录日志。
- TC收集PreCommit结果,决定提交或回滚。
五、3PC的坑与解决方案
1. 网络分区(脑裂)
• 问题:协调者与部分参与者断开,导致部分节点提交,部分回滚。
• 解决方案:
• 心跳检测:参与者定期向协调者发送心跳,超时则视为异常。
• 多阶段重试:在PreCommit阶段引入超时重试机制。
2. 重复提交
• 问题:协调者重启后,可能重复发送PreCommit指令。
• 解决方案:
• 全局事务ID(XID):每个事务唯一标识,参与者拒绝重复处理。
• 状态持久化:将事务状态(CanCommit/PreCommit)写入磁盘。
3. 本地事务失败
• 问题:参与者在PreCommit阶段执行本地事务失败(如扣款超限)。
• 解决方案:
• 自动回滚:通过 Undo Log
迅速恢复数据。
• 重试机制:结合Saga模式,通过补偿操作修复(如退款)。
4. 性能瓶颈
• 问题:三阶段通信增加延迟,尤其在网络不稳定的情况下。
• 解决方案:
• 异步化:将Commit阶段改为异步消息(如Kafka),降低同步阻塞。
• 批量处理:合并多个小事务为一个批量操作。
六、3PC vs 2PC vs TCC
对比维度 | 3PC | 2PC | TCC |
---|---|---|---|
阶段数 | 3 | 2 | 3(Try/Confirm/Cancel) |
阻塞时间 | 短(仅PreCommit同步) | 长(全程同步) | 低(异步补偿) |
单点故障 | 无(协调者集群化) | 有(单协调者) | 无(服务独立) |
适用场景 | 高并发、低延迟场景 | 强一致性、短事务 | 复杂业务逻辑、长事务 |
开发成本 | 中(需实现三阶段逻辑) | 低(标准协议) | 高(需编写补偿代码) |
七、总结与行动建议
- 掌握基础:先通过代码示例理解3PC的三阶段流程。
- 使用框架:生产环境推荐Seata或RocketMQ事务消息,避免重复造轮子。
- 避坑指南:
• 集群化部署协调者(如基于Raft的Seata TC)。
• 设置合理超时时间(如CanCommit阶段1秒,PreCommit阶段2秒)。
• 业务代码幂等(如通过订单ID防重)。 - 进阶学习:
• 书籍:《分布式系统模式》(Unmesh Joshi)。
• 文档:Seata官方文档、RocketMQ事务消息指南。
最后思考:
3PC是2PC的优化,但并非完美。在实际项目中,需根据业务需求权衡:
• 高并发场景(如电商秒杀):优先用3PC。
• 复杂业务逻辑(如订单退款):结合Saga模式更灵活。
• 金融核心系统:若强一致性要求极高,可退回到2PC+备用协调者方案。