首先我们先看看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一个存储结构是怎么样的
索引文件由索引文件头IndexHeader, 槽位Slot和消息的索引内容三部分构成,接下来对每部分进行分析
- IndexHeader:索引文件头信息由40个字节组成
//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数据了
//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());
//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();
}
}
}