RocketMQ存储篇——数据文件的访问入口(DefaultMessageStore)

来源:互联网 发布:紫川软件招聘 编辑:程序博客网 时间:2024/06/14 18:41

1 根据topic和queueId查找ConsumeQueue(findConsumeQueue)

调用findConsumeQueue(String topic, int queueId)方法获得,从ConsumeQueue集合consumeQueueTable: ConcurrentHashMap

2 根据物理偏移量和数据大小获取消息内容(lookMessageByOffset)

调用lookMessageByOffset(long commitLogOffset, int size)方法获取从指定开始位置读取size大小的消息内容。在该方法中,调用commitlog的getMessage方法从commitlog中获取消息,并调用MessageDecoder.decode方法对commitlog消息进行解码,最后返回MessageExt对象。其中MessageExt对象对象的MsgId长度为16字节,有8字节的消息存储本机的IP:PORT和8个字节的该消息的物理偏移offset值构成;

3 将消息写入commitlog中(putMessage)

调用putMessage(MessageExtBrokerInner msg)方法完成消息的写入工作。具体步骤如下:
1)检查是否shutdown,若是则直接返回服务不可用的错误;
2)检查是否为备用Broker,若是则直接返回服务不可用的错误;
3)检查是否有写的权限,若是则直接返回服务不可用的错误;
4)检查topic的长度是否大于最大值127,若是则返回消息不合法;
5)检查peroperties的长度是否大于32767,若是则返回消息不合法;
6)上述检查全部通过之后,调用CommitLog对象的putMessage方法进行消息的写入;
7)完成消息写入之后,调用StoreStatsService对象进行相关统计工作;

4 读取commitlog消息(getMessage)

调用DefaultMessageStore.getMessage(String group,Stringtopic,int queueId,long queueoffset, int maxMsgNums, SubscriptionData subscriptionData)方法读取消息。大致逻辑如下:
1、调用CommitLog.getMaxOffset()方法获取commitlog文件目前写入的最大位置maxOffsetPy;
2、以topic和queueId为参数调用findConsumeQueue方法,从DefaultMessageStore.consumeQueueTable中获取ConsumeQueue对象,若没有获取到该ConsumeQueue对象(一般不会出现此情况)表示该topic和queuId下面的队列不存在,则返回GetMessageResult对象信息,该对象中STATUS=NO_MATCHED_LOGIC_QUEUE,NextBeginOffset=0的消息,若取到了ConsumeQueue对象,即该topic和queueId下面有队列存在,则继续下面的逻辑;
3、获取该ConsumeQueue对象的最小逻辑offset(命名:minOffset=minLogicOffset/20)和最大逻辑 offset(命名:maxOffset=MapedFileQueue.getMaxOffset()/20);
4、对请求参数中的queueoffset进行有效性检验:
4.1)若maxOffset等于0,则status= NO_MESSAGE_IN_QUEUE,nextBeginOffset=0;
4.2)若请求消息中的queueoffset小于minOffset,则status= OFFSET_TOO_SMALL,nextBeginOffset=minOffset;
4.3)若请求消息中的queueoffset等于maxOffset,则status=OFFSET_OVERFLOW_ONE,nextBeginOffset=offset;
4.4)若请求消息中的queueoffset大于maxOffset,则status=OFFSET_OVERFLOW_BADLY;此时若minOffset=0,则nextBeginOffset=0,否则nextBeginOffset=maxOffset;
4.5)若请求消息中的queueoffset在minOffset与maxOffset之间,则继续执行后续操作步骤读取消息。
5、根据queueoffset为开始读取位置调用getIndexBuffer(final long startIndex)方法获取consumequeue数据;若未获取到数据(返回值为null)则GetMessageResult对象信息,该对象中STATUS=OFFSET_FOUND_NULL,NextBeginOffset等于该queueoffset所在consumequeue文件的下一个文件的起始偏移量(调用ConsumeQueue.rollNextFile方法得到)的消息;若获取到了数据,则继续下面的执行步骤;
6、遍历从ConsumeQueue获取的数据中的每个数据单元,在遍历开始时设计一个标志位nextPhyFileStartOffset,初始为Long.MIN_VALUE,用于检测每个数据单元所保存的物理偏移量的commitlog数据是否还存在,若不存在则继续解析下一个数据单元。大概步骤如下:
6.1)以20个字节为单元格逐个解析每个数据单元,在数据单元中读取第1-8个字节为物理偏移量offsetPy;第9-12个字节为消息大小sizePy;第13-16个字节为tagsCode;
6.2)检查nextPhyFileStartOffset是否等于初始值Long.MIN_VALUE,若不是初始值(说明之前有通过物理偏移量从commitlog中获取数据未获取到,将nextPhyFileStartOffset重置为了未获取到数据的物理偏移量所在文件的下一个文件),并且此次解析的数据单元的物理偏移量小于该nextPhyFileStartOffset值,则跳过下面的处理逻辑,继续解析下一个数据单元,即重新从6.1步开始执行;
6.3)检查数据是磁盘中还是在内存中,检查方式:以最大物理偏移量maxOffsetPy(在第1步中获取了)减去该读取到的物理偏移量offsetPy,若差值大于可使用的内存大小,则认为数据在磁盘中否则可以从内存中获取;
6.4)调用DefaultMessageStore.isTheBatchFull(int sizePy, int maxMsgNums, int bufferTotal, int messageTotal, boolean isInDisk)方法检查此次读取的数据量是否大于了阀值,若大于了则停止继续解析,跳出此次遍历至第7步。检查已拉取的消息总大小或消息总个数是否超过阀值,标准如下:
A)若是从磁盘中读取数据:一次被拉取的消息字节数不得大于1024 * 64,一次被拉取的消息个数不得大于8;
B)若是从内存中读取数据:一次被拉取的消息字节数不得大于1024 * 256,一次被拉取的消息个数不得大于25;
6.5)消息过滤。根据SubscriptionData对象和数据块中的tagsCode调用DefaultMessageFilter.isMessageMatched(SubscriptionData subscriptionData,long tagsCode)方法进行消息过滤。当满足如下过滤条件的任一个视为匹配成功,可以进行后续的逻辑处理,若没有匹配成功并且获取的数据总大小为零则暂时将status=NO_MATCHED_MESSAGE;
A)SubscriptionData对象为空;
B)SubscriptionData.classFilterMode变量为true;
C)SubscriptionData对象的subString变量等于*;
D)SubscriptionData对象的codeSet集合包含tagsCode值;
6.6)根据解析到的offsetPy和sizePy为参数调用CommitLog.getMessage(long offsetPy,int size)方法从commitlog中获取指定读取偏离量和消息大小的数据;若未获取到数据并且获取的数据总大小为零,则暂时将status=MESSAGE_WAS_REMOVING,并且调用CommitLog.rollNextFile方法获取该物理偏移量offsetPy所在文件的下一个commitlog文件的起始偏移量,因为以该offsetPy为读取开始位置未获取到数据说明对应的commitlog文件被删除了,应该更新下一次读取位置值nextPhyFileStartOffset等于该被删文件的下个文件的起始偏移量;
6.7)若获取到commitlog数据,即CommitLog.getMessage方法的返回对象SelectMapedBufferResult不等于null;
A)调用GetMessageResult.AddMessage(SelectMapedBufferResultmapedBuffer)方法:首先,将SelectMapedBufferResult对象存入GetMessageResult.messageMapedList:List变量中,然后,将SelectMapedBufferResult对象d的二进制块ByteBuffer存入GetMessageResult.messageBufferList:List变量中;最后,累加GetMessageResult.bufferTotalSize的值;
B)更新status=FOUND,nextPhyFileStartOffset恢复为初始值Long.MIN_VALUE;
C)将统计服务StoreStatsService.getMessageTransferedMsgCount变量加1;
6.8)继续从6.1步开始解析下一个数据单元,直到解析读取的consumequeue数据为止;
7、计算下一次读取数据时的开始偏移量NextBeginOffset,计算方式是:请求消息中的offset+上面遍历完之后最后一个offset值除以20;
8、检查未拉取的消息的大小是否大于最大可使用内存,若大于,则建议从备用Broker拉取消息,即设置GetMessageResult.suggestPullingFromSlave等于true;未拉取的消息的大小计算方式是:commitlog的最大物理偏移offset减去此次拉取的最后一个消息的物理偏移offset即为还未拉取的消息大小;
9、若status=FOUND,则将StoreStatsService.getMessageTimesTotalFound变量加1;否则将StoreStatsService.getMessageTimesTotalMiss变量加1;
10、返回GetMessageResult对象,该对象中包括status、NextBeginOffset、consumequeue中的最大offset和最小offset等信息;

5 获取最大物理偏移量(getMaxPhyOffset)

最大物理偏移量就是在commitlog中当前写入消息的位置,调用DefaultMessageStore.getMaxPhyOffset() 方法获取最大物理偏移量,在该方法中调用CommitLog.getMaxOffset()方法获取。

6 获取指定偏移量之后的所有commitlog数据(getCommitLogData)

调用getCommitLogData(final long offset)方法从指定偏移量开始读取之后的所有commitlog数据,在该方法中调用CommitLog对象的getData方法获取。

7 从指定位置开始追加commitlog数据(appendToCommitLog)

调用DefaultMessageStore.appendToCommitLog(long startOffset, byte[] data)方法将数据从指定位置追加到commitlog文件中,大致步骤如下:
1、调用CommitLog对象的appendData方法进行数据的追加;
2、若追加数据成功之后,要完成该数据对应的consumequeue和index索引的创建,唤醒DefaultMessageStore.ReputMessageService服务线程,由该线程来执行。在备用Broker启动时首先用本地的commitlog最大偏移量(最后写入消息位置)来初始化该线程的reputFromOffset变量;然后启动该线程。

8 DefaultMessageStore.ReputMessageService服务线程

该服务线程会一直不间断的监听reputFromOffset偏移量之后的commitlog数据,若commitlog数据有增加,即reputFromOffset偏移量之后新增了数据,则获取出来并创建consumequeue和index,同时更新reputFromOffset偏移量。大致逻辑如下:
1、以reputFromOffset值作为开始读取偏移量从commitlog中获取该值之后的所有数据,调用CommitLog对象的getData方法;
2、若没有获取到数据则退出,该线程等待1秒之后再次从reputFromOffset值开始获取commitlog数据;若有数据则执行下面的步骤用于连续解析获取到的commitlog数据;
3、根据commitlog的数据结构先解析一个消息单元的数据;包括topic、queueId、physicOffset、totalSize、tagsCode、storeTimestamp 、queueOffset 、keys、sysFlag、preparedTransactionOffset这10个参数,并封装成DispatchRequest对象;
4、若消息大小大于零(totalSize>0),则调用DefaultMessageStore.putDispatchRequest(DispatchRequest dispatchRequest)方法,将DispatchRequest该对象放入DefaultMessageStore.DispatchMessageService服务线程的requestsWrite队列中,等待执行;然后将reputFromOffset的值累加totalSize;
5、继续解析获取的commitlog数据的下一个消息单元,按照第3/4步进行解析并处理,直到所有的消息单元被解析完为止;

9 DefaultMessageStore.DispatchMessageService服务线程

该线程会负责处理DispatchRequest请求,为请求中的信息创建consumequeue数据和index索引。当调用者调用DispatchMessageService.putRequest(DispatchRequest dispatchRequest)方法时,即唤醒该线程,并将requestsWrite队列和requestsRead队列互换,其中requestsRead队列一般是空的,然后遍历requestsRead队列。遍历逻辑如下:
1)从DispatchRequest对象的sysFlag变量中判断消息类型;
2)若是非事务消息或者是提交事务(Commit类型)的消息,则调用DefaultMessageStore.putMessagePostionInfo(String topic, int queueId, long offset, int size, long tagsCode, long storeTimestamp, long logicOffset)方法将相关信息存入consumequeue文件中;在该方法中,先根据topic和queueId调用findConsumeQueue方法查找ConsumeQueue对象,然后调用该对象的putMessagePostionInfoWrapper方法,将consumequeue的相关数据写入该对象的缓存中;若是PREPARED事务或者ROLLBACKE事务,则暂时不将该信息放入ConsumeQueue文件中,即保证了在没有commit之前Consumer端暂时消费不到该信息;
3)若DispatchRequest对象的producerGroup变量不为null,则写事务消息的状态(tranStateTable文件);若请求消息sysflag中标记的事务类型为PREPARED事务,则调用TransactionStateService.appendPreparedTransaction(long commitLogOffset, int size, int timestamp, int groupHashCode)方法,将物理偏移offset、消息大小、存储时间戳、producerGroup的hash值存入tranStateTable文件中;若请求消息sysflag中标记的事务类型为COMMIT或者ROLLBACKE事务,则调用TransactionStateService.updateTransactionState(long tsOffset, long clOffset, int groupHashCode, int state)方法,其中tsOffset参数表示在tranStateTable文件中的物理位置,在该方法中取该物理位置的消息,并将该消息的事务状态state置为COMMIT或者ROLLBACKE;
4)写tranRedoLog日志文件。若消息是PREPARED事务消息,则调用TransactionStateService.tranRedoLog:ConsumeQueue对象的putMessagePostionInfoWrapper方法将commitlogoffset、消息大小、tagsCode(等于-1)存入tranRedoLog文件中;若事务消息类型为COMMIT或者ROLLBACKE事务,则调用TransactionStateService.tranRedoLog:ConsumeQueue对象的putMessagePostionInfoWrapper方法将commitlogoffset、消息大小、tagsCode(等于DispatchRequest.preparedTransactionOffset变量值,该值为消息的commitlogoffset值)存入tranRedoLog文件中;
5)检查消息存储是否开启消息索引功能(MessageStoreConfig.messageIndexEnable参数控制,默认为true),若开启了,则调用IndexService.putRequest(Object[] reqs) 将这些请求对象放入IndexService.requestQueue队列中;IndexService线程服务不间断的监听该队列中的数据,在IndexService线程服务中会调用buildIndex来创建该信息的索引;
6)清理requestsRead队列;

10 加载ConsumeQueue队列数据(loadConsumeQueue)

在初始化Broker的时候,调用DefaultMessageStore.loadConsumeQueue方法加载$HOME /store/consumequeue目录下面的消息文件。该目录下面的文件结构是:以主题为名的一级目录,然后是以queueID为名的二级目录,在二级目录下面是消息文件内容。对每个二级目录下的文件初始化一个Consumequeue对象,调用Consumequeue.load方法,在该方法中调用MapedFileQueue的load方法完成消息内容的加载,其中MapedFile的wrotePostion和CommittedPosition两个变量均初始化为文件大小。将consumequeue文件的内容存入consumeQueueTable:ConcurrentHashMap

11 获取指定队列的最大逻辑Offset(getMaxOffsetInQuque)

调用DefaultMessageStore.getMaxOffsetInQuque(String topic,int queueId)方法获取该topic和queueId下面的队列的最大逻辑Offset。
首先,调用findConsumeQueue方法找到给topic和queueId下面的队列对应的ConsumeQueue对象;
然后调用该对象的getMaxOffsetInQuque方法,在该方法中,获取MapedFileQueue对象的MapedFile队列中最大Offset值,再除以consumequeue消息单元的大小(20),即得到指定队列的最大逻辑Offset值。

12 获取指定队列的最小逻辑Offset(getMinOffsetInQuque)

调用DefaultMessageStore.getMinOffsetInQuque(String topic,int queueId)方法获取该topic和queueId下面的队列的最小逻辑Offset。
调用findConsumeQueue方法找到给topic和queueId下面的队列对应的ConsumeQueue对象;若该对象为空则返回-1;否则调用该对象的getMinOffsetInQuque方法,在该方法中,获取ConsumeQueue对象的minLogicOffest值,再除以consumequeue消息单元的大小(20),即得到指定队列的最小逻辑Offset值,当Broker刚刚启动此值有可能为0;
因此,该最小值返回-1表示还未创建该topic和queueId下面的consumequeue;返回0可能是才启动没多久,没有来得及更新最小逻辑minLogicOffest值。