先定义Raft结构体
type NodeState int
const (
Follower NodeState = iota
Candidate
Leader
)
type Raft struct {
mu sync.Mutex // 加锁以保护对该节点状态的共享访问
peers []*labrpc.ClientEnd // RPC端点
persister *tester.Persister // 用于保存此节点已持久化状态的对象
me int // 此节点在 peers 数组中的索引
dead int32 // 由 Kill() 函数设置
// 持久性状态
currentTerm int // 记录当前的任期
votedFor int // 投给了谁
log []logEntry // 日志条目数组
// 易失性状态
commitIndex int // 已被提交的日志索引
lastApplied int // 已提交的日志索引
// leader状态
nextIndex []int // 对于每一个server,需要发送给他下一个日志条目的索引
matchIndex []int // 对于每一个server,已经复制给该server的最后日志条目下标
// 快照状态
// 自己的一些变量状态
state NodeState // follower/candidate/leader
timer Timer
voteCount int // 票数
}
入口函数是MAKE函数
备注写在代码里
func Make(peers []*labrpc.ClientEnd, me int,
persister *tester.Persister, applyCh chan raftapi.ApplyMsg) *Raft {
// 输入是四个值
// 1.peers[] 所有raft节点的列表信息,我们不用过多考虑
// 2.me 自身节点的编号,对应在peers[]中的位置
// 3. persister 一个指向 tester.Persister 类型的指针,用于持久化当前节点的状态信息,负责当前节点状态信息的持久化存储和读取
// 4.作为节点与状态机之间的通信桥梁,用于传递已提交的日志条目相关信息,以便实现状态的更新和数据的应用,是 Raft 算法与外部组件进行交互的关键途径。
rf := &Raft{}
// 初始化一个raft节点
rf.peers = peers
rf.persister = persister
rf.me = me
// 把make函数的输入保存到raft节点中
// 初始当前任期
rf.currentTerm = 0
// 没有给任何人投票
rf.votedFor = -1
// 初始化日志条目切片,初始长度为 0
rf.log = make([]logEntry, 0)
// 初始化已提交日志的最大索引为 -1
rf.commitIndex = -1
// 初始化最后应用到状态机的日志索引为 -1
rf.lastApplied = -1
// 初始化 nextIndex 切片,用于记录需要发送给每个节点的下一个日志条目的索引
rf.nextIndex = make([]int, len(peers))
// 初始化 matchIndex 切片,用于记录每个节点已经复制的最大日志条目的索引
rf.matchIndex = make([]int, len(peers))
// 初始状态设置为 Follower
rf.state = Follower
// 初始时获得的投票数为 0
rf.voteCount = 0
// 初始化选举超时定时器,超时时间在 150 到 350 毫秒之间随机
rf.timer = Timer{timer: time.NewTicker(time.Duration(150+rand.Intn(200)) * time.Millisecond)}
// 从持久化存储中读取之前崩溃前保存的状态信息
rf.readPersist(persister.ReadRaftState())
// 启动一个 goroutine 来处理选举定时器
go rf.ticker()
// 返回初始化好的 Raft 节点实例
return rf
}
都是一些初始化操作,最重要的是 go rf.ticker()
,这是我们lab3A部分的关键实现
下面来看具体的 rf.ticker()
首先介绍一点timer定时器
// 重置定时器,重新开始计时
rf.timer.reset()
func (t *Timer) reset() {
randomTime := time.Duration(150+rand.Intn(200)) * time.Millisecond // 200~350ms
t.timer.Reset(randomTime) // 重置时间
}
通过reset方法,给timer传入一个200-350ms的时间值,那么,在时间到了的时候,会向timer.C这个chanel会发送信号
// ticker 方法是一个循环函数,用于处理 Raft 节点的选举和心跳逻辑。
// 它会在节点未被杀死的情况下持续运行,根据定时器的触发来执行不同的操作。
func (rf *Raft) ticker() {
// 只要节点未被标记为已杀死,就持续循环
for rf.killed() == false {
// 使用 select 语句监听定时器的通道
select {
// 当定时器到期时,执行以下操作
case <-rf.timer.timer.C:
// rf.timer.timer.C传来信号,就说明计时器到时间了,那么根据此时节点的状态做出相应的操作
// 加锁以保证线程安全,避免多个 goroutine 同时修改节点状态
rf.mu.Lock()
// 根据节点的当前状态执行不同的逻辑
switch rf.state {
// 如果当前是 Follower 状态
case Follower: //如果此时节点的状态是Follwer,及在follower状态下超时了,那么根据raft的设计
//此时应该转变为 Candidate 状态,开始参与选举
rf.state = Candidate
// 打印调试信息,注释掉以避免干扰正常输出
// fmt.Println(rf.me, "进入candidate状态")
// fallthrough 关键字让程序继续执行下一个 case 分支
fallthrough
// 如果当前是 Candidate 状态
case Candidate:
// 增加当前任期号,表示开始新的选举周期
rf.currentTerm++
// 给自己投一票,初始得票数为 1
rf.voteCount = 1
// 重置定时器,重新开始计时
rf.timer.reset()
// 记录自己把票投给了自己
rf.votedFor = rf.me
// 遍历所有节点,向其他节点发送投票请求
for i := 0; i < len(rf.peers); i++ {
// 排除自己,不向自己发送投票请求
if rf.me == i {
continue
}
// 构造投票请求的参数
args := RequestVoteArgs{
Term: rf.currentTerm,
CandidateId: rf.me,
LastLogIndex: len(rf.log) - 1,
}
// 如果日志不为空,设置最后一条日志的任期号
if len(rf.log) > 0 {
args.LastLogTerm = rf.log[len(rf.log)-1].Term
}
// 初始化投票响应结构体
reply := RequestVoteReply{}
// 启动一个新的 goroutine 异步发送投票请求
go rf.sendRequestVote(i, &args, &reply)
}
// 如果当前是 Leader 状态
case Leader:
// 重置心跳定时器,开始新的心跳周期
rf.timer.resetHeartBeat()
// 遍历所有节点,向其他节点发送心跳消息(附加日志条目请求)
for i := 0; i < len(rf.peers); i++ {
// 排除自己,不向自己发送心跳消息
if i == rf.me {
continue
}
// 构造附加日志条目请求的参数
args := AppendEntriesArgs{
Term: rf.currentTerm,
LeaderId: rf.me,
}
// 初始化附加日志条目响应结构体
reply := AppendEntriesReply{}
// 启动一个新的 goroutine 异步发送附加日志条目请求
go rf.sendAppendEntries(i, &args, &reply)
}
}
// 解锁,允许其他 goroutine 访问节点状态
rf.mu.Unlock()
}
}
}
然后我们来看,如果当前是 Candidate 状态出发了定时器,说明要启动选举了
在初始化一些参数后,for循环启动
对于每一个peers成员(其他节点)
都启动一个 go rf.sendRequestVote(i, &args, &reply)
来请求投票
我们来看
// sendRequestVote 函数用于向指定的服务器发送投票请求,并处理投票响应。
// 它会持续尝试发送 RPC 请求,直到成功。根据响应结果更新节点的状态和投票计数。
func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {
// 尝试向指定服务器发送投票请求的 RPC 调用
ok := rf.peers[server].Call("Raft.RequestVote", args, reply)
// 如果第一次调用失败,则持续尝试,直到成功
for !ok {
ok = rf.peers[server].Call("Raft.RequestVote", args, reply)
}
// 加锁以保证线程安全,避免多个 goroutine 同时修改节点状态
rf.mu.Lock()
// 函数结束时自动解锁
defer rf.mu.Unlock()
// 收到过期的 RPC 回复,即请求的任期号小于当前节点的任期号,不处理该回复
if args.Term < rf.currentTerm {
return false
}
// 如果对方同意投票
if reply.VoteGranted {
// 增加当前节点获得的投票数
rf.voteCount++
// 如果获得的投票数超过节点总数的一半
if rf.voteCount > len(rf.peers)/2 {
// 打印调试信息,可注释掉以避免干扰正常输出
// fmt.Println("新王登基,他的 ID 是:", rf.me)
// 当前节点成为领导者
rf.state = Leader
// 重置定时器为心跳定时器,开始发送心跳消息
rf.timer.resetHeartBeat()
}
} else {
// 对方拒绝投票,此处可添加更多处理逻辑,当前仅作占位
}
return true
}
然后我们可以看到
const HeartBeatTimeout = 125 * time.Millisecond
func (t *Timer) resetHeartBeat() {
t.timer.Reset(HeartBeatTimeout)
}
也就是说,leader节点超时的时间是固定125ms,而其他的节点则是随机200-350ms
leader可以在其他节点超时之前,通过go rf.sendAppendEntries(i, &args, &reply)
刷新定时器的时间
// sendAppendEntries 函数用于向指定的服务器发送附加日志条目请求(AppendEntries RPC),
// 该请求可用于领导者发送心跳消息或复制日志条目。函数会持续尝试发送 RPC 请求,直到成功,
// 并根据响应结果更新当前节点的状态和任期信息。
func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {
// 尝试向指定服务器发送附加日志条目请求的 RPC 调用
ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
// 如果第一次调用失败,则持续尝试,直到成功
for !ok {
ok = rf.peers[server].Call("Raft.AppendEntries", args, reply)
}
// 加锁以保证线程安全,避免多个 goroutine 同时修改节点状态
rf.mu.Lock()
// 函数结束时自动解锁
defer rf.mu.Unlock()
// 1. 收到过期的 RPC 回复
// 如果请求中的任期号小于当前节点的任期号,说明该请求是过期的,直接返回 false 不处理
if args.Term < rf.currentTerm {
return false
}
// 2. 心跳或日志追加未成功
// 如果响应中的 Success 字段为 false,表示附加日志条目操作未成功,
// 通常意味着该节点的任期号已经过时,需要进行状态转换
if !reply.Success {
// 当前节点转变为追随者状态
rf.state = Follower
// 更新当前节点的任期号为响应中携带的任期号
rf.currentTerm = reply.Term
// 重置已投票节点的信息,表示还未投票给任何节点
rf.votedFor = -1
// 重置投票计数
rf.voteCount = 0
// 重置定时器,开始新的选举计时周期
rf.timer.reset()
}
// 若执行到这里,说明请求和响应处理基本正常,返回 true
return true
}
到此为止,3A完成