你是否遇到过这样的情况:公司的微服务集群中,多个节点需要选出主节点,但因为网络故障却导致两个节点同时认为自己是"主"?或者在容器编排系统中,因为通信延迟导致不同节点看到的系统状态不一致,引发了一连串莫名其妙的错误?在分布式系统中,这些场景时有发生,而它们本质上都指向一个核心问题:如何在不可靠的网络环境中,让多个独立节点对某个决策达成一致?

这个看似简单的问题却难倒了无数系统设计师。幸运的是,Leslie Lamport 提出的 Paxos 协议为我们提供了一个优雅的数学解决方案。今天,我们就像拆解一台精密钟表那样,一步步剖析 Paxos 协议的三个关键阶段,理解它的数学原理,并通过 Java 代码将其具体化。

Paxos 协议:基础理论

Paxos 协议是解决分布式一致性问题的基础算法,它能确保在一个由多个节点组成的系统中,即使部分节点出现故障或网络不稳定,系统仍能对某个提议达成一致。

在 Paxos 中,有三种角色参与决策过程:

  • 提议者(Proposer):提出决策提案,包含提案编号和提议值
  • 接受者(Acceptor):对提案进行投票,决定接受或拒绝
  • 学习者(Learner):学习最终被选定的提案值

从数学上看,Paxos 保证了以下关键特性:

  • 安全性:最多只有一个值能被选定
  • 一致性:一旦值被选定,学习者最终都能学习到这个值
  • 活性:如果大多数节点正常运行且网络最终恢复,系统总能完成决策

Paxos 协议分为三个主要阶段:

  1. Prepare 阶段(准备阶段)
  2. Accept 阶段(接受阶段)
  3. Learn 阶段(学习阶段)

下面我们逐一深入探讨每个阶段的工作原理。

1. Prepare 阶段:建立决策基础

核心原理

Prepare 阶段的目标是让提议者取得"发言权"并了解系统的历史状态。具体来说:

  1. 提议者生成一个提案编号 n,向多数接受者发送 Prepare 请求
  2. 接受者检查收到的提案编号 n:
  • 如果 n 大于它之前承诺过的任何编号,则:

    • 承诺不再接受编号小于 n 的任何提案
    • 返回它已接受的编号最大的提案信息(若有)
  • 否则拒绝此请求

这个机制确保了即使有多个提议者同时活动,系统也能够"收敛"到唯一的决策值。数学上,它利用了"多数派交集"原理:任意两组超过半数的接受者必然至少有一个共同成员,这保证了不同提议者之间能够感知彼此的存在。

图解说明

下图展示了一个典型的 Prepare 阶段流程:

sequenceDiagram
    participant P as 提议者
    participant A1 as 接受者1
    participant A2 as 接受者2
    participant A3 as 接受者3

    P->>A1: Prepare(n=5)
    P->>A2: Prepare(n=5)
    P->>A3: Prepare(n=5)

    A1-->>P: Promise(n=5, 已接受:n=3,value="数据库")
    A2-->>P: Promise(n=5, 无已接受值)
    A3-->>P: Promise(n=5, 无已接受值)

    Note over P,A3: 提议者收到多数Promise必须采纳n=3的值"数据库"

在这个例子中,提议者可能原本想提议"文件系统"作为存储方式,但因为收到接受者 1 返回的历史值"数据库"(来自编号 n=3 的旧提案),必须放弃自己的初始想法,转而在 Accept 阶段提议"数据库"。这是 Paxos 保证安全性的关键机制。

Java 实现

下面是 Prepare 阶段的 Java 代码实现:

public class PaxosProposer<V> {
    private AtomicInteger proposalNumber;
    private Set<PaxosAcceptor<V>> acceptors;
    private V proposalValue;

    /**
     * Prepare阶段实现
     * 向多数接受者发送Prepare请求并处理响应
     */
    public boolean prepare() {
        // 递增提案编号,确保全局单调递增
        int newProposalNumber = proposalNumber.incrementAndGet();
        System.out.println("提议者发起Prepare,提案编号=" + newProposalNumber);

        int acceptCount = 0;
        V highestAcceptedValue = null;
        int highestAcceptedProposalNumber = 0; // 跟踪已接受的最大提案编号

        for (PaxosAcceptor<V> acceptor : acceptors) {
            try {
                Promise<V> promise = acceptor.prepare(newProposalNumber);
                if (promise != null && promise.isPromised()) {
                    acceptCount++;

                    // 如果接受者已经接受过值,且该值的提案编号最大
                    // 则记录此值和编号
                    if (promise.getAcceptedValue() != null &&
                        promise.getAcceptedProposalNumber() > highestAcceptedProposalNumber) {
                        highestAcceptedValue = promise.getAcceptedValue();
                        highestAcceptedProposalNumber = promise.getAcceptedProposalNumber();
                    }
                }
            } catch (Exception e) {
                // 处理通信失败
                System.err.println("通信失败: " + e.getMessage());
            }
        }

        // 成功条件:收到严格多于半数的承诺
        if (acceptCount > acceptors.size() / 2) {
            // 关键逻辑:如果有历史接受值,必须采用它
            if (highestAcceptedValue != null) {
                proposalValue = highestAcceptedValue;
                System.out.println("采用历史值: " + proposalValue);
            } else {
                System.out.println("使用原始值: " + proposalValue);
            }
            return true;
        }

        return false; // Prepare失败
    }
}

public class PaxosAcceptor<V> {
    private String id;

    // 承诺过的最大提案编号(Prepare阶段更新)
    private int highestPromisedNumber = 0;

    // 实际接受过的提案编号(Accept阶段更新)
    private int acceptedProposalNumber = 0;

    // 接受过的提案值
    private V acceptedValue = null;

    /**
     * 处理Prepare请求
     */
    public synchronized Promise<V> prepare(int proposalNumber) {
        System.out.println(id + ": 收到Prepare, 编号=" + proposalNumber);

        // 核心决策逻辑:只承诺编号更大的提案
        if (proposalNumber > highestPromisedNumber) {
            highestPromisedNumber = proposalNumber;

            // 返回承诺和已接受的值(如果有)
            return new Promise<>(true, acceptedProposalNumber, acceptedValue);
        }

        // 拒绝编号较小的Prepare请求
        return new Promise<>(false, 0, null);
    }
}

简单来说,Prepare 阶段就像是提议者在询问:"我想讨论编号为 n 的提案,大家能否接受?如果有人已经接受过其他提案,请告诉我。"而接受者的回答决定了提议者下一步的行动。

2. Accept 阶段:尝试达成共识

核心原理

在 Accept 阶段,如果提议者在 Prepare 阶段获得了多数接受者的承诺,它会发送 Accept 请求,要求接受者接受提议值。这里有个关键点:提议者必须使用 Prepare 阶段获得的历史值(如果有的话),否则使用自己的初始值。

接受者收到 Accept 请求后,如果提案编号不小于它承诺过的最大编号(即proposalNumber >= highestPromisedNumber),则接受该提案。这确保了接受者不会违背之前的承诺。

数学上,Accept 阶段巧妙地实现了"只有一个值最终被选定"的保证:

  • 如果值 v 在某个提案中被多数派接受,那么任何更高编号的提案,在 Prepare 阶段都必然能发现 v
  • 因此,更高编号的提案只能提议相同的值 v,而不是新值

图解说明

下图展示了 Accept 阶段的流程:

sequenceDiagram
    participant P as 提议者
    participant A1 as 接受者1
    participant A2 as 接受者2
    participant A3 as 接受者3

    Note over P: 基于Prepare结果提议值为"数据库"

    P->>A1: Accept(n=5, value="数据库")
    P->>A2: Accept(n=5, value="数据库")
    P->>A3: Accept(n=5, value="数据库")

    A1-->>P: Accepted(n=5)
    A2-->>P: Accepted(n=5)
    A3-xP: 拒绝(已承诺n=7)

    Note over P,A3: 多数接受者(1+2)接受提案值"数据库"被选定

在这个例子中,即使接受者 3 因为已经承诺了更高编号的提案而拒绝,但由于多数接受者(1 和 2)已经接受了提案,因此"数据库"被选定为最终决策。

Java 实现

下面是 Accept 阶段的 Java 代码实现:

public class PaxosProposer<V> {
    // 前面的代码省略...

    /**
     * Accept阶段实现
     * 向多数接受者发送Accept请求
     */
    public boolean accept() {
        int currentProposalNumber = proposalNumber.get();
        System.out.println("提议者发起Accept, 编号=" + currentProposalNumber + ", 值=" + proposalValue);

        int acceptCount = 0;

        for (PaxosAcceptor<V> acceptor : acceptors) {
            try {
                boolean accepted = acceptor.accept(currentProposalNumber, proposalValue);
                if (accepted) {
                    acceptCount++;
                }
            } catch (Exception e) {
                System.err.println("通信失败: " + e.getMessage());
            }
        }

        // 成功条件:严格多于半数的接受者接受提案
        return acceptCount > acceptors.size() / 2;
    }
}

public class PaxosAcceptor<V> {
    // 前面的代码省略...

    /**
     * 处理Accept请求
     */
    public synchronized boolean accept(int proposalNumber, V value) {
        System.out.println(id + ": 收到Accept, 编号=" + proposalNumber + ", 值=" + value);

        // 核心决策逻辑:如果提案编号不小于承诺的最大编号,则接受
        if (proposalNumber >= highestPromisedNumber) {
            highestPromisedNumber = proposalNumber;
            acceptedProposalNumber = proposalNumber;
            acceptedValue = value;

            // 在实际系统中应持久化这些状态
            return true;
        }

        return false; // 拒绝Accept请求(提案编号小于承诺编号)
    }
}

Accept 阶段就像是提议者对接受者说:"既然大家同意讨论编号 n 的提案,那么我提议值为 v,请大家投票。"而接受者则根据自己之前的承诺决定是否接受这个提议。

3. Learn 阶段:传播最终决策

核心原理

Learn 阶段的目标是确保所有系统节点(学习者)都了解最终选定的值。这对于系统实际工作至关重要,因为只有知道决策结果,节点才能正确执行相应操作。

Learn 阶段有两种实现方式:

  1. 被动学习:提议者主动通知所有学习者选定的值
  2. 主动学习:学习者自己查询多数接受者,获取选定的值

第二种方式更为健壮,因为它不依赖于提议者的可靠性,即使提议者在通知前崩溃,学习者仍能获知选定的值。

图解说明

下图展示了 Learn 阶段的两种学习方式:

sequenceDiagram
    participant P as 提议者
    participant A as 接受者集合
    participant L1 as 学习者1
    participant L2 as 学习者2

    Note over P: 提案"数据库"已被选定

    rect rgb(200, 240, 200)
    Note over P,L2: 被动学习
    P->>L1: Learn(value="数据库")
    P->>L2: Learn(value="数据库")
    end

    rect rgb(240, 220, 200)
    Note over L1,L2: 主动学习(提议者故障时)
    L1->>A: 查询当前值
    A-->>L1: 多数返回"数据库"
    end

    Note over L1,L2: 所有学习者获知最终决策

Java 实现

下面是 Learn 阶段的 Java 代码实现:

public class PaxosProposer<V> {
    // 前面的代码省略...
    private Set<PaxosLearner<V>> learners;

    /**
     * Learn阶段实现:通知所有学习者
     */
    public void informLearners() {
        System.out.println("提议者通知学习者, 选定值: " + proposalValue);

        for (PaxosLearner<V> learner : learners) {
            try {
                learner.learn(proposalValue);
            } catch (Exception e) {
                System.err.println("通知学习者失败: " + e.getMessage());
            }
        }
    }

    /**
     * 运行完整的Paxos流程
     */
    public boolean runPaxos(V initialValue) {
        if (initialValue != null) {
            this.proposalValue = initialValue;
        }

        // 依次执行三个阶段
        if (prepare() && accept()) {
            informLearners();
            return true;
        }
        return false;
    }
}

public class PaxosLearner<V> {
    private String id;
    private V learnedValue = null;
    private Set<PaxosAcceptor<V>> acceptors; // 用于主动学习

    /**
     * 被动学习:从提议者接收选定值
     */
    public synchronized void learn(V value) {
        this.learnedValue = value;
        System.out.println(id + ": 学习到值: " + value);
    }

    /**
     * 主动学习:查询多数接受者
     */
    public synchronized V activeLearn() {
        System.out.println(id + ": 正在主动查询当前系统状态");
        Map<V, Integer> valueCount = new HashMap<>();

        // 查询所有接受者
        for (PaxosAcceptor<V> acceptor : acceptors) {
            try {
                AcceptorState<V> state = acceptor.getState();
                if (state.getAcceptedValue() != null) {
                    valueCount.put(
                        state.getAcceptedValue(),
                        valueCount.getOrDefault(state.getAcceptedValue(), 0) + 1
                    );
                }
            } catch (Exception e) {
                // 部分接受者可能不可用,这是正常的
            }
        }

        // 检查是否有值被多数接受者接受
        for (Map.Entry<V, Integer> entry : valueCount.entrySet()) {
            if (entry.getValue() > acceptors.size() / 2) {
                V majorityValue = entry.getKey();
                this.learnedValue = majorityValue;
                System.out.println(id + ": 主动学习到值: " + majorityValue);
                return majorityValue;
            }
        }

        return null; // 没有值被多数接受者接受
    }
}

Learn 阶段确保了系统中的所有组件最终都能了解到最终决策,无论它们是否参与了决策过程。

完整的 Java 实现与测试

下面提供一个完整的 Paxos 协议 Java 实现,包括异常处理、故障模拟和测试场景:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Paxos协议完整实现示例
 */
public class PaxosDemo {
    public static void main(String[] args) {
        // 创建3个接受者和2个学习者
        Set<PaxosAcceptor<String>> acceptors = new HashSet<>();
        for (int i = 0; i < 3; i++) {
            acceptors.add(new PaxosAcceptor<>("接受者-" + i));
        }

        Set<PaxosLearner<String>> learners = new HashSet<>();
        for (int i = 0; i < 2; i++) {
            learners.add(new PaxosLearner<>("学习者-" + i, acceptors));
        }

        // 创建提议者并运行Paxos
        PaxosProposer<String> proposer = new PaxosProposer<>(acceptors, learners);
        boolean success = proposer.runPaxos("数据库");

        System.out.println("Paxos协议执行" + (success ? "成功" : "失败") +
                          ",最终选定值: " + proposer.getProposalValue());

        // 测试多提议者竞争
        testCompetingProposers(acceptors, learners);

        // 测试接受者故障恢复
        testAcceptorFailure(acceptors, learners);
    }

    /**
     * 测试多提议者竞争场景
     */
    private static void testCompetingProposers(
            Set<PaxosAcceptor<String>> acceptors,
            Set<PaxosLearner<String>> learners) {
        System.out.println("\n=== 多提议者竞争测试 ===");

        // 重置接受者状态
        for (PaxosAcceptor<String> acceptor : acceptors) {
            acceptor.reset();
        }

        // 创建两个提议者,分别提出不同的值
        PaxosProposer<String> proposerA = new PaxosProposer<>(acceptors, learners);
        PaxosProposer<String> proposerB = new PaxosProposer<>(acceptors, learners);

        // 设置不同的优先级,影响提案编号
        proposerA.setPriority(1); // 较低优先级
        proposerB.setPriority(2); // 较高优先级

        // 启动两个线程模拟并发提议
        Thread threadA = new Thread(() -> {
            boolean success = proposerA.runPaxos("文件系统");
            System.out.println("提议者A: " + (success ? "成功" : "失败") +
                              ",最终值: " + proposerA.getProposalValue());
        });

        Thread threadB = new Thread(() -> {
            boolean success = proposerB.runPaxos("数据库");
            System.out.println("提议者B: " + (success ? "成功" : "失败") +
                              ",最终值: " + proposerB.getProposalValue());
        });

        // 启动线程
        threadA.start();
        threadB.start();

        try {
            threadA.join();
            threadB.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 验证所有学习者学到的是相同的值
        Set<String> learnedValues = new HashSet<>();
        for (PaxosLearner<String> learner : learners) {
            learnedValues.add(learner.getLearnedValue());
        }

        System.out.println("所有学习者学到的值" +
                          (learnedValues.size() == 1 ? "一致" : "不一致") +
                          ": " + learnedValues);
    }

    /**
     * 测试接受者故障场景
     */
    private static void testAcceptorFailure(
            Set<PaxosAcceptor<String>> acceptors,
            Set<PaxosLearner<String>> learners) {
        System.out.println("\n=== 接受者故障测试 ===");

        // 重置接受者状态
        for (PaxosAcceptor<String> acceptor : acceptors) {
            acceptor.reset();
        }

        // 模拟一个接受者故障
        PaxosAcceptor<String> failingAcceptor = acceptors.iterator().next();
        failingAcceptor.simulateFailure();
        System.out.println(failingAcceptor.getId() + " 发生故障");

        // 创建提议者并运行Paxos
        PaxosProposer<String> proposer = new PaxosProposer<>(acceptors, learners);
        boolean success = proposer.runPaxos("内存缓存");

        System.out.println("故障存在时,Paxos执行" + (success ? "成功" : "失败") +
                          ",选定值: " + proposer.getProposalValue());

        // 模拟接受者恢复
        failingAcceptor.simulateRecovery();
        System.out.println(failingAcceptor.getId() + " 已恢复");

        // 学习者主动学习
        for (PaxosLearner<String> learner : learners) {
            String activeLearnedValue = learner.activeLearn();
            System.out.println(learner.getId() + " 主动学习结果: " + activeLearnedValue);
        }
    }
}

关键实现类的完整代码(省略上文已展示的核心方法):

class PaxosProposer<V> {
    private AtomicInteger proposalNumber;
    private final Set<PaxosAcceptor<V>> acceptors;
    private final Set<PaxosLearner<V>> learners;
    private V proposalValue;
    private int priority = 1;
    private static final int MAX_RETRIES = 3;

    // 构造器和其他方法...

    /**
     * 设置提议者优先级
     */
    public void setPriority(int priority) {
        this.priority = priority;
    }

    /**
     * 获取当前提案值
     */
    public V getProposalValue() {
        return proposalValue;
    }

    /**
     * 运行Paxos协议,包含重试逻辑
     */
    public boolean runPaxos(V initialValue) {
        if (initialValue != null) {
            this.proposalValue = initialValue;
        }

        for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
            if (prepare() && accept()) {
                informLearners();
                return true;
            }

            // 失败后随机退避,避免活锁
            try {
                long backoffTime = (long) (Math.random() * 100 * (attempt + 1));
                System.out.println("提议者退避 " + backoffTime + "ms 后重试");
                Thread.sleep(backoffTime);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        return false; // 重试次数用尽仍失败
    }
}

class PaxosAcceptor<V> {
    // 字段和核心方法...

    /**
     * 模拟故障
     */
    public void simulateFailure() {
        this.failure = true;
    }

    /**
     * 模拟恢复
     * 从持久存储中加载状态
     */
    public synchronized void simulateRecovery() {
        // 从持久存储中恢复状态
        this.failure = false;

        // 真实系统中,这里应当从磁盘/数据库加载最新状态
        if (persistentStorage.containsKey("highestPromisedNumber")) {
            this.highestPromisedNumber = (Integer) persistentStorage.get("highestPromisedNumber");
        }

        if (persistentStorage.containsKey("acceptedValue")) {
            @SuppressWarnings("unchecked")
            V value = (V) persistentStorage.get("acceptedValue");
            this.acceptedValue = value;
        }
    }

    /**
     * 重置状态(用于测试)
     */
    public synchronized void reset() {
        this.highestPromisedNumber = 0;
        this.acceptedProposalNumber = 0;
        this.acceptedValue = null;
        this.persistentStorage.clear();
        this.failure = false;
    }

    /**
     * 获取接受者状态(供学习者主动学习)
     */
    public synchronized AcceptorState<V> getState() {
        if (failure) {
            throw new RuntimeException("接受者不可用");
        }
        return new AcceptorState<>(acceptedProposalNumber, acceptedValue);
    }
}

Paxos 算法的挑战与优化方向

在实际应用中,Paxos 会面临一些挑战:

1. 并发提议者导致的反复重试

当多个提议者同时提出提议时,可能出现"互相阻塞"的情况:

flowchart TD
    A[提议者A: Prepare n=5] --> B{接受者群}
    C[提议者B: Prepare n=6] --> B
    B --> D[接受者承诺n=6]
    D --> E[A的Accept被拒绝]
    E --> F[A提出更大编号n=7]
    F --> G[B的Accept被拒绝]
    G --> H[B提出更大编号n=8]
    H --> I[循环往复...]

    style I fill:#f99,stroke:#333

解决方法

  • 随机退避:提议者失败后等待随机时间再重试
  • 选举唯一协调者:在系统中选出主提议者,只有主提议者才能提出提议
  • 分区编号:不同提议者使用不同数字段(如奇偶分开)

2. 优化通信轮次

Multi-Paxos: 多数系统采用的优化方式

  • 选出固定的主提议者后,后续提案可跳过 Prepare 阶段
  • 大大减少消息数量和通信轮次
  • 将多个值的决策打包处理

协议通信对比

3. 状态持久化与恢复

在实际系统中,接受者必须将关键状态持久化存储:

  • highestPromisedNumber:保证不违背承诺
  • acceptedProposalNumberacceptedValue:恢复后能报告正确历史
// 持久化关键状态示例
public synchronized void persistState() {
    try (FileOutputStream fos = new FileOutputStream("acceptor_state.dat");
         ObjectOutputStream oos = new ObjectOutputStream(fos)) {
        PaxosState state = new PaxosState(
            highestPromisedNumber, acceptedProposalNumber, acceptedValue);
        oos.writeObject(state);
    } catch (IOException e) {
        throw new RuntimeException("持久化状态失败", e);
    }
}

从数学角度理解 Paxos 的正确性

Paxos 协议的安全性(一致性)基于以下数学性质:

正确性证明核心

定理 1: 如果值 v 在提案编号 n 被选定,那么任何编号大于 n 的被接受的提案,其值必然是 v。

证明思路:

  1. 假设提案(n,v)被多数派 M1 接受
  2. 任何更高编号 m 的提案必须在 Prepare 阶段获得多数派 M2 的响应
  3. 由于两个多数派必有交集,至少有一个接受者同时在 M1 和 M2 中
  4. 该接受者会在响应 m 的 Prepare 时返回(n,v)
  5. 如果 m 的提议者看到的最大已接受提案是(n,v),它必须提议值 v
  6. 因此更高编号的提案只能包含值 v,不能是其他值

这个数学性质确保了即使有多个提议者竞争,最终也只有一个值被选定。

实际应用示例

让我们用一个具体例子说明 Paxos 的工作流程:

场景:三节点集群需要决定使用哪种数据存储方式

  1. 初始状态:
  • 3 个接受者:A1, A2, A3
  • 2 个提议者:P1 想提议"Redis",P2 想提议"MySQL"
  1. 执行过程:
  • P1 发送 Prepare(n=5),获得多数派承诺
  • P1 发送 Accept(n=5, v="Redis"),成功获得多数派接受
  • 此时"Redis"被选定,学习者学习到这个值
  • 系统开始使用 Redis 作为存储方式
  • 如果 P2 尝试提议"MySQL",必须使用更大编号(如 n=10)
  • 但 P2 会在 Prepare 阶段发现已有值"Redis"被选定
  • P2 必须放弃"MySQL",转而提议"Redis"

这个例子展示了 Paxos 如何在分布式环境中确保一致性决策。

总结

下表对比了 Paxos 协议三个阶段的关键特性:

阶段名称主要目的核心操作数学基础成功条件
Prepare 阶段获取"发言权"并了解历史提议者发送提案编号;接受者承诺不接受更小编号通过多数派交集保证值的连续性多于半数接受者承诺
Accept 阶段尝试让多数接受者接受值提议者发送(编号,值);接受者在未违背承诺情况下接受多数派机制确保唯一值选定多于半数接受者接受
Learn 阶段传播最终决策通知或查询选定值确保所有节点最终获知决策所有学习者了解决策

异常君
7 声望7 粉丝

在 Java 的世界里,永远有下一座技术高峰等着你。我愿做你登山路上的同频伙伴,陪你从看懂代码到写出让自己骄傲的代码。咱们,代码里见!