MIT6.5840 lab3A

先定义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完成

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值