通过这三个文件彻底搞懂Rocketmq的存储原理

开发 前端
我们知道RocketMQ的架构是producer、NameServer、broker、Consumer,producer是生产消息的,NameServer是路由中心,负责服务的注册发现以及路由管理这些。

[[429030]]

RocketMQ是阿里开发的一个高性能的消息队列,支持各种消息类型,而且支持事务消息,可以说是现在的很多系统中的香饽饽了,所以呢,怎么使用大家肯定是要学习的。

我们作为一个有梦想的程序员,在学习一门技术的时候,肯定是不能光知其然,这是远远不够的,我们必须要知其所以然,这样才能在面试的时候侃侃而谈,啊呸,不对,这样我们才能在工作中遇到问题的时候,理性的去思考如何解决问题。

我们知道RocketMQ的架构是producer、NameServer、broker、Consumer,producer是生产消息的,NameServer是路由中心,负责服务的注册发现以及路由管理这些。

Consumer是属于消费消息的,broker则属于真正的存储消息,以及进行消息的持久化,也就是存储消息的文件和索引消息的文件都在broker上。

消息队列的主要作用是解耦异步削峰,也就意味着消息队列中的存储功能是必不可少的,而随着时代的发展,业务量的增加也对消息队列的存储功能的强度的要求越来越高了。

也就是说你不能光性能好,你得存储的消息也得足够支撑我的业务量,你只能存储100MB的消息,我这系统每分钟的消息业务量可能500MB了,那肯定不够使啊,那还削个啥的峰啊,峰来了你自己都顶不住。

RocketMQ凭借其强大的存储能力和强大的消息索引能力,以及各种类型消息和消息的特性脱颖而出,于是乎,我们这些有梦想的程序员学习RocketMQ的存储原理也变得尤为重要。

而要说起这个存储原理,则不得不说的就是RocketMQ的消息存储文件commitLog文件,消费方则是凭借着巧妙的设计Consumerqueue文件来进行高性能并且不混乱的消费,还有RocketMQ的强大的支持消息索引的特性,靠的就是indexfile索引文件。

我们这篇文章就从这commitLog、Consumerqueue、indexfile这三个神秘的文件说起,搞懂这三个文件,RocketMQ的核心就被你掏空了。

先上个图,写入commitLog文件时commitLog和Consumerqueue、indexfile文件三者的关系:

01 Commitlog文件

  • 大小和命名规则

RocketMQ中的消息存储文件放在${ROCKET_HOME}/store 目录下,当生产者发送消息时,broker会将消息存储到Commit文件夹下,文件夹下面会有一个commitLog文件,但是并不是意味着这个文件叫这个,文件命名是根据消息的偏移量来决定的。

文件有自己的生成规则,每个commitLog文件的大小是1G,一般情况下第一个 CommitLog 的起始偏移量为 0,第二个 CommitLog 的起始偏移量为 1073741824 (1G = 1073741824byte)。

也正是因为该文件的文件名字规则,所以也可以更好的知道消息处于哪个文件中,假设物理偏移量是1073741830,则相对的偏移量是6(6 = 1073741830 - 1073741824),于是判断出该消息位于第二个commitLog文件上,下面要说的Consumerqueue文件和indexfile文件都是通过偏移量来计算出消息位于哪个文件,进行更为精准的定位,减少了IO次数。

  • 文件存储规则和特点

commitLog文件的最大的一个特点就是消息的顺序写入,随机读写,关于commitLog的文件的落盘有两种,一种是同步刷盘,一种是异步刷盘,可通过 flushDiskType 进行配置。

在写入commitLog的时候内部会有一个mappedFile内存映射文件,消息是先写入到这个内存映射文件中,然后根据刷盘策略写到硬盘中,对于producer的角度来说就是,同步就是当消息真正的写到硬盘的时候才会给producer返回成功,而异步就是当消息到达内存的时候就返回成功了,然后异步的去刷盘。

跑题了,最大的特点顺序写入,所有的topic的消息都存储到commitLog文件中,顺序写入可以充分的利用磁盘顺序减少了IO争用数据存储的性能,kafka也是通过硬盘顺序存盘的。

大家都常说硬盘的速度比内存慢,其实这句话也是有歧义的,当硬盘顺序写入和读取的时候,速度不比内存慢,甚至比内存速度快,这种存储方式就好比数组,我们如果知道数组的下标,则可以直接通过下标计算出位置,找到内存地址,众所周知,数组的读取是很快的,但是数组的缺点在于插入数据比较慢,因为如果在中间插入数据需要将后面的数据往后移动。

而对于数组来说,如果我们只会顺序的往后添加,数组的速度也是很快的,因为数组没有后续的数据的移动,这一操作很耗时。

回到RocketMQ中的commitLog文件,也是同样的道理,顺序的写入文件也就不需要太多的去考虑写入的位置,直接找到文件往后放就可以了,而取数据的时候,也是和数组一样,我们可以通过文件的大小去精准的定位到哪一个文件,然后再精准的定位到文件的位置。

当然,至于这个索引位置就是靠下面的Consumerqueue文件和indexfile文件来找到消息的位置的,也就是索引地址。

哦对了,数组的元素大小是一样的,并不意味这commitLog文件的各个消息存储空间一样。

  • 简单看下源码

这部分源码在DefaultMessageStore.putMessage。

  1. @Override 
  2.   public PutMessageResult putMessage(MessageExtBrokerInner msg) { 
  3.       if (this.shutdown) { 
  4.           log.warn("message store has shutdown, so putMessage is forbidden"); 
  5.           return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); 
  6.       } 
  7.  
  8.       // 从节点不允许写入 
  9.       if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) { 
  10.           long value = this.printTimes.getAndIncrement(); 
  11.           if ((value % 50000) == 0) { 
  12.               log.warn("message store is slave mode, so putMessage is forbidden "); 
  13.           } 
  14.  
  15.           return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); 
  16.       } 
  17.  
  18.       // store是否允许写入 
  19.       if (!this.runningFlags.isWriteable()) { 
  20.           long value = this.printTimes.getAndIncrement(); 
  21.           if ((value % 50000) == 0) { 
  22.               log.warn("message store is not writeable, so putMessage is forbidden " + this.runningFlags.getFlagBits()); 
  23.           } 
  24.  
  25.           return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); 
  26.       } else { 
  27.           this.printTimes.set(0); 
  28.       } 
  29.  
  30.       // topic过长 
  31.       if (msg.getTopic().length() > Byte.MAX_VALUE) { 
  32.           log.warn("putMessage message topic length too long " + msg.getTopic().length()); 
  33.           return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); 
  34.       } 
  35.  
  36.       // 消息附加属性过长 
  37.       if (msg.getPropertiesString() != null && msg.getPropertiesString().length() > Short.MAX_VALUE) { 
  38.           log.warn("putMessage message properties length too long " + msg.getPropertiesString().length()); 
  39.           return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null); 
  40.       } 
  41.  
  42.       if (this.isOSPageCacheBusy()) { 
  43.           return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null); 
  44.       } 
  45.  
  46.       long beginTime = this.getSystemClock().now(); 
  47.       // 添加消息到commitLog 
  48.       PutMessageResult result = this.commitLog.putMessage(msg); 
  49.  
  50.       long eclipseTime = this.getSystemClock().now() - beginTime; 
  51.       if (eclipseTime > 500) { 
  52.           log.warn("putMessage not in lock eclipse time(ms)={}, bodyLength={}", eclipseTime, msg.getBody().length); 
  53.       } 
  54.       this.storeStatsService.setPutMessageEntireTimeMax(eclipseTime); 
  55.  
  56.       if (null == result || !result.isOk()) { 
  57.           this.storeStatsService.getPutMessageFailedTimes().incrementAndGet(); 
  58.       } 
  59.  
  60.       return result; 
  61.   } 

中间的commitLog.putMessage就是负责实现消息写入commitLog文件,这个太长了,我就不给大家截了。

大致流程就是组装消息,放入属性,然后通过MappedFile对象写入文件,紧接着根据刷盘策略刷盘,最后进行主从同步。

02 consumerQueue文件

RocketMQ是分为多个topic,消息所属主题,属于消息类型,每一个topic有多个queue,每个queue放着不同的消息,在同一个消费者组下的消费者,可以同时消费同一个topic下的不同queue队列的消息。不同消费者下的消费者,可以同时消费同一个topic下的相同的队列的消息。而同一个消费者组下的消费者,不可以同时消费不同topic下的消息。

而每个topic下的queue队列都会对应一个Consumerqueue文件,例如Topic中有三个队列,每个队列中的消息索引都会有一个编号,编号从0开始,往上递增。并由此一个位点offset的概念,有了这个概念,就可以对Consumer端的消费情况进行队列定义。

消息消费完成后,需要将消费进度存储起来,即前面提到的offset。广播模式下,同消费组的消费者相互独立,消费进度要单独存储;集群模式下,同一条消息只会被同一个消费组消费一次,消费进度会参与到负载均衡中,故消费进度是需要共享的。

消费进度,也就是由Broker管理每一个消费者消费Topic的进度,包含正常提交消费进度和重置消费进度,消费进度管理的目的是保证消费者在正常运行状态、重启、异常关闭等状态下都能准确续接“上一次”未处理的消息。

在RocketMQ中,实现的消费语义叫“至少投递一次”,也就是所有的消息至少有一次机会消费不用担心会丢消息。用户需要实现消费幂等来避免重复投递对业务实际数据的影响。

幂等是啥应该不用我多说了吧,亲爱的你们肯定知道了。

如上图所示,消费者一般在两种情况下“上报”消费进度,消费成功后(包含正常消费成功、重试消费成功)和重置消费进度。

而消费进度的标准就是Consumerqueue文件,这个文件中存储的是投递到某一个messagequeue中的位置信息。

比如我们知道消息存储到commitLog文件中,一个消费者A对应着消费messagequeueA这个队列,但是无法确定在commitLog文件中该队列中的消息的位置,于是就有了ConsumerqueueA这个文件,这个文件对应一个messagequeueA,消费者A便可以通过ConsumerqueueA来确定自己的消费进度,获取消息在commitLog文件中的具体的offset和大小。

  • 存放位置和结构

consumequeue存放在store文件里面,里面的consumequeue文件里面按照topic排放,然后每个topic默认4个队列,里面存放的consumequeue文件。

ConsumeQueue中并不需要存储消息的内容,而存储的是消息在CommitLog中的offset。也就是说ConsumeQueue其实是CommitLog的一个索引文件。

consumequeue是定长结构,每个记录固定大小20个字节,单个consumequeue文件默认包含30w个条目,所以单个文件大小大概6M左右。

很显然,Consumer消费消息的时候,要读2次:先读ConsumeQueue得到offset,再通过offset找到CommitLog对应的消息内容。

  • ConsumeQueue的作用

消费者通过broker保存的offset(offsetTable.offset json文件中保存的ConsumerQueue的下标)可以在ConsumeQueue中获取消息,从而快速的定位到commitLog的消息位置,由于每个消息的大小是不一样的,也可以通过size获取到消息的大小,从而读取完整的消息。

过滤tag是也是通过遍历ConsumeQueue来实现的(先比较hash(tag)符合条件的再到具体消息比较tag)。

  • offsetTable.offset

和commitLog的offset不是一回事,这个offset是ConsumeQueue文件的(已经消费的)下标/行数,可以直接定位到ConsumeQueue并找到commitlogOffset从而找到消息体原文。这个offset是消息消费进度的核心,不同的消费模式,保存地址不同。

广播模式:DefaultMQPushConsumer的BROADCASTING模式,各个Consumer没有互相干扰,使用LoclaFileOffsetStore,把Offset存储在Consumer本地。

集群模式:DefaultMQPushConsumer的CLUSTERING模式,由Broker端存储和控制Offset的值,使用RemoteBrokerOffsetStore。

  • 简单看下构建过程

在Broker中,构建ComsummerQueue不是存储完CommitLog就马上同步构建的,而是通过一个线程任务异步的去做这个事情。在DefaultMessageStore中有一个ReputMessageService成员,它就是负责构建ComsumerQueue的任务线程。

ReputMessageService继承自ServiceThread,表明其是一个服务线程,它的run方法很简单,如下所示:

  1. public void run() { 
  2.             while (!this.isStopped()) { 
  3.                 try { 
  4.                     Thread.sleep(1); 
  5.                     this.doReput(); // 构建ComsumerQueue 
  6.                 } catch (Exception e) { 
  7.                     DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e); 
  8.                 } 
  9.             } 
  10.         } 

在run方法里,每休息1毫秒就进行一次构建ComsumerQueue的动作。因为必须先写入CommitLog,然后才能进行ComsumerQueue的构建。那么不排除构建ComsumerQueue的速度太快了,而CommitLog还没写入新的消息。这时就需要sleep下,让出cpu时间片,避免浪费CPU资源。

我们点进去这个doReput()看核心处理逻辑:

  1. private void doReput() { 
  2.             for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) { 
  3.                 SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);// 拿到所有的最新写入CommitLog的数据 
  4.                 if (result != null) { 
  5.                     try { 
  6.                         this.reputFromOffset = result.getStartOffset(); 
  7.  
  8.                         for (int readSize = 0; readSize < result.getSize() && doNext; ) { 
  9.                             DispatchRequest dispatchRequest = 
  10.                             DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), falsefalse); // 一条一条的读消息 
  11.                             int size = dispatchRequest.getMsgSize(); 
  12.  
  13.                             if (dispatchRequest.isSuccess()) { 
  14.                                 if (size > 0) { 
  15.                                     DefaultMessageStore.this.doDispatch(dispatchRequest); // 派发消息,进行处理,其中就包括构建ComsumerQueue 
  16.                                     this.reputFromOffset += size
  17.                                     readSize += size
  18.                                 } else if (size == 0) { //  
  19.                                     this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset); 
  20.                                     readSize = result.getSize(); 
  21.                                 } 
  22.                             } else if (!dispatchRequest.isSuccess()) { // 获取消息异常 
  23.  
  24.                                 if (size > 0) { 
  25.                                     log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset); 
  26.                                     this.reputFromOffset += size
  27.                                 } else { 
  28.                                     doNext = false
  29.                                     if (DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) { 
  30.                                         this.reputFromOffset += result.getSize() - readSize; 
  31.                                     } 
  32.                                 } 
  33.                             } 
  34.                         } 
  35.                     } finally { 
  36.                         result.release(); 
  37.                     } 
  38.                 } else { 
  39.                     doNext = false
  40.                 } 
  41.             } 
  42.         } 

我在这里省略了一些和构建ComsumerQueue不相干的代码。

其实在doReput里面就做了三件事:

1、获取最新写入到CommitLog中的数据byteBuffer。

2、从byteBuffer中一条条的读取消息,并派发出去处理。

3、更新reputFromOffset位移。

感兴趣的可以打断点走一遍。

03 indexFile文件

RocketMQ还支持通过MessageID或者MessageKey来查询消息,使用ID查询时,因为ID就是用broker+offset生成的(这里msgId指的是服务端的),所以很容易就找到对应的commitLog文件来读取消息。

对于用MessageKey来查询消息,MessageStore通过构建一个index来提高读取速度。

  • 文件结构 

indexfile文件存储在store目录下的index文件里面,里面存放的是消息的hashcode和index内容,文件由一个文件头组成:长40字节。500w个hashslot,每个4字节。2000w个index条目,每个20字节。

所以这里我们可以估算每个indexfile的大小为:40+500w4+2000w20个字节,大约400M左右。

  • 文件详细信息

IndexHeader:索引文件头信息由40个字节组成。

  1. //8位 该索引文件的第一个消息(Message)的存储时间(落盘时间) 
  2. this.byteBuffer.putLong(beginTimestampIndex, this.beginTimestamp.get()); 
  3. //8位 该索引文件的最后一个消息(Message)的存储时间(落盘时间) 
  4. this.byteBuffer.putLong(endTimestampIndex, this.endTimestamp.get()); 
  5. //8位 该索引文件第一个消息(Message)的在CommitLog(消息存储文件)的物理位置偏移量(可以通过该物理偏移直接获取到该消息) 
  6. this.byteBuffer.putLong(beginPhyoffsetIndex, this.beginPhyOffset.get()); 
  7. //8位 该索引文件最后一个消息(Message)的在CommitLog(消息存储文件)的物理位置偏移量 
  8. this.byteBuffer.putLong(endPhyoffsetIndex, this.endPhyOffset.get()); 
  9. //4位 该索引文件目前的hash slot的个数 
  10. this.byteBuffer.putInt(hashSlotcountIndex, this.hashSlotCount.get()); 
  11. //4位 索引文件目前的索引个数 
  12. this.byteBuffer.putInt(indexCountIndex, this.indexCount.get()); 

Slot槽位,默认每个文件配置的slot是500万个,每个slot是4位的整型数据,Slot每个节点保存当前已经拥有多少个index数据了。

  1. //slot的数据存放位置 40 + keyHash %(500W)* 4 
  2. int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize; 
  3.  
  4. //Slot Table 
  5. //4字节 
  6. //记录该slot当前index,如果hash冲突(即absSlotPos一致)作为下一次该slot新增的前置index 
  7. this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount()); 

索引消息内容,消息长度固定为20位。

  1. //Index Linked list 
  2. //topic+message key的hash值 
  3. this.mappedByteBuffer.putInt(absIndexPos, keyHash); 
  4. //消息在CommitLog的物理文件地址, 可以直接查询到该消息(索引的核心机制) 
  5. this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset); 
  6. //消息的落盘时间与header里的beginTimestamp的差值(为了节省存储空间,如果直接存message的落盘时间就得8bytes) 
  7. this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff); 
  8. //9、记录该slot上一个index 
  9. //hash冲突处理的关键之处, 相同hash值上一个消息索引的index(如果当前消息索引是该hash值的第一个索引,则prevIndex=0, 也是消息索引查找时的停止条件),每个slot位置的第一个消息的prevIndex就是0的 
  10. this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue); 
  • 再论结构

文件结构slot和indexLinkedList可以理解成java中的HashMap。

哎,你说HashMap我可不困了啊,你可别蒙我,这个我熟,什么负载因子、默认大小、扩容机制、红黑树,还有多线程下不安全这些。

乖,我知道你熟悉,你跟着我一起学习,这些当然了如指掌,只需要你了解HashMap的结构和冲突即可。

每放入一个新消息的index进来,首先会取MessageKey的HashCode,然后用Hashcode对slot的总数进行取模,决定该消息key的位置,slot的总数默认是500W个。

只要取hash就必然面临着hash冲突的问题,indexfile也是采用链表结构来解决hash冲突,这一点和HashMap一样的,不过这个不存在红黑树转换这一说,个人猜测这个的冲突数量也达不到很高的级别,所以进行这方面的设计也没啥必要,甚至变成了强行增加indexfile的文件结构难度。

还有,在indexfile中的slot中放的是最新的index的指针,因为一般查询的时候大概率是优先查询最近的消息。

每个slot中放的指针值是索引在indexfile中的偏移量,也就是后面index的位置,而index中存放的就是该消息在commitlog文件中的offset,每个index的大小是20字节,所以根据当前索引是这个文件中的第几个偏移量,也就很容易定位到索引的位置,根据前面的固定大小可以很快把真实坐标算出来,以此类推,形成一个链表的结构。

  • 查询流程

由于indexHeader,slot,index都是固定大小,所以:

  • 公式1:第n个slot在indexFile中的起始位置是这样:40+(n-1)*4
  • 公式2:第s个index在indexFile中的起始位置是这样:40+5000000*4+(s-1)*20

查询的传入值除了key外,还包含一个时间起始值以及截止值。

为啥还要传时间范围呢?

一个indexFile写完一个会继续写下一个,仅仅一个key无法定位到具体的indexFile,时间范围就为了更精确的定位到具体的indexFile,缩小查找的范围,indexFile文件名是一个时间戳,根据这个日期就可以定位到传入的日期范围对应在哪个或者哪些indexFile中,是不是很棒。

好了,我们接着说查询流程。

key-->计算hash值-->hash值对500万取余算出对应的slot序号-->根据40+(n-1)*4(公式1)算出该slot在文件中的位置-->读取slot值,也就是index序号-->根据40+5000000*4+(s-1)*20(公式2)算出该index在文件中的位置-->读取该index-->将key的hash值以及传入的时间范围与index的keyHash值以及timeDiff值进行比对。

不满足则根据index中的preIndexNo找到上一个index,继续上一步;满足则根据index中的phyOffset拿到commitLog中的消息。

为啥比对时还要带上时间范围呢?

只比key不行吗?答案是不行,因为key可能会重复,producer在消息生产时可以指定消息的key,这个key显然无法保证唯一性,那自动生成的msgId呢?也不能保证唯一,你可以去看看msgId的生成规则。

包括当前机器IP+进程号+MessageClientIDSetter.class.getClassLoader()的hashCode值+消息生产时间与broker启动时间的差值+broker启动后从0开始单调自增的int值,前面三项很明显可能重复,后面两项一个是时间差,一个是重启归零,也可能重复。

  • 简单看下源码,感兴趣的下载源码去研究。

indexfile的添加消息索引的过程

  1. public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) { 
  2.         //1. 判断该索引文件的索引数小于最大的索引数,如果>=最大索引数,IndexService就会尝试新建一个索引文件 
  3.         if (this.indexHeader.getIndexCount() < this.indexNum) { 
  4.             //2. 计算该message key的hash值 
  5.             int keyHash = indexKeyHashMethod(key); 
  6.             //3. 根据message key的hash值散列到某个hash slot里 
  7.             int slotPos = keyHash % this.hashSlotNum; 
  8.             //4. 计算得到该hash slot的实际文件位置Position 
  9.             int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize; 
  10.  
  11.             try { 
  12.                 //5. 根据该hash slot的实际文件位置absSlotPos得到slot里的值 
  13.                 //这里有两种情况: 
  14.                 //1). slot=0, 当前message的key是该hash值第一个消息索引 
  15.                 //2). slot>0, 该key hash值上一个消息索引的位置 
  16.                 int slotValue = this.mappedByteBuffer.getInt(absSlotPos); 
  17.  
  18.                 //6. 数据校验及修正 
  19.                 if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) { 
  20.                     slotValue = invalidIndex; 
  21.                 } 
  22.  
  23.                 long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp(); 
  24.  
  25.                 timeDiff = timeDiff / 1000; 
  26.  
  27.                 if (this.indexHeader.getBeginTimestamp() <= 0) { 
  28.                     timeDiff = 0; 
  29.                 } else if (timeDiff > Integer.MAX_VALUE) { 
  30.                     timeDiff = Integer.MAX_VALUE; 
  31.                 } else if (timeDiff < 0) { 
  32.                     timeDiff = 0; 
  33.                 } 
  34.  
  35.                 //7. 计算当前消息索引具体的存储位置(Append模式) 
  36.                 int absIndexPos = 
  37.                     IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize 
  38.                         + this.indexHeader.getIndexCount() * indexSize; 
  39.                 //8. 存入该消息索引 
  40.                 this.mappedByteBuffer.putInt(absIndexPos, keyHash); 
  41.                 this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset); 
  42.                 this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff); 
  43.                 this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue); 
  44.  
  45.                 //9. 关键之处:在该key hash slot处存入当前消息索引的位置,下次通过该key进行搜索时 
  46.                 //会找到该key hash slot -> slot value -> curIndex ->  
  47.                 //if(curIndex.prevIndex>0) pre index (一直循环 直至该curIndex.prevIndex==0就停止) 
  48.                 this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount()); 
  49.  
  50.                 if (this.indexHeader.getIndexCount() <= 1) { 
  51.                     this.indexHeader.setBeginPhyOffset(phyOffset); 
  52.                     this.indexHeader.setBeginTimestamp(storeTimestamp); 
  53.                 } 
  54.  
  55.                 this.indexHeader.incHashSlotCount(); 
  56.                 this.indexHeader.incIndexCount(); 
  57.                 this.indexHeader.setEndPhyOffset(phyOffset); 
  58.                 this.indexHeader.setEndTimestamp(storeTimestamp); 
  59.  
  60.                 return true
  61.             } catch (Exception e) { 
  62.                 log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e); 
  63.             }  
  64.         } else { 
  65.             log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount() 
  66.                 + "; index max num = " + this.indexNum); 
  67.         } 
  68.  
  69.         return false
  70.  } 

indexfile的索引搜索源码

  1. public void selectPhyOffset(final List<Long> phyOffsets, final String key, final int maxNum, 
  2.         final long begin, final long end, boolean lock) { 
  3.         if (this.mappedFile.hold()) { 
  4.             //1. 计算该key的hash 
  5.             int keyHash = indexKeyHashMethod(key); 
  6.             //2. 计算该hash value 对应的hash slot位置 
  7.             int slotPos = keyHash % this.hashSlotNum; 
  8.             //3. 计算该hash value 对应的hash slot物理文件位置 
  9.             int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize; 
  10.  
  11.             FileLock fileLock = null
  12.             try { 
  13.                 //4. 取出该hash slot 的值 
  14.                 int slotValue = this.mappedByteBuffer.getInt(absSlotPos); 
  15.  
  16.                 //5. 该slot value <= 0 就代表没有该key对应的消息索引,直接结束搜索 
  17.                 //   该slot value > maxIndexCount 就代表该key对应的消息索引超过最大限制,数据有误,直接结束搜索 
  18.                 if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount() 
  19.                     || this.indexHeader.getIndexCount() <= 1) { 
  20.                 } else { 
  21.                     //6. 从当前slot value 开始搜索 
  22.                     for (int nextIndexToRead = slotValue; ; ) { 
  23.                         if (phyOffsets.size() >= maxNum) { 
  24.                             break; 
  25.                         } 
  26.  
  27.                         //7. 找到当前slot value(也就是index count)物理文件位置 
  28.                         int absIndexPos = 
  29.                             IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize 
  30.                                 + nextIndexToRead * indexSize; 
  31.  
  32.                         //8. 读取消息索引数据 
  33.                         int keyHashRead = this.mappedByteBuffer.getInt(absIndexPos); 
  34.                         long phyOffsetRead = this.mappedByteBuffer.getLong(absIndexPos + 4); 
  35.  
  36.                         long timeDiff = (long) this.mappedByteBuffer.getInt(absIndexPos + 4 + 8); 
  37.                         //9. 获取该消息索引的上一个消息索引index(可以看成链表的prev 指向上一个链节点的引用) 
  38.                         int prevIndexRead = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8 + 4); 
  39.                         //10. 数据校验 
  40.                         if (timeDiff < 0) { 
  41.                             break; 
  42.                         } 
  43.  
  44.                         timeDiff *= 1000L; 
  45.  
  46.                         long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff; 
  47.                         boolean timeMatched = (timeRead >= begin) && (timeRead <= end); 
  48.                         //10. 数据校验比对 hash值和落盘时间 
  49.                         if (keyHash == keyHashRead && timeMatched) { 
  50.                             phyOffsets.add(phyOffsetRead); 
  51.                         } 
  52.  
  53.                         //当prevIndex <= 0 或prevIndex > maxIndexCount 或prevIndexRead == nextIndexToRead 或 timeRead < begin 停止搜索 
  54.                         if (prevIndexRead <= invalidIndex 
  55.                             || prevIndexRead > this.indexHeader.getIndexCount() 
  56.                             || prevIndexRead == nextIndexToRead || timeRead < begin) { 
  57.                             break; 
  58.                         } 
  59.  
  60.                         nextIndexToRead = prevIndexRead; 
  61.                     } 
  62.                 } 
  63.             } catch (Exception e) { 
  64.                 log.error("selectPhyOffset exception ", e); 
  65.             } finally { 
  66.  
  67.                 this.mappedFile.release(); 
  68.             } 
  69.         } 
  70.     } 

本文转载自微信公众号「Java贼船」

责任编辑:姜华 来源: Java贼船
相关推荐

2017-01-06 10:07:39

Linuxwindowsatime

2021-03-10 14:50:44

数字人民币加密货币货币

2021-03-10 09:42:30

数字人民币数字货币区块链

2023-05-29 08:12:38

2023-10-18 10:55:55

HashMap

2022-08-26 13:24:03

version源码sources

2021-10-09 19:05:06

channelGo原理

2021-10-11 11:58:41

Channel原理recvq

2021-09-07 07:55:22

Linux CPULinux 系统

2021-04-16 09:17:39

机器学习人工智能AI

2018-04-28 11:03:58

2017-11-02 13:15:18

Linux

2021-11-30 14:50:29

编译源代码开源

2021-07-16 11:35:20

Java线程池代码

2021-03-30 15:10:50

Java序列化

2021-12-23 10:05:43

机器学习人工智能黑盒模型

2022-03-30 14:19:36

云安全云计算风险

2015-09-23 09:48:04

2023-10-26 07:29:06

mongodb十六进制ID

2021-07-08 10:08:03

DvaJS前端Dva
点赞
收藏

51CTO技术栈公众号