mini-lsm通關筆記Week1Day3

余为民同志發表於2024-06-22

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中的資料複製一份作為基礎,再將offsetsnum_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,再解碼出offsetsdata部分:

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_firstseek_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

實現的一個內部函式,用於跳到指定索引位。

  1. 獲取偏移量,並找到資料的位置
  2. 解碼key的長度,get_u16會自動向後移動2個位元組
  3. 解碼key,複製
  4. 解碼value的長度
  5. 將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);
    }
}

這是一種偷懶的實現,應該採用二分查詢的方式

相關文章