分布式一致性算法---Raft初探

news2024/9/22 21:20:56

读Raft论文也有一段时间了,但是自己总是以目前并没有完全掌握为由拖着这篇博客。今天先以目前的理解程度(做了6.824的lab2A和lab2B)对这篇论文做一个初步总结,之后有了更深入的理解之后再进行迭代,关于本文有任何疑问欢迎评论交流。另外需要说明的是本篇博客并没有对Raft算法的背景和基础知识进行全面介绍,所以需要有一定的基础之后进行阅读。

基本概念

三种状态及相互转换关系

Raft算法中每个服务器处于这三种状态中的一个。

  • 领导者(Leader):负责处理客户端请求、发送心跳、进行日志同步。正常情况下每个时刻只能有一个领导者。
  • 跟随者(Follower):完全被动的处理请求,也就是处理来自领导者的日志同步请求(心跳包含在其中),以及来自候选者的投票请求,服务器大多数情况属于此状态。
  • 候选者(Candidate):在特殊情况下候选者通过选举可以成为领导者,它是一个中间状态。

以下是三种状态的转换关系图。
raft状态转换.png

日志形式

日志以下图的形式组织,Raft算法通过索引和任期号唯一的标识一条日志条目,并且只有被存储在超过一半节点的日志条目才能认为该日志为已提交(committed)。
image.png

算法总览

论文中的图2给出了Raft算法的整体框架图,下面给出图二的内容。整体分为四部分,第一部分是在代码实现时Raft这个结构体需要定义的属性,第二部分是进行日志同步日志时的RPC定义,第三部分是进行投票选举的RPC的定义,第四部分是每个Server的职责和三种角色的职责。
图片.png
Raft.png

领导者选举

实现流程(理论)

起初Server是Follower,在一段时间内没有收到心跳,或者投出自己的选票则会转换为Candidate。成为Candidate的流程是状态转变为Candidate,当前的任期加一,给自己投一票,然后通过RequestVote RPC向其他节点索要选票。接下来会发生下面三种情况中的一个:(1)如果在超时时间内收到多数选票,则转为Leader并立马发送心跳;(2)如果超时时间过去了没有收到多数选票,则重新开始上面的成为Candidate的流程;(3)如果在选举过程中收到已有领导者的消息则转为Follower。
情况(1):Candidate通过RequestVote rpc向其他节点发送索要选票请求时,会将当前任期,ID,最后一条日志的索引,最后一条日志的任期作为请求参数发送到每个Follower节点。Follower节点在收到请求后会首先进行任期对齐(如果Candidate的任期比我Follower小,则返回给他我的任期,并且不给他投票);然后当我还没给别人投票时,会比较Candidate的最后一条日志与我Follower的最后一条日志谁更新(up-to-date),如果你Candidate更新,那么好我给你我的选票,不然我Follower更新的话就不给你选票。这里谁更新(up-to-date)的规则在下面的 **2.3.1.比较日志新旧规则 **部分给出。
情况(2):这种情况出现的场景就是多个Follower同时成为Candidate,选票被分割,没人当选Leader,重启新的选举流程之后选票又被分割,选不出来Leader,好像进入了死循环,这个情况怎么解决呢?在 **2.3.2.分割选票 **部分给出。
情况(3):这种情况是当Candidate收到一个AppendEntries RPC请求时,并且发送这个请求的Leader的任期大于等于Candidate的任期,则认为当前时刻已有合法的Leader,并且Candidate你需要转换成为Follower。

实现流程(代码)

这里只给出核心的代码实现逻辑。需要定义一个选举计时器,如果当前是Follower或者Candidate并且达到超时时间了,则需要转变为Candidate并开始一轮选举。

func (rf *Raft) electionTicker() {
	for !rf.killed() {
		rf.mu.Lock()
        // rf.isElectionTimeoutLocked() 包含了随机超时时间逻辑
        // 关联细节2.3.2
		if rf.role != Leader && rf.isElectionTimeoutLocked() { 
			rf.becomeCandidateLocked() // 转变为Candidate
			go rf.startElection(rf.currentTerm) // 开始选举给每个节点发送索要投票请求
		}
		rf.mu.Unlock()
		ms := 50 + (rand.Int63() % 300)
		time.Sleep(time.Duration(ms) * time.Millisecond)
	}
}

转变为Candidate的实现如下:

func (rf *Raft) becomeCandidateLocked() {
	if rf.role == Leader {
		return
	}
	rf.currentTerm++  // 任期加一
	rf.role = Candidate  // 状态转变为Candidate
	rf.votedFor = rf.me  // 给自己投一票
    rf.resetElectionTimerLocked() // 重置超时计时器
}

开始选举给每个节点发送索要投票请求的实现:

func (rf *Raft) startElection(term int) {
    // 统计选票
    votes := 0
    rf.mu.Lock()
    l := len(rf.log)
    for peer := 0; peer < len(rf.peers); peer++ {
        if peer == rf.me {
            votes++
            continue
        }
        args := &RequestVoteArgs{
            Term:         rf.currentTerm,
            CandidateId:  rf.me,
            LastLogIndex: l - 1,   // 最后一条日志的索引
            LastLogTerm:  rf.log[l-1].Term,  // 最后一条日志的任期
        }
        go askVoteFromPeer(peer, args)  
    }
    rf.mu.Unlock()
    // 定义一个给其他节点发送索要选票并对回复信息进行处理的匿名函数
    askVoteFromPeer := func(peer int, args *RequestVoteArgs) {
        reply := &RequestVoteReply{}
        // 发送索要选票的请求
        ok := rf.sendRequestVote(peer, args, reply)
        rf.mu.Lock()
        defer rf.mu.Unlock()
        if !ok {
            return
        }
        // 如果发送到的节点的任期更大,则说明本Candidate没资格当Leader
        if reply.Term > rf.currentTerm {
            rf.becomeFollowerLocked(reply.Term)
            return
        }
        // 统计选票,获取过半数选票时转变为Leader并发送心跳
        if reply.VoteGranted {
            votes++
            if votes > len(rf.peers)/2 {
                rf.becomeLeaderLocked()
                go rf.replicationTicker(term)
            }
        }
    }

}

给一个节点发送索要投票请求的函数:

func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {
	ok := rf.peers[server].Call("Raft.RequestVote", args, reply)
	return ok
}

索要投票的RPC ,或者叫Follower对Candidate的请求的回调函数的定义:

func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()
	reply.Term = rf.currentTerm
	reply.VoteGranted = false
    // 如果你Candidate的任期比我小,那你该退下去了
	if args.Term < rf.currentTerm {
		return
	}
    // 如果你Candidate的任期比我大,那我如果是Candidate就得边Follower
	if args.Term > rf.currentTerm {
		rf.becomeFollowerLocked(args.Term)
	}
	if rf.votedFor != -1 {
		return
	}
	// 比较Candidate的最后一条日志与本Follower的最后一条日志谁更新
    // 关联细节2.3.1
	if rf.isMoreUpToDateLocked(args.LastLogIndex, args.LastLogTerm) {
		return
	}
	reply.VoteGranted = true
	rf.votedFor = args.CandidateId
	rf.resetElectionTimerLocked()
}

需要注意的细节

比较日志新旧

论文中关于两个日志谁更新的比较规则是下面这样描述的。总结一下就是首先比较任期,任期大的更新;任期相同则比较索引,索引更大的更新。

Raft determines which of two logs is more up-to-date by comparing the index and term of the last entries in the logs. If the logs have last entries with different terms, then the log with the later term is more up-to-date. If the logs end with the same term, then whichever log is longer is more up-to-date.

分割选票

关于分割选票可以采用随机超时时间的方法来解决。选举超时时间通常在[T, 2T]区间内(例如150ms~300ms)。由于随机性节点不太可能再同时开始竞选,所以先竞选的节点有足够的时间来向其他节点索要选票。

日志同步/心跳

实现流程(理论)

整体上看是:Leader服务来着客户端的请求,每个客户端请求都包含一个要由复制状态机执行的命令,Leader将命令作为新条目追加到其日志中,然后并行地向其他每个服务器发出AppendEntries RPC以同步该命令到全部节点。
详细来说:开始日志同步的过程中Leader需要通过AppendEntries RPC进行日志同步和心跳,该RPC的请求参数包含term、leaderID、prevLogIndex、prevLogTerm、entries、leaderCommit。其中prevLogIndex是根据Leader中定义的nextIndex数组中该Follower的值得到的,prevLogTerm是Leader中prevLogIndex处的日志的任期,entries则包含prevLogIndex之后的所有日志,leaderCommit是Leader已经commit的日志的索引用来推进Leader的apply和Follower日志的commit、apply。当Follower节点收到AppendEntries RPC指令之后,首先比较任期如果领导的任期小于自己则直接返回当前任期让Leader下台。接下来看Follower中prevLogIndex索引处的日志是否任期是prevLogTerm,如果是则匹配成功,接下来把请求中的entries复制到本地的日志中,并且更新该节点的nextIndex数组和matchIndex数组;否则返回返回false让Leader的prevLogIndex继续向前试探,重新发送AppendEntries RPC请求给Follower直到匹配成功。这一部分的更细节的描述见 3.3.1.日志同步请求能否成功的逻辑
对于Leader来说:在Follower接收AppendEntries RPC并成功把日志复制到本地之后,会更新matchindex数组,这个数组更新了之后就可能使得有新的日志已经从Leader被大多数节点复制完成,这时Leader就需要更新自己的commitIndex,同时如果commitIndex一更新就需要触发apply日志的Ticker进行日志应用,具体细节见 3.3.3.apply日志的业务逻辑。这里关于commitIndex的更新有个细节就是Leader只能提交当前任期的日志而不能提交之前任期的日志,具体细节见 3.3.2.不能commit非当前任期的日志
对于Follower来说:Follower会根据AppendEntries RPC的请求参数中的leaderCommit进行自身commitIndex的更新进而推进日志的apply。Leader发送请求的请求参数中的leaderCommit是依据自身的commitIndex来确定的,所以上面的Leader的commitIndex的更新就会使得Leader发送请求的请求参数中的leaderCommit进行更新,进而推进Follower日志的apply。Follower的commitIndex一更新就需要触发apply日志的Ticker进行日志应用,具体细节见 3.3.3.apply日志的业务逻辑。这里还有另外一个细节就是不能直接把Leader的leaderCommit赋值给Follower的commitIndex更新Follower已提交日志索引,还需要考虑Follower的长度,具体细节见 3.3.4.更新Follower的commitIndex时需注意的点

实现细节 (代码)

承接第二部分当Candidate当选Leader后就会go一个replicationTicker协程定时发送心跳以及日志同步请求。

func (rf *Raft) replicationTicker(term int) {
	for !rf.killed() {
		ok := rf.startReplication(term)
		if !ok {
			break
		}
		time.Sleep(replicateInterval)  // replicateInterval为一个固定的值
	}
}

Leader向所有其他节点进行同步日志并对返回结果处理的逻辑如下:

func (rf *Raft) startReplication(term int) bool {
    rf.mu.Lock()
    for peer := 0; peer < len(rf.peers); peer++ {
        if peer == rf.me {
            rf.matchIndex[peer] = len(rf.log) - 1
            rf.nextIndex[peer] = len(rf.log)
            continue
        }
        // 根据要发送节点的nextIndex数组中的数值确定prevLogIndex
        prevIdx := rf.nextIndex[peer] - 1
        prevTerm := rf.log[prevIdx].Term
        args := &AppendEntriesArgs{
            Term:         rf.currentTerm,
            LeaderId:     rf.me,
            PrevLogIndex: prevIdx,
            PrevLogTerm:  prevTerm,
            Entries:      rf.log[prevIdx+1:],  // 将prevLogIndex之后的日志填充进来
            LeaderCommit: rf.commitIndex,  // 根据Leader的commitIndex确定发送的LeaderCommit
        }
        go replicateToPeer(peer, args)
    }
	rf.mu.Unlock()
    return true
    // Leader对单个节点发送日志同步请求并处理响应的匿名函数的定义
    replicateToPeer := func(peer int, args *AppendEntriesArgs) {
        reply := &AppendEntriesReply{}
        ok := rf.sendAppendEntries(peer, args, reply)
        rf.mu.Lock()
        defer rf.mu.Unlock()
        if !ok {
            return
        }
        // 如果得到的返回任期比我Leader,那么就需要变成Follower并return
        if reply.Term > rf.currentTerm {
            rf.becomeFollowerLocked(reply.Term)
            return
        }
        // 如果不匹配则需要试探更前面的日志,最后通过更新nextIndex数组来保存要试探的索引
        // 关联细节3.3.1
        if !reply.Success {
            idx, term := args.PrevLogIndex, args.PrevLogTerm
            for idx > 0 && rf.log[idx].Term == term {
                idx--
            }
            rf.nextIndex[peer] = idx + 1
            return
        }
    	// 如果匹配成功则需要更新matchIndex,nextIndex两个数组
        rf.matchIndex[peer] = args.PrevLogIndex + len(args.Entries) 
        rf.nextIndex[peer] = rf.matchIndex[peer] + 1
    	// 匹配成功则需要判断是否要更新commitIndex
        majorityMatched := rf.getMajorityIndexLocked()
        // 关联细节3.3.2
        if majorityMatched > rf.commitIndex && rf.log[majorityMatched].Term == rf.currentTerm {
            rf.commitIndex = majorityMatched
            // 关联细节3.3.3
            rf.applyCond.Signal()  // 如果更新了commitIndex那么触发Leader节点进行apply
        }
    }
}
func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {
	ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
	return ok
}

AppendEntries RPC的定义也就是Follower对Leader的日志同步请求的回调函数如下:

func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()
	reply.Term = rf.currentTerm
	reply.Success = false
    // 如果Leader任期比我Follower还小,那你应该下台
	if args.Term < rf.currentTerm {
		return
	}
    // 如果你Leader的任期比我Candidate或者Follower大,那我需要变成Follower
	if args.Term >= rf.currentTerm {
		rf.becomeFollowerLocked(args.Term)
	}
	// 如果我Leader要尝试给你同步的日志索引比你Follower的全部日志还长,
    // 那说明你缺日志,不能从PrevLogIndex处开始同步
    // 关联细节3.3.1
	if args.PrevLogIndex >= len(rf.log) {
		return
	}
    // 如果日志不匹配PrevLogIndex索引处的任期一致说明不匹配
    // 关联细节3.3.1
	if rf.log[args.PrevLogIndex].Term != args.PrevLogTerm {
		return
	}
    // 能运行到这里说明匹配成功,就需要把你Leader要发给我的Entries我放到我的日志中
	rf.log = append(rf.log[:args.PrevLogIndex+1], args.Entries...)
	reply.Success = true
	// 如果你Leader的已提交日志索引比我Follower大,
    // 那么我就需要更新我的commitIndex并触发日志apply
	if args.LeaderCommit > rf.commitIndex {
        // 关联细节3.3.4
		rf.commitIndex = min(args.LeaderCommit, len(rf.log)-1)
        // 关联细节3.3.3
		rf.applyCond.Signal()
	}
	rf.resetElectionTimerLocked()  // 收到了心跳重置超时计时器
}

需要注意的细节

日志同步请求能否成功的逻辑

在第一部分中我们说到Raft算法通过索引和任期号唯一的标识一条日志条目,所以判断Leader发送给Follower的AppendEntries RPC的请求能否成功的关键就是看Follower的prevLogIndex索引处的任期是否等于prevLogTerm,相等则可以将请求中的entries添加的Follower的日志中,否则失败需要减小nextIndex数组中该Follower的索引值,进而减小下次试探的prevLogIndex的值。
另外还需要注意一个容易忽视的可能导致的bug就是不匹配有两种情况,一种是Follower的prevLogIndex索引处的任期不一致,还有一种就是Follower中缺日志进而导致Follower中的日志长度<=prevLogIndex,这个情况应该提前判断避免数组越界。

不能commit非当前任期的日志

在论文中给出一个反例,我们详细看一下这个反例。
image.png
(a) S1 是领导者,复制索引2 处的日志条目到S2。
(b) S1崩溃,通过来自 S3、S4 和自身的投票,S5 被选为term 3的领导者,并在日志索引 2 处接收到一个来自客户端的跟S1、S2不同的日志条目。
© S5崩溃,S1重新启动,被选为leader,并继续复制日志到其他节点。此时,term 2中的日志条目已在大多数服务器上复制,但尚未提交。
(d) 如果S1又崩溃了,S5可以被选为leader(来自S2、S3和S4的投票),并用自己在term 3中的条目覆盖索引2的term 2的条目。
(e) 但是,如果S1在崩溃之前在大多数服务器上复制其当前任期的条目,如(e)所示,则该条目被提交(S5无法赢得选举)。此时,也提交了日志中前面的所有条目。
通过上面的(d)可以看到如果我在©处当term 2中的日志条目已在大多数服务器上复制时如果进行提交,那么在(d)处会把已提交的term 2的日志覆盖掉,这种情况是极其不允许的!而且通过(e)可以看到如果S1在崩溃之前在大多数服务器上复制其当前任期的条目,那么一方面能压制S5获取不到大多数投票,另一方面在提交当前term 4的日志时也会间接的term 2的日志进行提交。

apply日志的业务逻辑

可以分为Leader的apply日志和Follower的apply日志。
首先讨论一下Leader可能apply日志的时机,在replicateToPeer匿名函数中,Leader向单个Follower同步成功日志之后需要判断一下commitIndex需不需要更新,如果需要更新,那么说明有新的日志被提交也就会触发Leader日志apply。
其次说一下Follower可能apply日志的时机,当上一段中commitIndex更新之后由于leaderCommit = commitIndex,也就会导致leaderCommit更新,在AppendEntries函数的最后可以看到如果leaderCommit更新变大了就会触发Follower的日志apply。

更新Follower的commitIndex时需注意的点

这个是极易忽视的一点,在Follower处理AppendEntries RPC请求时,最后如果发送过来的leaderCommit参数比我Follower当前的commitIndex大,那么我Follower需要更新我的commitIndex,但是,你注意但是啊,这个commitIndex再大也不能超过我日志的总长度-1吧,这个是及其容易忽视的一点。

总结

在本文结合Raft论文和mit 6.5840(原6.824)的lab2的partA和partB实验对Raft算法的基础概念以及两大重要部分投票选举和日志同步对Raft算法进行了细致的讨论。在后面的实验中还有关于日志持久化和日志压缩的内容,之后会在学习完成之后进行相应内容的更新。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1404387.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

20240122在WIN10+GTX1080下使用字幕小工具V1.2的使用总结(whisper)

20240122在WIN10GTX1080下使用字幕小工具V1.2的使用总结 2024/1/22 19:52 结论&#xff1a;这个软件如果是习作&#xff0c;可以打101分&#xff0c;功能都实现了。 如果作为商业软件/共享软件&#xff0c;在易用性等方面&#xff0c;可能就只能有70分了。 【百分制】 可选的改…

推荐IDEA一个小插件,实用性很高!!

插件&#xff1a; Convert YAML and Properties File 由于每个人的开发习惯不同&#xff0c;在开发过程中会遇到各种小细节的问题。今天给大家介绍一个小插件&#xff0c;作用不大&#xff0c;细节很足。 就是properties类型文件和yml文件互相自由转换 解决&#xff1a;…

[晓理紫]每日论文分享(有中文摘要,源码或项目地址)--机器人、强化学习

专属领域论文订阅 VX 扫吗关注{晓理紫|小李子}&#xff0c;每日更新论文&#xff0c;如感兴趣&#xff0c;请转发给有需要的同学&#xff0c;谢谢支持 如果你感觉对你有帮助可以扫吗关注&#xff0c;每日准时为你推送最新论文 分类: 大语言模型LLM视觉模型VLM扩散模型视觉导航…

Maven 打包时,依赖配置正确,但是类引入出现错误,一般是快照(Snapshot)依赖拉取策略问题

问题描述&#xff1a; 项目打包时&#xff0c;类缺少依赖&#xff0c;操作 pom.xml -> Maven -> Reload project &#xff0c;还是不生效&#xff0c;但是同事&#xff08;别人&#xff09;那里正常。 问题出现的环境&#xff1a; 可能项目是多模块项目&#xff0c;结构…

快速上手MyBatis Plus:简化CRUD操作,提高开发效率!

MyBatisPlus 1&#xff0c;MyBatisPlus入门案例与简介1.1 入门案例步骤1:创建数据库及表步骤2:创建SpringBoot工程步骤3:勾选配置使用技术步骤4:pom.xml补全依赖步骤5:添加MP的相关配置信息步骤6:根据数据库表创建实体类步骤7:创建Dao接口步骤8:编写引导类步骤9:编写测试类 1.2…

使用Go进行HTTP客户端认证

在Go语言中&#xff0c;HTTP客户端认证可以通过net/http包来实现。下面是一个简单的示例&#xff0c;展示如何使用Go进行HTTP客户端认证。 首先&#xff0c;确保你已经安装了Go语言环境&#xff0c;并设置好了相关的环境变量。 Go语言中的HTTP客户端认证主要涉及到设置请求头…

MB6S-ASEMI小功率家用电源MB6S

编辑&#xff1a;ll MB6S-ASEMI小功率家用电源MB6S 型号&#xff1a;MB6S 品牌&#xff1a;ASEMI 正向电流&#xff08;Id&#xff09;&#xff1a;1A 反向耐压&#xff08;VRRM&#xff09;&#xff1a;600V 正向浪涌电流&#xff1a;30A 正向电压&#xff08;VF&…

基于 Spring Boot+MySQL实现的在线考试系统源码+数据库,基于不同类型的客观题,进行自动组卷、批卷等功能的考试系统

1. 部署相关 1.1. 介绍 一个 JAVA 实现的在线考试系统,主要实现一套基于不同类型的客观题,进行自动组卷、批卷等功能的考试系统&#xff08;没有主观题&#xff09; 1.2. 系统架构 后端技术栈基于 Spring Boot数据库MySQLORMMyBatis & MyBatis-plus缓存Redis、guava的L…

2024年安全员-C证证考试题库及安全员-C证试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年安全员-C证证考试题库及安全员-C证试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特种设备作业人员上岗证考试大纲随机出的…

PG14.2异构迁移_数据目录拷贝方式

本文源库和目标库都是采用二进制tar包进行的安装&#xff0c;非rpm和源码编译方式安装。 采用的办法是编译安装数据库软件 拷贝数据目录的方式 迁移要求 由于Centos即将停止维护&#xff0c;客户强烈要求操作系统更改成Ubuntu18.04&#xff0c;需将Centos的PG14迁移至Ubuntu…

opencv#29 图像噪声的产生

在上一节的图像卷积我们了解到图像卷积可以用于去除图像中的噪声&#xff0c;那么对于现实生活中每一张采集到的图像都会包含噪声&#xff0c;也就是我们通过相机无法得到不包含噪声的图像&#xff0c;如果我想衡量噪声去除能力的强弱&#xff0c;就必须在一张不含噪声的图像中…

架构篇11:架构设计流程-设计备选方案

文章目录 架构设计第 2 步&#xff1a;设计备选方案设计备选方案实战小结 上一期我讲了架构设计流程第 1 步识别复杂度&#xff0c;确定了系统面临的主要复杂度问题后&#xff0c;方案设计就有了明确的目标&#xff0c;我们就可以开始真正进行架构方案设计了。今天我来讲讲架构…

【Go面试向】Go程序的执行顺序

【Go】Go程序的执行顺序 大家好 我是寸铁&#x1f44a; 总结了一篇Go程序的执行顺序的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; Go程序内容 go程序通常包含: 包、常量、变量、init()、main()等元素 下面从这几个方面分别去梳理&#xff01; 包的执行顺序 程序中的包 …

【数据结构】链表(单链表与双链表实现+原理+源码)

博主介绍&#xff1a;✌全网粉丝喜爱、前后端领域优质创作者、本质互联网精神、坚持优质作品共享、掘金/腾讯云/阿里云等平台优质作者、擅长前后端项目开发和毕业项目实战✌有需要可以联系作者我哦&#xff01; &#x1f345;附上相关C语言版源码讲解&#x1f345; &#x1f44…

【GoLang入门教程】Go语言工程结构详述

程序员裁员潮&#xff1a;技术变革下的职业危机 文章目录 程序员裁员潮&#xff1a;技术变革下的职业危机前言总结:专栏集锦强烈推荐写在最后 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网…

瑞金市城北社区开展新时代文明实践文艺汇演

为发扬中华民族优秀传统文化&#xff0c;促进社区居民邻里交流&#xff0c;丰富居民业余文化生活&#xff0c;1月18日&#xff0c;瑞金市城市社区城北社区新时代文明实践站在金盛小区开展新时代文明实践文艺汇演活动。 社区文艺爱好者们自编自演的节目丰富多彩&#xff0c;现场…

基于open3d的半径滤波

概念原理 半径滤波器比较简单粗暴。以某点为中心画一个圆计算落在该圆中点的数量&#xff0c;当数量大于给定值时&#xff0c;则保留该点&#xff0c;数量小于给定值则剔除该点。此算法运行速度快&#xff0c;依序迭代留下的点一定是最密集的&#xff0c;但是圆的半径和圆内点…

5.命令源码文件及命令行参数

目录 概述命令源码文件接收参数查看参数的使用说明结束 概述 命令源码文件接收参数 命令源码文件是程序的运行入口&#xff0c;是每个可独立运行的程序必须拥有的 无论是 Linux 还是 Windows&#xff0c;如果用过命令行&#xff08;command line&#xff09;的话&#xff0c;肯…

泥石流监测识别摄像机

泥石流监测识别摄像机是一种基于图像识别技术的监测设备&#xff0c;主要用于实时监测和识别泥石流的发生和演变过程&#xff0c;以预警和减灾为目的。这种摄像机通常采用高清晰度摄像头和图像处理系统&#xff0c;能够实时拍摄泥石流事件&#xff0c;并对图像进行处理和分析&a…

植物神经功能紊乱是什么?

植物神经也叫自律神经&#xff0c;它是一种自发的&#xff0c;非主观意识控制的&#xff0c;低级的神经活动。包括呼吸的、心律的、汗腺的、胃肠道的调节等等&#xff0c;都叫植物神经功能调节。 植物神经它的一旦出现了障碍可以有两种倾向&#xff0c;一种倾向就是出汗、兴奋…