https://blog.csdn.net/rodbate/article/details/78763379
前引: RocketMQ – 阿里贡献给Apache的消息中间件,现已升级为Apache顶级项目, GITHUB项目地址。
一,索引文件(IndexFile)物理结构
一个索引文件从整体上可以分为header和其它部分(body)。整个索引文件对应类org.apache.rocketmq.store.index.IndexFile, header对应类org.apache.rocketmq.store.index.IndexHeader, 还有索引服务(建立索引等功能)org.apache.rocketmq.store.index.IndexService 这个后面再详细说。
1. IndexHeader
索引文件header的大小和结构都是固定的,大小是40个字节,源码如下:
public class IndexHeader {
public static final int INDEX_HEADER_SIZE = 40;
private static int beginTimestampIndex = 0;
private static int endTimestampIndex = 8;
private static int beginPhyoffsetIndex = 16;
private static int endPhyoffsetIndex = 24;
private static int hashSlotcountIndex = 32;
private static int indexCountIndex = 36;
....
}
1
2
3
4
5
6
7
8
9
10
11
这个头部结构示意图如下,
名词解释:
1. beginTimestamp : 该索引文件的第一个消息(Message)的存储时间(落盘时间) 物理位置(pos: 0-7) 8bytes
2. endTimestamp : 该索引文件的最后一个消息(Message)的存储时间(落盘时间) 物理位置(pos: 8-15) 8bytes
3. beginPhyoffset : 该索引文件第一个消息(Message)的在CommitLog(消息存储文件)的物理位置偏移量(可以通过该物理偏移直接获取到该消息) 物理位置(pos: 16-23) 8bytes
4. beginPhyoffset : 该索引文件最后一个消息(Message)的在CommitLog(消息存储文件)的物理位置偏移量 (pos: 24-31) 8bytes
5. hashSlotCount : 该索引文件目前的hash slot的个数 (pos: 32-35) 4bytes
6. indexCount : 该索引文件目前的索引个数 (pos: 36-39) 4bytes
注: 可能有些名词还不能理解其意义,后面会有更详尽的说明。
1
2
3
4
5
6
7
8
索引文件除了Header(元数据)之外,就是索引的核心内容。在此之前,先简单概括下RocketMQ索引文件的实现 – Hash存储方式。用了Hash就会存在冲突的情况,RocketMQ处理hash冲突的方式也是比较普遍通用的方法 – 链表方式(不了解的可自行去查阅学习)。话说回来,索引文件剩余的部分就是hash slots(就是连续固定个数长度的hash格)和索引(连续固定个数长度),也就是在某个索引文件创建之初它的大小和文件格式就固定下来了, 创建索引源码(使用了mmap方式 – 内存映射,不了解的可自行去查阅学习):
public IndexFile(final String fileName, final int hashSlotNum, final int indexNum,
final long endPhyOffset, final long endTimestamp) throws IOException {
//该索引文件大小
int fileTotalSize =
IndexHeader.INDEX_HEADER_SIZE /*40bytes 见上文*/+ (hashSlotNum/*hash格数*/ * hashSlotSize/*4bytes 见下文*/) + (indexNum/*20bytes 见下文*/ * indexSize/*索引个数*/);
this.mappedFile = new MappedFile(fileName, fileTotalSize);
this.fileChannel = this.mappedFile.getFileChannel();
this.mappedByteBuffer = this.mappedFile.getMappedByteBuffer();
this.hashSlotNum = hashSlotNum;
this.indexNum = indexNum;
ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
this.indexHeader = new IndexHeader(byteBuffer);
if (endPhyOffset > 0) {
this.indexHeader.setBeginPhyOffset(endPhyOffset);
this.indexHeader.setEndPhyOffset(endPhyOffset);
}
if (endTimestamp > 0) {
this.indexHeader.setBeginTimestamp(endTimestamp);
this.indexHeader.setEndTimestamp(endTimestamp);
}
}
....
hashSlotNum和indexNum都是可配置的,在org.apache.rocketmq.store.config.MessageStoreConfig
//默认值
private int maxHashSlotNum = 5000000;
//hash冲突均值4 也就是平均有个索引的hash值是相同的 当然可能也会存在这种可能: 索引个数达到了maxIndexNum,hashSlot确没有填充满(这种可能是很小的)
private int maxIndexNum = 5000000 * 4;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
注:判断索引文件是否写满了需要创建新的索引文件的条件就是该索引文件的索引个数indexCount 达到了maxIndexNum最大值。
2. 索引文件HashSlot部分
索引文件紧跟header之后的是hash slots,也就是固定长度和个数的一连串的数据。假设hash slot个数是MQ的默认值5000000,每一个slot的大小是4个byte, 总大小就是5000000*4 bytes, 索引文件创建之初这些hash slots填充的都是字节00x0。
那每一个hash slot里存储的是什么数据呢? 存储的是相对应的hash值索引的在当前的索引文件索引个数(第几个索引indexCount),因为indexCount >= 1,就会造成hash slots与index entries之间有20bytes的间隙。– 注:这里有点粗糙,等index结构完了之后,再来模拟索引创建和搜索的过程。
3. 索引文件Indexes部分
每一个index的结构如下,大小20bytes
名词解释:
1. key hash value: message key的hash值
2. phyOffset: message在CommitLog的物理文件地址, 可以直接查询到该消息(索引的核心机制)
3. timeDiff: message的落盘时间与header里的beginTimestamp的差值(为了节省存储空间,如果直接存message的落盘时间就得8bytes)
4. prevIndex: hash冲突处理的关键之处, 相同hash值上一个消息索引的index(如果当前消息索引是该hash值的第一个索引,则prevIndex=0, 也是消息索引查找时的停止条件。)
注: 可能有些名词还不能理解其意义,后面会有更详尽的说明。
1
2
3
4
5
6
7
4. 添加消息索引的过程
IndexFile添加索引源码:
public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
//1. 判断该索引文件的索引数小于最大的索引数,如果>=最大索引数,IndexService就会尝试新建一个索引文件
if (this.indexHeader.getIndexCount() < this.indexNum) {
//2. 计算该message key的hash值
int keyHash = indexKeyHashMethod(key);
//3. 根据message key的hash值散列到某个hash slot里
int slotPos = keyHash % this.hashSlotNum;
//4. 计算得到该hash slot的实际文件位置Position
int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
try {
//5. 根据该hash slot的实际文件位置absSlotPos得到slot里的值
//这里有两种情况:
//1). slot=0, 当前message的key是该hash值第一个消息索引
//2). slot>0, 该key hash值上一个消息索引的位置
int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
//6. 数据校验及修正
if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
slotValue = invalidIndex;
}
long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
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. 计算当前消息索引具体的存储位置(Append模式)
int absIndexPos =
IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
+ this.indexHeader.getIndexCount() * indexSize;
//8. 存入该消息索引
this.mappedByteBuffer.putInt(absIndexPos, keyHash);
this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
//9. 关键之处:在该key hash slot处存入当前消息索引的位置,下次通过该key进行搜索时
//会找到该key hash slot -> slot value -> curIndex ->
//if(curIndex.prevIndex>0) pre index (一直循环 直至该curIndex.prevIndex==0就停止)
this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
if (this.indexHeader.getIndexCount() <= 1) {
this.indexHeader.setBeginPhyOffset(phyOffset);
this.indexHeader.setBeginTimestamp(storeTimestamp);
}
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);
}
} else {
log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
+ "; index max num = " + this.indexNum);
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
5. 消息索引搜索的过程
IndexFile索引搜索源码:
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的hash
int keyHash = indexKeyHashMethod(key);
//2. 计算该hash value 对应的hash slot位置
int slotPos = keyHash % this.hashSlotNum;
//3. 计算该hash value 对应的hash slot物理文件位置
int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
FileLock fileLock = null;
try {
//4. 取出该hash slot 的值
int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
//5. 该slot value <= 0 就代表没有该key对应的消息索引,直接结束搜索
// 该slot value > maxIndexCount 就代表该key对应的消息索引超过最大限制,数据有误,直接结束搜索
if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()
|| this.indexHeader.getIndexCount() <= 1) {
} else {
//6. 从当前slot value 开始搜索
for (int nextIndexToRead = slotValue; ; ) {
if (phyOffsets.size() >= maxNum) {
break;
}
//7. 找到当前slot value(也就是index count)物理文件位置
int absIndexPos =
IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
+ nextIndexToRead * indexSize;
//8. 读取消息索引数据
int keyHashRead = this.mappedByteBuffer.getInt(absIndexPos);
long phyOffsetRead = this.mappedByteBuffer.getLong(absIndexPos + 4);
long timeDiff = (long) this.mappedByteBuffer.getInt(absIndexPos + 4 + 8);
//9. 获取该消息索引的上一个消息索引index(可以看成链表的prev 指向上一个链节点的引用)
int prevIndexRead = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8 + 4);
//10. 数据校验
if (timeDiff < 0) {
break;
}
timeDiff *= 1000L;
long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff;
boolean timeMatched = (timeRead >= begin) && (timeRead <= end);
//10. 数据校验比对 hash值和落盘时间
if (keyHash == keyHashRead && timeMatched) {
phyOffsets.add(phyOffsetRead);
}
//当prevIndex <= 0 或prevIndex > maxIndexCount 或prevIndexRead == nextIndexToRead 或 timeRead < begin 停止搜索
if (prevIndexRead <= invalidIndex
|| prevIndexRead > this.indexHeader.getIndexCount()
|| prevIndexRead == nextIndexToRead || timeRead < begin) {
break;
}
nextIndexToRead = prevIndexRead;
}
}
} catch (Exception e) {
log.error("selectPhyOffset exception ", e);
} finally {
this.mappedFile.release();
}
}
}