我们可以知道,broker的消息完全顺序地存储在commitLog上,且每条消息的大小不一致,如果我们根据不同的主题查,或者根据消息id查找都要遍历整个commitLog文件,那肯定是不合理的。(要提一下,消息的一个主题topic,在一个broker上可以分成多个消息队列,默认是4个,也就是消息队列是基于topic+broker),所以RocketMQ采用了ConsumerQueue文件存储定长的索引数据(20字节,8字节commitLog偏移量+4字节消息长度size+4字节tagHashCode);采用了index文件存储id对应的消息的offset。
DefaultMessageStore的ReputMessageService服务专门解决这个问题,我们重点看下。ReputMessageService的启动在DefaultMessageStore的启动中。
启动后,每隔1ms调用一次doReput()方法
先根据offset调用getData()从commitLog中获取数据,如果获取不到那么结束循环结束此次rePut
我们可以关注下SelectMappedBufferResult的成员
根据pos与定位到的mappedFile,取出相应的数据存入byteBuffer,并将数据封装在SelectMappedBufferResult中返回。
然后调用了checkMessageAndReturnSize,解析读出的数据封装成DispatchRequest并返回。如果成功,且消息的大小大于0,那么调用doDispatch方法分发消息至consumequeue,index文件。
可以看到遍历dispatchList元素分别调用对应的dispatch方法,在DefaultMessageStore的构造中可以看到dispatchList。
其中consumequeue,index分别对应CommitLogDispatcherBuildConsumeQueue与CommitlogDispatcherBuildIndex。我们先从CommitLogDispatcherBuildConsumeQueue开始分析。
核心的方法putMessagePositionInfo
通过消息得到topic跟消息队列的id从本broker的consumeQueueTable的对应的topic下的consumeQueueMap中通过消息队列的id获取对应的Logic消息队列(并非真正消息队列,仅存储定长消息索引的抽象类)。再调用对应ConsumerQueue的putMessagePositionInfoWrapper()方法,将具体的dispatchRequest引入。
先判断ConsumerQueue是否可写,如果配置了消息队列的额外信息的可写,那么构造ConsumeQueueExt的内部类存储单元的CqExtUnit的实例,保存消息的bitMap、storeTime、TagsCode等信息,并加入到ConsumeQueueExt中管理。
通过putMessagePositionInfo方法,将消息写入到ConsumerQueue中的逻辑文件mappedFileQueue中。
先判断偏移量,如果小于maxPhysicOffset说明已经重复放过,那么返回成功。将信息(offset、size、tagCodes)存入到byteBuffer,并计算偏移量求出对应的MappedFile。如果mappedFile是第一个且队列位置非第一个且mappedFile未写入内容,那么在指定写入位置前面填充空白占位符。如果consumeQueue存储位置不合法,那么日志报错。然后将maxPhysicOffset更新为offset,并且调用appendMessage方法实际向ConsumerQueue的指定mappedFile写入20字节的数据。
如果上面步骤成功,那么将消息在ConsumerQuquq中的存储时间设为storeCheckpoint的logicMsg存储时间戳。否则sleep1s然后重试,知道超过重试次数或者成功。
找了下storePath 跟 queueDir
根据mappedFileSize的值可以求出一个ConsumerQueue文件默认大小
30W记录,每条记录20字节。
再理一理,一个指定topic、指定queueId对应一个ConsumerQueue,ConsumerQueue管理着一个大小默认为mappedFileSize的mappedFileQueue可存储30W条记录。
下面我们来分析下CommitlogDispatcherBuildIndex
如果配置了MessageIndexEnable,那么会根据请求建立Index索引。
调用retryGetAndCreateIndexFile得到可写的indexFile
可以看到这个方法尝试多次(默认3次)调用getAndCreateLastIndexFile得到可以的indexFile。如果一次失败,等待1秒重试。若是都没得到、报错。
先加读锁,找到最后一个indexFile文件,看其是否满了,如果没满,那么返回该文件,否则重新创建新的indexFile文件,并且加写锁,将其新创建的indexFile加入到indexFileList方便管理,解锁。然后创建一个守护线程,负责把前一个indexFile文件刷新到硬盘中。
在得到了可写的indexFile之后,得到消息的topic跟keys,如果消息类型是TRANSACTION_ROLLBACK_TYPE那么到这里就直接返回了。先把消息的uniqKey进行putKey。我们先来看下什么是消息的key跟uinqKey
keys,uniqKey存放在消息的propertiesmap中,keys:用户在发送消息时候,可以指定,多个key用英文逗号隔。uniqKey:消息唯一键,与消息ID不一样,因为消息ID在commitlog文件中并不是唯一的,消息消费重试时,发送的消息的消息ID与原先的一样。
可以看到uniqKey的组成由FIX_STRING(10bit)+currentTime(4bit)+count(2bit),FIX_STRING由ip、pid、classLoader的hashCode组成。整个用于标记消息的唯一性。
继续回到IndexServer的buildIndex()方法中。
buildKey无非在key之前加topic+"#"。
将uniqKey进行putKey
调用indexFile的putkey方法,如果失败,说明写满了,那么重新调用retryGetAndCreateIndexFile方法,继续写。看下indexFile的putkey方法,传入的是commitLog中的偏移量,消息存入commitLog的时间戳。
如果目前index file存储的条目数小于允许的条目数,则存入当前文件中,如果超出,则返回false说明indexfile文件写满。得到key的hashCode再%hashSlotNum定位到key的相应hashSlot位置,再通过IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize计算出其对应的bit位置。读取key所在hashslot下标处的值slotValue(4个字节),如果小于0或超过当前包含的indexCount,则设置为0;计算消息的存储时间与当前IndexFile存放的最小时间差额timeDiff;然后根据当前存入的index的数目,计算出当前的index该存放的位置下标absIndexPos=IndexHeader.INDEX_HEADER_SIZE+this.hashSlotNum*hashSlotSize+ this.indexHeader.getIndexCount() * indexSize,其不过是indexHeadSize+hashSlotsSize+当前index偏移量。
然后在absIndexPos开始存入当前消息的数据,(KeyHash(4字节)、phyOffset(8字节)、时间差timeDiff(4字节)、slotValue(4个字节,这个表示key的相同hashCode的上一条消息的偏移量IndexCount)),然后在absSlotPos位置写下当前消息的IndexCount。最后更新IndexFile头部相关字段,比如最小时间,当前最大时间等。
然后遍历keys的每个key,调用putKey。此时保存到内存映射文件中,并没有执行刷盘操作。
整个流程到这里告一段落。
想必读putKey方法时,我们已经看出了indexFile的数据结构。
IndexFileHeader : beginTimestamp(8字节) + endTimestamp(8字节) + beginOffset(8字节) + endOffset(8字节) + hashCodeCount(4字节) + indexCount(4字节)
indexFile : IndexFileHeader + 500W个HashSlot + N个Index(每个20字节)
我们看下从indexFile取数据操作
根据key取得hashCode、absSlotPos,根据absSlotPos去取得相应HashSlot槽中的slotValue,如果slotValue合法,从slotValue指定的index条目开始找,与查询条件匹配如果符合,那么把物理偏移量加入到phyOffsets中,否则取出上一条的之前偏移量继续找(可以看成类似的前项指针、这样这个类似于顺序存储的链表形式,整体可以理解成顺序存储形式的大的hashMap)。
运行过程中,消息从生产者客户端发来,broker接收后,消息发送到commitlog文件存储后,ReputMessageService线程会同步将消息转发到Logic消息队列(ConsumeQueue)、index文件(IndexFile)。形成索引,方便消费者查找。