基于Raft分布式Kv存储:RequestVote

发布时间:2026/7/29 16:46:06
基于Raft分布式Kv存储:RequestVote 整体流程收到 RequestVote → 加锁 → 请求任期是否过期 → 请求任期是否更新 → Candidate 日志是否足够新 → 本任期是否已经投给别人 → 记录投票 → 重置选举计时器 → 持久化并返回一、整个处理过程加锁std::lock_guard lg(m_mtx);RequestVote 会读写m_currentTerm m_status m_votedFor m_logs m_lastResetElectionTime必须在同一个临界区内完成否则两个 Candidate 的请求并发到达时可能都看到m_votedFor -1然后节点在同一任期投出两票。持锁还能保证日志新旧检查和实际投票之间日志状态不会发生变化。二、DEFER persist()的作用源码紧接着注册DEFER { persist(); };因此无论从哪个分支return最后都会执行持久化。需要持久化的核心状态是currentTerm votedFor假设节点给 Candidate A 投票后立即宕机如果没有持久化重启后可能忘记这张票又在同一任期投给 Candidate B。按照这段代码的构造顺序延迟对象会在lock_guard之前析构所以设计意图是先持久化再释放互斥锁。三、请求任期小于本地任期if (args-term() m_currentTerm) { reply-set_term(m_currentTerm); reply-set_votestate(Expire); reply-set_votegranted(false); return; }例如Candidate 任期7 本节点任期 9这说明请求可能来自网络中延迟的旧消息必须拒绝voteGranted false reply.term 9Candidate 收到响应后会发现自己任期落后并切换成 Follower。注意这里不会重置选举计时器。过期 Candidate 不应该通过不断发送旧请求阻止本节点正常超时选举。四、 请求任期大于本地任期if (args-term() m_currentTerm) { m_status Follower; m_currentTerm args-term(); m_votedFor -1; }例如Candidate 任期10 本节点任期 8本节点必须执行“三变”角色变成 Follower 任期更新为 10 清空旧任期的投票记录Raft 规定任何 RPC 请求或响应中发现更高任期都必须更新本地任期并转成 Follower。但是更新到第 10 任期不意味着一定投票。接下来还要检查 Candidate 的日志。五、 为什么要断言任期相等完成前面的判断后理论上只有args-term() m_currentTerm所以源码执行myAssert(args-term() m_currentTerm, ...);此后的判断都发生在同一任期内不再讨论任期新旧只讨论Candidate 的日志是否够新 本节点是否已经投票六、UpToDate()如何比较日志源码调用UpToDate(args-lastlogindex(), args-lastlogterm());其核心逻辑是return candidateLastTerm localLastTerm || (candidateLastTerm localLastTerm candidateLastIndex localLastIndex);也就是把日志新旧表示成二元组(lastLogTerm, lastLogIndex)进行字典序比较先比较任期再比较索引。例如本节点最后日志是本节点(term6, index20)不同 Candidate 的结果Candidate A(5, 100) 拒绝最后任期更旧 Candidate B(6, 19) 拒绝同任期但索引更小 Candidate C(6, 20) 接受完全一样新 Candidate D(6, 25) 接受同任期但日志更长 Candidate E(7, 10) 接受最后任期更新即使 E 的索引只有 10只要最后日志任期是 7也比(6,20)更新。Raft 使用这个选举限制避免缺少已提交日志的节点当选 Leader。七、快照如何参与日志比较UpToDate()通过getLastLogIndexAndTerm()获取本地日志末尾。如果m_logs非空取 m_logs 最后一项的 index 和 term如果日志已经被快照压缩m_logs为空取 m_lastSnapshotIncludeIndex 取 m_lastSnapshotIncludeTerm这非常重要。生成快照不代表节点忘记了已压缩日志的历史位置否则一个日志很旧的 Candidate 可能因为本节点m_logs为空而错误获得选票。八、 日志不够新时拒绝if (!UpToDate(...)) { reply-set_term(m_currentTerm); reply-set_votegranted(false); return; }即使 Candidate 的任期更高只要日志不够新本节点仍然拒绝投票。例如本节点currentTerm8lastLog(term7,index20) 请求方term9lastLog(term6,index100)处理过程是本节点更新到任期9并变成 Follower 清空 votedFor 但因为请求方日志更旧拒绝投票这里也不重置选举计时器。日志过旧的 Candidate 不应该通过不断请求让拥有更新日志的节点无法发起选举。九、已经投给其他节点时拒绝if (m_votedFor ! -1 m_votedFor ! args-candidateid()) { reply-set_votegranted(false); return; }只有两种情况可以投票m_votedFor -1 本任期还没有投票 m_votedFor candidateId 之前已经投给同一个 Candidate如果已经投给另一个 Candidate就必须拒绝。这保证一个节点在一个任期内最多投给一个 Candidate。十、 为什么允许重复投给同一个 Candidate假设节点 B 投给了 Candidate A B 的响应在网络中丢失 A 重新发送同一个 RequestVote此时m_votedFor args-candidateid()B 应该再次返回voteGrantedtrue。这不是第二张票而是同一个投票决定的幂等重放。否则响应丢失就可能让 Candidate 永远无法知道自己已经获得这张票