• RocketMQ源码阅读(七)ConsumeQueue和IndexFile


    目录

    ConsumeQueue

    IndexFile

    putKey

    selectPhyOffset


    ConsumeQueue

    生产者将消息发给broker,broker将所有消息全部写入commitLog一个文件中,那消费者如何根据topic获取消息,RocketMQ使用的是逻辑上的topic队列,即ConsumeQueue。

    消息写入commitLog同时,将消息在commitLog的offset偏移量、消息大小、tags信息写入对应的topic队列(ConsumeQueue),消费者获取消息时先从ConsumeQueue获取消息的offset,在根据offset从commitLog中查询真正的消息数据。

    引入别人一张图直观了解

    文件的存储结构:

    文件名是Topic名 

     

     一个Topic对应多个队列,名是queueId,每个文件夹都是一个ConsumeQueue

    一个队列中每个文件大小默认是6000000字节 

    实例化代码:

    根据topic文件路径和queueId创建一个MappedFileQueue

     核心方法:putMessagePositionInfoWrapper

    将请求的消息的offset,size,tags等写入MappedFileQueue

    1. public void putMessagePositionInfoWrapper(DispatchRequest request) {
    2. final int maxRetries = 30;
    3. boolean canWrite = this.defaultMessageStore.getRunningFlags().isCQWriteable();
    4. for (int i = 0; i < maxRetries && canWrite; i++) {
    5. long tagsCode = request.getTagsCode();
    6. // 忽略部分无关代码
    7. boolean result = this.putMessagePositionInfo(request.getCommitLogOffset(),
    8. request.getMsgSize(), tagsCode, request.getConsumeQueueOffset());
    9. if (result) {
    10. if (this.defaultMessageStore.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE ||
    11. this.defaultMessageStore.getMessageStoreConfig().isEnableDLegerCommitLog()) {
    12. this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(request.getStoreTimestamp());
    13. }
    14. this.defaultMessageStore.getStoreCheckpoint().setLogicsMsgTimestamp(request.getStoreTimestamp());
    15. return;
    16. } else {
    17. // XXX: warn and notify me
    18. log.warn("[BUG]put commit log position info to " + topic + ":" + queueId + " " + request.getCommitLogOffset()
    19. + " failed, retry " + i + " times");
    20. try {
    21. Thread.sleep(1000);
    22. } catch (InterruptedException e) {
    23. log.warn("", e);
    24. }
    25. }
    26. }
    27. // XXX: warn and notify me
    28. log.error("[BUG]consume queue can not write, {} {}", this.topic, this.queueId);
    29. this.defaultMessageStore.getRunningFlags().makeLogicsQueueError();
    30. }
    31. private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
    32. final long cqOffset) {
    33. if (offset + size <= this.maxPhysicOffset) {
    34. log.warn("Maybe try to build consume queue repeatedly maxPhysicOffset={} phyOffset={}", maxPhysicOffset, offset);
    35. return true;
    36. }
    37. // 一条消息在consumeQueue中的大小为定长20字节
    38. this.byteBufferIndex.flip();
    39. this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
    40. this.byteBufferIndex.putLong(offset);// 在commitLog中的偏移量 8字节
    41. this.byteBufferIndex.putInt(size);// 消息大小 4字节
    42. this.byteBufferIndex.putLong(tagsCode);// tags 标签的hashCode 8字节
    43. final long expectLogicOffset = cqOffset * CQ_STORE_UNIT_SIZE;
    44. MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);
    45. if (mappedFile != null) {
    46. if (mappedFile.isFirstCreateInQueue() && cqOffset != 0 && mappedFile.getWrotePosition() == 0) {
    47. this.minLogicOffset = expectLogicOffset;
    48. this.mappedFileQueue.setFlushedWhere(expectLogicOffset);
    49. this.mappedFileQueue.setCommittedWhere(expectLogicOffset);
    50. this.fillPreBlank(mappedFile, expectLogicOffset);
    51. log.info("fill pre blank space " + mappedFile.getFileName() + " " + expectLogicOffset + " "
    52. + mappedFile.getWrotePosition());
    53. }
    54. if (cqOffset != 0) {
    55. long currentLogicOffset = mappedFile.getWrotePosition() + mappedFile.getFileFromOffset();
    56. if (expectLogicOffset < currentLogicOffset) {
    57. log.warn("Build consume queue repeatedly, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}",
    58. expectLogicOffset, currentLogicOffset, this.topic, this.queueId, expectLogicOffset - currentLogicOffset);
    59. return true;
    60. }
    61. }
    62. this.maxPhysicOffset = offset + size;
    63. // 消息写入
    64. return mappedFile.appendMessage(this.byteBufferIndex.array());
    65. }
    66. return false;
    67. }

    IndexFile

    RocketMQ支持根据消息的key进行消息查询,和ConsumeQueue类似,如果消息中有key,就为这条消息建立Hash索引,即将消息在CommitLog中的偏移量写入索引文件,

     每个broker有一组indexFile,文件名即当时创建时的时间戳

    每个indexFile由三部分组成

    1、indexHeader, 长度为40字节,包含六个内容:biginTimestamp(第一条消息存储时间戳),endTimestamp(最后一条消息存储时间戳),biginPhyoffset(第一条消息在commitlog中的偏移量,即commitlog offset),endPhyoffset(最后一条消息在commitlog中的偏移量),hashSlotCount(含有index的slot数量),indexCount(包含的索引单元的个数) 

    2、slots,  参数控制,默认是500万,每个slot大小为4字节,槽中的4个字节存放的是索引块的位置

    3、index, 参数控制,默认是2000万,每个索引块大小为20字节, 4字节的hashKey,8字节的commitLog偏移量, 4字节的 当前key对应消息的存储时间与indexFile的时间差,4字节,当前索引块的前一个索引块位置

    1. public class IndexFile {
    2. private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
    3. private static int hashSlotSize = 4;// 每个槽大小 4字节
    4. private static int indexSize = 20;// 每个索引块大小, 20字节
    5. private static int invalidIndex = 0;
    6. private final int hashSlotNum; // 槽数量,默认500万
    7. private final int indexNum;// 索引块数量,默认2000万
    8. private final MappedFile mappedFile;
    9. private final FileChannel fileChannel;
    10. private final MappedByteBuffer mappedByteBuffer;
    11. private final IndexHeader indexHeader;
    12. public IndexFile(final String fileName, final int hashSlotNum, final int indexNum,
    13. final long endPhyOffset, final long endTimestamp) throws IOException {
    14. // 文件总大小:header+500万的槽+2000万的索引块
    15. int fileTotalSize =
    16. IndexHeader.INDEX_HEADER_SIZE + (hashSlotNum * hashSlotSize) + (indexNum * indexSize);
    17. this.mappedFile = new MappedFile(fileName, fileTotalSize);
    18. this.fileChannel = this.mappedFile.getFileChannel();
    19. this.mappedByteBuffer = this.mappedFile.getMappedByteBuffer();
    20. this.hashSlotNum = hashSlotNum;
    21. this.indexNum = indexNum;
    22. ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
    23. // 取前40个字节封装为IndexHeader
    24. this.indexHeader = new IndexHeader(byteBuffer);
    25. if (endPhyOffset > 0) {
    26. this.indexHeader.setBeginPhyOffset(endPhyOffset);
    27. this.indexHeader.setEndPhyOffset(endPhyOffset);
    28. }
    29. if (endTimestamp > 0) {
    30. this.indexHeader.setBeginTimestamp(endTimestamp);
    31. this.indexHeader.setEndTimestamp(endTimestamp);
    32. }
    33. }
    34. }

    putKey

    将某个消息key和对应commitLog的offset写入indexFile 

    1. public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
    2. // 头中记录的索引块数量 小于 文件最大值
    3. if (this.indexHeader.getIndexCount() < this.indexNum) {
    4. // 对key的hashCode取模,算出slot的位置
    5. int keyHash = indexKeyHashMethod(key);
    6. int slotPos = keyHash % this.hashSlotNum;
    7. int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
    8. FileLock fileLock = null;
    9. try {
    10. // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
    11. // false);
    12. // 取对应位置槽的值,也就是槽对应的索引块位置
    13. int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
    14. if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
    15. slotValue = invalidIndex;
    16. }
    17. long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
    18. timeDiff = timeDiff / 1000;
    19. if (this.indexHeader.getBeginTimestamp() <= 0) {
    20. timeDiff = 0;
    21. } else if (timeDiff > Integer.MAX_VALUE) {
    22. timeDiff = Integer.MAX_VALUE;
    23. } else if (timeDiff < 0) {
    24. timeDiff = 0;
    25. }
    26. // 当前索引块末尾空闲的位置
    27. int absIndexPos =
    28. IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
    29. + this.indexHeader.getIndexCount() * indexSize;
    30. // 写入索引块的数据
    31. this.mappedByteBuffer.putInt(absIndexPos, keyHash);// 4字节的hashKey
    32. this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);// 8字节的commitLog偏移量
    33. this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);// 4字节的 当前key对应消息的存储时间与indexFile的时间差
    34. this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);// 4字节,当前索引块的前一个索引块位置
    35. // 更新槽上的索引块位置
    36. this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
    37. // 更新indexHeader中的各种标志位
    38. if (this.indexHeader.getIndexCount() <= 1) {
    39. this.indexHeader.setBeginPhyOffset(phyOffset);
    40. this.indexHeader.setBeginTimestamp(storeTimestamp);
    41. }
    42. if (invalidIndex == slotValue) {
    43. this.indexHeader.incHashSlotCount();
    44. }
    45. this.indexHeader.incIndexCount();
    46. this.indexHeader.setEndPhyOffset(phyOffset);
    47. this.indexHeader.setEndTimestamp(storeTimestamp);
    48. return true;
    49. } catch (Exception e) {
    50. log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
    51. } finally {
    52. if (fileLock != null) {
    53. try {
    54. fileLock.release();
    55. } catch (IOException e) {
    56. log.error("Failed to release the lock", e);
    57. }
    58. }
    59. }
    60. } else {
    61. log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
    62. + "; index max num = " + this.indexNum);
    63. }
    64. return false;
    65. }

    selectPhyOffset

    从indexFile根据某个key查询符合条件的所有消息,返回List<Long> ,列表中的值是消息在commitLog中的偏移量

    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. // 获取hashKey所对应的槽位置
    5. int keyHash = indexKeyHashMethod(key);
    6. int slotPos = keyHash % this.hashSlotNum;
    7. int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
    8. FileLock fileLock = null;
    9. try {
    10. if (lock) {
    11. // fileLock = this.fileChannel.lock(absSlotPos,
    12. // hashSlotSize, true);
    13. }
    14. int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
    15. // if (fileLock != null) {
    16. // fileLock.release();
    17. // fileLock = null;
    18. // }
    19. if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()
    20. || this.indexHeader.getIndexCount() <= 1) {
    21. } else {
    22. for (int nextIndexToRead = slotValue; ; ) {
    23. if (phyOffsets.size() >= maxNum) {
    24. break;
    25. }
    26. // 获取槽对应的index块位置,索引块是根据后4字节被串成了一个链表,需要下边循环查询
    27. int absIndexPos =
    28. IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
    29. + nextIndexToRead * indexSize;
    30. int keyHashRead = this.mappedByteBuffer.getInt(absIndexPos);
    31. long phyOffsetRead = this.mappedByteBuffer.getLong(absIndexPos + 4);
    32. long timeDiff = (long) this.mappedByteBuffer.getInt(absIndexPos + 4 + 8);
    33. int prevIndexRead = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8 + 4);// 前一个索引块
    34. if (timeDiff < 0) {
    35. break;
    36. }
    37. timeDiff *= 1000L;
    38. // commitLog中消息的存储时间需要在参数begin和end之间
    39. long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff;
    40. boolean timeMatched = (timeRead >= begin) && (timeRead <= end);
    41. // 如果keyHash相同,即选中加入list
    42. if (keyHash == keyHashRead && timeMatched) {
    43. phyOffsets.add(phyOffsetRead);
    44. }
    45. // 在循环前一个索引块
    46. if (prevIndexRead <= invalidIndex
    47. || prevIndexRead > this.indexHeader.getIndexCount()
    48. || prevIndexRead == nextIndexToRead || timeRead < begin) {
    49. break;
    50. }
    51. nextIndexToRead = prevIndexRead;
    52. }
    53. }
    54. } catch (Exception e) {
    55. log.error("selectPhyOffset exception ", e);
    56. } finally {
    57. if (fileLock != null) {
    58. try {
    59. fileLock.release();
    60. } catch (IOException e) {
    61. log.error("Failed to release the lock", e);
    62. }
    63. }
    64. this.mappedFile.release();
    65. }
    66. }
    67. }

  • 相关阅读:
    MyBatis与Spring框架整合实现对数据库的增删改查
    基于python-socket的端口扫描
    修改例程flags_asyncio.py使能在python311环境下运行
    rpc的正确打开方式|读懂Go原生net/rpc包
    Docker 01 概述
    短视频账号矩阵系统saas管理私信回复管理系统
    吉时利2602A数字源表-安泰测试
    独处是一种修行
    湖北大学2024年成人高考函授报名高起专大数据与会计专业介绍
    uniapp-历史搜索记录
  • 原文地址:https://blog.csdn.net/xyjy11/article/details/125407306