基于Raft分布式Kv存储:sendRequestVote

sendRequestVote()Candidate 端处理单个 RequestVote RPC 的函数。它负责向指定节点发送投票请求,并根据响应更新本地任期、累计票数,必要时把自己提升为 Leader。

它不是接收方的 RequestVote()

复制代码
Candidate                         Follower
doElection()
    |
    | 创建 args、reply、votedNum
    v
sendRequestVote(server, ...)
    |
    |---- RequestVote RPC ------> RequestVote(args, reply)
    |                              检查任期、日志、投票记录
    |<----------- reply ----------
    |
    v
处理回复、累计选票、可能成为 Leader

源码将每个目标节点的 sendRequestVote() 放进独立线程并行执行。

函数参数

可以抽象成:

复制代码
bool sendRequestVote(
    int server,
    shared_ptr<RequestVoteArgs> args,
    shared_ptr<RequestVoteReply> reply,
    shared_ptr<int> votedNum
);

各参数含义:

复制代码
server     目标节点在 m_peers 中的下标
args       本轮选举的请求快照
reply      目标节点填写的响应
votedNum   本轮选举的共享计票器

args 中包含:

复制代码
term          发起选举时的任期
candidateId   Candidate 的节点编号
lastLogIndex  Candidate 最后一条日志的索引
lastLogTerm   Candidate 最后一条日志的任期

reply 主要包含:

复制代码
term          接收节点当前看到的任期
voteGranted   是否同意投票

这正是 Raft 的标准 RequestVote 请求和响应结构。

一、执行网络调用

核心调用是:

复制代码
bool ok =
    m_peers[server]->RequestVote(args.get(), reply.get());

这里:

复制代码
args.get()   取得请求对象的裸指针
reply.get()  取得响应对象的裸指针

shared_ptr 仍然负责对象生命周期,所以 RPC 在线程中执行时,参数和响应对象不会因为 doElection() 返回而被销毁。

尤其要区分:

复制代码
ok == true              RPC 通信成功并收到响应
reply->votegranted()    对方是否真的投票

因此 ok == true 完全可能同时满足:

复制代码
reply->votegranted() == false;

对方可能成功收到请求,但因为已经投过票、日志不够新或请求任期过期而拒绝。

二、 RPC 失败时直接返回

复制代码
if (!ok) {
    return false;
}

RPC 失败可能意味着:

复制代码
目标节点宕机
网络分区
请求丢失
响应丢失
连接超时

这个函数不会在内部无限重试。若最终无法获得多数票,electionTimeOutTicker() 会再次超时,启动一个更高任期的新选举。

这不会影响安全性;只要 Candidate 得不到多数票,它就不能成为 Leader。Raft 的可用性依赖多数节点能够相互通信。

三、 为什么网络调用期间不持锁

网络 RPC 可能长时间阻塞。如果发送前就持有 m_mtx

复制代码
RPC 等待几百毫秒
→ Raft 主锁也被占用几百毫秒
→ 无法处理心跳
→ 无法处理其他投票请求
→ 无法更新任期

因此该函数先执行网络调用,收到响应后才加锁:

复制代码
std::lock_guard<std::mutex> lg(m_mtx);

这是典型的并发结构:

复制代码
锁内创建请求快照
→ 锁外执行慢速网络操作
→ 锁内验证响应并修改状态

不过,释放锁意味着等待 RPC 时本地状态可能已经发生变化,所以处理响应时必须重新验证任期和角色。

四、 响应任期更高

复制代码
if (reply->term() > m_currentTerm) {
    m_status = Follower;
    m_currentTerm = reply->term();
    m_votedFor = -1;
    persist();
    return true;
}

例如:

复制代码
自己当前任期:8
对方响应任期:10

这说明本节点已经落后。无论当前是 Candidate 还是 Leader,都必须:

复制代码
切换为 Follower
currentTerm 更新为 10
清空当前任期投票记录
持久化 currentTerm 和 votedFor
放弃处理这张选票

Raft 的通用规则是:任何 RPC 请求或响应中出现更高任期,都要更新本地任期并转为 Follower。

这里不能因为:

复制代码
reply->votegranted() == true

就继续计票。任期已经变化,旧选举立即失效。

五、 响应任期更低

复制代码
else if (reply->term() < m_currentTerm) {
    return true;
}

例如:

复制代码
请求发出时:第8任期
等待过程中:本节点已经进入第9任期
返回响应:  第8任期

这个响应属于过去的一轮选举,必须丢弃。

这说明 sendRequestVote() 对应的线程可能还活着,但它代表的选举已经失效。判断任期可以防止旧 RPC 响应污染新任期。

六、 任期相等但拒绝投票

经过前两个分支后:

复制代码
reply->term() == m_currentTerm

源码先进行断言,然后检查:

复制代码
if (!reply->votegranted()) {
    return true;
}

同一任期拒绝投票通常有两个原因:

复制代码
1. 对方本任期已经投给其他 Candidate
2. 当前 Candidate 的日志不够新

"日志足够新"的比较顺序是:

复制代码
先比较 lastLogTerm
任期相同再比较 lastLogIndex

Candidate 的日志只有至少和接收方一样新,才有资格获得选票。(raft.github.io)

注意函数仍然返回 true,因为返回值表达的是 RPC 是否成功,不是是否获得选票。

七、获得一张赞成票

复制代码
*votedNum = *votedNum + 1;

votedNumdoElection() 中初始化为 1,因为 Candidate 已经投给自己。

假设有 5 个节点:

复制代码
初始:自己的一票,votedNum = 1
节点B同意:        votedNum = 2
节点C同意:        votedNum = 3

多数票计算为:

复制代码
m_peers.size() / 2 + 1

对于不同规模:

复制代码
3 个节点需要 2 票
5 个节点需要 3 票
7 个节点需要 4 票

虽然多个线程共享普通 int,但票数的读取和修改都发生在 m_mtx 的保护下,所以这里不会出现两个线程同时覆盖计票结果。

八、达到多数票后成为 Leader

复制代码
if (*votedNum >= m_peers.size() / 2 + 1) {
    *votedNum = 0;
    m_status = Leader;
    ...
}

获得多数票后,这一轮选举已经成功。Raft 只要求多数节点同意,不需要等待所有节点响应。

源码把票数设为 0,目的是避免后续迟到的赞成票再次触发晋升逻辑。不过更清晰的设计通常是维护:

复制代码
bool electionWon;

或者检查:

复制代码
if (m_status != Candidate) {
    return true;
}

九、 初始化 Leader 的复制状态

成为 Leader 后初始化:

复制代码
m_nextIndex[i] = lastLogIndex + 1;
m_matchIndex[i] = 0;

含义是:

复制代码
nextIndex[i]
下一次准备发送给节点 i 的日志索引

matchIndex[i]
已知节点 i 成功复制的最高日志索引

如果 Leader 最后一条日志索引是 10

复制代码
nextIndex[i] = 11

Leader 会先假设 Follower 已经拥有前面的日志,然后从索引 11 开始尝试;如果 AppendEntries 返回日志不匹配,再逐步回退。Raft 规定 Leader 当选后重新初始化这两个易失状态。

十、立即发送第一次心跳

源码创建一个新线程调用:

复制代码
Raft::doHeartBeat()

新 Leader 不等待下一个心跳周期,而是立即广播 AppendEntries:

复制代码
向其他节点宣布 Leader 身份
让其他 Candidate 退回 Follower
重置 Follower 的选举计时器
开始日志同步

线程创建时 sendRequestVote() 还持有 m_mtx,所以新线程进入 doHeartBeat() 后会暂时阻塞;当前函数释放锁后,它才能正式发送心跳。

相关推荐
笨鸟先飞的橘猫9 小时前
游戏后端分布式学习——消息队列在游戏的用法
分布式·学习·游戏
Wang's Blog9 小时前
Go-Zero项目开发40: 基于Jaeger的分布式链路跟踪实战
开发语言·分布式·golang
wWYy.9 小时前
基于Raft分布式Kv存储:sendAppendEntries
分布式
范什么特西14 小时前
关于kafka
分布式·kafka
wWYy.14 小时前
基于Raft分布式Kv存储:AppendEntries
分布式
霸道流氓气质15 小时前
分布式系统中跨进程上下文传递方案
分布式
wWYy.15 小时前
基于Raft的分布式Kv存储项目:raft.h
开发语言·分布式·qt
wWYy.15 小时前
基于Raft分布式Kv存储:日志寻找匹配
分布式
ACP广源盛1392462567316 小时前
YLB3118 存储桥接芯片完整机会点@ACP#联动曙光 8000
大数据·人工智能·分布式·单片机·嵌入式硬件