RocketMQ之消息查询IndexFile(四)

首先我们先看看indexfile的流程是怎么样的,然后对其一步步分析源码调试,MessageStore中存储的消息除了通过ConsumeQueue提供给consumer消费之外,还支持通过MessageID或者MessageKey来查询消息;使用ID查询时,因为ID就是用broker+offset生成的(这里msgId指的是服务端的),所以很容易就找到对应的commitLog文件来读取消息。对于用MessageKey来查询消息,MessageStore通过构建一个index来提高读取速度

引用空档原话:

整个slotTable+indexLinkedList可以理解成java的HashMap。每当放一个新的消息的index进来,首先取MessageKey的hashCode,然后用hashCode对slot总数取模,得到应该放到哪个slot中,slot总数系统默认500W个。只要是取hash就必然面临hash冲突的问题,跟HashMap一样,IndexFile也是使用一个链表结构来解决hash冲突。只是这里跟HashMap稍微有点区别的地方是,slot中放的是最新index的指针。这个是因为一般查询的时候肯定是优先查最近的消息。
每个slot中放的指针值是索引在indexFile中的偏移量,如上图,每个索引大小是20字节,所以根据当前索引是这个文件中的第几个(偏移量),就很容易定位到索引的位置。然后每个索引都保存了跟它同一个slot的前一个索引的位置,以此类推形成一个链表的结构。下面通过代码来看下新建一个索引的过程

 

 

1、IndexFile文件存储结构

通过源码我们知道IndexFile一个存储结构是怎么样的,下面的图引用自CSDN斩秋的文章RocketMQ原理解析

索引文件由索引文件头IndexHeader, 槽位Slot和消息的索引内容三部分构成,接下来对每部分进行分析

 

  • IndexHeader:索引文件头信息由40个字节组成
    webp
        //8位 该索引文件的第一个消息(Message)的存储时间(落盘时间)
        this.byteBuffer.putLong(beginTimestampIndex, this.beginTimestamp.get());
        //8位 该索引文件的最后一个消息(Message)的存储时间(落盘时间)
        this.byteBuffer.putLong(endTimestampIndex, this.endTimestamp.get());
        //8位 该索引文件第一个消息(Message)的在CommitLog(消息存储文件)的物理位置偏移量(可以通过该物理偏移直接获取到该消息)
        this.byteBuffer.putLong(beginPhyoffsetIndex, this.beginPhyOffset.get());
        //8位 该索引文件最后一个消息(Message)的在CommitLog(消息存储文件)的物理位置偏移量
        this.byteBuffer.putLong(endPhyoffsetIndex, this.endPhyOffset.get());
        //4位 该索引文件目前的hash slot的个数
        this.byteBuffer.putInt(hashSlotcountIndex, this.hashSlotCount.get());
        //4位 索引文件目前的索引个数
        this.byteBuffer.putInt(indexCountIndex, this.indexCount.get());
  • Slot槽位,默认每个文件配置的slot是500万个,每个slot是4位的整型数据
    Slot每个节点保存当前已经拥有多少个index数据了
    webp
            //slot的数据存放位置 40 + keyHash %(500W)* 4
            int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
    
    
            //Slot Table
            //4字节
            //记录该slot当前index,如果hash冲突(即absSlotPos一致)作为下一次该slot新增的前置index
            this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
  •  索引消息内容,消息长度固定为20位
    webp
            //Index Linked list
            //topic+message key的hash值
            this.mappedByteBuffer.putInt(absIndexPos, keyHash);
            //消息在CommitLog的物理文件地址, 可以直接查询到该消息(索引的核心机制)
            this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
            //消息的落盘时间与header里的beginTimestamp的差值(为了节省存储空间,如果直接存message的落盘时间就得8bytes)
            this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
            //9、记录该slot上一个index
            //hash冲突处理的关键之处, 相同hash值上一个消息索引的index(如果当前消息索引是该hash值的第一个索引,则prevIndex=0, 也是消息索引查找时的停止条件),每个slot位置的第一个消息的prevIndex就是0的
            this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);

     

2、IndexFile文件数据写入

 

 /**
     *
     * @param key topic + uniqKey
     * @param phyOffset 物理偏移量
     * @param storeTimestamp
     * @return
     */
    public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
        //1、判断index是否已满,返回失败
        if (this.indexHeader.getIndexCount() < this.indexNum) {
            //2、计算key的非负数hashCode
            int keyHash = indexKeyHashMethod(key);
            //3、key应该存放的slot keyHash % 500W
            int slotPos = keyHash % this.hashSlotNum;
            //3、slot的数据存放位置 40 + keyHash %(500W)* 4
            int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;

            FileLock fileLock = null;

            try {

                // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
                // false);
                //5、如果存在hash冲突,获取这个slot存的前一个index的计数,如果没有则值为0
                int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
                if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
                    slotValue = invalidIndex;
                }

                //6、计算当前msg的存储时间和第一条msg相差秒数
                long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();

                //这里为了节约空间;直接timestamp是8位
                timeDiff = timeDiff / 1000;

                if (this.indexHeader.getBeginTimestamp() <= 0) {
                    timeDiff = 0;
                } else if (timeDiff > Integer.MAX_VALUE) {
                    timeDiff = Integer.MAX_VALUE;
                } else if (timeDiff < 0) {
                    timeDiff = 0;
                }

                //7、获取该条index实际存储position
                //40 + 500W * 4 + index的顺序数 * 40;
                int absIndexPos =
                    IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
                        + this.indexHeader.getIndexCount() * indexSize;

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

                //Slot Table
                //4字节
                //10、记录该slot当前index,如果hash冲突(即absSlotPos一致)作为下一次该slot新增的前置index
                this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());

                //11、如果是第一条消息,更新header中的起始offset和起始time
                if (this.indexHeader.getIndexCount() <= 1) {
                    this.indexHeader.setBeginPhyOffset(phyOffset);
                    this.indexHeader.setBeginTimestamp(storeTimestamp);
                }

                //12、累计indexHeader
                this.indexHeader.incHashSlotCount();
                this.indexHeader.incIndexCount();
                this.indexHeader.setEndPhyOffset(phyOffset);
                this.indexHeader.setEndTimestamp(storeTimestamp);

                return true;
            } catch (Exception e) {
                log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
            } finally {
                if (fileLock != null) {
                    try {
                        fileLock.release();
                    } catch (IOException e) {
                        log.error("Failed to release the lock", e);
                    }
                }
            }
        } else {
            log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
                + "; index max num = " + this.indexNum);
        }

        return false;
    }

以上代码是index数据写入过程,第五步获取slot的值是否存在,如果存在则hash冲突,则在第九步把value设置为当前index的前一个index,同时第十步将slot的值设置为当前index;这里有点类似HashMap的链表操作。刷盘则是在创建文件时候,建立了一个相应的守护线程进行异步刷盘操作

 

3、IndexFile数据查询

    /**
     *
     * @param topic 按topic维度来查询消息,因为索引生成的时候key是用的topic+MessageKey
     * @param key MessageKey
     * @param maxNum 最多返回的消息数,因为key是由用户设置的,并不保证唯一,所以可能取到多个消息;同时index中只存储了hash,所以hash相同的消息也会取出来
     * @param begin 起始时间
     * @param end 结束时间
     * @return
     */
    public QueryOffsetResult queryOffset(String topic, String key, int maxNum, long begin, long end) {
        List<Long> phyOffsets = new ArrayList<Long>(maxNum);

        long indexLastUpdateTimestamp = 0;
        long indexLastUpdatePhyoffset = 0;
        //不会超过64条
        maxNum = Math.min(maxNum, this.defaultMessageStore.getMessageStoreConfig().getMaxMsgsNumBatch());
        try {
            this.readWriteLock.readLock().lock();
            if (!this.indexFileList.isEmpty()) {
                //1、从最后一个文件开始往前查找,最后一个文件是最新的
                for (int i = this.indexFileList.size(); i > 0; i--) {
                    IndexFile f = this.indexFileList.get(i - 1);
                    boolean lastFile = i == this.indexFileList.size();
                    if (lastFile) {
                        indexLastUpdateTimestamp = f.getEndTimestamp();
                        indexLastUpdatePhyoffset = f.getEndPhyOffset();
                    }

                    //2、判断index文件的时间包含了begin和end的全部或者部分
                    if (f.isTimeMatched(begin, end)) {
                        //3、从index文件中获取offset
                        f.selectPhyOffset(phyOffsets, buildKey(topic, key), maxNum, begin, end, lastFile);
                    }

                    if (f.getBeginTimestamp() < begin) {
                        break;
                    }

                    if (phyOffsets.size() >= maxNum) {
                        break;
                    }
                }
            }
        } catch (Exception e) {
            log.error("queryMsg exception", e);
        } finally {
            this.readWriteLock.readLock().unlock();
        }

        return new QueryOffsetResult(phyOffsets, indexLastUpdateTimestamp, indexLastUpdatePhyoffset);
    }
public void selectPhyOffset(final List<Long> phyOffsets, final String key, final int maxNum,
    final long begin, final long end, boolean lock) {
    if (this.mappedFile.hold()) {
        //1、计算key的非负数hashCode
        int keyHash = indexKeyHashMethod(key);
        //2、key应该存放的slot keyHash % 500W
        int slotPos = keyHash % this.hashSlotNum;
        //3、slot的数据存放位置 40 + keyHash %(500W)* 4
        int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;

        FileLock fileLock = null;
        try {
            if (lock) {
                // fileLock = this.fileChannel.lock(absSlotPos,
                // hashSlotSize, true);
            }

            //4、获取slot最后存储的index位置进行回溯
            int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
            // if (fileLock != null) {
            // fileLock.release();
            // fileLock = null;
            // }

            if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()
                || this.indexHeader.getIndexCount() <= 1) {
            } else {
                for (int nextIndexToRead = slotValue; ; ) {
                    //5、查询条目满足则返回
                    if (phyOffsets.size() >= maxNum) {
                        break;
                    }

                    //6、获取该条index实际存储position
                    int absIndexPos =
                        IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
                            + nextIndexToRead * indexSize;

                    int keyHashRead = this.mappedByteBuffer.getInt(absIndexPos);
                    //7、物理偏移量即commitLog的offset
                    long phyOffsetRead = this.mappedByteBuffer.getLong(absIndexPos + 4);

                    //当前msg的存储时间和第一条msg相差秒数
                    long timeDiff = (long) this.mappedByteBuffer.getInt(absIndexPos + 4 + 8);
                    int prevIndexRead = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8 + 4);

                    if (timeDiff < 0) {
                        break;
                    }

                    timeDiff *= 1000L;

                    long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff;
                    boolean timeMatched = (timeRead >= begin) && (timeRead <= end);

                    //8、hash一致并且时间在begin和end之间,加入结果集中
                    if (keyHash == keyHashRead && timeMatched) {
                        phyOffsets.add(phyOffsetRead);
                    }

                    //9、读取到0,说明没数据可读
                    if (prevIndexRead <= invalidIndex
                        || prevIndexRead > this.indexHeader.getIndexCount()
                        || prevIndexRead == nextIndexToRead || timeRead < begin) {
                        break;
                    }

                    //10、前一条不等于0,继续读取前一条,往前回溯
                    nextIndexToRead = prevIndexRead;
                }
            }
        } catch (Exception e) {
            log.error("selectPhyOffset exception ", e);
        } finally {
            if (fileLock != null) {
                try {
                    fileLock.release();
                } catch (IOException e) {
                    log.error("Failed to release the lock", e);
                }
            }

            this.mappedFile.release();
        }
    }
}

转载于:https://my.oschina.net/u/3180962/blog/3064407

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值