【258源码 cn】【滑稽神锁源码】【锋芒分时公式源码】Key定位源码_定位app源码

1.BoltDB源码解析(六)Get操作
2.单片机c源码中uchar temp,位源位key其中的key是什么意思
3.uni-app实现定位功能
4.vue中key的原理
5.简单说说ConcurrentSkipListMap
6.一次由 RocketMQ 顺序消费延迟的问题定位

Key定位源码_定位app源码

BoltDB源码解析(六)Get操作

       在我们深入了解BoltDB的DB文件结构后,接下来我们将分析其CRUD操作中的码定Get方法。首先来看Bucket的源码Get API,这个操作相对简单,位源位无论是码定读事务还是写事务,都可以通过它获取Bucket中指定key的源码258源码 cnvalue。以下是位源位关键代码:

       代码的核心是Cursor对象,其seek方法在B-tree上定位key,码定返回存储在B-tree页面中的源码key和value指针。

       特别需要注意的位源位是,如果查到的码定value是另一个Bucket,函数会返回nil,源码因为Get方法主要针对普通key,位源位而非Bucket。码定如果需要操作Bucket,源码应使用Bucket方法,如rootBucket.Bucket("user"),就像在MySQL中操作表一样。

       Get方法和Bucket查找过程相似,滑稽神锁源码都通过Cursor.seek定位,但Bucket方法多了openBucket步骤。相似的原因在于BoltDB将Bucket视为value类型存储在B-tree中,这样可以共用一个数据结构来存储Bucket和普通value,Cursor.seek则负责在不分类型的B-tree中查找。

       seek方法会在key不存在时返回大于该key的下一个key,这有利于通用性,包括insert、update和delete操作。Cursor的search方法递归查找,根据isLeaf属性决定是在node还是page上进行。

       BoltDB的写事务会先copy页面到node进行修改,因此读写操作在node和page的处理有所区别。Cursor的search方法根据当前事务类型,选择在node(写事务)或page(读事务)上搜索。

       searchNode和searchPage分别针对node和page执行递归搜索,使用一个stack记录递归路径,确保Cursor能够支持遍历B-tree的锋芒分时公式源码操作。BoltDB的高效体现在读操作中,全程基于mmap的page指针操作,实现了真正的零拷贝。

单片机c源码中uchar temp,key其中的key是什么意思

       根据电路的排布,temp和key分别代表行和列,行、列都确定后,就可以定位是那个按键响应了~

       按键按下后,都会进入key4x4()的函数,先用temp来区分出是哪一行(列),再用key来区分具体为那一列(行),从而确定动作按键的具体位置;

uni-app实现定位功能

       uni-app实现定位功能的步骤如下:

       首先,获取用户地理位置权限。使用uni-app内置的authorize方法,请求用户授权。在manifest.json文件中,点击"源码视图",在mp-weixin配置部分添加相关配置代码。凤尾开花指标源码

       接下来,确保在app.json文件中也配置好权限请求。运行项目到微信开发者工具,再次配置相关代码。在authorize方法中,设置scope参数为userLocation,以请求获取位置信息。若用户拒绝授权,提示他们访问小程序设置页面。

       在实际使用前,要检查是否已获取到定位权限。如果未授权,应适时提示用户并请求授权。

       若需实现精准定位,可以借助腾讯地图。首先,注册腾讯地图开发者,获取key并下载qqmap-wx-jssdk.min.js。url速度检测源码然后,在该文件末尾替换相关代码,并将SDK文件放入libs文件夹。创建腾讯地图对象后,调用逆地址解析方法获取位置信息。

       对于常见问题,解决方案包括:

       - 如果微信小程序定位出错,检查manifest.json的配置,确保已添加正确的权限代码,并在app.json中同步配置。然后,重新编译项目并启动,uni.getLocation方法应该能正常返回经纬度。此外,务必确认AppID已正确配置,可在manifest.json的"微信小程序配置"部分查看。

vue中key的原理

       ä¸€ã€Key是什么

       å¼€å§‹ä¹‹å‰ï¼Œæˆ‘们先还原两个实际工作场景

       å½“我们在使用v-for时,需要给单元加上key

<ul><liv-for="iteminitems":key="item.id">...</li></ul>

       ç”¨+newDate()生成的时间戳作为key,手动强制触发重新渲染

<Comp:key="+newDate()"/>

       é‚£ä¹ˆè¿™èƒŒåŽçš„逻辑是什么,key的作用又是什么?一句话来讲key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确,更快的找到对应的vnode节点

场景背后的逻辑

       å½“我们在使用v-for时,需要给单元加上key

       å¦‚果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。

       å¦‚果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed用+newDate()生成的时间戳作为key,手动强制触发重新渲染

       å½“拥有新值的rerender作为key时,拥有了新key的Comp出现了,那么旧keyComp会被移除,新keyComp触发渲染

二、设置key与不设置key区别

       ä¸¾ä¸ªä¾‹å­ï¼šåˆ›å»ºä¸€ä¸ªå®žä¾‹ï¼Œ2秒后往items数组插入数据

<body><divid="demo"><pv-for="iteminitems":key="item">{ { item}}</p></div><scriptsrc="../../dist/vue.js"></script><script>//创建实例constapp=newVue({ el:'#demo',data:{ items:['a','b','c','d','e']},mounted(){ setTimeout(()=>{ this.items.splice(2,0,'f')//},);},});</script></body>

       åœ¨ä¸ä½¿ç”¨key的情况,vue会进行这样的操作:

       åˆ†æžä¸‹æ•´ä½“流程:

       æ¯”较A,A,相同类型的节点,进行patch,但数据相同,不发生dom操作

       æ¯”较B,B,相同类型的节点,进行patch,但数据相同,不发生dom操作

       æ¯”较C,F,相同类型的节点,进行patch,数据不同,发生dom操作

       æ¯”较D,C,相同类型的节点,进行patch,数据不同,发生dom操作

       æ¯”较E,D,相同类型的节点,进行patch,数据不同,发生dom操作

       å¾ªçŽ¯ç»“束,将E插入到DOM中一共发生了3次更新,1次插入操作

       åœ¨ä½¿ç”¨key的情况:vue会进行这样的操作:

       æ¯”较A,A,相同类型的节点,进行patch,但数据相同,不发生dom操作

       æ¯”较B,B,相同类型的节点,进行patch,但数据相同,不发生dom操作

       æ¯”较C,F,不相同类型的节点

       æ¯”较E、E,相同类型的节点,进行patch,但数据相同,不发生dom操作

       æ¯”较D、D,相同类型的节点,进行patch,但数据相同,不发生dom操作

       æ¯”较C、C,相同类型的节点,进行patch,但数据相同,不发生dom操作

       å¾ªçŽ¯ç»“束,将F插入到C之前一共发生了0次更新,1次插入操作通过上面两个小例子,可见设置key能够大大减少对页面的DOM操作,提高了diff效率

设置key值一定能提高diff效率吗?

       å…¶å®žä¸ç„¶ï¼Œæ–‡æ¡£ä¸­ä¹Ÿæ˜Žç¡®è¡¨ç¤ºå½“Vue.js用v-for正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue将不会移动DOM元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时DOM状态(例如:表单输入值)的列表渲染输出建议尽可能在使用?v-for?时提供?key,除非遍历输出的DOM内容非常简单,或者是刻意依赖默认行为以获取性能上的提升

三、原理分析

       æºç ä½ç½®ï¼šcore/vdom/patch.js里判断是否为同一个key,首先判断的是key值是否相等如果没有设置key,那么key为undefined,这时候undefined是恒等于undefined

functionsameVnode(a,b){ return(a.key===b.key&&((a.tag===b.tag&&a.isComment===b.isComment&&isDef(a.data)===isDef(b.data)&&sameInputType(a,b))||(isTrue(a.isAsyncPlaceholder)&&a.asyncFactory===b.asyncFactory&&isUndef(b.asyncFactory.error))))}

       updateChildren方法中会对新旧vnode进行diff,然后将比对出的结果用来更新真实的DOM

functionupdateChildren(parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly){ ...while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){ if(isUndef(oldStartVnode)){ ...}elseif(isUndef(oldEndVnode)){ ...}elseif(sameVnode(oldStartVnode,newStartVnode)){ ...}elseif(sameVnode(oldEndVnode,newEndVnode)){ ...}elseif(sameVnode(oldStartVnode,newEndVnode)){ //Vnodemovedright...}elseif(sameVnode(oldEndVnode,newStartVnode)){ //Vnodemovedleft...}else{ if(isUndef(oldKeyToIdx))oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx)idxInOld=isDef(newStartVnode.key)?oldKeyToIdx[newStartVnode.key]:findIdxInOld(newStartVnode,oldCh,oldStartIdx,oldEndIdx)if(isUndef(idxInOld)){ //NewelementcreateElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}else{ vnodeToMove=oldCh[idxInOld]if(sameVnode(vnodeToMove,newStartVnode)){ patchVnode(vnodeToMove,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)oldCh[idxInOld]=undefinedcanMove&&nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm)}else{ //samekeybutdifferentelement.treatasnewelementcreateElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}}newStartVnode=newCh[++newStartIdx]}}...}原文:/post/

简单说说ConcurrentSkipListMap

       åŸºæœ¬ä»‹ç»

       è·³è·ƒè¡¨çš„性质如下:

       æœ€åº•å±‚的数据节点按照关键字key升序排列;

       åŒ…含多级索引,每个级别的索引节点按照其关联的数据节点的关键字key升序排列;

       é«˜çº§åˆ«ç´¢å¼•æ˜¯å…¶ä½Žçº§åˆ«ç´¢å¼•çš„子集;

       å¦‚果关键字key在级别level=i的索引中出现,则级别level<=i的所有索引都包含该key。

       è·³è·ƒè¡¨ConcurrentSkipListMap的数据结构如下图所示,下图一共有三层索引,最底下为数据节点,同一层索引中,索引节点之间使用right指针相连,上层索引节点的down指针指向下层的索引节点。

源码分析核心字段分析

       head 指向 node(BASE_HEADER) 的顶层索引。

/***Thetopmostheadindexoftheskiplist.*/privatetransientvolatileHeadIndex<K,V>head;

       BASE_HEADER 头结点,即最顶层索引的头节点的value值

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()

       Node 静态内部类,即数据节点

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}

       Index 静态内部类,即普通索引节点

/***普通索引节点*/staticclassIndex<K,V>{ finalNode<K,V>node;//索引节点指向的数据节点finalIndex<K,V>down;//当前索引节点的正下方索引节点volatileIndex<K,V>right;//当前索引节点的右索引节点/***Createsindexnodewithgivenvalues.*/Index(Node<K,V>node,Index<K,V>down,Index<K,V>right){ this.node=node;this.down=down;this.right=right;}}

       HeadIndex 静态内部类,即当前级别索引的头节点

/***当前级别索引的头节点*/staticfinalclassHeadIndex<K,V>extendsIndex<K,V>{ finalintlevel;//所处索引级别/***node:当前索引指向的数据节点*down:当前索引节点的正下方索引节点*right:当前索引节点的右索引节点*level:当前索引头节点所处的索引级别*/HeadIndex(Node<K,V>node,Index<K,V>down,Index<K,V>right,intlevel){ super(node,down,right);this.level=level;}}查询

       æ ¹æ®æŒ‡å®šçš„key查询节点,源码如下:

publicVget(Objectkey){ //调用doGet方法returndoGet(key);}/***真正实现查询方法*/privateVdoGet(Objectkey){ if(key==null)thrownewNullPointerException();Comparator<?superK>cmp=comparator;outer:for(;;){ for(Node<K,V>b=findPredecessor(key,cmp),n=b.next;;){ Objectv;intc;if(n==null)breakouter;Node<K,V>f=n.next;if(n!=b.next)//inconsistentreadbreak;if((v=n.value)==null){ //nisdeletedn.helpDelete(b,f);break;}if(b.value==null||v==n)//bisdeletedbreak;if((c=cpr(cmp,key,n.key))==0){ @SuppressWarnings("unchecked")Vvv=(V)v;returnvv;}if(c<0)breakouter;b=n;n=f;}}returnnull;}

       åœ¨ä¸Šè¿°ä»£ç ä¸­ï¼Œouter处的for自旋中,首先查看findPredecessor:查询指定key节点的前驱节点。该方法在下面的好多地方会调用,例如插入元素,删除元素以及删除元素对应的索引时都会调用。

       findPredecessor方法源码如下:

/***作用1:找到key对应节点的前驱节点,不一定的真的前驱节点,也可能是前驱结点的前驱节点*作用2:删除无效的索引,即要删除节点时,将节点的索引也删除掉*/privateNode<K,V>findPredecessor(Objectkey,Comparator<?superK>cmp){ if(key==null)thrownewNullPointerException();//don'tpostponeerrorsfor(;;){ //r为q节点的右指针指向的节点,r为当前比较节点,每次都比较r节点的key跟查找的key的大小关系for(Index<K,V>q=head,r=q.right,d;;){ if(r!=null){ Node<K,V>n=r.node;Kk=n.key;//该节点已经删除,需要删除其对应的索引if(n.value==null){ //该节点已经删除,需要删除其对应的索引if(!q.unlink(r))break;//restartr=q.right;//rereadrcontinue;}//当前查找的key比r节点的key大,所以r、q节点都向右移动if(cpr(cmp,key,k)>0){ q=r;r=r.right;continue;}}//当q的下方索引节点为空,则说明已经到数据节点层了,需要退出进行后续查找处理if((d=q.down)==null)returnq.node;/***此时当前查找的key小于r节点的key,需要往下一级索引查找*d节点赋值为为q节点为正下方节点,即下一级索引的正下方节点*/q=d;r=d.right;}}}

       findPredecessor方法的查找过程图示如下:假设要查找节点6

       ç”±äºŽå½“前r节点的key比查询的key小,所以,r、q节点都向右移动,即执行如下代码:

//当前查找的key比r节点的key大,所以r、q节点都向右移动if(cpr(cmp,key,k)>0){ q=r;r=r.right;continue;}

       æ­¤æ—¶r节点指向的数据节点为,节点的key比6节点的key大,此时需要执行如下代码:

/***此时当前查找的key小于r节点的key,需要往下一级索引查找*d节点赋值为为q节点为正下方节点,即下一级索引的正下方节点*/q=d;r=d.right;

       æ­¤æ—¶r节点指向的数据节点为5,5节点的key比6节点的key小,q、r节点向右移动,如下图所示

       æ­¤æ—¶r节点指向的数据节点为,节点的key比6节点的key大,同理需要往下级索引走,如下图所示:

       æ­¤æ—¶r节点指向的数据节点为,节点的key比6节点的key大,同理需要往下级索引走,但是此时下一级索引为空了,即(d = q.down) == null了,此时执行的代码如下, 返回q索引指向的节点,即返回节点5.

//当q的下方索引节点为空,则说明已经到数据节点层了,需要退出进行后续查找处理if((d=q.down)==null)returnq.node;

       ä»¥ä¸Šå°±æ˜¯æ–¹æ³•findPredecessor的查找流程,咱们接着继续看上面的doGet方法

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()0

       é¦–先初始化b、n、f三个节点,如下图所示

        发现此时n节点指向的节点就是要查询的节点,于是执行如下代码:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()1

       ç›´æŽ¥è¿”回n节点的value值。查询操作完成。

插入

       è·³è·ƒè¡¨çš„插入操作分以下四种情况:

       æƒ…况1:跳跃表内存在key一致元素,做替换

       æƒ…况2:插入新元素,无须给新元素生成索引节点

       æƒ…况3:插入新元素,需要给新元素生成索引节点,且索引高度 < maxLevel

       æƒ…况4:插入新元素,需要给新元素生成索引节点,且索引高度 > maxLevel

       æºç å¦‚下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()2

       é¦–先还是跟查询操作类似,调用findPredecessor方法先查找到待插入key的前驱节点,举个例子,例如我们想要插入节点7,如下图所示:

       æŽ¥ç€è·ŸæŸ¥è¯¢æ“ä½œä¸€æ ·çš„步骤如下,直接看图:

        此时r节点指向数据节点1,节点1的key小于待插入的节点7的key,于是节点q、r同时向右移动。

       æ­¤æ—¶r节点指向数据节点,节点的key大于待插入节点7的key,于是往下一层索引继续查找,执行的代码如下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()3

       åŽé¢çš„操作类似

       æ­¤æ—¶r节点的key大于待插入的节点6的key,但是q节点的down指针已为空,此时直接返回q节点指向的节点5。

       æŽ¥ç€å›žåˆ°doPut方法,先来查看outer循环,如下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()4

       é¦–先初始化三个节点b、n、f,n节点为b节点的下一个节点,而f节点为n节点的下一个节点,如下图所示

       æŽ¥ç€æ¯”较节点n与待插入的key的大小,此时n节点的key小于待插入节点的key,于是b、n、f三个节点均向下移动如下图所示

       æ­¤æ—¶n节点的key大于待插入的key,此时执行如下代码,通过cas方式修改b节点的下一个节点为z节点,接着跳出outer循环。

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()5

       ç„¶åŽæˆ‘们知道doPut剩下的代码无非就是判断是否给新插入的节点z创建索引,如果需要创建对应的索引。

       é¦–先通过int rnd = ThreadLocalRandom.nextSecondarySeed();计算出一个随机数,接着进行如下判断:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()6

       å¦‚æžœrnd & 0x) == 0就给新插入的z节点创建索引,我们知道0x = 即最高位和最后一位为1,其余全部是0,

       æ¡ä»¶ï¼š(rnd & 0x) == 0什么时候成立?

       rnd这个随机数最低位和最高位同时是0的时候,条件成立,概率是1/4

       ä¸¾ä¸ªä¾‹å­ï¼šä¾‹å¦‚rnd = = 3条件就成立。

       å¦‚果条件成立的话,接着计算到底给z节点创建几级索引,代码如下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()7

       é€šè¿‡while条件((rnd >>>= 1) & 1) != 0满足几次就创建几级索引。例如:

       rnd = 计算出来的level => 3

       rnd = 计算出来的level => 8

       ç„¶åŽæŽ¥ç€æ¯”较计算出来的z节点的索引跟现有的跳跃表的索引级别大小。

       æƒ…况一:z节点计算出来的索引level比跳跃表的level小

       æƒ…况二:z节点计算处理的索引level比跳跃表的level大。此时会选择最终的level为原来的调表的level + 1

       æƒ…况一

       ç»™z节点创建索引的步骤如下图所示,此时z节点的索引还没有加入跳跃表现有的索引队列中

       æŽ¥ç€ç»§ç»­æ‰§è¡Œsplice循环,代码如下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()8

       åˆå§‹åŒ–q、r节点如下图所示

       æ­¤æ—¶r节点的key比新插入z节点,即7节点小,于是两个节点q、t都向右移动如下图所示

       æ­¤æ—¶r节点的key比新插入z节点,即7节点大,执行如下代码:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()9

       æ­¤æ—¶r节点的key比新插入z节点,即7节点小,于是两个节点q、t都向右移动如下图所示

       æ­¤æ—¶r节点的key比新插入z节点,即7节点大,同理,直接看图

       æƒ…况二

       è·Ÿæƒ…况一类似,这里就不一一画图了

删除

       åˆ é™¤æ–¹æ³•å®Œæˆçš„任务如下:

       è®¾ç½®æŒ‡å®šå…ƒç´ value为null

       å°†æŒ‡å®šnode从node链表移除

       å°†æŒ‡å®šnode的index节点 从 对应的 index 链表移除

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}0

       åŒæ ·ï¼Œé¦–先通过findPredecessor方法查找到要删除key的前驱节点,就不一一画图了,直接看找到的前驱节点的图,如下:

       æŽ¥æ¯”较n节点的key与待删除的key的大小,此时n节点的key小于待删除的key,即7节点的key,于是将b、n、f三个节点都向右移动,如下图:

       æ­¤æ—¶n节点的key跟待删除的key一样,于是执行如下代码:

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}1

       æœ€åŽå†è°ƒç”¨findPredecessor清楚无效的索引,即上面删除的节点的索引。

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}2

       é‡ç‚¹é å¦‚下代码块删除索引的:

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}3

       æˆ‘们知道在上面已经将待删除的7节点的value置为null了,直接看图:

       æ­¤æ—¶r节点的key小于待删除节点的key,于是r、q节点都向右移动。

       æ­¤æ—¶r,n节点指向的数据节点的value值为null于是执行上面的q.unlink(r)代码,将q的右指针指向r的右指针指向的节点,即就是删除了该level上的7节点的索引节点,如下图所示

       æ­¤æ—¶r节点的key大于待删除节点的key,于是往下一索引走,如下图所示

       æ­¤æ—¶r节点的key小于待删除节点的key,于是r、q节点都向右移动。

       æ­¤æ—¶r,n节点指向的数据节点的value值为null于是执行上面的q.unlink(r)代码,将q的右指针指向r的右指针指向的节点,即就是删除了该level上的7节点的索引节点,如下图所示

       åŽç»­æ“ä½œåŒç†ï¼Œæœ€ç»ˆå°†7节点的索引一一删除完,最终的图下所示

一次由 RocketMQ 顺序消费延迟的问题定位

       昨晚,我们接收到线上业务消费消息的报警,发现消息延迟了秒。查看RocketMQ监控,发现消息积压较多。从RocketMQ控制台查看Topic的消费者,发现业务要求有序消费,因此在发送和消费时均使用了顺序模式,并指定了业务Key。

       RocketMQ集群中有三个Broker,每个Broker有8个ReadQueue和WriteQueue。发送消息时使用WriteQueue返回路由信息,消费时使用ReadQueue返回路由信息。物理文件层面只有WriteQueue创建文件,设置WriteQueueNum为8,ReadQueueNum为4,将创建8个文件夹,代表0到7这8个队列。但消费时,路由信息返回4个,实际只会消费0到3的队列消息,4到7队列未被消费。反之,设置WriteQueueNum为4,ReadQueueNum为8,生产时只会往0到3队列中生产,消费时则会从所有队列0到7中消费,但4到7队列实际上没有消息。我们通常设置这两个值相同,只有在需要调整Topic队列数量时才会不同。

       首先,我们猜测是消费线程卡住了。通过JFR采集,我们未发现应用异常。接下来,我们关注RocketMQ的日志信息。由于消息发送时指定了hashKey,通过hashKey可以定位到是哪个Broker。我们找到了消息的hashKey,并通过代码定位到是Broker-2上的队列5。我们查看Broker-2的日志,发现lock.log中有异常记录,持续了秒左右,与线程park时间和消息延迟相符合。

       异常日志表明,一个实例尝试锁住queueId=5失败,因为另一个实例正在持有这个锁。接下来,我们深入分析RocketMQ多队列顺序消费的原理。为了实现多队列顺序消费,首先需要指定hashKey,消息会被放入特定队列,消费者在单线程消费时保证同一队列内有序。

       为了确保每个队列单线程消费,Broker维护一个ConcurrentMap,锁对象LockEntry包含多个字段。RebalanceLockManager类处理客户端发送的LOCK_BATCH_MQ请求,封装为LockEntry并尝试更新Map。如果更新成功,表示获取到锁;失败则未获取锁。Broker的更新逻辑较为复杂,详情可查看源码或跳过,不影响理解。每个MQ客户端定时发送LOCK_BATCH_MQ请求,并在本地维护获取到锁的队列列表。

       在问题中,客户端发送LOCK_BATCH_MQ的间隔默认为秒,Broker端锁过期时间为秒。我们的集群使用k8s进行容器编排,并具有实例迁移功能。在高压力场景下,集群自动扩容Node,创建新的服务实例。当压力较小,Node回收时,未等待ConsumeMessageOrderlyService关闭,导致锁未主动释放。锁过期后,新实例才开始消费队列,引发问题。

       为解决此问题,需调整客户端发送LOCK_BATCH_MQ的间隔和Broker锁过期时间。同时,优化k8s集群管理策略,避免实例迁移导致的锁未释放问题。通过优化配置和管理,可以有效解决RocketMQ多队列顺序消费延迟的问题。

linkedmultivaluemap获取key的值

       é€šè¿‡key找到的值是集合

       åœ¨LinkedMultiValueMap源码中并没有对并发进行处理所以这个集合存在着线程安全问题,并发条件下需要对其处理,或封装,也可以其他的集合代替来达到需求需要

更多内容请点击【娱乐】专栏