mini-lsm通关笔记Week1Day3
Task1-Block Builder
在前两章中,你已经实现了LSM存储引擎的所有内存结构。现在是时候构建磁盘上的结构了。磁盘结构的基本单元是块。块的大小通常为4 KB(大小可能因存储介质而异),这相当于操作系统中的页面大小和SSD上的页面大小。块存储有序的键值对。一个SST由多个Block组成。当memtable的数量超过系统限制时,它会将memtable刷新为SST。在本章中,你将实现一个块的编码和解码。
在此任务中,您需要修改:
src/block/builder.rs src/block.rs
我们教程中的块编码格式如下:
---------------------------------------------------------------------------------------------------- | Data Section | Offset Section | Extra | ---------------------------------------------------------------------------------------------------- | Entry #1 | Entry #2 | ... | Entry #N | Offset #1 | Offset #2 | ... | Offset #N | num_of_elements | ----------------------------------------------------------------------------------------------------
每个条目都是一个键值对。
----------------------------------------------------------------------- | Entry #1 | ... | ----------------------------------------------------------------------- | key_len (2B) | key (keylen) | value_len (2B) | value (varlen) | ... | -----------------------------------------------------------------------
键和值的长度都是2个字节,这意味着它们的最大长度都是65535。(内部存储为u16)
我们假设键永远不会为空,而值可以为空。空值意味着相应的键在系统的其他部分的视图中已被删除。对于BlockBuilder和BlockIterator,我们只需按原样处理空值。
在每个块的末尾,我们将存储每个条目的偏移量和条目的总数。例如,如果第一个条目位于块的第0个位置,而第二个条目位于块的第12个位置。
------------------------------- |offset|offset|num_of_elements| ------------------------------- | 0 | 12 | 2 | -------------------------------
块的页脚将如上。每个数字存储为u16。
块有大小限制,即target_size。除非第一个键值对超过目标块大小,否则应确保编码后的块大小始终小于或等于target_size。(在提供的代码中,这里的target_size本质上就是block_size)
调用构建时,BlockBuilder将生成数据部分和未编码的条目偏移量。这些信息将存储在Block结构中。由于键值条目以原始格式存储,偏移量存储在单独的数组中,这减少了解码数据时不必要的内存分配和处理开销——您需要做的是简单地将原始块数据复制到数据数组中,并每隔2个字节解码条目偏移量,而不是创建类似Vec<(Vec
)Vec >将所有的键值对存储在内存中的一个块中。这种紧凑的内存布局非常高效。 在Block::coding和Block::decode中,您需要按照上述格式对块进行编码/解码。
BlockBuilder
构造函数&&is_empty&&build
构造函数就是成员变量的初始化:
pub fn new(block_size: usize) -> Self {
BlockBuilder {
offsets: Vec::new(),
data: Vec::new(),
block_size,
first_key: KeyVec::new(),
}
}
is_empty就是判断data
或者offsets
中是否有值:
pub fn is_empty(&self) -> bool {
self.offsets.is_empty()
}
build构造一个Block
:
pub fn build(self) -> Block {
Block {
data: self.data,
offsets: self.offsets,
}
}
add
大小的判断:self.data.len() + self.offsets.len() + 2
就是现在Block
的大小。2 + key.raw_ref().len() + 2 + value.len()
就是加进来的键值对新增的大小。如果该大小block_size
且不是第一个键值对则返回false
。
pub fn add(&mut self, key: KeySlice, value: &[u8]) -> bool {
if self.data.len() + self.offsets.len() + 6 + key.raw_ref().len() + value.len()
> self.block_size
&& !self.is_empty()
{
return false;
}
self.offsets.push(self.data.len() as u16);
self.data.put_u16(key.raw_ref().len() as u16);
self.data.put(&key.raw_ref()[..]);
self.data.put_u16(value.len() as u16);
self.data.put(&value[..]);
true
}
Block
encode
编码,先将data
中的数据复制一份作为基础,再将offsets
、num_of_elements
添加进去:
pub fn encode(&self) -> Bytes {
let mut encode_buf = self.data.clone();
let num_of_elements = self.offsets.len();
for x in &self.offsets {
encode_buf.put_u16(*x);
}
encode_buf.put_u16(num_of_elements as u16);
encode_buf.into()
}
decode
先解码最后两个字节为num_of_elements
,再解码出offsets
、data
部分:
pub fn decode(data: &[u8]) -> Self {
let num_of_elements = (&data[data.len() - 2..]).get_u16() as usize;
let offsets_vec = &data[data.len() - 2 - num_of_elements * 2..data.len() - 2];
let offsets = offsets_vec.chunks(2).map(|mut x| x.get_u16()).collect();
let data = data[..data.len() - 2 - num_of_elements * 2].to_vec();
Self { offsets, data }
}
Task2-Block Iterator
在此任务中,您需要修改:
src/block/iterator.rs
现在我们有了一个被编码的块(
Block
),我们需要实现BlockIterator
接口,以便用户可以查找/扫描块中的键。
BlockIterator
可以使用Arc<Block>
创建。如果调用create_and_seek_to_first
,它将定位在块中的第一个键。如果调用create_and_seek_to_key
,则迭代器将定位在>=
提供的键的第一个键处。例如,如果1、3、5在一个块中。let mut iter=BlockIterator::create_and_seek_to_key(block,b"2"); assert_eq!(iter.key(),b"3");
上面的查找2将使迭代器定位在2的下一个可用键,在本例中是3。
迭代器应该从块中复制key,并将它们存储在迭代器内部(我们将来会有key压缩,你必须这样做)。对于值,您应该只将开始/结束偏移量存储在迭代器中,而不复制它们。
当调用next时,迭代器将移动到下一个位置。如果到达块的末尾,我们可以将key设置为空,并从is_valid返回false,这样调用者可以在可能的情况下切换到另一个块。
create_and_seek_to_first&&create_and_seek_to_key
先调用BlockIterator::new
创建出对应的迭代器,在分别调用seek_to_first
、seek_to_key
跳到对应的位置,在将迭代器返回。
pub fn create_and_seek_to_first(block: Arc<Block>) -> Self {
let mut iterator = BlockIterator::new(block);
iterator.seek_to_first();
iterator
}
pub fn create_and_seek_to_key(block: Arc<Block>, key: KeySlice) -> Self {
let mut iterator = BlockIterator::new(block);
iterator.seek_to_key(key);
iterator
}
key&&value&&is_valid
返回结构体中的成员变量
pub fn key(&self) -> KeySlice {
self.key.as_key_slice()
}
pub fn value(&self) -> &[u8] {
&self.block.data[self.value_range.0..self.value_range.1]
}
pub fn is_valid(&self) -> bool {
!self.key.is_empty()
}
seek_to_index
实现的一个内部函数,用于跳到指定索引位。
- 获取偏移量,并找到数据的位置
- 解码key的长度,get_u16会自动向后移动2个字节
- 解码key,复制
- 解码value的长度
- 将value的起始位置与结束位置记录
fn seek_to_index(&mut self, index: usize) {
self.idx = index;
// 获取偏移量,并找到数据的位置
let offset = self.block.offsets[index] as usize;
let mut entry = &self.block.data[offset..];
// 解码key的长度,get_u16会自动向后移动2个字节
let key_length = entry.get_u16() as usize;
// 解码key,复制
let key = &entry[..key_length];
self.key.clear();
self.key.append(key);
entry.advance(key_length);
// 解码value的长度
let value_length = entry.get_u16() as usize;
let value_offset_begin = offset + 2 + key_length + 2;
let value_offset_end = value_offset_begin + value_length;
// 将value的起始位置与结束位置记录
self.value_range = (value_offset_begin, value_offset_end);
}
next
如果索引范围超出下标,则将key
设为空。否则调用刚刚实现的私有方法移动下标。
pub fn next(&mut self) {
let mut index = self.idx + 1;
if index >= self.block.offsets.len() {
self.key = KeyVec::new();
return;
}
self.seek_to_index(index);
}
seek_to_first&&seek_to_key
seek_to_first
只需要将下标设置为0:
pub fn seek_to_first(&mut self) {
self.seek_to_index(0);
}
seek_to_key
则从头比较,找到大于等于传入的key为止:
pub fn seek_to_key(&mut self, key: KeySlice) {
self.seek_to_first();
let mut result = self.key().cmp(&key);
while result == Less {
self.next();
if !self.is_valid() {
break;
};
result = self.key().cmp(&key);
}
}
这是一种偷懒的实现,应该采用二分查找的方式