跟着deepseek浅学分布式事务(3) - 三阶段提交(3PC)


一、方案由来

2PC的缺陷

  • 同步阻塞:参与者在Prepare后阻塞,协调者宕机会导致资源长期锁定。
  • 单点故障:协调者宕机后,参与者无法自主决策。
  • 数据不一致风险:网络分区时可能部分提交、部分回滚。

3PC的改进目标

  • 减少阻塞时间:引入超时机制和预提交阶段。
  • 允许参与者自主决策:在协调者失联时,参与者能安全终止或继续事务。

二、核心原理

3PC在2PC的PrepareCommit之间插入一个预提交(Pre-Commit)阶段,将事务分为三个阶段:

  1. CanCommit:协调者询问参与者是否“可执行事务”(不锁定资源)。
  2. PreCommit:参与者预执行并锁定资源,但未提交。
  3. DoCommit:最终提交或回滚。

关键设计

  • 超时机制:参与者在每个阶段等待超时后,可根据当前状态自主决策。
  • 状态一致性:通过PreCommit阶段确保多数参与者已就绪,降低不一致风险。

三、流程详解

阶段1:CanCommit

  1. 协调者向所有参与者发送CanCommit?请求。
  2. 参与者检查自身状态(如资源是否可用),不锁定资源,回复:
    • Yes:表示可以参与事务。
    • No:拒绝(如资源不足)。

阶段2:PreCommit

  • Case 1:所有参与者回复Yes
    1. 协调者发送PreCommit命令。
    2. 参与者执行事务操作(锁定资源,写Undo/Redo日志),回复Ack
  • Case 2:任一参与者回复No或超时
    协调者发送Abort命令,参与者终止事务。

阶段3:DoCommit

  • Case 1:协调者收到所有PreCommitAck
    1. 发送DoCommit命令。
    2. 参与者提交事务,释放锁,回复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

特性2PC3PC
阻塞时间长(Prepare后阻塞)短(PreCommit后可超时提交)
单点故障严重依赖协调者参与者可超时自主决策
一致性强一致弱一致(网络分区时可能不一致)
复杂度简单较高

七、现实应用建议

  • 慎用3PC:实际系统中因实现复杂,更多采用TCC/SAGA等补偿事务。
  • 替代方案:ZooKeeper的ZAB协议、Raft等共识算法更适合协调者容灾。

总结

这些资料整理自DeepSeek,如有版权问题请联系我进行修改。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值