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


一、Raft 的设计哲学

Paxos 虽然被证明正确,但极其难懂、难以实现。Raft 的目标是 可理解性------把共识问题拆解成三个相对独立的子问题:

复制代码
Raft 的三个子问题
┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────┐  │
│  │  Leader 选举     │  │  日志复制        │  │  安全性      │  │
│  │                  │  │                  │  │              │  │
│  │  • 任期 Term     │  │  • AppendEntries │  │  • 选举限制  │  │
│  │  • 心跳          │  │  • 日志匹配      │  │  • 提交规则  │  │
│  │  • 超时随机化    │  │  • 批量与管道    │  │  • 领导者唯⼀ │  │
│  └─────────────────┘  └─────────────────┘  └─────────────┘  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

与 Paxos 的关键区别:

特性 Paxos Raft
角色 Proposer/Acceptor/Learner Leader/Follower/Candidate
领导者 无固定 Leader,多 Proposer 竞争 强 Leader,所有写入经过 Leader
日志连续性 不保证连续 强制连续(日志空洞不允许)
成员变更 复杂(需要联合共识) 简单(两阶段成员变更)
可理解性 极差 较好(论文被评为最易理解的共识算法)

二、节点状态与角色转换

复制代码
┌─────────────┐
                      │             │
         ┌───────────│  Follower   │◄──────────┐
         │           │             │           │
         │           └──────┬──────┘           │
         │                  │                  │
         │     选举超时      │  发现更高任期     │
         │    (无心跳)       │                  │
         │                  ▼                  │
         │           ┌─────────────┐           │
         │           │             │           │
         ├──────────►│  Candidate  │───────────┤
         │           │             │  选举失败   │
         │           └──────┬──────┘           │
         │                  │                  │
         │            获得多数票                │
         │                  │                  │
         │                  ▼                  │
         │           ┌─────────────┐           │
         │           │             │           │
         └───────────│   Leader    │───────────┘
                     │             │  崩溃/网络分区
                     └─────────────┘

核心规则:

  • 每个节点初始为 Follower
  • 收不到 Leader 心跳 → 变成 Candidate,发起选举
  • 获得多数派选票 → 成为 Leader
  • 发现更高任期的节点 → 立即降级为 Follower

三、核心数据结构

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

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

// NodeRole 节点角色
type NodeRole int

const (
    RoleFollower  NodeRole = iota
    RoleCandidate
    RoleLeader
)

func (r NodeRole) String() string {
    switch r {
    case RoleFollower:
        return "Follower"
    case RoleCandidate:
        return "Candidate"
    case RoleLeader:
        return "Leader"
    default:
        return "Unknown"
    }
}

// LogEntry 日志条目
type LogEntry struct {
    Index   uint64      `json:"index"`
    Term    uint64      `json:"term"`
    Command interface{} `json:"command"`
}

// RaftState Raft 节点状态
type RaftState struct {
    mu sync.RWMutex

    // 持久化状态(所有节点)
    CurrentTerm uint64      `json:"current_term"` // 当前任期
    VotedFor    string      `json:"voted_for"`    // 当前任期投给谁
    Log         []*LogEntry `json:"log"`          // 日志条目集合

    // 易失状态(所有节点)
    CommitIndex uint64 `json:"commit_index"` // 已提交的最大索引
    LastApplied uint64 `json:"last_applied"` // 已应用到状态机的最大索引

    // 易失状态(Leader 专用)
    NextIndex  map[string]uint64 `json:"next_index"`  // 发给每个 Follower 的下一条日志索引
    MatchIndex map[string]uint64 `json:"match_index"` // 已复制到每个 Follower 的最高日志索引
}

// RequestVoteRequest 请求投票 RPC
type RequestVoteRequest struct {
    Term         uint64
    CandidateID  string
    LastLogIndex uint64
    LastLogTerm  uint64
}

// RequestVoteResponse 投票响应
type RequestVoteResponse struct {
    Term        uint64
    VoteGranted bool
}

// AppendEntriesRequest 追加日志 RPC
type AppendEntriesRequest struct {
    Term         uint64
    LeaderID     string
    PrevLogIndex uint64
    PrevLogTerm  uint64
    Entries      []*LogEntry
    LeaderCommit uint64
}

// AppendEntriesResponse 追加日志响应
type AppendEntriesResponse struct {
    Term    uint64
    Success bool
    // 优化:快速回退
    ConflictTerm  uint64
    ConflictIndex uint64
}

// RaftNode Raft 节点
type RaftNode struct {
    ID         string
    PeerIDs    []string
    State      *RaftState
    Role       NodeRole
    Transport  Transport

    // 计时器
    ElectionTimer    *time.Timer
    HeartbeatTimer   *time.Timer
    ElectionTimeout  time.Duration
    HeartbeatTimeout time.Duration

    // 通道
    ApplyCh chan *LogEntry // 应用到状态机的通道

    // 内部
    stopCh chan struct{}
    doneCh chan struct{}
}

// Transport 网络传输抽象
type Transport interface {
    SendRequestVote(target string, req *RequestVoteRequest) (*RequestVoteResponse, error)
    SendAppendEntries(target string, req *AppendEntriesRequest) (*AppendEntriesResponse, error)
    BroadcastRequestVote(req *RequestVoteRequest) map[string]*RequestVoteResponse
}

四、Leader 选举实现

复制代码
// distributed/raft/election.go
package raft

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

// StartElection 开始选举
func (rn *RaftNode) StartElection() {
    rn.State.mu.Lock()

    // 转为 Candidate
    rn.Role = RoleCandidate
    rn.State.CurrentTerm++
    rn.State.VotedFor = rn.ID

    term := rn.State.CurrentTerm
    lastLogIndex := uint64(0)
    lastLogTerm := uint64(0)

    if len(rn.State.Log) > 0 {
        lastLogIndex = uint64(len(rn.State.Log))
        lastLogTerm = rn.State.Log[len(rn.State.Log)-1].Term
    }

    rn.State.mu.Unlock()

    log.Printf("[RAFT] Node %s starting election for term %d", rn.ID, term)

    // 构造投票请求
    req := &RequestVoteRequest{
        Term:         term,
        CandidateID:  rn.ID,
        LastLogIndex: lastLogIndex,
        LastLogTerm:  lastLogTerm,
    }

    // 向所有节点请求投票
    votes := 1 // 自己投自己
    responses := rn.Transport.BroadcastRequestVote(req)

    for peerID, resp := range responses {
        rn.State.mu.Lock()

        // 如果发现更高任期,降级
        if resp.Term > rn.State.CurrentTerm {
            rn.State.CurrentTerm = resp.Term
            rn.Role = RoleFollower
            rn.State.VotedFor = ""
            rn.State.mu.Unlock()
            log.Printf("[RAFT] Node %s stepped down: higher term %d", rn.ID, resp.Term)
            return
        }

        if resp.VoteGranted {
            votes++
            log.Printf("[RAFT] Node %s granted vote from %s (votes=%d/%d)",
                rn.ID, peerID, votes, len(rn.PeerIDs)+1)
        } else {
            log.Printf("[RAFT] Node %s denied vote from %s", rn.ID, peerID)
        }

        rn.State.mu.Unlock()
    }

    // 检查是否获得多数票
    majority := len(rn.PeerIDs)/2 + 1
    if votes >= majority {
        rn.State.mu.Lock()
        rn.Role = RoleLeader
        rn.State.mu.Unlock()

        log.Printf("[RAFT] Node %s ELECTED as leader for term %d (votes=%d/%d)",
            rn.ID, term, votes, len(rn.PeerIDs)+1)

        // 初始化 Leader 状态
        rn.initLeaderState()

        // 停止选举计时器,启动心跳计时器
        rn.ElectionTimer.Stop()
        rn.sendHeartbeats()
    } else {
        log.Printf("[RAFT] Node %s election FAILED for term %d (votes=%d, need %d)",
            rn.ID, term, votes, majority)
    }
}

// initLeaderState 初始化 Leader 状态
func (rn *RaftNode) initLeaderState() {
    rn.State.mu.Lock()
    defer rn.State.mu.Unlock()

    nextIndex := uint64(len(rn.State.Log)) + 1
    rn.State.NextIndex = make(map[string]uint64)
    rn.State.MatchIndex = make(map[string]uint64)

    for _, peerID := range rn.PeerIDs {
        rn.State.NextIndex[peerID] = nextIndex
        rn.State.MatchIndex[peerID] = 0
    }
}

// resetElectionTimer 重置选举超时(随机化)
func (rn *RaftNode) resetElectionTimer() {
    // 随机化超时时间:150-300ms
    timeout := time.Duration(150+rand.Intn(150)) * time.Millisecond
    rn.ElectionTimeout = timeout
    rn.ElectionTimer.Reset(timeout)
}

// HandleRequestVote 处理投票请求
func (rn *RaftNode) HandleRequestVote(req *RequestVoteRequest) *RequestVoteResponse {
    rn.State.mu.Lock()
    defer rn.State.mu.Unlock()

    resp := &RequestVoteResponse{
        Term:        rn.State.CurrentTerm,
        VoteGranted: false,
    }

    // 1. 如果请求的任期小于当前任期,拒绝
    if req.Term < rn.State.CurrentTerm {
        log.Printf("[RAFT] Node %s rejects vote: req term %d < current term %d",
            rn.ID, req.Term, rn.State.CurrentTerm)
        return resp
    }

    // 2. 如果请求的任期大于当前任期,更新并降级
    if req.Term > rn.State.CurrentTerm {
        rn.State.CurrentTerm = req.Term
        rn.Role = RoleFollower
        rn.State.VotedFor = ""
    }

    // 3. 检查是否已经投票
    if rn.State.VotedFor != "" && rn.State.VotedFor != req.CandidateID {
        log.Printf("[RAFT] Node %s already voted for %s in term %d",
            rn.ID, rn.State.VotedFor, req.Term)
        return resp
    }

    // 4. 检查候选人的日志是否至少和自己一样新
    lastLogIndex := uint64(0)
    lastLogTerm := uint64(0)
    if len(rn.State.Log) > 0 {
        lastLogIndex = uint64(len(rn.State.Log))
        lastLogTerm = rn.State.Log[len(rn.State.Log)-1].Term
    }

    upToDate := false
    if req.LastLogTerm > lastLogTerm {
        upToDate = true
    } else if req.LastLogTerm == lastLogTerm && req.LastLogIndex >= lastLogIndex {
        upToDate = true
    }

    if !upToDate {
        log.Printf("[RAFT] Node %s rejects vote: candidate log not up-to-date (candidate: term=%d index=%d, me: term=%d index=%d)",
            rn.ID, req.LastLogTerm, req.LastLogIndex, lastLogTerm, lastLogIndex)
        return resp
    }

    // 投票给候选人
    rn.State.VotedFor = req.CandidateID
    resp.VoteGranted = true

    // 重置选举超时(收到合法候选人的请求,说明有其他节点也在选举)
    rn.resetElectionTimer()

    log.Printf("[RAFT] Node %s GRANTS vote to %s for term %d", rn.ID, req.CandidateID, req.Term)
    return resp
}

五、日志复制实现

复制代码
// distributed/raft/replication.go
package raft

import (
    "log"
    "sync"
    "time"
)

// Propose 客户端提案(Leader 专用)
func (rn *RaftNode) Propose(command interface{}) error {
    rn.State.mu.Lock()

    if rn.Role != RoleLeader {
        rn.State.mu.Unlock()
        return fmt.Errorf("not the leader")
    }

    // 追加到本地日志
    entry := &LogEntry{
        Index: uint64(len(rn.State.Log)) + 1,
        Term:  rn.State.CurrentTerm,
        Command: command,
    }
    rn.State.Log = append(rn.State.Log, entry)
    logIndex := entry.Index

    rn.State.mu.Unlock()

    log.Printf("[RAFT] Leader %s proposed command at index %d", rn.ID, logIndex)

    // 并行复制到所有 Follower
    rn.replicateLog()

    return nil
}

// replicateLog 复制日志到所有 Follower
func (rn *RaftNode) replicateLog() {
    var wg sync.WaitGroup

    for _, peerID := range rn.PeerIDs {
        wg.Add(1)
        go func(peer string) {
            defer wg.Done()
            rn.replicateToPeer(peer)
        }(peerID)
    }

    wg.Wait()
}

// replicateToPeer 复制日志到单个 Follower
func (rn *RaftNode) replicateToPeer(peerID string) {
    rn.State.mu.Lock()

    // 获取要发送的日志条目
    nextIndex := rn.State.NextIndex[peerID]
    if nextIndex > uint64(len(rn.State.Log))+1 {
        nextIndex = uint64(len(rn.State.Log)) + 1
    }

    prevLogIndex := nextIndex - 1
    prevLogTerm := uint64(0)

    if prevLogIndex > 0 && prevLogIndex <= uint64(len(rn.State.Log)) {
        prevLogTerm = rn.State.Log[prevLogIndex-1].Term
    }

    var entries []*LogEntry
    if nextIndex <= uint64(len(rn.State.Log)) {
        entries = rn.State.Log[nextIndex-1:]
    }

    leaderCommit := rn.State.CommitIndex
    term := rn.State.CurrentTerm

    rn.State.mu.Unlock()

    // 如果没有要复制的日志,发送心跳
    if len(entries) == 0 {
        rn.sendHeartbeat(peerID)
        return
    }

    // 构造 AppendEntries 请求
    req := &AppendEntriesRequest{
        Term:         term,
        LeaderID:     rn.ID,
        PrevLogIndex: prevLogIndex,
        PrevLogTerm:  prevLogTerm,
        Entries:      entries,
        LeaderCommit: leaderCommit,
    }

    // 发送并处理响应
    resp, err := rn.Transport.SendAppendEntries(peerID, req)
    if err != nil {
        log.Printf("[RAFT] Failed to send AppendEntries to %s: %v", peerID, err)
        return
    }

    rn.State.mu.Lock()
    defer rn.State.mu.Unlock()

    // 如果发现更高任期,降级
    if resp.Term > rn.State.CurrentTerm {
        rn.State.CurrentTerm = resp.Term
        rn.Role = RoleFollower
        rn.State.VotedFor = ""
        log.Printf("[RAFT] Leader %s stepped down: higher term %d", rn.ID, resp.Term)
        return
    }

    if resp.Success {
        // 更新 MatchIndex 和 NextIndex
        matchIndex := prevLogIndex + uint64(len(entries))
        if matchIndex > rn.State.MatchIndex[peerID] {
            rn.State.MatchIndex[peerID] = matchIndex
        }
        rn.State.NextIndex[peerID] = matchIndex + 1

        log.Printf("[RAFT] Leader %s: peer %s replicated up to index %d",
            rn.ID, peerID, matchIndex)

        // 尝试推进 commitIndex
        rn.advanceCommitIndex()
    } else {
        // 日志不一致,回退 NextIndex
        if resp.ConflictTerm != 0 {
            // 找到冲突 term 的最后一条日志
            lastIndexInConflictTerm := uint64(0)
            for i := len(rn.State.Log) - 1; i >= 0; i-- {
                if rn.State.Log[i].Term == resp.ConflictTerm {
                    lastIndexInConflictTerm = uint64(i) + 1
                    break
                }
            }

            if lastIndexInConflictTerm > 0 {
                rn.State.NextIndex[peerID] = lastIndexInConflictTerm
            } else {
                rn.State.NextIndex[peerID] = resp.ConflictIndex
            }
        } else {
            rn.State.NextIndex[peerID] = resp.ConflictIndex
        }

        log.Printf("[RAFT] Leader %s: peer %s log mismatch, retrying at index %d",
            rn.ID, peerID, rn.State.NextIndex[peerID])
    }
}

// advanceCommitIndex 尝试推进提交索引
func (rn *RaftNode) advanceCommitIndex() {
    // 找出能被多数派复制的最高日志索引
    matchIndices := make([]uint64, 0, len(rn.PeerIDs)+1)
    matchIndices = append(matchIndices, uint64(len(rn.State.Log))) // Leader 自己的日志

    for _, matchIndex := range rn.State.MatchIndex {
        matchIndices = append(matchIndices, matchIndex)
    }

    // 排序取中位数
    sort.Slice(matchIndices, func(i, j int) bool {
        return matchIndices[i] < matchIndices[j]
    })

    majorityIndex := matchIndices[len(matchIndices)/2]

    // 只能提交当前任期的日志
    if majorityIndex > rn.State.CommitIndex &&
        rn.State.Log[majorityIndex-1].Term == rn.State.CurrentTerm {
        rn.State.CommitIndex = majorityIndex
        log.Printf("[RAFT] Leader %s advanced commit index to %d", rn.ID, majorityIndex)

        // 应用到状态机
        rn.applyLogEntries()
    }
}

// applyLogEntries 应用已提交的日志到状态机
func (rn *RaftNode) applyLogEntries() {
    for rn.State.LastApplied < rn.State.CommitIndex {
        rn.State.LastApplied++
        entry := rn.State.Log[rn.State.LastApplied-1]

        // 发送到应用通道
        select {
        case rn.ApplyCh <- entry:
            log.Printf("[RAFT] Applied entry index=%d term=%d command=%v",
                entry.Index, entry.Term, entry.Command)
        default:
            log.Printf("[RAFT] Apply channel full, dropping entry %d", entry.Index)
        }
    }
}

// HandleAppendEntries 处理 AppendEntries RPC
func (rn *RaftNode) HandleAppendEntries(req *AppendEntriesRequest) *AppendEntriesResponse {
    rn.State.mu.Lock()
    defer rn.State.mu.Unlock()

    resp := &AppendEntriesResponse{
        Term:    rn.State.CurrentTerm,
        Success: false,
    }

    // 1. 任期检查
    if req.Term < rn.State.CurrentTerm {
        return resp
    }

    // 2. 如果请求的任期大于等于当前任期,承认 Leader
    if req.Term >= rn.State.CurrentTerm {
        rn.State.CurrentTerm = req.Term
        rn.Role = RoleFollower
        rn.State.VotedFor = ""
    }

    // 3. 重置选举超时(收到合法 Leader 的心跳)
    rn.resetElectionTimer()

    // 4. 检查日志匹配
    if req.PrevLogIndex > 0 {
        if req.PrevLogIndex > uint64(len(rn.State.Log)) {
            // 日志不够长
            resp.ConflictIndex = uint64(len(rn.State.Log)) + 1
            resp.ConflictTerm = 0
            return resp
        }

        if rn.State.Log[req.PrevLogIndex-1].Term != req.PrevLogTerm {
            // 日志 term 不匹配
            resp.ConflictTerm = rn.State.Log[req.PrevLogIndex-1].Term
            for i := req.PrevLogIndex - 1; i > 0; i-- {
                if rn.State.Log[i-1].Term != resp.ConflictTerm {
                    resp.ConflictIndex = i + 1
                    break
                }
            }
            if resp.ConflictIndex == 0 {
                resp.ConflictIndex = 1
            }
            return resp
        }
    }

    // 5. 追加新日志(删除冲突的)
    logInsertIndex := req.PrevLogIndex
    for i, entry := range req.Entries {
        if logInsertIndex+uint64(i)+1 <= uint64(len(rn.State.Log)) {
            // 检查已有日志是否冲突
            if rn.State.Log[logInsertIndex+uint64(i)].Term != entry.Term {
                // 截断冲突日志
                rn.State.Log = rn.State.Log[:logInsertIndex+uint64(i)]
                rn.State.Log = append(rn.State.Log, entry)
            }
        } else {
            // 追加新日志
            rn.State.Log = append(rn.State.Log, entry)
        }
    }

    // 6. 更新 commitIndex
    if req.LeaderCommit > rn.State.CommitIndex {
        lastNewIndex := req.PrevLogIndex + uint64(len(req.Entries))
        if req.LeaderCommit < lastNewIndex {
            rn.State.CommitIndex = req.LeaderCommit
        } else {
            rn.State.CommitIndex = lastNewIndex
        }

        // 应用到状态机
        rn.applyLogEntries()
    }

    resp.Success = true
    return resp
}

// sendHeartbeats 发送心跳到所有 Follower
func (rn *RaftNode) sendHeartbeats() {
    for _, peerID := range rn.PeerIDs {
        rn.sendHeartbeat(peerID)
    }
}

// sendHeartbeat 发送心跳到单个 Follower
func (rn *RaftNode) sendHeartbeat(peerID string) {
    rn.State.mu.Lock()

    prevLogIndex := uint64(0)
    prevLogTerm := uint64(0)

    if len(rn.State.Log) > 0 {
        prevLogIndex = uint64(len(rn.State.Log))
        prevLogTerm = rn.State.Log[len(rn.State.Log)-1].Term
    }

    req := &AppendEntriesRequest{
        Term:         rn.State.CurrentTerm,
        LeaderID:     rn.ID,
        PrevLogIndex: prevLogIndex,
        PrevLogTerm:  prevLogTerm,
        Entries:      nil, // 心跳没有日志条目
        LeaderCommit: rn.State.CommitIndex,
    }

    rn.State.mu.Unlock()

    go func() {
        resp, err := rn.Transport.SendAppendEntries(peerID, req)
        if err != nil {
            return
        }

        rn.State.mu.Lock()
        if resp.Term > rn.State.CurrentTerm {
            rn.State.CurrentTerm = resp.Term
            rn.Role = RoleFollower
            rn.State.VotedFor = ""
        }
        rn.State.mu.Unlock()
    }()
}

六、Raft 节点主循环

复制代码
// distributed/raft/node.go
package raft

import (
    "log"
    "time"
)

// Run 启动 Raft 节点
func (rn *RaftNode) Run() {
    log.Printf("[RAFT] Node %s starting (role=%s, peers=%v)", rn.ID, rn.Role, rn.PeerIDs)

    // 初始化选举超时
    rn.resetElectionTimer()

    for {
        select {
        case <-rn.ElectionTimer.C:
            // 选举超时,发起选举
            rn.State.mu.Lock()
            role := rn.Role
            rn.State.mu.Unlock()

            if role != RoleLeader {
                log.Printf("[RAFT] Node %s election timeout, starting election", rn.ID)
                rn.StartElection()
            }

        case <-rn.stopCh:
            log.Printf("[RAFT] Node %s stopping", rn.ID)
            close(rn.doneCh)
            return
        }
    }
}

// Stop 停止 Raft 节点
func (rn *RaftNode) Stop() {
    close(rn.stopCh)
    <-rn.doneCh
}

七、使用示例

复制代码
// examples/raft_demo.go
package main

import (
    "fmt"
    "log"
    "time"

    "distributed/raft"
)

// SimpleTransport 简单的内存传输实现
type SimpleTransport struct {
    nodes map[string]*raft.RaftNode
}

func (t *SimpleTransport) SendRequestVote(target string, req *raft.RequestVoteRequest) (*raft.RequestVoteResponse, error) {
    node, exists := t.nodes[target]
    if !exists {
        return nil, fmt.Errorf("node %s not found", target)
    }
    return node.HandleRequestVote(req), nil
}

func (t *SimpleTransport) SendAppendEntries(target string, req *raft.AppendEntriesRequest) (*raft.AppendEntriesResponse, error) {
    node, exists := t.nodes[target]
    if !exists {
        return nil, fmt.Errorf("node %s not found", target)
    }
    return node.HandleAppendEntries(req), nil
}

func (t *SimpleTransport) BroadcastRequestVote(req *raft.RequestVoteRequest) map[string]*raft.RequestVoteResponse {
    responses := make(map[string]*raft.RequestVoteResponse)
    for id, node := range t.nodes {
        if id == req.CandidateID {
            continue
        }
        responses[id] = node.HandleRequestVote(req)
    }
    return responses
}

func main() {
    fmt.Println("=== Raft 共识算法演示 ===\n")

    // 1. 创建 3 个节点
    nodeIDs := []string{"node-1", "node-2", "node-3"}
    nodes := make(map[string]*raft.RaftNode)
    transport := &SimpleTransport{nodes: nodes}

    for _, id := range nodeIDs {
        node := &raft.RaftNode{
            ID:      id,
            PeerIDs: nodeIDs,
            State: &raft.RaftState{
                CurrentTerm: 0,
                VotedFor:    "",
                Log:         make([]*raft.LogEntry, 0),
                CommitIndex: 0,
                LastApplied: 0,
                NextIndex:   make(map[string]uint64),
                MatchIndex:  make(map[string]uint64),
            },
            Role:             raft.RoleFollower,
            Transport:        transport,
            ElectionTimer:    time.NewTimer(time.Hour), // 稍后重置
            HeartbeatTimer:   time.NewTimer(time.Hour),
            HeartbeatTimeout: 50 * time.Millisecond,
            ApplyCh:          make(chan *raft.LogEntry, 100),
            stopCh:           make(chan struct{}),
            doneCh:           make(chan struct{}),
        }
        nodes[id] = node
    }

    // 更新 transport 引用
    transport.nodes = nodes

    // 2. 启动所有节点
    for _, node := range nodes {
        go node.Run()
    }

    // 3. 等待选举完成
    fmt.Println("等待 Leader 选举...")
    time.Sleep(2 * time.Second)

    // 4. 找到 Leader
    var leader *raft.RaftNode
    for _, node := range nodes {
        node.State.mu.RLock()
        if node.Role == raft.RoleLeader {
            leader = node
            fmt.Printf("Leader elected: %s (term %d)\n", node.ID, node.State.CurrentTerm)
        }
        node.State.mu.RUnlock()
    }

    if leader == nil {
        log.Fatal("No leader elected!")
    }

    // 5. 通过 Leader 提交提案
    fmt.Println("\n=== 提交提案 ===")
    commands := []string{"set x=1", "set y=2", "set z=3"}

    for _, cmd := range commands {
        fmt.Printf("Proposing: %s\n", cmd)
        if err := leader.Propose(cmd); err != nil {
            log.Printf("Propose error: %v", err)
        }
        time.Sleep(100 * time.Millisecond)
    }

    // 6. 查看各节点的日志状态
    fmt.Println("\n=== 节点日志状态 ===")
    for _, node := range nodes {
        node.State.mu.RLock()
        fmt.Printf("Node %s (role=%s):\n", node.ID, node.Role)
        fmt.Printf("  Term: %d\n", node.State.CurrentTerm)
        fmt.Printf("  Log length: %d\n", len(node.State.Log))
        fmt.Printf("  CommitIndex: %d\n", node.State.CommitIndex)
        fmt.Printf("  LastApplied: %d\n", node.State.LastApplied)
        for i, entry := range node.State.Log {
            fmt.Printf("  Log[%d]: term=%d command=%v\n", i+1, entry.Term, entry.Command)
        }
        node.State.mu.RUnlock()
    }

    // 7. 模拟 Leader 宕机
    fmt.Println("\n=== 模拟 Leader 宕机 ===")
    fmt.Printf("Stopping leader %s...\n", leader.ID)
    leader.Stop()
    delete(nodes, leader.ID)
    delete(transport.nodes, leader.ID)

    // 更新其他节点的 PeerIDs
    remainingIDs := make([]string, 0, len(nodes))
    for id := range nodes {
        remainingIDs = append(remainingIDs, id)
    }
    for _, node := range nodes {
        node.PeerIDs = remainingIDs
    }

    // 等待新 Leader 选举
    fmt.Println("等待新 Leader 选举...")
    time.Sleep(3 * time.Second)

    // 8. 找到新 Leader
    var newLeader *raft.RaftNode
    for _, node := range nodes {
        node.State.mu.RLock()
        if node.Role == raft.RoleLeader {
            newLeader = node
            fmt.Printf("New leader elected: %s (term %d)\n", node.ID, node.State.CurrentTerm)
        }
        node.State.mu.RUnlock()
    }

    if newLeader != nil {
        // 通过新 Leader 继续提案
        fmt.Println("\n=== 通过新 Leader 提交提案 ===")
        newCommands := []string{"set a=10", "set b=20"}
        for _, cmd := range newCommands {
            fmt.Printf("Proposing: %s\n", cmd)
            if err := newLeader.Propose(cmd); err != nil {
                log.Printf("Propose error: %v", err)
            }
            time.Sleep(100 * time.Millisecond)
        }

        // 查看最终状态
        fmt.Println("\n=== 最终节点状态 ===")
        for _, node := range nodes {
            node.State.mu.RLock()
            fmt.Printf("Node %s (role=%s): log=%d entries\n",
                node.ID, node.Role, len(node.State.Log))
            node.State.mu.RUnlock()
        }
    }

    // 9. 清理
    for _, node := range nodes {
        node.Stop()
    }

    fmt.Println("\n演示完成!")
}

八、生产坑位

# 坑位 现象 解决方案
1 选举超时设置不当​ 频繁选举或选举迟迟无法完成 超时时间随机化(150-300ms);根据网络延迟调整
2 PreVote 缺失​ 网络抖动导致 Leader 频繁被罢免 实现 PreVote 机制:Candidate 先问大家能不能选
3 日志空洞​ 日志索引不连续导致一致性破坏 Raft 强制日志连续,不允许空洞
4 快照未及时压缩​ 日志无限增长,重启恢复慢 定期生成快照;安装快照 RPC
5 只读请求也走 Raft​ 读请求也走一遍共识,性能差 ReadIndex / LeaseRead 优化
6 成员变更时脑裂​ 新旧配置同时生效 联合共识(Joint Consensus);单节点变更

九、Raft vs Paxos 对比

维度 Paxos Raft
理解难度 极高 中等
实现复杂度 ~2000 行 ~600 行(核心)
领导者 可选(Multi-Paxos 有 Leader) 强 Leader
日志连续性 不保证 强制连续
成员变更 联合共识(复杂) 单节点变更(简单)
工业应用 Google Chubby etcd, Consul, TiKV, MongoDB
教学价值 理论基石 工程实践

十、本讲总结

复制代码
┌──────────────────────────────────────────────────────────────────────────┐
│                    第3讲 核心收获                                          │
│                                                                           │
│  ✅ 理解了 Raft 的三个子问题:                                             │
│     - Leader 选举(任期、心跳、随机超时)                                  │
│     - 日志复制(AppendEntries、日志匹配、提交规则)                       │
│     - 安全性(选举限制、领导者唯一性)                                    │
│                                                                           │
│  ✅ 实现了 Raft 核心逻辑:                                                 │
│     - 节点角色转换(Follower → Candidate → Leader)                      │
│     - 选举流程(RequestVote RPC)                                         │
│     - 日志复制(AppendEntries RPC + 快速回退)                            │
│     - 提交与应用到状态机                                                  │
│                                                                           │
│  ✅ 通过实验验证了:                                                       │
│     - Leader 宕机后自动选举出新 Leader                                    │
│     - 新 Leader 继承旧 Leader 的日志                                      │
│     - 所有节点最终日志一致                                                │
│                                                                           │
│  ✅ 覆盖了 6 个生产坑位与解决方案                                         │
│  ✅ 对比了 Raft 与 Paxos 的异同                                          │
│                                                                           │
└──────────────────────────────────────────────────────────────────────────┘

十一、下一讲预告

第4讲:《分布式存储基石:复制状态机》

你将学到:

  • 状态机复制的核心原理
  • WAL(Write-Ahead Log)实现
  • 状态机快照(Snapshot)
  • 线性一致读(ReadIndex / LeaseRead)
  • 从零实现一个 KV 存储引擎

🧰 开发之余的小工具推荐

在调试 Raft 集群的日志复制一致性时,zz365.top 的文本差异比较工具可以快速对比不同节点的日志序列,精确定位日志冲突的位置和原因。其 JSON 格式化工具则可以将 Raft 节点状态导出为结构化数据,便于分析选举过程中的投票分布。所有处理均在浏览器本地完成,不会泄露你的集群数据。

相关推荐
今天背单词了吗9801 小时前
AI 网关:大模型时代的统一接入与安全防线
网络·人工智能·安全·ai·1024程序员节
在世修行1 小时前
智学文枢:AI 客户端封装
python·http·ai·1024程序员节·pyside
紧跟先前的步伐2 小时前
Go AI Agent 实战 13|大模型流式输出 Streaming 的实现
人工智能·ai·golang·1024程序员节
这是谁的博客?4 小时前
AI 芯片存算一体(PIM/CIM)架构深度解析:从冯·诺依曼瓶颈与近存计算到 RRAM 模拟矩阵乘的 LLM 推理显存墙突破原理
ai·pim·1024程序员节·cim·存算一体·hbm-pim·lpddr5x-pim
紧跟先前的步伐4 小时前
Go AI Agent 实战 14|多轮对话与上下文历史管理
人工智能·ai·golang·1024程序员节
weixin_468466855 小时前
让机器像视网膜一样“看”:事件相机与脉冲神经网络如何重构视觉感知
人工智能·神经网络·ai·机器视觉·1024程序员节
紧跟先前的步伐5 小时前
Go AI Agent 实战 15|context.Context、超时与取消机制
人工智能·安全·ai·golang·1024程序员节
老歌老听老掉牙14 小时前
WorkBuddy 如何接入 PrePoMax 的 MCP 服务器
服务器·ai·mcp