准备手写Simple Raft(二): 跑通最基本的Leader选举

作者背景:20年IT一线开发经验,15年架构师经验。擅长分布式、微服务、搜索引擎,手写过各类中间件。曾对Jacoco、夜莺等进行二次开发,负责过200多个节点的大规模集群,涉及K8s、微服务监控、多个中间件的运维。自研过加解密中间件,现服务于一家金融公司,担任架构师。

完整代码已开源: gitee.com/sh_wangwanb... 本章代码分支:ch2


上一篇讲了Raft的核心思想,这篇开始写代码。

从理论到实现是最难跨越的鸿沟。Nacos的JRaft有5万行代码,直接看会很困惑。

所以这次我决定自己动手写一个最简化的Raft,从最基础的选举开始。不求完美,先跑通再说。

一、MVP的边界:只做选举

实现完整的Raft容易半途而废,更好的方式是从最基础的选举开始。

这次我们只做这三件事:

  1. 选举出一个Leader
  2. Leader定时发心跳维持地位
  3. 持久化term和votedFor

暂时不做:

  • 日志复制(下一篇再搞)
  • 快照
  • 配置变更
  • ReadIndex

为什么?因为选举是地基,必须先打牢。日志复制是80%的工作量,放后面慢慢啃。

二、架构设计:能简单就不复杂

技术选型上选择了最简单的方案:用HTTP而不是gRPC,因为HTTP够用,出问题还能用curl调试。

bash 复制代码
simple-raft/
├── surfing-raft-core      # 核心算法(纯Java)
│   ├── RaftNode           # 状态机核心
│   ├── rpc/               # RPC消息定义
│   └── storage/           # 持久化
└── surfing-raft-node      # 节点服务(Spring Boot)
    ├── RaftService        # 定时调度
    ├── RaftController     # HTTP接口
    └── RaftRpcClient      # HTTP客户端

为什么分两层?

  • core层:纯算法逻辑,不依赖框架,方便单元测试
  • node层:处理定时任务、HTTP通信这些工程问题

算法和工程分离的好处是:核心逻辑不会被框架的注解搞得一团糟。

模块调用关系:

graph TB subgraph "surfing-raft-node(工程层)" Controller[RaftController
REST API] Service[RaftService
定时调度+RPC编排] Client[RaftRpcClient
HTTP客户端] end subgraph "surfing-raft-core(算法层)" Node[RaftNode
核心状态机] Storage[FileStatePersistence
持久化] RPC[RPC消息
Request/Response] end Controller -->|转发RPC| Service Service -->|调用算法| Node Service -->|发送RPC| Client Client -->|HTTP POST| Controller Node -->|读写| Storage Service -->|构造消息| RPC style Node fill:#ffe6e6 style Storage fill:#e6f3ff style Service fill:#e6ffe6

数据流示例(收到投票请求):

sequenceDiagram participant Peer as 其他节点 participant Controller as RaftController participant Service as RaftService participant Node as RaftNode participant Storage as FileStatePersistence Peer->>Controller: POST /raft/vote
RequestVoteRequest Controller->>Service: handleRequestVote(request) Service->>Node: handleRequestVote(request) rect rgb(255, 240, 240) Note over Node: 加锁处理 Node->>Node: 检查term Node->>Node: 决定是否投票 alt 同意投票 Node->>Storage: save(term, votedFor) Storage-->>Node: 持久化成功 Node->>Node: resetElectionTimeout() end end Node-->>Service: RequestVoteResponse Service-->>Controller: RequestVoteResponse Controller-->>Peer: HTTP 200
返回响应

三、核心逻辑:少即是多

3.1 持久化:只存两个字段

Raft论文明确说了,必须持久化的只有两个:

java 复制代码
public class PersistentState {
    private long currentTerm;   // 当前任期
    private String votedFor;    // 投给谁了
}

为什么不存state(LEADER/FOLLOWER)?

重启后默认都是Follower,通过心跳或选举自然就恢复了。能不存就不存,这是分布式系统的基本原则。

持久化状态越多,系统升级时的兼容性问题就越复杂。

3.2 原子性写入:保证数据完整

java 复制代码
public void save(PersistentState state) {
    // 先写临时文件
    File tempFile = new File(stateFilePath + ".tmp");
    objectMapper.writeValue(tempFile, state);
    
    // 原子性重命名
    Files.move(tempFile.toPath(), new File(stateFilePath).toPath(), 
        StandardCopyOption.ATOMIC_MOVE);
}

这种"写临时文件+原子重命名"的模式在很多存储系统中都有使用,比如PostgreSQL的WAL机制。

为什么要这样做?

先写临时文件,成功后再原子性重命名。这样即使写入过程中宕机,要么新文件成功,要么旧文件还在,不会出现文件损坏的情况。

3.3 选举逻辑:term是核心

java 复制代码
public void becomeCandidate() {
    lock.lock();
    try {
        state = CANDIDATE;
        currentTerm++;              // 关键:任期递增
        votedFor = nodeId;          // 投给自己
        votesReceived = 1;          // 自己算一票
        
        savePersistentState();      // 立即持久化!
        resetElectionTimeout();
    } finally {
        lock.unlock();
    }
}

这里有几个需要注意的点:

注意1:term必须先递增再发投票请求 如果发完请求才递增term,其他节点会因为term过旧而拒绝投票。

注意2:投票后必须立即持久化 如果不持久化就重启,节点可能忘记已投票,在同一term投多次票,导致两个Leader。这是Raft安全性的根基。

注意3:votesReceived要包含自己 候选人自己也算一票。如果忘记这点,3节点集群会需要3票才能当选,永远选不出Leader。

3.4 投票规则:同一任期只投一次

java 复制代码
public RequestVoteResponse handleRequestVote(RequestVoteRequest request) {
    lock.lock();
    try {
        // 拒绝过期请求
        if (request.getTerm() < currentTerm) {
            return new RequestVoteResponse(currentTerm, false);
        }
        
        // 发现更高term,立即降级
        if (request.getTerm() > currentTerm) {
            becomeFollower(request.getTerm());
        }
        
        // 检查是否已投票
        boolean canVote = (votedFor == null || 
                          votedFor.equals(request.getCandidateId()));
        
        if (canVote) {
            votedFor = request.getCandidateId();
            savePersistentState();    // 又是立即持久化
            resetElectionTimeout();
            return new RequestVoteResponse(currentTerm, true);
        }
        
        return new RequestVoteResponse(currentTerm, false);
    } finally {
        lock.unlock();
    }
}

这里体现了Raft的精髓:term是逻辑时钟

任何时候收到更高的term,不管你是Leader还是Candidate,立即认怂变Follower。这保证了集群在最高term下只有一个合法Leader。

3.5 心跳机制:Leader的生命线

java 复制代码
private void sendHeartbeats() {
    if (raftNode.getState() != NodeState.LEADER) {
        return;  // 只有Leader发心跳
    }
    
    AppendEntriesRequest request = new AppendEntriesRequest(
        raftNode.getCurrentTerm(),
        raftNode.getNodeId(),
        0, 0, 0  // 暂时没有日志,这些字段都是0
    );
    
    // 并行发送给所有Peer
    for (String peer : raftNode.getPeers()) {
        executor.submit(() -> {
            AppendEntriesResponse response = rpcClient.appendEntries(peer, request);
            if (response != null && response.getTerm() > raftNode.getCurrentTerm()) {
                // 发现更高term,Leader降级
                raftNode.becomeFollower(response.getTerm());
            }
        });
    }
}

为什么心跳间隔是50ms?

选举超时是150-300ms,心跳间隔应该远小于它,一般是1/3。这样即使偶尔丢包,也不会触发选举。

如果心跳间隔设置得过长(比如100ms心跳配120ms超时),网络稍有抖动就会频繁选举,集群难以稳定。

为什么并行发送?

串行发送的话,3个节点就要150ms(50ms * 3),早就超时了。并行发送能保证心跳及时送达。

注意:不要用CompletableFuture.allOf()等待所有响应。 一个节点慢了,整个心跳就慢了。正确做法是发完就完事,响应异步处理。

3.6 完整的选举流程

用时序图看得更清楚:

sequenceDiagram participant N1 as Node-1
(Follower) participant N2 as Node-2
(Follower) participant N3 as Node-3
(Follower) Note over N1,N3: 所有节点启动,初始状态都是Follower
term=0, votedFor=null Note over N1: 选举超时(200ms)
没收到心跳 rect rgb(255, 240, 240) Note over N1: 开始选举 N1->>N1: becomeCandidate()
term: 0→1
votedFor: node-1
votes: 1 N1->>N1: 持久化(term=1, votedFor=node-1) end par 并发发送投票请求 N1->>N2: RequestVote
{term:1, candidateId:node-1} N1->>N3: RequestVote
{term:1, candidateId:node-1} end rect rgb(240, 255, 240) Note over N2: 处理投票请求 N2->>N2: term: 0→1
votedFor: node-1 N2->>N2: 持久化(term=1, votedFor=node-1) N2->>N2: 重置选举超时 N2-->>N1: VoteGranted: true end rect rgb(240, 255, 240) Note over N3: 处理投票请求 N3->>N3: term: 0→1
votedFor: node-1 N3->>N3: 持久化(term=1, votedFor=node-1) N3->>N3: 重置选举超时 N3-->>N1: VoteGranted: true end rect rgb(240, 240, 255) Note over N1: 收到多数派投票 N1->>N1: votes: 3/3 >= 2
becomeLeader() Note over N1: 成为Leader end loop 每50ms发送心跳 N1->>N2: AppendEntries
{term:1, leaderId:node-1} N1->>N3: AppendEntries
{term:1, leaderId:node-1} N2-->>N1: Success: true N3-->>N1: Success: true end Note over N1,N3: 集群稳定
Node-1是Leader, term=1

关键时刻:

  1. 0-200ms: 所有节点等待,Node-1先超时
  2. 200ms: Node-1发起选举,term变成1
  3. 200-250ms: 投票请求在网络中传输(假设延迟50ms)
  4. 250ms: Node-2和Node-3收到请求,投票给Node-1
  5. 250-300ms: 投票响应返回
  6. 300ms: Node-1收到多数票,成为Leader
  7. 300ms后: Leader每50ms发一次心跳

3.7 Leader失联后的重新选举

sequenceDiagram participant L as Node-1
(Leader) participant F1 as Node-2
(Follower) participant F2 as Node-3
(Follower) Note over L,F2: 稳定状态: Node-1是Leader, term=1 loop Leader正常发送心跳 L->>F1: AppendEntries{term:1} L->>F2: AppendEntries{term:1} F1-->>L: Success F2-->>L: Success end rect rgb(255, 200, 200) Note over L: Leader宕机/网络断开 end Note over F1,F2: Followers等待心跳...
选举超时开始计时 Note over F1: 选举超时(250ms) rect rgb(255, 240, 240) F1->>F1: becomeCandidate()
term: 1→2
votedFor: node-2
votes: 1 F1->>F1: 持久化(term=2, votedFor=node-2) end F1->>F2: RequestVote
{term:2, candidateId:node-2} rect rgb(240, 255, 240) F2->>F2: term: 1→2
votedFor: node-2 F2->>F2: 持久化(term=2, votedFor=node-2) F2-->>F1: VoteGranted: true end rect rgb(240, 240, 255) F1->>F1: votes: 2/2 >= 2
becomeLeader() Note over F1: Node-2成为新Leader end loop 新Leader发送心跳 F1->>F2: AppendEntries{term:2, leaderId:node-2} F2-->>F1: Success: true end Note over F1,F2: 集群恢复
Node-2是Leader, term=2 opt Leader恢复后 L->>F1: AppendEntries{term:1} rect rgb(255, 240, 240) Note over F1: 发现旧term F1-->>L: {term:2, success:false} end rect rgb(255, 200, 200) Note over L: 收到更高term L->>L: becomeFollower(2)
state: LEADER→FOLLOWER
持久化 end end

这个流程体现了Raft的两个关键特性:

  1. 任期单调递增:新Leader的term=2 > 旧Leader的term=1
  2. 旧Leader自动降级:收到更高term后,立即变成Follower

3.8 节点状态转换规则

Raft节点有3个状态:Follower、Candidate、Leader。状态转换的触发条件如下:

当前状态 触发条件 转换到 执行动作
Follower 选举超时,未收到心跳 Candidate term++,投票给自己,持久化
Follower 收到合法Leader心跳 Follower 重置选举超时
Follower 收到更高term Follower 更新term,持久化
Candidate 获得多数票 Leader 开始发送心跳
Candidate 选举超时,未获得多数票 Candidate term++,发起新一轮选举
Candidate 收到更高term Follower 更新term,持久化
Candidate 收到合法Leader心跳 Follower 更新term,重置选举超时
Leader 收到更高term Follower 立即降级,更新term,持久化
Leader 正常运行 Leader 定时发送心跳维持地位

关键规则:

  • 任何状态收到更高term,都要更新term并转为Follower
  • Follower和Candidate收到合法Leader心跳,立即转为Follower并重置选举超时
  • Candidate获得多数票(包括自己),立即转为Leader
  • Leader只有在收到更高term时才会降级

四、运行效果:眼见为实

Talk is cheap, show me the code. 不,show me the running cluster!

4.1 启动集群

bash 复制代码
cd simple-raft
./start-cluster.sh

这个脚本会:

  1. 清理旧数据(/tmp/raft-data
  2. 编译项目(Maven)
  3. 后台启动3个节点
yaml 复制代码
清理旧数据...
编译项目...
[INFO] BUILD SUCCESS
启动节点 1...
节点 1 PID: 55160
启动节点 2...
节点 2 PID: 55166
启动节点 3...
节点 3 PID: 55170

三个节点已启动:
  节点 1: http://localhost:8081 (PID: 55160)
  节点 2: http://localhost:8082 (PID: 55166)
  节点 3: http://localhost:8083 (PID: 55170)

启动后的内部流程:

sequenceDiagram participant Spring as Spring Boot participant Service as RaftService participant Node as RaftNode participant Storage as FileStatePersistence Spring->>Service: @PostConstruct
start() Service->>Storage: 创建持久化 Service->>Node: 创建RaftNode Node->>Storage: loadPersistentState() alt 首次启动(文件不存在) Storage-->>Node: {term:0, votedFor:null} Note over Node: 初始化为Follower
term=0, votedFor=null else 重启(文件存在) Storage-->>Node: {term:5, votedFor:node-2} Note over Node: 恢复为Follower
term=5, votedFor=node-2 end Node->>Node: 生成随机选举超时
150-300ms Service->>Service: 启动定时任务 rect rgb(240, 255, 240) Note over Service: 定时任务1: 选举超时检查(50ms) loop 每50ms执行 Service->>Node: 检查是否超时 alt 超时且未收到心跳 Service->>Service: startElection() end end end rect rgb(240, 240, 255) Note over Service: 定时任务2: 心跳发送(50ms) loop 每50ms执行 alt 当前是Leader Service->>Service: sendHeartbeats() end end end Note over Spring,Storage: 节点启动完成,进入运行状态

3个节点的配置差异:

配置项 Node-1 Node-2 Node-3
server.port 8081 8082 8083
raft.node-id node-1 node-2 node-3
raft.peers localhost:8082, localhost:8083 localhost:8081, localhost:8083 localhost:8081, localhost:8082
raft.storage-dir /tmp/raft-data/ node-1 /tmp/raft-data/ node-2 /tmp/raft-data/ node-3

持久化文件:

  • Node-1: /tmp/raft-data/node-1/node-1-state.json
  • Node-2: /tmp/raft-data/node-2/node-2-state.json
  • Node-3: /tmp/raft-data/node-3/node-3-state.json

文件内容示例:

json 复制代码
{
  "currentTerm": 1,
  "votedFor": "node-1"
}

4.2 查看状态

bash 复制代码
curl http://localhost:8081/raft/status | jq
json 复制代码
{
  "nodeId": "node-1",
  "state": "LEADER",
  "currentTerm": 1,
  "leaderId": "node-1"
}
bash 复制代码
curl http://localhost:8082/raft/status | jq
json 复制代码
{
  "nodeId": "node-2",
  "state": "FOLLOWER",
  "currentTerm": 1,
  "leaderId": "node-1"
}

看到没?node-1是Leader,其他两个是Follower。集群稳定了。

4.3 测试Leader失联

bash 复制代码
# 杀掉Leader
kill 55160

# 等2秒,让选举完成
sleep 2

# 查看新状态
curl http://localhost:8082/raft/status | jq
json 复制代码
{
  "nodeId": "node-2",
  "state": "LEADER",
  "currentTerm": 2,
  "leaderId": "node-2"
}

node-2成为新Leader了,term从1变成2。这就是Raft的容错能力。

4.4 观察日志:选举的完整过程

我在代码里加了详细的日志,你可以实时看到选举过程:

bash 复制代码
tail -f /tmp/raft-node-1.log

完整的日志输出(带时间戳和解读):

ini 复制代码
2024-11-18 22:49:23.156 [main] INFO  RaftNode - Node node-1 initialized as FOLLOWER
2024-11-18 22:49:23.158 [main] INFO  FileStatePersistence - No persistent state file found, initializing with defaults
2024-11-18 22:49:23.159 [main] INFO  RaftNode - Loaded persistent state: term=0, votedFor=null
2024-11-18 22:49:23.162 [main] INFO  RaftService - RaftService started for node node-1

↑ 节点启动,初始化为Follower,term=0


yaml 复制代码
2024-11-18 22:49:23.450 [pool-2-thread-1] INFO  RaftNode - Election timeout reached, current state: FOLLOWER
2024-11-18 22:49:23.451 [pool-2-thread-1] INFO  RaftNode - State changed: FOLLOWER -> CANDIDATE, term: 0 -> 1
2024-11-18 22:49:23.452 [pool-2-thread-1] INFO  FileStatePersistence - Saved persistent state: term=1, votedFor=node-1

↑ 选举超时(~300ms),转为Candidate,term变成1,投票给自己并持久化


vbscript 复制代码
2024-11-18 22:49:23.453 [pool-2-thread-1] INFO  RaftService - Starting election for term 1
2024-11-18 22:49:23.454 [pool-3-thread-1] INFO  RaftService - Sending vote request to localhost:8082
2024-11-18 22:49:23.454 [pool-3-thread-2] INFO  RaftService - Sending vote request to localhost:8083

↑ 发起选举,并行向其他两个节点发送投票请求


ini 复制代码
2024-11-18 22:49:23.512 [pool-3-thread-1] INFO  RaftService - Received vote response from localhost:8082: granted=true, term=1
2024-11-18 22:49:23.513 [pool-3-thread-2] INFO  RaftService - Received vote response from localhost:8083: granted=true, term=1

↑ 收到其他两个节点的投票,都是同意(granted=true)


yaml 复制代码
2024-11-18 22:49:23.514 [pool-3-thread-1] INFO  RaftNode - Received majority votes (3/3), becoming LEADER
2024-11-18 22:49:23.514 [pool-3-thread-1] INFO  RaftNode - State changed: CANDIDATE -> LEADER

↑ 获得多数票(3票≥2票),成为Leader


ini 复制代码
2024-11-18 22:49:23.565 [pool-2-thread-2] INFO  RaftService - Sending heartbeats to 2 peers, term=1
2024-11-18 22:49:23.615 [pool-2-thread-2] INFO  RaftService - Sending heartbeats to 2 peers, term=1
2024-11-18 22:49:23.665 [pool-2-thread-2] INFO  RaftService - Sending heartbeats to 2 peers, term=1

↑ Leader每50ms发送一次心跳


同时看Node-2的日志(Follower视角):

bash 复制代码
tail -f /tmp/raft-node-2.log
ini 复制代码
2024-11-18 22:49:23.480 [http-nio-8082-exec-1] INFO  RaftController - Received RequestVote: {term:1, candidateId:node-1}
2024-11-18 22:49:23.481 [http-nio-8082-exec-1] INFO  RaftNode - Received RequestVote from node-1 for term 1
2024-11-18 22:49:23.482 [http-nio-8082-exec-1] INFO  RaftNode - Granting vote to node-1 for term 1
2024-11-18 22:49:23.483 [http-nio-8082-exec-1] INFO  FileStatePersistence - Saved persistent state: term=1, votedFor=node-1

↑ Follower收到投票请求,同意投票,持久化votedFor=node-1


ini 复制代码
2024-11-18 22:49:23.570 [http-nio-8082-exec-2] INFO  RaftController - Received AppendEntries: {term:1, leaderId:node-1}
2024-11-18 22:49:23.571 [http-nio-8082-exec-2] INFO  RaftNode - Received heartbeat from Leader node-1, term=1
2024-11-18 22:49:23.571 [http-nio-8082-exec-2] INFO  RaftNode - Resetting election timeout

↑ Follower收到Leader的心跳,重置选举超时


css 复制代码
2024-11-18 22:49:23.620 [http-nio-8082-exec-3] INFO  RaftController - Received AppendEntries: {term:1, leaderId:node-1}
2024-11-18 22:49:23.670 [http-nio-8082-exec-4] INFO  RaftController - Received AppendEntries: {term:1, leaderId:node-1}

↑ 持续收到心跳,每50ms一次


日志时间线分析:

gantt title 选举过程时间线(毫秒级) dateFormat x axisFormat %L section Node-1 启动初始化 :0, 23156 等待选举超时 :23156, 23450 成为Candidate :23450, 23452 发送投票请求 :23453, 23454 收到投票响应 :23512, 23514 成为Leader :23514, 23515 发送心跳 :23565, 150000 section Node-2 启动初始化 :0, 23160 等待 :23160, 23480 收到投票请求 :23480, 23481 投票给Node-1 :23481, 23483 收到心跳 :23570, 150000

关键时间点:

  • 23.156s: Node-1启动
  • 23.450s: Node-1选举超时(等了294ms)
  • 23.453s: 发起选举
  • 23.480s: Node-2收到投票请求(网络延迟27ms)
  • 23.512s: Node-1收到第一张投票(总延迟59ms)
  • 23.514s: 获得多数票,成为Leader(总耗时64ms)
  • 23.565s: 开始发送心跳

五、与Nacos JRaft的对比

Simple Raft只有500行代码,而JRaft有5万行,差距在哪?

我整理了个对比表:

特性 Simple Raft Nacos JRaft
代码量 ~500行 ~5万行
选举机制 完全一致 完全一致
持久化 文件(JSON) RocksDB
RPC HTTP + JSON gRPC/Bolt
日志复制 未实现 完整实现
快照 未实现 完整实现
性能 够用(demo) 生产级(万级TPS)
PreVote优化
Pipeline复制

核心思想是一样的,差距在工程实现上。

JRaft做了大量优化:

  • 批量复制日志
  • Pipeline降低延迟
  • RocksDB持久化
  • 详尽的监控指标

但这些都不是Raft的核心。我们先把核心搞懂,优化以后再说。

六、常见的实现问题

问题1:并发安全

仅用volatile不够,currentTerm++state=CANDIDATE不是原子操作,可能出现term已经+1但state还是FOLLOWER的情况。

需要用ReentrantLock保护所有状态变更操作。

问题2:选举超时未随机化

如果所有节点的选举超时都是固定值(比如200ms),会导致多个节点同时超时、同时发起选举、互相投票给自己,一直平票。

解决方案:设置随机的选举超时(150-300ms),让某个节点先发起选举。

问题3:心跳失败后Leader未降级

Leader和Follower断网后,Leader可能继续认为自己是Leader。

原因:心跳失败后只打日志,没有检查响应的term。正确做法:收到更高term,立即降级

问题4:持久化时机错误

java 复制代码
// 错误写法
votedFor = candidateId;
return new RequestVoteResponse(currentTerm, true);
savePersistentState();  // 永远执行不到

// 正确写法
votedFor = candidateId;
savePersistentState();  // 先持久化
return new RequestVoteResponse(currentTerm, true);

状态变更必须先持久化再返回,这是Raft安全性的基础。

七、当前实现的局限

通过这个MVP版本,我们跑通了Raft的Leader选举和心跳机制。3个节点能正常选举,Leader能维持统治,节点重启后能正确恢复。

但这只是Raft的入门。当前实现还缺少最核心的功能:

没有日志复制

现在的Leader只会发心跳,不会处理任何业务请求。选举只是解决了"谁来协调",还没有解决"怎么协调"。

回顾第一篇说的:Raft的核心是保证所有节点按相同顺序执行相同的操作。现在我们只有Leader,但没有"操作"和"顺序"。

没有状态机

日志复制后,需要应用到状态机。这才是Raft的最终目的:构建一个一致性的状态机。

没有过半提交机制

Leader如何知道日志已经被多数节点接收?什么时候可以告诉客户端"写入成功"?这些都需要实现。

日志一致性检查

如果Follower的日志和Leader不一致怎么办?如何通过prevLogIndex/prevLogTerm检查一致性?冲突了如何处理?

这些问题,都在下一篇解决。

下一篇将实现日志复制机制,包括:

  • AppendEntries携带日志条目
  • 日志一致性检查
  • 过半确认与提交
  • 简单的KV状态机

那时候才算真正理解了Raft的核心。


如果你也在学习Raft,或者对分布式共识有自己的理解,欢迎在评论区交流。有问题也可以直接留言,看到会回复。

代码已开源,欢迎Star:gitee.com/sh_wangwanb...

相关推荐
爱学习的小可爱卢18 分钟前
JavaEE进阶——SpringBoot拦截器详解:从入门到实战
java·spring boot·后端
汤姆yu1 小时前
基于springboot的运动服服饰销售购买商城系统
java·spring boot·后端
期待のcode1 小时前
Springboot数据层开发—Springboot整合JdbcTemplate和Mybatis
spring boot·后端·mybatis
Jul1en_1 小时前
【Spring】实现验证码功能
java·后端·spring
IT_陈寒1 小时前
Java并发编程避坑指南:从volatile到ThreadLocal,8个实战案例解析线程安全核心原理
前端·人工智能·后端
Victor3561 小时前
Netty(10)Netty的粘包和拆包问题是什么?如何解决它们?
后端
全栈独立开发者1 小时前
软考架构师实战:Spring Boot 3.5 + DeepSeek 开发 AI 应用,上线 24 小时数据复盘(2C1G 服务器抗压实录)
java·spring boot·后端
Victor3562 小时前
Netty(9)如何实现基于Netty的UDP客户端和服务器?
后端
在坚持一下我可没意见2 小时前
Spring 开发小白学习过程中常用通用配置文件,即拿即用!(持续更新中)
java·数据库·后端·学习·spring·tomcat·mybatis
一 乐2 小时前
心理健康管理|基于springboot + vue心理健康管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端