etcdv3.6源码流程---Get

ops/2024/10/21 10:15:34/

线性一致性需要满足的要求:
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.ppmy.cn/ops/34330.html

相关文章

(HAL)STM32F103C8T6——基于STM32的RFID智能门锁系统

一、系统实现的主要功能 a、显示系统初始化界面、功能菜单界面以及实时时间界面&#xff0c;后二者可以随时切换&#xff1b; b、具有4种模式&#xff0c;分别为刷卡解锁、IC卡信息管理、密码解锁、修改密码&#xff0c;并且有LED灯进行提示&#xff1b; c、成功解锁时&…

gateway基本配置详解

Spring Cloud Gateway 是 Spring Cloud 的一个组件&#xff0c;它基于 WebFlux 框架&#xff0c;用于构建 API 网关。API 网关是微服务架构中的一个重要组件&#xff0c;它作为系统的入口&#xff0c;负责处理客户端请求&#xff0c;并将请求路由到相应的服务。以下是 Spring C…

大模型时序预测初步调研20240506

AI预测相关目录 AI预测流程&#xff0c;包括ETL、算法策略、算法模型、模型评估、可视化等相关内容 最好有基础的python算法预测经验 EEMD策略及踩坑VMD-CNN-LSTM时序预测对双向LSTM等模型添加自注意力机制K折叠交叉验证optuna超参数优化框架多任务学习-模型融合策略Transform…

中文输入法导致的高频事件

场景&#xff1a; input.addEventListener(input, (e) > {console.log(e.target.value) }); 当给一个输入框绑定了 input 事件&#xff0c;输入法切换到中文正在拼写的过程中也会触发 input 事件。 解决办法&#xff1a; 在中文拼写开始和结束的时候分别会触发 composit…

软考网络工程师 第六章 第二部分 第五节 ICMP协议

ICMP基础 ICMP&#xff0c;协议号为1&#xff0c;封装在IP报文中&#xff0c;用来传递差错、控制、查询等信息&#xff0c;典型应用ping/tracer依赖ICMP报文 ICMP报文类型与代码 ICMP应用-ping Echo Request和Echo Reply分别用来查询和响应某些信息&#xff0c;进行差错检测。…

滑动验证码登陆测试编程示例

一、背景及原理 处理登录时的滑动验证码有两个难点&#xff0c;第一个是找到滑块需要移动的距离&#xff0c;第二个是模拟人手工拖动的轨迹。模拟轨迹在要求不是很严的情况下可以用先加速再减速拖动的方法&#xff0c;即路程的前半段加速度为正值&#xff0c;后半段为负值去模…

Essential Input and Output

How to read data from the keyboard? How to format data for output on the screen? How to deal with character output? 一、Input from the Keyboard the scanf_s() function that is the safe version of scanf() int scanf_s(const char * restrict format, ... );…

Baidu Comate智能代码助手-高效代码编程体验

关于Baidu Comate智能代码助手 智能代码助手简介 代码助手可以快速的帮我们补充代码&#xff0c;修改代码&#xff0c;添加注释&#xff0c;翻译中英文&#xff0c;起变量函数名字等操作&#xff0c;十分的友好&#xff0c;这类代码助手现阶段有较多的产品&#xff0c;比如&a…