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);
}
}
這是一種偷懶的實現,應該採用二分查詢的方式