为了提高查找消息的性能,为每一个日志文件添加2个索引索引文件:OffsetIndex 和 TimeIndex,她俩分别对应着磁盘上两个索引文件,与FileMessageSet共同构成一个LogSegment对象。
OffsetIndex索引文件的格式: 每一个索引项为8字节,其中相对offset占用4字节,消息的物理地址(position)占用4个字节
这样就实现了相对offset与物理地址的映射,相对offset表示消息相对于baseOffSet的偏移量,例如分段后的一个日志文件的baseOffset是32450,它的文件名就是32450.log,那么offset为32455的消息在相对offset就是32455-32450 = 5。
另外OffsetIndex是稀疏索引,也就是说不会存储所有的消息的相对offset和position
OffsetIndexhe核心字段:
file: 指向磁盘上的索引文件
baseOffset: 对应日志文件第一个消息的offset
mmap: 用来操作索引文件的MappedByteBuffer
lock: ReentrantLock对象,在mmap进行操作的时候需要加锁保护
_entries:当前索引文件索引项的个数
_maxEntries: 当前索引文件中最多能够保存索引项个数
_lastOffset: 保存最后一个索引项的offset
/**
* 因为会有多个handler线程并发写入索引文件,所以这些字段使用@volatile,保证线程之间的可见性
*/
@volatile
protected var mmap: MappedByteBuffer= {
// 如果索引文件不存在则创建返回true,否则存在返回false
val newlyCreated = _file.createNewFile()
val raf = new RandomAccessFile(_file, "rw")
try {
/* 如有必要进行预分配空间 */
if(newlyCreated) {
//maxIndexSize值大小对索引文件分配大小,分配结果是小于maxIndexSize *8
if(maxIndexSize < entrySize)
throw new IllegalArgumentException("Invalidmax index size: " + maxIndexSize)
raf.setLength(roundDownToExactMultiple(maxIndexSize, entrySize))
}
/* 内存映射 */
val len = raf.length()
val idx = raf.getChannel.map(FileChannel.MapMode.READ_WRITE, 0, len)
/* 蒋新创建的索引文件的position设置为0,从头开始写文件 */
if(newlyCreated)
idx.position(0)
else
// 对于原来就存在的索引文件,则将position移动到索引项的结束位置,防止数据覆盖
idx.position(roundDownToExactMultiple(idx.limit, entrySize))
idx
} finally {
CoreUtils.swallow(raf.close())
}
}
// 添加指定offset/location对到索引中
def append(offset: Long, position: Int) {
inLock(lock) {
require(!isFull, "Attempt to append to a full index (size = " + _entries + ").")
if (_entries == 0 || offset > _lastOffset) {
debug("Adding index entry %d => %d to %s.".format(offset, position, file.getName))
mmap.putInt((offset - baseOffset).toInt)
mmap.putInt(position)
_entries += 1
_lastOffset = offset
require(_entries * entrySize == mmap.position, entries + " entries but file position in index is " + mmap.position + ".")
} else {
throw new InvalidOffsetException("Attempt to append an offset (%d) to position %d no larger than the last offset appended (%d) to %s."
.format(offset, entries, _lastOffset, file.getAbsolutePath))
}
}
}
查找索引:
// 查找那些大于或者等于给定的offset的offset和position
def lookup(targetOffset: Long): OffsetPosition = {
maybeLock(lock) {
val idx = mmap.duplicate // 创建一个副本
val slot = indexSlotFor(idx, targetOffset, IndexSearchType.KEY)
if(slot == -1)
OffsetPosition(baseOffset, 0)
else
parseEntry(idx, slot).asInstanceOf[OffsetPosition]
}
}
// 查找那些大于或者等于指定offset存储在哪一个slot上的
protected def indexSlotFor(idx: ByteBuffer, target: Long, searchEntity: IndexSearchEntity): Int = {
// check if the index is empty
if(_entries == 0)
return -1
// check if the target offset is smaller than the least offset
if(compareIndexEntry(parseEntry(idx, 0), target, searchEntity) > 0)
return -1
// 二分查找算法
var lo = 0
var hi = _entries - 1
while(lo < hi) {
val mid = ceil(hi/2.0 + lo/2.0).toInt
val found = parseEntry(idx, mid)
val compareResult = compareIndexEntry(found, target, searchEntity)
if(compareResult > 0)
hi = mid - 1
else if(compareResult < 0)
lo = mid
else
return mid
}
lo
}
TimeIndex索引文件格式:它是映射时间戳和相对offset, 时间戳和相对offset作为entry,供占用12字节,时间戳占用8字节,相对offset占用4字节,这个索引也是稀疏索引,没有保存全部的消息的entry
TimeIndex查找添加算法和OffsetIndex差不多,不再赘述