etcdv3.6源码流程---Get

线性一致性需要满足的要求:
1.任何一次读都能读到某个数据的最近一次写的数据。即每次都是直接读最新的数据
2.系统中的所有进程,看到的操作顺序,都与全局时钟下的顺序一致。一旦某个请求在时刻a读到了版本为b的某个值,那么时刻a以后的任何读请求都必须能够读到这个版本或者更新版本的值。
etcd线性一致性读:通过ReadIndex来实现线性一致性读。线性一致性读就是相当于整个集群只有一个副本,一旦一个客户端读到了某个数据,那么后续所有的客户端都可以读到该数据,因为每个日志都有一个索引,且这个索引是单调递增的,每个读请求都对应一个ReadIndex,这个值是读请求到来的时候集群最新的commitedIndex即集群已达成共识的最新的数据,所以只要当appliedIndex>=ReadIndex的时候就可以去bolt数据库查询了,后续就一定不会读到版本比ReadIndex还旧的数据了。

个人笔记:
1:要求读最新数据。一旦请求被放行,那么他就可以去读最新的数据,而不是被限制只能读某个版本之前的数据

2:数据一旦发布,就必须对所有的请求可见

etcd里面是get请求对应的是range函数


etcdserver.EtcdServer.Range                      #就两步:1:先阻塞,直到其他线程通知他可以读;2:去数据库读取最新数据if !serilizable{                               #serilizable表示直接读leader,!serilizable表示ReadIndex即线性一致性读etcdserver.EtcdServer.linearizableReadNotify #执行等待,直到appliedIndex>=ReadIndex后才去数据库读数据s.readwaitc <- struct{}{}                  #发消息到readwatic chan 来通知linearizableReadLoop函数有人需要ReadIndex#ReadIndex:就是leader首先确认自己此刻是不是leader,因为有可能网络分区等原因导致leader实际不是leader#如果是,那么当前leader的commitedIndex就是此读请求对应的ReadIndexanother thread 1{                          #当过半节点都承认leader节点有效地时候,#thread1 会通过写chan来通知上层程序leader当前是有效的,即leader的commitedIndex是有效地#后续上层程序只需要等待appliedIndex>=confirmIndex即可#confirmIndex即请求到来时leader的commitedIndexetcdserver.EtcdServer.linearizableReadLoop #一个死循环,获取此刻最新的commitedIndex,通过chan接受上层发来的ReadIndex请求,#然后也通过chan把处理结果返回给发请求的线程for{                                    #就是一个死循环,监听某个chan,如果收到上层发来请求就唤醒,#然后创建一个chan,发给其他线程,然后等待其他线程发回结果idutil.Generator.Next                 #为每个请求生成一个唯一reqid,后续用来检索case <- s.readwaitc                   #阻塞,直到从readwaitc收到其他线程发来的ReadIndex的请求oldnotifier:=etcdserver.notifier      #保存当前的notifieretcdserver.newNotifier                #创建一个新的notifier,后续到来的读请求都会阻塞在这个新的notifier上,#然后这个新notifier对应的读请求会在下一次循环时处理#之所以这样是因为他是串行处理,当前notifier没处理完就一直阻塞在这里#但是又不能阻止上层继续发来读请求,所以直接在阻塞之前就创建一个新的notifier#然后当他睡眠的时候就把所有的请求都挂到这个notifier下面,当当前的notifer收到结果#后就会结束本次循环,然后下次循环就把这个新的notifer拿来用,然后再创一个新的,如此往复etcdserver.EtcdServer.requestCurrentIndex     #获取此刻commitedIndex的值并保存到一个叫confirmIndex的变量中#注意,一个notifer下面会挂一大串请求,但是他这里只需要请求一次就行#因为请求等待的readIndex不会大于此刻的commitedIndex#所以当applied>commitedIndex时表示所有readIndex<commitedIndex的#读请求的一致性要求都可以满足#从而他会一次性唤醒所有readIndex<此刻commmitedIndex的读请求etcdserver.EtcdServer.sendReadIndex         #获取最新commitedIndexraft.node.ReadIndexraft.node.step(pb.MsgReadIndex,reqid)   #构造一个MsgReadIndex消息#前面生成的reqid作为数据部分放在消息的data字段中,然后处理raft.node.stepWithWaitOptioncase n.recvc <- m                   #把前面构造的的MsgReadIndex消息发到recvc,#然后让他去走一遍stepLeader或者stepFollower里对应的流程(根据节点角色决定)#这个recvc就是专门收发其他节点发来的消息,当然也可以自己发给自己#读取实践中有三种方式:1:Log(每次读也写一条日志)#2:readIndex:就录一个commitedIndex,直到appliedIndex>=记录的commitedIndex#3:直接从本地读,不经过leader#log方式太慢了;readIndex还是需要一轮广播;直接本地读,不安全return nilanother thread 2{                   #node.recvc收到etcdserver发来的ReadIndex请求即发来的MsgReadIdnex消息raft.node.runfor{case m := <-n.recvc           #etcdserver发来的MsgReadIndex消息raft.raft.Stepraft.stepLeadercase pb.MsgReadIndex    #处理思想就是:走一遍heartbeat流程。#如果heartbeat流程中有过半节点拥护当前节点,那么当前节点就是有效地leader#那么此leader当前的commitedIndex就是此请求对应的ReadIndex#即后面说的变量confirmIndex#对MsgReadIndex消息的处理流程如下:#1:用一个map acks保存所有节点对该ReadIndex的投票情况,map的key是节点id#2:发送MsgHeartbeat消息给所有节点#3:收到一个MsgHeartbeatResp时不但要标记该节点x是活跃的,#还要同时令acks[x]=true即认为该节点是赞同当前leader和ReadIndex的if !raft.raft.committedEntryInCurrentTerm   #如果当前leader在任期内还没有提交过日志,#那么就直接挂起这个ReadIndex,然后直接返回#因为在处理完一个ReadIndex时会同时唤醒所有index#在他之前的所有ReadIndex请求,#所以这里可以安心挂起,因为后续的ReadIndex会唤醒它#本文后面会解释append(r.pendingReadIndexMessages)        #挂起就是把请求放到一个pending数据,然后直接不管这个请求了return raft.sendMsgReadIndexResponse                        #发送heartbeat消息给所有peer节点#两步:1:leader自己给自己投一票;2:发消息给followercase ReadOnlySafe:                                 #ReadOnlySafe表示ReadIndex读raft.readOnly.addRequest(r.raftLog.committed, m) #MsgReadIndex消息的数据字段包含了本次ReadIndex的reqid#即reqid对应的这个ReadIndex请求正在等待commttedIndex的当前值#当ReadIndex处理完毕后那么保存的这个commitedIndex值#就是confirmIndexraft.readOnly.recvAck(r.id, m.Entries[0].Data)   #消息的Data字段实际就是ReadIndex对应的reqid,r.id表示leader本身#这里就是leader默认是投自己一票ro:=pendingReadIndex[reqid]                    #获取reqid对应的投票信息ro.acks[id]=true                               #对于reqid对应的这个ReadIndex,leader肯定是表示支持的#只有活跃节点才会放到这个map acks中,#如果后续检测到这个map中有过半节点数#那么就认为reqid对应的ReadIndex被批准了,#就可以通过chan来通知上层可以去数据库读数据了r.bcastHeartbeatWithCtx                          #广播heartbeat消息给所有peer节点                                                #follower节点对heartbeat消息的处理很简单,#就简单返回本身的commitedIndex给leadercase ReadOnlyLeaseBased                            #ReadOnlyLeaseBased表示LeaseRead即副本读,这里忽略}}//another threa 2another thread 3{                               #thread 3处理MsgHeartbetaResp消息,即follower发回来的响应#如果有过半节点承认reqid对应的ReadIndex就通知上层读当前leader是有效的,#reqid对应的ReadIndex请求可以结束等待了raft.node.runfor{case m := <-n.recvc                       #peer节点发来的MsgHeartBeatRespraft.raft.Stepraft.stepLeadercase pb.MsgHeartbeatRes             #对MsgHeartBeatResp的处理主要包括三步:#1:标记该节点x是近期活跃的#2:标记reqid对应的acks[x]=true#3:如果过半,则写chan来通知上层可以结束对该reqid对应的ReadIndex的等待了progress.RecentActive=true          #1:标记该节点是近期活跃if pr.Match < r.raftLog.lastIndex   #follower节点会把自己的commitedIndex告知leader节点,#此处发现follower节点落后了,所以发送MsgApp通知他追赶  raft.raft.sendAppendraft.readOnly.recvAck(perrId,reqid) #2:标记reqid.acks[perrId]=true即该peer节点支持leaderquorum.JointConfig.VoteResult       #3计算投票结果,看reqid对应的map acks中是否有过半节点,#如果有则通知上层linearizableReadLoop reqid对应的读请求可以结束等待了#就是一个count,看是否过半rss=raft.readOnly.advance(m.Index)  #从等待队列移除所有index在m.Index之前的所有pendingRequest,并返回这些pendingRequest#我们前面把reqid和一个commitedIndex(假设值为x)绑定在一起#当x可以结束等待时,那些commitedIndex小于x的ReadIndex请求肯定可以结束等待了raft.raft.responseToReadIndexReq   #根据rss中的请求构造MsgReadIndexResp消息,#这个MsgReadIndexResp消息中包含了reqid对应的ReadIndex值即当时的commitedIndex值#如果消息来源是follower,则把MsgReadIndexResp消息发给follower,#follower再把该req放到readState中(readState用来保存当前已经批准的的读请求)#然后会把readState中的元素发到指定的r.readStateC,#linearizableReadLoop 每次循环就是在等待这个readStateC#我们通过前面的步骤已经确定了该req对应的读请求所等待的commitedIndex值,#因为客户端如果请求的是follower节点,follower节点会把请求转发给follower,#leader会把批准的ReadIndex值放到这个MsgReadIndexResp中(假设用变量confirmIndex表示),#这样后续当follower节点发现本机appliedIndex>=confirmIndex时#就可以遍历readState中的所有读请求,#凡是req.confirmIndex<=appliedIndex的读请求都可以解除阻塞#如果消息来源是leader自己,一样的把他加到leader节点自己的readState数组#在node.run在下一次循环中会检测到readState不为空,然后就触发case rd<-r.Ready()raft.raft.send(pb.MsgReadIndexResp)#消息目的地设置为leader自己,然后发给自己}}//another thread 3another thread 4{                           #上面已经把批准的ReadIndex请求放到readState了,然后readState不为空会被node Ready()检测到#然后raftnode.run会把readState最后一个元素发到指定的r.readStateC以激活下一步raft.raftNode.runfor{selectcase rd := <-r.Ready()              #peer节点发来的MsgHeartBeatRespif len(rd.ReadStates) != 0 {      #我们在thread 3里面append了readStates,所以这次肯定不为空case r.readStateC <- rd.ReadStates[len(rd.ReadStates)-1] #发送最新的readState到指定chan,激活相关线程,#因为他是不断循环的,只要readState不为空,那么就会继续ready,#继续处理,直到为空}}//another thread 4for {                                 #这是一个死循环,等待其他线程处理完MsgReadIndex消息并通过填充readStateC chan来解除死循环#这个for循环是上面那个requestCurrentIndex函数里的#requestCurrentIndex函数会阻塞,直到node.run中把readState中的chan发给他来唤醒它select case rs := <-s.r.readStateC       #阻塞在readStateC上,其他线程处理完MsgReadIndex消息后,#会在上面的thread4中把readstate中的响应填充到readStateC来通知这里结束等待#整个etcdserver.EtcdServer.linearizableReadNotify有三种结束等待的方式:#1:超时或者error结束等待;2:readStateC;3:notifier#一个notifier对象可能对应1批ReadIndex请求,只要这一批有一个请求完成了,#那么他完成时会通知本批次所有请求都结束等待return rs.ReadIndex             #到达这里说明该请求已经被批准了,此处返回结果  case <-firstCommitInTermNotifier #收到了当前任期第一次提交发来的通知。即当客户端发来ReadIndex的时候本leader才刚获得leader资格#在他的这个任期内集群还没有发生过commited事件,所以必须等待,假设旧leader提交到了x+3然后崩溃#然后新leader当选,因为此时集群变了比如旧leader崩溃了,导致没有过半节点到达x+3,#那么新leader就不能从x+3开始提交#需要重新确定commited,这是一个不断尝试的过程,也就是说这是一个不断变化的过程,#所以在新leader确定commited之前#不能读取,也就是新leader第一次提交之前发出的sendReadIndex操作需要在新leader第一次提交之后重新尝试etcdserver.EtcdServer.sendReadIndex time.Timer.Reset               #重置定时器case <-retryTimer.C:etcdserver.EtcdServer.sendReadIndex time.Timer.Reset case <-leaderChangedNotifier     #如果leader变了,则放弃所有读请求,并返回错误returncase <-errorTimer.C              #超时,返回错误return }//当requestCurrentIndex返回后,就可以获取此刻得appliedIndexetcdserver.EtcdServer.getAppliedIndex         #获取当前的appliedIndexif appliedIndex<confirmIndex                  #如果还没有apply到confirmIndex(即读请求到来时的有效commitedIndex值)就继续等待case <- wait.timelist.Wait(confirmIndex)    #继续阻塞,直到etcdserver.EtcdServer.applyAll线程#在完成一此apply操作后主动唤醒所有appliedIndex之前的读请求#这个Wait会创建一个chan,applyAll唤醒它时调用close(ch)来填充这个chan,来结束阻塞etcdserver.notifier.notify                  #当属于同一个notifier的一批请求中的某个被批准的时候#会唤醒所有在等待这个oldnotifier的读请求close(oldnotifier chan)                     #close(chan)会唤醒所有等待这个chan的线程}//another thread 1}<- s.readNotifier #等待readNotifier发来通知。当linearizableReadLoop发现可以进行ReadIndex 读取的时候就会close这个readNotifier来唤醒}//end:if !serilizable etcdserver.EtcdServer.doSerialize #Lease Read指直接从从bbolt数据库读取数据#而ReadIndex读则相当于在LeaseRead之前增加了一个wait操作,直到appliedIndex>=commitedIndex#线性读要求读最新数据,这里就直接去数据库读了,我是这样理解的:只要请求还在服务器上,那么他们就还算是同一批请求,还没有分出先后#因为etcd这里时可以有多个请求都在执行doSerialize的,而他们之间是没有先后的,谁都可以先读完#所以我才认为只有当客户端收到了一个响应,这个请求才算先于后续客户端发出的请求,就是说这个先后还是在客户端这里定义的。#当然,这是我瞎猜的#如果是从不同的节点读也没关系,因为所有请求最终都会统一走一遍leadertxn.Range                       #doSerialize就是LeaseRead,当ReadIndex读请求被放行以后就执行LeaseRead#这个Lease Read就是调用txn.Range来读取#txn.Range就是走一遍STM(软件事务内存)中的读事务,至于STM就在下一篇笔记了

一点随想与杂记(不一定对,可能是错的,因为我也没怎么搞懂到底先后是什么意思,暂时不想搞懂了,说不定哪天就灵光一现了):

http://blog.mrcroxx.com/posts/

v3.1 中利用 batch 来提高写事务的吞吐量,所有的写请求会按固定周期 commit 到 boltDB。当上层向底层 boltdb 层发起读写事务时,都会申请一个事务锁(如以下代码片段),该事务锁的粒度较粗,所有的读写都将受限。对于较小的读事务,该锁仅仅降低了事务的吞吐量,而对于相对较大的读事务(后文会有详细解释),则可能阻塞读、写,甚至 member 心跳都有可能出现超时。

https://maimai.cn/article/detail?fid=1338198277&efid=1QlHCPNjVaVznBt7QlxjHw

https://zyy.rs/post/

https://keys961.github.io/2020/11/06/etcd-raft-7/

Raft 算法就可以保障 CAP 中的 C 和 P,但无法保障 A:

commitedIndex一定是大于appliedIndex

对于同一个key,a先读b后读,b看到的数据版本必须大于等于a读到的版本,一旦一个值被读到,那么后续所有的都必须能读到这个值,也就是说b不能读到旧值.b读到一个值,这个值是不是旧的,肯定需要一个比较对象,这个比较对象坑定是在b读请求之前完成。!!!这个谁先谁后肯定是看时间,因为可能a在节点1上读,b在节点2上读,那么这个先后肯定说的是全局逻辑时钟下的先后。在全局逻辑时钟下,假设a在时刻x处完成读取,b后读,在x+1时刻完成读取,因为客户端只能读取applied的数据,appliedIndex又是单调递增的,所以时刻x处的appliedIndex必定小于时刻x+1处的appliedIndex,而某一时刻,appliedIndex必定小于commitedIndex,所以时刻x处的appliedIndex必定小于时刻x+1处的commited,所以我们当一个读请求到来的时候,我们记下此刻的commitedIndex值k,然后等到appliedIndex>=k的时候再去读就可以保证这个请求不会读到旧值,也就是说这个请求读完成的时间定格在记下commitedIndex的那一刻。我们回到etcd代码,整个etcd程序只有一个线程在运行linearizableReadLoop,也就是说所有请求都会经过linearizableReadLoop这个函数,而linearizableReadLoop这个函数内部又是死循环,每次循环处理一个请求,并且是处理完一个请求才处理下一个请求,并没有开启其他的线程,这样所有请求在linearizableReadLoop中就都是串行处理的,也就是说linearizableReadLoop函数里就给所有的请求都排了一个先后了,所以linearizableReadLoop函数里循环的id(即当前是linearizableReadLoop的第几次循环)就可以看作leader上的逻辑时钟(所以当客户端从follower节点读取的时候也能使用同一个逻辑时钟,所以follower节点的读请求也会通过leader节点来获取commitedIndex),linearizableReadLoop在每次循环中调用requestCurrentIndex函数,这个函数会把当前请求和当前的commitedIndex绑定在一起,也就是说记录下了这个读请求完成的时刻,只要记录下了这个请求对应的时刻,我们就可以任意处理这个请求了,把它发送到其他chan就可以不用管了,然后继续处理下一个请求,因为下一个请求必定是下一次循环了,所以下一次循环中通过requestCurrentIndex获取的commiteIndex必定要大于等于本次循环中获取的commitedIndex,也就是这里保证了顺序。另外一点是,etcd中的chan基本都是容量为1,也就是说本次处理如果没有完毕,那么当上游线程再次准备填充chan的时候就会阻塞,这里也有意无意的提供了一个先后顺序(所以etcd中如果是发送一个数据,可以用chan,也可以通过定时轮询来获取最新数据,但是如果是多个数据,那么一般就是放到数组,然后通过定时轮询来获取新数据,往往不用chan,比如readState和entry)。再举个例子,客户端先后发送a,b两个请求读取同一个key,如果在在收到a请求响应之前就发送了b请求,那么a、b请求就没有先后,他们可能返回任意结果,比如a先b后或者b先a后,但是如果收到了a的请求结果,在此之后再发送b请求,那么b请求必不会读到比a版本还旧的数据,因为b读取时的appliedIndex必定大于等于a读取时的appliedIndex。!!补充说明:只要这一批请求都还在服务器中,那么他们就没有先后,如果客户端已经收到了某个请求的响应,那么该请求就一定是先于此时服务器中的所有请求的。现在的疑点就是这个先后到底是哪里定义的,应该是客户端看到的结果先后吧。在服务器上没有先后的意思这样的,我在etcd代码里看到的是一旦一个ReadIndex请求被唤醒,他就把这个请求丢去LeaseRead,就不管了,也就是说可能有多个请求同时LeaseRead,而LeaseRead完成顺序不一定和唤醒顺序相同

下面是网上文章的摘抄,不记得是网址了:

1.1 Log Read
Raft算法通过Raft算法实现线性一致性读最简单的方法就是让读请求也通过Raft算法的日志机制实现。即将读请求也作为一条普通的Raft日志,在应用该日志时将读取的状态返回给客户端。这种方法被称为Log Read。
Log Read的实现非常简单,其仅依赖Raft算法已有的机制。但显然,Log Read算法的延迟、吞吐量都很低。因为其既有达成一轮共识所需的开销,又有将这条Raft日志落盘的开销。因此,为了优化只读请求的性能,就要想办法绕过Raft算法完整的日志机制。然而,直接绕过日志机制存在一致性问题,因为Raft算法是基于quorum确认的算法,因此即使日志被提交,也无法保证所有节点都能反映该应用了该日志后的结果。
在Raft算法中,所有的日志写入操作都需要通过leader节点进行。只有leader确认一条日志复制到了quorum数量的节点上,才能确认日志被提交。因此,只要仅通过leader读取数据,那么一定是能保证只读操作的线性一致性的。然而,在一些情况下,leader可能无法及时发现其已经不是合法的leader。这一问题在介绍Raft选举算法的Check Quorum优化是讨论过这一问题。当网络分区出现时,处于小分区的leader可能无法得知集群中已经选举出了新的leader。如果此时原leader还在为客户端提供只读请求的服务,可能会出现stale read的问题。为了解决这一问题,《CONSENSUS: BRIDGING THEORY AND PRACTICE》给出了两个方案:Read Index和Lease Read。
1.2 ReadIndex
显然,只读请求并没有需要写入的数据,因此并不需要将其写入Raft日志,而只需要关注收到请求时leader的commit index。只要在该commit index被应用到状态机后执行读操作,就能保证其线性一致性。因此使用了ReadIndex的leader在收到只读请求时,会按如下方式处理:
记录当前的commit index,作为read index。
向集群中的所有节点广播一次心跳,如果收到了数量达到quorum的心跳响应,leader可以得知当收到该只读请求时,其一定是集群的合法leader。
继续执行,直到leader本地的apply index大于等于之前记录的read index。此时可以保证只读操作的线性一致性。
让状态机执行只读操作,并将结果返回给客户端。
可以看出,ReadIndex的方法只需要一轮心跳广播,既不需要落盘,且其网络开销也很小。ReadIndex方法对吞吐量的提升十分显著,但由于其仍需要一轮心跳广播,其对延迟的优化并不明显。
需要注意的是,实现ReadIndex时需要注意一个特殊情况。当新leader刚刚当选时,其commit index可能并不是此时集群的commit index。因此,需要等到新leader至少提交了一条日志时,才能保证其commit index能反映集群此时的commit index。幸运的是,新leader当选时为了提交非本term的日志,会提交一条空日志。因此,leader只需要等待该日志提交就能开始提供ReadIndex服务,而无需再提交额外的空日志。
通过ReadIndex机制,还能实现follower read。当follower收到只读请求后,可以给leader发送一条获取read index的消息,当leader通过心跳广播确认自己是合法的leader后,将其记录的read index返回给follower,follower等到自己的apply index大于等于其收到的read index后,即可以安全地提供满足线性一致性的只读服务。
1.3 Lease Read
ReadIndex虽然提升了只读请求的吞吐量,但是由于其还需要一轮心跳广播,因此只读请求延迟的优化并不明显。而Lease Read在损失了一定的安全性的前提下,进一步地优化了延迟。
Lease Read同样是为了确认当前的leader为合法的leader,但是其实通过心跳与时钟来检查自身合法性的。当leader的heartbeat timeout超时时,其需要向所有节点广播心跳消息。设心跳广播前的时间戳为startstartstart,当leader收到了至少quorum数量的节点的响应时,该leader可以认为其lease的有效期为[start,start+electiontimeout/clockdriftbound)[start, start + election \ timeout / clock\ drift\ bound) [ start , start + election** timeout/clock** drift bound)。因为如果在startstartstart时发送的心跳获得了至少quorum数量节点的响应,那么至少要在election timeout后,集群才会选举出新的leader。但是,由于不同节点的cpu时钟可能有不同程度的漂移,这会导致在一个很小的时间窗口内,即使leader认为其持有lease,但集群已经选举出了新的leader。这与Raft选举优化Leader Lease存在同样的问题。因此,一些系统在实现Lease Read时缩小了leader持有lease的时间,选择了一个略小于election timeout的时间,以减小时钟漂移带来的影响。
当leader持有lease时,leader认为此时其为合法的leader,因此可以直接将其commit index作为read index。后续的处理流程与ReadIndex相同。
etcd有两种读模式:
ReadIndex:将读请求发送到Leader,Leader收到请求,记录下当前的committed index,然后向其他节点发送心跳,通过收到大多数节点的响应,来确认自己还是Leader,等待当前committed index指向的日志Entry,等到applied index > 该committed index,就读取数据响应给客户端。
LeaseRead:使用lease机制,保证Leader的有效性,Leader在处理读操作时无需向Follower发送心跳确认自己的Leader身份,等applied index > 该committed index后可以直接响应数据给客户

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

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

相关文章

LeetCode题练习与总结:分隔链表--86

一、题目描述 给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每个节点的初始相对位置。 示例 1&#xff1a; 输入&#xff1a;head [1,4,3,2,5,2]…

MyScaleDB:SQL+向量驱动大模型和大数据新范式

大模型和 AI 数据库双剑合璧&#xff0c;成为大模型降本增效&#xff0c;大数据真正智能的制胜法宝。 大模型&#xff08;LLM&#xff09;的浪潮已经涌动一年多了&#xff0c;尤其是以 GPT-4、Gemini-1.5、Claude-3 等为代表的模型你方唱罢我登场&#xff0c;成为当之无愧的风口…

第五十三节 Java设计模式 - 工厂模式

Java设计模式 - 工厂模式 工厂模式是一种创建模式&#xff0c;因为此模式提供了更好的方法来创建对象。 在工厂模式中&#xff0c;我们创建对象而不将创建逻辑暴露给客户端。 例子 在以下部分中&#xff0c;我们将展示如何使用工厂模式创建对象。 由工厂模式创建的对象将是…

因果推断 | 潜在结果框架的基础知识

文章目录 1 引言2 框架描述2.1 问题定义2.2 数学表达式 3 实现方案3.1 随机实验数据3.2 一般数据 4 方案评估5 总结6 相关阅读 1 引言 在上一篇文章&#xff08;运筹从业者也需要的因果推断入门&#xff1a;基础概念解析和体系化方法理解&#xff09;中&#xff0c;已经对因果…

Linux下Palabos源码编译安装及使用

目录 软件介绍 基本依赖 其它可选依赖 一、源码下载 二、解压缩&#xff08;通过方式1下载源码.zip格式&#xff09; 三、编译安装 3.1 自带算例 ​编辑3.2 自行开发算例 四、简单使用 4.1 串行运行 4.2 并行运行 4.3 查看结果 软件介绍 Palabos是一款基于LBM&…

EXCEL怎样把筛选后含有公式的数据,复制粘贴到同一行的其它列?

自excel2003版之后&#xff0c;常规情况下&#xff0c;复制筛选后的数据&#xff0c;会忽略隐藏行&#xff0c;仅复制其筛选后的数据&#xff0c;粘贴则是粘贴到连续单元格区域&#xff0c;不管行是在显示状态还是隐藏状态。 一、初始数据&#xff1a; 二、题主的复制粘贴问题…

windows驱动开发-内核调度(一)

驱动层面的调度和同步一向是内核中比较困难的部分&#xff0c;和应用层不一样&#xff0c;内核位于系统进程下&#xff0c;所以它的调度和同步一旦出现纰漏&#xff0c;那会影响所有的程序&#xff0c;而内核并不具备对于这种情况下的纠错能力&#xff0c;没有异常手段能够让挂…

植物生态化学计量主要理论和假说

1 功能关联假说 描述化学计量特征与植物生长功能的关联, 主要包括: (1) 生长速率假说(Growth Rate Hypothesis) (Sterner & Elser, 2002): 随生长速率增加, 植物N:P和C:P呈降低趋势, 而P 含量呈增加趋势。该假说有助于理解植物生长速率的调控机制, 但受其他因素调控…

计算机网络实验——学习记录七(IP协议)

1. Linux下虚拟的以太网卡的MTU1500&#xff1b; 2. nping --udp -p4499 -g40321 -c1 --data-length 1400 192.168.57.254 &#xff08;1&#xff09;nping 命令创建指定大小的数据包&#xff1b; &#xff08;2&#xff09;--udp 指定nping命令创建的数据包是udp数据报&…

深入了解JS作用域

在JavaScript中&#xff0c;作用域&#xff08;Scope&#xff09;是指变量的可访问性或可见性的范围。了解作用域是非常重要的&#xff0c;因为它决定了变量在代码中的访问范围&#xff0c;以及变量的生命周期。 1.全局作用域&#xff08;Global Scope&#xff09;&#xff1a…

EPAI手绘建模APP动画编辑器、信息、工程图

④ 动画&#xff1a;打开关闭动画编辑器。APP中动画包含两个部分&#xff0c;动画编辑器和动画控制器。动画编辑器用来编辑动画。具体来说&#xff0c;选中一个模型后&#xff0c;给模型添加移动、旋转、缩放三种关键帧&#xff0c;不同的模型添加不同的关键帧&#xff0c;实现…

40.乐理基础-拍号-什么是一拍

拍&#xff1a; 首先 以Y分音符的时长为一拍 这一句话&#xff0c;然后拍是音乐中的时长单位&#xff0c;但这个时长单位有点特殊&#xff0c;它并不是完全绝对的某一个时间&#xff0c;而正是因为如此&#xff0c;所以不能用 秒 之类的&#xff0c;已经很确定很绝对的时间单位…

CMakeLists.txt语法规则:部分常用命令说明三

一. 简介 前面几篇文章学习了CMakeLists.txt语法中 add_executable命令&#xff0c;add_library命令&#xff0c;aux_source_directory命令&#xff0c;include_directories命令&#xff0c;add_subdirectory 命令的简单使用。文章如下&#xff1a; CMakeLists.txt语法规则&…

matlab例题大全

1.第1章 MATLAB系统环境 1.1 注&#xff1a;plot函数为画图函数。例plot&#xff08;x1,y1,:,x2,y2,*&#xff09;; 1.2 注&#xff1a;root为求根函数。p为方程变量前面系数矩阵。 1.3 注&#xff1a; 2*x3y-1*z 2; 8*x2*y3*z 4; 45*x3*y9*z 23 求&#xff1a;x,y,z的…

前端工程化的基本介绍

文章目录 一、概念二、前端工程化的细节模块化组件化规范化 一、概念 工程化&#xff0c;可以理解为使用一些方式&#xff0c;去改良提高行业中现有的步骤、设计、应用方式。前端工程化&#xff0c;就是指对前端进行一些流程的标准化&#xff0c;让开发变得更有效率&#xff0…

IDEA 算法要点和难点,实际案例应用,代码实例和解析

"IDEA" 全称是 "International Data Encryption Algorithm"(国际数据加密算法),它是一种对称密钥块加密算法。IDEA 是由瑞士联邦理工学院(ETH Zrich)的 Xuejia Lai 和 James L. Massey 在 1990 年提出的。该算法设计用于替代 DES(Data Encryption St…

关于位操作符的实际应用<C语言>

前言 位操作符在C语言初学阶段相对其他操作符来说&#xff0c;是一种难度比较大的操作符&#xff0c;且运用较少的一类操作符&#xff0c;但是位操作符并不是“一无是处”&#xff0c;合理运用的位操作符&#xff0c;在某些场景下可以优化算法&#xff0c;提高代码的执行效率&a…

Vue框架知识点表格总结

今年的就业形势是真的严峻&#xff0c;后端java的薪资直线下降&#xff0c;不仅薪资大幅下降&#xff0c;而且要求也提高了不少&#xff0c;很多后端java开发岗位&#xff0c;都要求会前端Vue框架以及一些其他前端框架。所以以后前后端都得熟练开发。以后前后端分离&#xff0c…

PyQt5:Qt Designer使用重载的自定义类提升控件

1&#xff0c;以QPushButton举例 2&#xff0c;右击需要提升的控件&#xff0c;选择【提升为...】 3&#xff0c;添加自定义类&#xff0c;不用管 .h 的后缀&#xff0c;不影响使用。 4&#xff0c;完成 5&#xff0c;说明&#xff1a;自定义类的&#xff1a;__init__()方法…

基于STC12C5A60S2系列1T 8051单片机的IIC通信的0.96寸4针OLED12864显示16行点x16列点字模的功能

基于STC12C5A60S2系列1T 8051单片机的IIC通信的0.96寸4针OLED12864显示16行点x16列点字模的功能 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍液晶显示器OLED12864简…