文章目录
一、方案由来
2PC的缺陷
- 同步阻塞:参与者在
Prepare
后阻塞,协调者宕机会导致资源长期锁定。 - 单点故障:协调者宕机后,参与者无法自主决策。
- 数据不一致风险:网络分区时可能部分提交、部分回滚。
3PC的改进目标
- 减少阻塞时间:引入超时机制和预提交阶段。
- 允许参与者自主决策:在协调者失联时,参与者能安全终止或继续事务。
二、核心原理
3PC在2PC的Prepare
和Commit
之间插入一个预提交(Pre-Commit)阶段,将事务分为三个阶段:
- CanCommit:协调者询问参与者是否“可执行事务”(不锁定资源)。
- PreCommit:参与者预执行并锁定资源,但未提交。
- DoCommit:最终提交或回滚。
关键设计:
- 超时机制:参与者在每个阶段等待超时后,可根据当前状态自主决策。
- 状态一致性:通过
PreCommit
阶段确保多数参与者已就绪,降低不一致风险。
三、流程详解
阶段1:CanCommit
- 协调者向所有参与者发送
CanCommit?
请求。 - 参与者检查自身状态(如资源是否可用),不锁定资源,回复:
Yes
:表示可以参与事务。No
:拒绝(如资源不足)。
阶段2:PreCommit
- Case 1:所有参与者回复
Yes
- 协调者发送
PreCommit
命令。 - 参与者执行事务操作(锁定资源,写Undo/Redo日志),回复
Ack
。
- 协调者发送
- Case 2:任一参与者回复
No
或超时
协调者发送Abort
命令,参与者终止事务。
阶段3:DoCommit
- Case 1:协调者收到所有
PreCommit
的Ack
- 发送
DoCommit
命令。 - 参与者提交事务,释放锁,回复
Ack
。
- 发送
- Case 2:协调者未收到全部
Ack
发送Abort
命令,参与者回滚。
参与者超时处理
- CanCommit阶段超时:直接终止事务。
- PreCommit阶段超时:若未收到
DoCommit
,则自主提交(因多数已就绪)。 - DoCommit阶段超时:自主提交(协调者已确认提交意图)。
四、适用场景
- 需要高可用性:容忍协调者临时故障
- 长事务:减少资源锁定时间
- 最终一致可接受:虽降低阻塞,但仍有不一致风险(如网络分区时)。
五、伪代码
1. 参与者(Participant)
public class Participant {
private final String participantId;
// 参与者状态:INIT, CAN_COMMIT, PRE_COMMIT, COMMITTED, ABORTED
private final AtomicReference<State> state = new AtomicReference<>(State.INIT);
// 事务日志(模拟持久化存储)
private final TransactionLog transactionLog = new TransactionLog();
// 用于模拟故障的测试标志
private boolean forceFail = false;
public Participant(String participantId) {
this.participantId = participantId;
}
public void setForceFail(boolean flag) {
this.forceFail = flag;
}
public enum State {INIT, CAN_COMMIT, PRE_COMMIT, COMMITTED, ABORTED}
/**
* Phase 1: 检查是否可以提交(不锁定资源)
*/
public synchronized boolean canCommit(String txId) {
if (state.get() != State.INIT) {
return false; // 已参与其他事务
}
boolean resourceAvailable = checkResourceAvailable(txId); // 模拟资源检查
if (resourceAvailable) {
state.set(State.CAN_COMMIT);
transactionLog.log(participantId, txId, "CAN_COMMIT");
return true;
}
return false;
}
/**
* Phase 2: 预提交(锁定资源,写日志)
*/
public synchronized boolean preCommit(String txId) {
if (forceFail) {
System.out.println("[Participant-" + participantId + "] 模拟PRE_COMMIT失败");
return false;
}
if (state.get() != State.CAN_COMMIT) {
return false;
}
lockResources(txId); // 模拟锁定资源
state.set(State.PRE_COMMIT);
transactionLog.log(participantId, txId, "PRE_COMMIT");
return true;
}
/**
* Phase 3: 最终提交
*/
public synchronized boolean doCommit(String txId) {
if (state.get() != State.PRE_COMMIT) {
return false;
}
commitTransaction(txId); // 模拟提交
state.set(State.COMMITTED);
transactionLog.log(participantId, txId, "COMMITTED");
return true;
}
/**
* 超时后的自主决策(根据3PC协议)
*/
public synchronized void handleTimeout(String txId) {
if (state.get() == State.PRE_COMMIT) {
// 超时后若处于PRE_COMMIT,则自主提交
doCommit(txId);
} else if (state.get() == State.CAN_COMMIT) {
// 超时后若处于CAN_COMMIT,则终止事务
state.set(State.ABORTED);
transactionLog.log(participantId, txId, "ABORTED (TIMEOUT)");
}
}
// ---- 模拟方法(实际需替换为真实操作)----
private boolean checkResourceAvailable(String txId) {
return true; // 假设资源总是可用
}
private void lockResources(String txId) {
System.out.println("[Participant-" + participantId + "] 锁定资源, TX: " + txId);
}
private void commitTransaction(String txId) {
System.out.println("[Participant-" + participantId + "] 提交事务, TX: " + txId);
}
// 获取参与者ID(供协调者调用)
public String getParticipantId() {
return participantId;
}
// 用于在事务失败时让参与者回滚操作
public synchronized boolean abort(String txId) {
if (state.get() == State.INIT || state.get() == State.CAN_COMMIT) {
// 如果还未预提交,直接终止事务
state.set(State.ABORTED);
System.out.println("[Participant-" + participantId + "] ABORTED: " + txId);
return true;
} else if (state.get() == State.PRE_COMMIT) {
// 如果已预提交,需要回滚(执行补偿操作)
rollback(txId);
state.set(State.ABORTED);
System.out.println("[Participant-" + participantId + "] ROLLBACK: " + txId);
return true;
}
return false; // 其他状态无法终止
}
private void rollback(String txId) {
// 实际项目这里需要:
// 1. 根据undo日志回滚数据
// 2. 释放锁资源
System.out.println("[Participant-" + participantId + "] 执行回滚操作, TX: " + txId);
}
}
2. 协调者(Coordinator)
public class Coordinator {
private final List<Participant> participants;
private final ScheduledExecutorService timeoutScheduler = Executors.newScheduledThreadPool(1);
public Coordinator(List<Participant> participants) {
this.participants = participants;
}
/**
* 执行3PC事务
*/
public boolean execute3PC(String txId, long timeoutMs) {
// Phase 1: CanCommit
if (!phaseCanCommit(txId)) {
return false;
}
// Phase 2: PreCommit
if (!phasePreCommit(txId)) {
sendAbortAll(txId);
return false;
}
// Phase 3: DoCommit (with timeout monitor)
return phaseDoCommit(txId, timeoutMs);
}
private boolean phaseCanCommit(String txId) {
for (Participant p : participants) {
if (!p.canCommit(txId)) {
System.out.println("[Coordinator] Participant " + p.getParticipantId() + " CAN_COMMIT失败");
return false;
}
}
return true;
}
private boolean phasePreCommit(String txId) {
for (Participant p : participants) {
if (!p.preCommit(txId)) {
System.out.println("[Coordinator] Participant " + p.getParticipantId() + " PRE_COMMIT失败");
return false;
}
}
return true;
}
private boolean phaseDoCommit(String txId, long timeoutMs) {
// 设置超时监控(若协调者崩溃,参与者会超时自主提交)
scheduleTimeoutCheck(txId, timeoutMs);
// 发送DoCommit命令
for (Participant p : participants) {
if (!p.doCommit(txId)) {
System.out.println("[Coordinator] Participant " + p.getParticipantId() + " DO_COMMIT失败");
return false;
}
}
return true;
}
private void sendAbortAll(String txId) {
participants.forEach(p -> {
if (!p.abort(txId)) {
System.out.println("[Coordinator] 无法终止参与者: " + p.getParticipantId());
}
});
}
/**
* 模拟超时检查(实际可用心跳机制替代)
*/
private void scheduleTimeoutCheck(String txId, long timeoutMs) {
timeoutScheduler.schedule(() -> {
participants.forEach(p -> p.handleTimeout(txId));
}, timeoutMs, TimeUnit.MILLISECONDS);
}
}
3. 模拟事务日志
public class TransactionLog {
public void log(String participantId, String txId, String action) {
System.out.println("[Participant-" + participantId + "] TX " + txId + " -> " + action);
}
}
4. 主程序
public class Main {
public static void main(String[] args) {
List<Participant> participants = Arrays.asList(
new Participant("DB1"),
new Participant("DB2"),
new Participant("DB3")
);
/**
* 强制让第二个参与者在preCommit阶段失败
* participants.get(1).setForceFail(true);
*/
Coordinator coordinator = new Coordinator(participants);
boolean success = coordinator.execute3PC("TX-123", 3000);
System.out.println("事务结果:" + (success ? "成功" : "失败"));
}
}
5. 控制台日志
// 事务成功
[Participant-DB1] TX TX-123 -> CAN_COMMIT
[Participant-DB2] TX TX-123 -> CAN_COMMIT
[Participant-DB3] TX TX-123 -> CAN_COMMIT
[Participant-DB1] 锁定资源, TX: TX-123
[Participant-DB1] TX TX-123 -> PRE_COMMIT
[Participant-DB2] 锁定资源, TX: TX-123
[Participant-DB2] TX TX-123 -> PRE_COMMIT
[Participant-DB3] 锁定资源, TX: TX-123
[Participant-DB3] TX TX-123 -> PRE_COMMIT
[Participant-DB1] 提交事务, TX: TX-123
[Participant-DB1] TX TX-123 -> COMMITTED
[Participant-DB2] 提交事务, TX: TX-123
[Participant-DB2] TX TX-123 -> COMMITTED
[Participant-DB3] 提交事务, TX: TX-123
[Participant-DB3] TX TX-123 -> COMMITTED
事务结果:成功
// 事务失败
[Participant-DB1] TX TX-123 -> CAN_COMMIT
[Participant-DB2] TX TX-123 -> CAN_COMMIT
[Participant-DB3] TX TX-123 -> CAN_COMMIT
[Participant-DB1] 锁定资源, TX: TX-123
[Participant-DB1] TX TX-123 -> PRE_COMMIT
[Participant-DB2] 模拟PRE_COMMIT失败
[Coordinator] Participant DB2 PRE_COMMIT失败
[Participant-DB1] 执行回滚操作, TX: TX-123
[Participant-DB1] ROLLBACK: TX-123
[Participant-DB2] ABORTED: TX-123
[Participant-DB3] ABORTED: TX-123
事务结果:失败
六、对比2PC vs 3PC
特性 | 2PC | 3PC |
---|---|---|
阻塞时间 | 长(Prepare后阻塞) | 短(PreCommit后可超时提交) |
单点故障 | 严重依赖协调者 | 参与者可超时自主决策 |
一致性 | 强一致 | 弱一致(网络分区时可能不一致) |
复杂度 | 简单 | 较高 |
七、现实应用建议
- 慎用3PC:实际系统中因实现复杂,更多采用TCC/SAGA等补偿事务。
- 替代方案:ZooKeeper的ZAB协议、Raft等共识算法更适合协调者容灾。
总结
这些资料整理自DeepSeek,如有版权问题请联系我进行修改。