第2讲:共识算法入门:Paxos 的精髓


一、Paxos 要解决什么问题

想象你有一个分布式系统,三个节点各自维护一份数据副本。当客户端请求写入 x=5 时,你需要确保:

  1. 只有一个值被选定 ------ 不能出现有的节点认为 x=5,有的认为 x=3
  2. 一旦选定,不可更改 ------ 不能先选了 x=5,后来又改成 x=3
  3. 即使部分节点宕机,系统仍能工作 ------ 只要多数派活着就能推进

这就是 共识问题。Paxos 是第一个被严格证明正确的共识算法。

复制代码
传统思维(错的):
  客户端 → 给所有节点发 "x=5"
          节点A: 收到 x=5
          节点B: 收到 x=5
          节点C: 收到 x=5  ← 但 C 宕机了,消息丢了
          
  客户端 → 重试,发给 B 和 C
          节点B: 我已经有 x=5 了
          节点C: 第一次收到,记下 x=5
          
  另一个客户端 → 发 "x=3" 给 A 和 C
          节点A: 我有 x=5,但新的来了,改成 x=3?
          节点C: 我有 x=5,但新的来了,改成 x=3?
          
  → 混乱:A 和 C 改成了 3,B 还是 5

Paxos 的做法:
  用提案编号排序,编号大的获胜
  一旦一个值被多数派接受,后面的提案必须接受这个值

二、Basic Paxos 角色与流程

角色
角色 职责 类比
**Proposer(提议者)**​ 发起提案,争取让自己的值被接受 竞标者
**Acceptor(接受者)**​ 投票决定是否接受提案 评委
**Learner(学习者)**​ 学习最终选定的值 观众

一个节点可以同时扮演多个角色。

两阶段流程
复制代码
第一阶段:Prepare(准备阶段)
  Proposer                              Acceptors
     │                                      │
     │  1. Prepare(n=1)                     │
     │ ──────────────────────────────────→  │
     │                                      │
     │  2. Promise(n=1, last_accepted=∅)    │
     │ ←──────────────────────────────────  │
     │                                      │
     │  3. Promise(n=1, last_accepted=∅)    │
     │ ←──────────────────────────────────  │
     │                                      │
     │  (收到多数派 Promise,进入第二阶段)   │
     
第二阶段:Accept(接受阶段)
  Proposer                              Acceptors
     │                                      │
     │  4. Accept!(n=1, value="x=5")        │
     │ ──────────────────────────────────→  │
     │                                      │
     │  5. Accepted(n=1, value="x=5")       │
     │ ←──────────────────────────────────  │
     │                                      │
     │  6. Accepted(n=1, value="x=5")       │
     │ ←──────────────────────────────────  │
     │                                      │
     │  (收到多数派 Accepted,值已选定)     │
关键约束

P1:Acceptor 必须接受它收到的第一个提案

P2:如果一个值为 v 的提案被选定,那么所有更高编号的提案也必须包含 v

P2 保证了:一旦选定,不可推翻。


三、核心数据结构

复制代码
// distributed/paxos/types.go
package paxos

import (
    "fmt"
    "sync"
    "time"
)

// Proposal 提案
type Proposal struct {
    Number uint64    // 提案编号(全局唯一、递增)
    Value  interface{} // 提案值
}

// Promise 承诺
type Promise struct {
    Number       uint64      // 承诺的提案编号
    LastAccepted *Proposal   // 该 Acceptor 上次接受的提案(如果有)
}

// Accepted 接受确认
type Accepted struct {
    Number uint64
    Value  interface{}
}

// AcceptorState Acceptor 的状态
type AcceptorState struct {
    mu            sync.RWMutex
    PromisedNum   uint64      // 承诺的最高编号
    AcceptedProposal *Proposal // 已接受的提案
}

// MessageType 消息类型
type MessageType int

const (
    MsgPrepare    MessageType = iota // Prepare 请求
    MsgPromise                       // Promise 响应
    MsgAccept                        // Accept 请求
    MsgAccepted                      // Accepted 响应
    MsgLearn                         // Learn 通知
)

// Message 节点间通信的消息
type Message struct {
    Type     MessageType
    From     string
    To       string
    Number   uint64
    Proposal *Proposal
}

// Node 共识节点
type Node struct {
    ID        string
    Addresses map[string]string // nodeID -> address
    acceptor  *AcceptorState
    proposer  *ProposerState
    learner   *LearnerState
    network   Network
}

// ProposerState Proposer 的状态
type ProposerState struct {
    mu           sync.Mutex
    CurrentNum   uint64
    HighestSeen  uint64
    Value        interface{}
    Promises     int
    Accepts      int
    Phase        int // 0=idle, 1=prepare, 2=accept
}

// LearnerState Learner 的状态
type LearnerState struct {
    mu       sync.Mutex
    Decided  bool
    Value    interface{}
    Accepts  map[string]*Accepted // nodeID -> accepted
}

// Network 网络抽象
type Network interface {
    Send(msg Message) error
    Receive() (Message, error)
    Broadcast(msg Message, targets []string) error
}

四、Basic Paxos 实现

复制代码
// distributed/paxos/basic.go
package paxos

import (
    "fmt"
    "log"
    "math/rand"
    "sync"
    "time"
)

// BasicPaxos Basic Paxos 实现
type BasicPaxos struct {
    nodes      map[string]*Node
    majority   int
    mu         sync.Mutex
    msgQueue   chan Message
    learnCh    chan struct{} // 值选定通知
    decidedVal interface{}
}

func NewBasicPaxos(nodeIDs []string) *BasicPaxos {
    bp := &BasicPaxos{
        nodes:    make(map[string]*Node),
        majority: len(nodeIDs)/2 + 1,
        msgQueue: make(chan Message, 1000),
        learnCh:  make(chan struct{}, 1),
    }

    for _, id := range nodeIDs {
        bp.nodes[id] = &Node{
            ID: id,
            acceptor: &AcceptorState{
                PromisedNum: 0,
            },
            proposer: &ProposerState{
                Phase: 0,
            },
            learner: &LearnerState{
                Accepts: make(map[string]*Accepted),
            },
        }
    }

    // 启动消息处理循环
    go bp.messageLoop()

    return bp
}

// Propose 发起提案
func (bp *BasicPaxos) Propose(value interface{}) (interface{}, error) {
    // 选择一个 Proposer 节点
    proposerID := bp.selectProposer()
    proposer := bp.nodes[proposerID]

    log.Printf("[PAXOS] Node %s proposing value: %v", proposerID, value)

    // 生成提案编号(使用时间戳 + 节点ID后缀保证唯一性)
    number := uint64(time.Now().UnixNano()) + uint64(rand.Intn(1000))

    // 第一阶段:Prepare
    log.Printf("[PAXOS] Phase 1: Prepare(n=%d) from %s", number, proposerID)
    promises := bp.broadcastPrepare(proposerID, number)

    if len(promises) < bp.majority {
        return nil, fmt.Errorf("prepare phase failed: got %d promises, need %d",
            len(promises), bp.majority)
    }

    // 检查是否有已接受的值
    highestNum := uint64(0)
    for _, promise := range promises {
        if promise.LastAccepted != nil && promise.LastAccepted.Number > highestNum {
            highestNum = promise.LastAccepted.Number
            value = promise.LastAccepted.Value
        }
    }

    // 第二阶段:Accept
    log.Printf("[PAXOS] Phase 2: Accept(n=%d, value=%v) from %s", number, value, proposerID)
    accepts := bp.broadcastAccept(proposerID, number, value)

    if len(accepts) < bp.majority {
        return nil, fmt.Errorf("accept phase failed: got %d accepts, need %d",
            len(accepts), bp.majority)
    }

    // 值已选定
    bp.decidedVal = value
    log.Printf("[PAXOS] Value DECIDED: %v (by %s, n=%d)", value, proposerID, number)

    // 通知所有 Learner
    bp.broadcastLearn(proposerID, number, value)

    return value, nil
}

// broadcastPrepare 广播 Prepare 请求
func (bp *BasicPaxos) broadcastPrepare(from string, number uint64) []*Promise {
    var mu sync.Mutex
    promises := make([]*Promise, 0)
    var wg sync.WaitGroup

    for id, node := range bp.nodes {
        if id == from {
            continue
        }

        wg.Add(1)
        go func(nodeID string, n *Node) {
            defer wg.Done()

            n.acceptor.mu.Lock()
            defer n.acceptor.mu.Unlock()

            if number > n.acceptor.PromisedNum {
                n.acceptor.PromisedNum = number
                promise := &Promise{
                    Number:       number,
                    LastAccepted: n.acceptor.AcceptedProposal,
                }

                mu.Lock()
                promises = append(promises, promise)
                mu.Unlock()

                log.Printf("[PAXOS] Node %s PROMISE(n=%d, last=%v)",
                    nodeID, number, promise.LastAccepted)
            } else {
                log.Printf("[PAXOS] Node %s REJECT prepare(n=%d): already promised %d",
                    nodeID, number, n.acceptor.PromisedNum)
            }
        }(id, node)
    }

    wg.Wait()
    return promises
}

// broadcastAccept 广播 Accept 请求
func (bp *BasicPaxos) broadcastAccept(from string, number uint64, value interface{}) []*Accepted {
    var mu sync.Mutex
    accepts := make([]*Accepted, 0)
    var wg sync.WaitGroup

    for id, node := range bp.nodes {
        if id == from {
            continue
        }

        wg.Add(1)
        go func(nodeID string, n *Node) {
            defer wg.Done()

            n.acceptor.mu.Lock()
            defer n.acceptor.mu.Unlock()

            // 只有在没有承诺更高编号时才接受
            if number >= n.acceptor.PromisedNum {
                proposal := &Proposal{Number: number, Value: value}
                n.acceptor.AcceptedProposal = proposal

                accepted := &Accepted{Number: number, Value: value}

                mu.Lock()
                accepts = append(accepts, accepted)
                mu.Unlock()

                log.Printf("[PAXOS] Node %s ACCEPTED(n=%d, value=%v)", nodeID, number, value)
            } else {
                log.Printf("[PAXOS] Node %s REJECT accept(n=%d): promised %d",
                    nodeID, number, n.acceptor.PromisedNum)
            }
        }(id, node)
    }

    wg.Wait()
    return accepts
}

// broadcastLearn 广播 Learn 通知
func (bp *BasicPaxos) broadcastLearn(from string, number uint64, value interface{}) {
    for id, node := range bp.nodes {
        if id == from {
            continue
        }

        node.learner.mu.Lock()
        node.learner.Decided = true
        node.learner.Value = value
        node.learner.mu.Unlock()

        log.Printf("[PAXOS] Node %s LEARNED(value=%v, n=%d)", id, value, number)
    }
}

// selectProposer 选择一个 Proposer
func (bp *BasicPaxos) selectProposer() string {
    ids := make([]string, 0, len(bp.nodes))
    for id := range bp.nodes {
        ids = append(ids, id)
    }
    return ids[rand.Intn(len(ids))]
}

// messageLoop 消息处理循环
func (bp *BasicPaxos) messageLoop() {
    for msg := range bp.msgQueue {
        switch msg.Type {
        case MsgPrepare:
            bp.handlePrepare(msg)
        case MsgPromise:
            bp.handlePromise(msg)
        case MsgAccept:
            bp.handleAccept(msg)
        case MsgAccepted:
            bp.handleAccepted(msg)
        case MsgLearn:
            bp.handleLearn(msg)
        }
    }
}

func (bp *BasicPaxos) handlePrepare(msg Message) {
    node := bp.nodes[msg.To]
    if node == nil {
        return
    }

    node.acceptor.mu.Lock()
    defer node.acceptor.mu.Unlock()

    if msg.Number > node.acceptor.PromisedNum {
        node.acceptor.PromisedNum = msg.Number
        log.Printf("[PAXOS] Node %s handles PREPARE(n=%d): PROMISE", msg.To, msg.Number)
    }
}

func (bp *BasicPaxos) handlePromise(msg Message) {
    // 由 Propose 方法处理
}

func (bp *BasicPaxos) handleAccept(msg Message) {
    node := bp.nodes[msg.To]
    if node == nil {
        return
    }

    node.acceptor.mu.Lock()
    defer node.acceptor.mu.Unlock()

    if msg.Number >= node.acceptor.PromisedNum {
        node.acceptor.AcceptedProposal = msg.Proposal
        log.Printf("[PAXOS] Node %s handles ACCEPT(n=%d): ACCEPTED", msg.To, msg.Number)
    }
}

func (bp *BasicPaxos) handleAccepted(msg Message) {
    // 由 Propose 方法处理
}

func (bp *BasicPaxos) handleLearn(msg Message) {
    node := bp.nodes[msg.To]
    if node == nil {
        return
    }

    node.learner.mu.Lock()
    node.learner.Decided = true
    node.learner.Value = msg.Proposal.Value
    node.learner.mu.Unlock()

    log.Printf("[PAXOS] Node %s LEARNS(value=%v)", msg.To, msg.Proposal.Value)
}

五、活锁问题与解决方案

复制代码
活锁场景:

  Proposer A (n=1)          Acceptor群          Proposer B (n=2)
      │                         │                     │
      │---- Prepare(1) -------->│                     │
      │<------- Promise --------│                     │
      │                         │                     │
      │                         │<--- Prepare(2) -----│
      │                         │------- Promise ---->│
      │                         │                     │
      │---- Accept!(1,v1) ---->│                     │
      │<--- REJECT(promised 2)-│                     │
      │                         │                     │
      │                         │<-- Accept!(2,v2) --│
      │                         │-- REJECT(promised?)│
      │                         │                     │
      │                         │                     │
      │  A 发现被拒,提更大编号 n=3                    │
      │  B 发现被拒,提更大编号 n=4                    │
      │  ... 无限循环下去 ...                          │

解决方案:随机退避 + Leader 选举

复制代码
// distributed/paxos/livelock.go
package paxos

import (
    "math/rand"
    "time"
)

// RandomizedProposer 带随机退避的 Proposer
type RandomizedProposer struct {
    baseBackoff time.Duration
    maxBackoff  time.Duration
}

func NewRandomizedProposer() *RandomizedProposer {
    return &RandomizedProposer{
        baseBackoff: 10 * time.Millisecond,
        maxBackoff:  5 * time.Second,
    }
}

// ProposeWithBackoff 带退避的提案
func (rp *RandomizedProposer) ProposeWithBackoff(paxos *BasicPaxos, value interface{}, attempt int) (interface{}, error) {
    for attempt <= 5 {
        result, err := paxos.Propose(value)
        if err == nil {
            return result, nil
        }

        // 随机退避
        backoff := rp.baseBackoff * time.Duration(1<<uint(attempt))
        jitter := time.Duration(rand.Int63n(int64(backoff)))
        totalBackoff := backoff + jitter

        if totalBackoff > rp.maxBackoff {
            totalBackoff = rp.maxBackoff
        }

        log.Printf("[PAXOS] Attempt %d failed, backing off %v", attempt, totalBackoff)
        time.Sleep(totalBackoff)
        attempt++
    }

    return nil, fmt.Errorf("proposal failed after %d attempts", attempt)
}

六、Multi-Paxos 优化思路

Basic Paxos 每次提案都需要两轮 RTT,性能很差。Multi-Paxos 的优化:

  1. 选一个稳定的 Leader:只有 Leader 才能发起 Prepare

  2. 一轮 Prepare 管多次 Accept:Leader 上任时跑一次 Prepare,后续 Accept 直接用这个编号

  3. 日志复制:把 Paxos 应用到日志条目序列上

    Basic Paxos(每条日志两轮 RTT):
    Prepare(1) → Promise → Accept!(1, v1) → Accepted → 日志条目1
    Prepare(2) → Promise → Accept!(2, v2) → Accepted → 日志条目2
    Prepare(3) → Promise → Accept!(3, v3) → Accepted → 日志条目3

    Multi-Paxos(一轮 Prepare 管所有日志):
    Prepare(1) → Promise → 领导权确立
    Accept!(1, v1) → Accepted → 日志条目1
    Accept!(2, v2) → Accepted → 日志条目2
    Accept!(3, v3) → Accepted → 日志条目3
    (后续 Accept 只需要一轮 RTT)


七、生产坑位

# 坑位 现象 解决方案
1 活锁​ 多个 Proposer 互相抢占,永远达不成共识 随机退避;Leader 选举
2 编号冲突​ 不同节点的提案编号重复 编号 = 时间戳高位 + 节点ID低位
3 Acceptor 持久化缺失​ 宕机后忘记已承诺的编号,导致一致性破坏 WAL 日志持久化 PromisedNum 和 AcceptedProposal
4 网络分区下的双 Leader​ 两个分区各自选出 Leader,都认为自己能提交 多数派原则:只有拿到多数派 Promise 的 Leader 才能提交
5 幽灵复现​ 旧 Leader 的提案在网络延迟后在任期外被接受 任期号机制(Raft 的做法);逻辑时钟验证
6 性能瓶颈​ 所有提案经过单一 Leader 成为瓶颈 并行 Paxos(不同日志条目并行);分片

八、本讲总结

复制代码
┌──────────────────────────────────────────────────────────────────────────┐
│                    第2讲 核心收获                                          │
│                                                                           │
│  ✅ 理解了 Paxos 的核心思想:                                              │
│     - 两阶段提交(Prepare → Promise → Accept → Accepted)                │
│     - 多数派原则保证一致性                                                │
│     - 一旦选定,不可更改(P2 约束)                                      │
│                                                                           │
│  ✅ 实现了 Basic Paxos:                                                   │
│     - Proposer/Acceptor/Learner 三种角色                                  │
│     - 提案编号生成(时间戳 + 随机数)                                    │
│     - 完整的 Prepare/Accept/Learn 流程                                    │
│                                                                           │
│  ✅ 理解了活锁问题及其解决方案:                                           │
│     - 随机退避                                                            │
│     - Leader 选举                                                        │
│                                                                           │
│  ✅ 理解了 Multi-Paxos 的优化思路:                                       │
│     - 稳定 Leader 减少 Prepare 次数                                       │
│     - 一轮 Prepare 管多次 Accept                                          │
│                                                                           │
│  ✅ 覆盖了 6 个生产坑位与解决方案                                         │
│                                                                           │
└──────────────────────────────────────────────────────────────────────────┘

九、下一讲预告

第3讲:《工业级共识:Raft 算法》

你将学到:

  • Raft 的三个子问题:Leader 选举、日志复制、安全性
  • 任期(Term)与心跳机制
  • 从零实现 Raft 核心逻辑(约 600 行 Go)
  • Raft 与 Paxos 的对比
  • etcd 中的 Raft 实践

🧰 开发之余的小工具推荐

在调试 Paxos 协议的活锁问题时,zz365.top 的文本差异比较工具可以快速对比不同 Proposer 的提案编号序列,帮助定位编号冲突和抢占模式。其 JSON 格式化工具则可以将节点状态导出为结构化数据,便于分析 Acceptor 的承诺链。所有处理均在浏览器本地完成,不会泄露你的协议数据。

相关推荐
攻城有术19 小时前
DeepSeek V4.1 Flash 1017 落地场景
ai
紧跟先前的步伐1 天前
Go AI Agent 实战 28|Agent 的预算、超时与停止条件
开发语言·人工智能·后端·ai·golang
艺杯羹1 天前
MCP本地服务集成避坑:从Stdio管道僵死到双向JSON-RPC稳健通信
人工智能·python·ai·系统架构·大语言模型·ai编程·mcp
xyzzklk1 天前
在Open-WebUI中提升向量化速度
网络·人工智能·爬虫·ai·语言模型·自然语言处理·安卓
又过二十1 天前
rea-video拆解视频测试和效果展示
ai·aigc
看浪的路人1 天前
番外1:Eval 工程化——从“跑分”到“防回归”
ai
XLYcmy1 天前
信息检索报告数字经济赋能城市低碳治理的路径研究——基于“传统数据库 + 生成式人工智能”双轮驱动的检索实践 检索实施与结果分析 Web of Science
数据库·ai·llm·prompt·ssci·deepseek·wos
AI小百科1 天前
AI 进游戏:游戏里的 NPC 开始有自己的性格
人工智能·游戏·ai
小lu飞2 天前
2026小红书小工具怎么制作:纯前端 ZIP 打包、沙箱限制与笔记挂载流程
前端·笔记·ai·ai编程