在今天的任務中主要是實現下面一層一層的迭代器:
Task 1: Memtable Iterator
在本章中,我們將實現LSM
scan
介面,scan
使用迭代器API按順序返回一系列鍵值對。在上一章中,您已經實現了get
API和建立不可變memtable的邏輯,您的LSM state現在應該有多個memtable。您需要首先在單個memtable上建立迭代器,然後在所有memtable上建立合併迭代器,最後實現迭代器的範圍限制。在此任務中,您需要修改:
src/mem_table.rs
所有的LSM迭代器都實現了
StorageIterator
特性。它有4個函式:key
、value
、next
和is_valid
。當迭代器被建立時,它的游標將停止在某個元素上,而key
/value
將返回memtable/block/SST中滿足開始條件的第一個鍵(即開始鍵)。這兩個介面會返回一個&[u8]
,以避免複製。請注意,這個迭代器介面不同於Rust風格的迭代器。
next
將游標移動到下一個位置。如果迭代器已經到達末尾或出錯,則返回is_valid
。你可以假設只有當is_valid
返回true
時才會呼叫next
。Task3中將有一個用於迭代器的FusedIterator
包裝器,當迭代器無效時,它會阻止對next
的呼叫,以避免使用者誤用迭代器。回到memtable迭代器。你應該已經發現迭代器沒有任何與之相關的生命週期。假設你建立了一個
Vec<u64>
並呼叫vec.iter()
,迭代器型別將是類似於VecIterator<'a>
的東西,其中'a
是vec
物件的生命週期。SkipMap
也是如此,它的iter
API返回一個具有生命週期的迭代器。然而,在我們的情況下,我們不希望在迭代器上有這樣的生命週期,以避免使系統過於複雜(並且難以編譯...)。如果迭代器沒有生命週期泛型引數,我們應該確保每當使用迭代器時,底層的skiplist物件都不會被釋放。實現這一點的唯一方法是將
Arc<SkipMap>
物件放入迭代器本身。要定義這樣的結構,pub struct MemtableIterator { map: Arc<SkipMap<Bytes, Bytes>>, iter: SkipMapRangeIter<'???>, }
好了,問題來了:我們想表達迭代器的生命週期和結構體中的map是一樣的。我們怎麼能做到呢?
這是你在本教程中遇到的第一個也是最棘手的Rust語言的東西——自引用結構。如果可以這樣寫:
pub struct MemtableIterator { // <- with lifetime 'this map: Arc<SkipMap<Bytes, Bytes>>, iter: SkipMapRangeIter<'this>, }
那麼問題就解決了!你可以在一些第三方庫的幫助下實現這一點,比如ouroboros。它提供了一種簡單的方法來定義自引用結構。使用不安全的Rust也可以做到這一點(事實上,我們自己內部使用不安全的Rust...)
我們已經為您定義了自引用
MemtableIterator
欄位,您需要實現MemtableIterator
和Memtable::scan
API。
進行本章的內容,需要先對ouroboros
三方工具庫有一定了解:https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html
對於以下定義的自引用結構體:
#[self_referencing]
struct MyStruct {
tail_field: i32,
int_data: i32,
float_data: f32,
#[borrows(int_data)]
int_reference: &'this i32,
#[borrows(mut float_data)]
float_reference: &'this mut f32,
}
其中int_data
有一個不可變的借用int_reference
,float_data
有一個可變的借用float_reference
,tail_field
是沒有被借用的欄位。
如果需要獲取欄位的值可以使用borrow_{field_name}()
方法:
my_value.borrow_tail_field()
my_value.borrow_int_reference()
my_value.borrow_int_data()
my_value.borrow_float_reference()
如果需要獲取到欄位的可變引用然後修改值,需要使用with_mut
或者with_{field_name}_mut
:
my_value.with_mut(|fields| {
**fields.float_reference = (**fields.int_reference as f32) * 2.0;
*fields.tail_field = 12;
});
my_value.with_float_reference_mut(|float_ref| {
**float_ref = 32.0
});
MemtableIterator
value方法:
結構體的屬性不能直接訪問,需要透過self.borrow_{field_name}
訪問。先透過self.borrow_item()
獲取到item
這個元組,再透過self.borrow_item().1
獲取到第二個元素,因為返回的是字串切片所以變成self.borrow_item().1[..]
,為了不產生複製最終變成:
&self.borrow_item().1[..]
藉助ouroboros::self_referencing
方法,能簡單的實現返回引用。
is_valid方法:
判斷元組第一個元素是否為空:
!self.borrow_item().0.is_empty()
key方法:
KeySlice::from_slice(&self.borrow_item().0[..])
next方法:
可以使用with_iter_mut
方法獲取到iter
的可變引用,透過with_item_mut
可以獲取到item
的可變引用。
let entry = self.with_iter_mut(|iter| {
let entry = iter.next();
match entry {
None => { (Bytes::new(), Bytes::new()) }
Some(entry) => {
(entry.key().clone(), entry.value().clone())
}
}
});
self.with_item_mut(|item|{
*item = entry;
});
Ok(())
scan
透過ouroboros::self_referencing
的建構函式生成物件,注意iter
欄位的複製需要使用閉包的方式賦值給iter_builder
。
let (lower, upper) = (map_bound(_lower), map_bound(_upper));
let mut iterator = MemTableIteratorBuilder {
map: self.map.clone(),
iter_builder: |map| map.range((lower, upper)),
item: (Bytes::new(), Bytes::new()),
}
.build();
iterator.next().unwrap();
iterator
Task 2-Merge Iterator
在此任務中,您需要修改:
src/iterors/merge_iterator.rs
現在你有了多個memtable,你將建立擁有多個memtable的迭代器。您需要合併memtables中的結果,並將每個鍵的最新版本返回給使用者。
MergeIterator在內部維護了一個二叉堆(BinaryHeap)。請注意,您需要處理錯誤(即當迭代器無效時),並確保鍵值對的最新版本。
例如,如果我們有以下資料:
iter1: b->del, c->4, d->5
iter2: a->1, b->2, c->3
iter3: e->4
合併迭代器輸出的順序應該是:
a->1、b->del、c->4、d->5、e->4
合併迭代器的建構函式接受迭代器的陣列Vec。我們假設索引較小的那個(即第一個)擁有最新的資料。
一個常見的陷阱是錯誤處理。例如:
let Some(mut inner_iter) = self.iters.peek_mut() { inner_iter.next()?; // <- will cause problem }
如果next返回錯誤(即,由於磁碟故障、網路故障、校驗和錯誤等),則不再有效。但是,當我們走出if條件並將錯誤返回給呼叫者時,PeekMut的drop將嘗試在堆中移動元素,這導致訪問無效的迭代器。因此,您需要自己完成所有錯誤處理,而不是使用?在PeekMut的範圍內。
我們希望儘量避免動態分派,因此我們在系統中不使用
Box<dyn StorageIterator>
。相反,我們更傾向於使用泛型進行靜態分派。另請注意,StorageIterator
使用泛型關聯型別(GAT),因此它可以同時支援KeySlice
和&[u8]
作為鍵型別。我們將更改KeySlice
以在第3週中包含時間戳,現在為它使用單獨的型別可以使過渡更加平滑。從本節開始,我們將使用
Key<T>
來表示LSM
鍵型別,並將它們與型別系統中的值區分開來。你應該使用Key<T>
提供的API,而不是直接訪問內部值。在第3部分中,我們將為這個鍵型別新增時間戳,使用鍵抽象將使轉換更加平滑。目前,KeySlice
等價於&[u8]
,KeyVec
等價於Vec<u8>
,KeyBytes
等價於Bytes
。
二叉堆(BinaryHeap)其實就是一個優先佇列,先檢視其比較規則:
match self.1.key().cmp(&other.1.key()) {
cmp::Ordering::Greater => Some(cmp::Ordering::Greater),
cmp::Ordering::Less => Some(cmp::Ordering::Less),
cmp::Ordering::Equal => self.0.partial_cmp(&other.0),
}
先比較其StorageIterator
當前元素的key
值,如果相同再比較Vec
索引下標的值。再驗證一下,修改BinaryHeap
中的元素,BinaryHeap
是否會重新排列:
use std::collections::BinaryHeap;
#[derive(Debug, Eq, PartialOrd, PartialEq)]
#[derive(Ord)]
struct MyStruct {
x: i32,
y: String,
}
fn main() {
let mut heap = BinaryHeap::new();
let xiaoming = MyStruct {
x: 16,
y: String::from("xiaoming"),
};
let xiaohong = MyStruct {
x: 17,
y: String::from("xiaohong"),
};
heap.push(xiaoming);
heap.push(xiaohong);
println!("{:?}", heap.peek());
let xiaohong = heap.peek_mut();
xiaohong.unwrap().x = 1;
println!("{:?}", heap.peek());
}
輸出:
Some(MyStruct { x: 17, y: "xiaohong" })
Some(MyStruct { x: 16, y: "xiaoming" })
可知BinaryHeap
的堆頂元素永遠是key
值最小且最新的那個。
create
建構函式實現:
let mut iter = MergeIterator {
iters: BinaryHeap::new(),
current: None,
};
if iters.iter().all(|x| !x.is_valid()) && !iters.is_empty() {
let mut iters = iters;
iter.current = Some(HeapWrapper(0, iters.pop().unwrap()));
return iter;
}
for (index, storage_iter) in iters.into_iter().enumerate() {
if storage_iter.is_valid() {
iter.iters.push(HeapWrapper(index, storage_iter));
}
}
if !iter.iters.is_empty() {
iter.current = Some(iter.iters.pop().unwrap())
}
iter
-
建立一個預設物件
-
判斷
Vec
陣列裡面的元素是否都為非法值,如果都是非法值則將current
隨便賦值一個。因為在使用MergeIterator
也會先呼叫is_valid
方法 -
將
Vec
陣列裡面有合法元素的迭代器都加到堆中 -
取堆頂的元素賦值給
current
key/value
current
元素中迭代器的key
/value
方法:self.current.as_ref().unwrap().1.key()
、self.current.as_ref().unwrap().1.value()
。
is_valid
先判斷是否為None
,再呼叫current
中迭代器的is_valid
方法:
if let None = self.current {
return false;
}
self.current.as_ref().unwrap().1.is_valid()
next
let current = self.current.as_mut().unwrap();
while let Some(mut inner_iter) = self.iters.peek_mut() {
if inner_iter.1.key() != current.1.key() {
break;
}
if let e @ Err(_) = inner_iter.1.next() {
PeekMut::pop(inner_iter);
return e;
}
if !inner_iter.1.is_valid() {
PeekMut::pop(inner_iter);
}
}
current.1.next()?;
if !current.1.is_valid() {
if let Some(iter) = self.iters.pop() {
*current = iter;
}
return Ok(());
}
if let Some(mut inner_iter) = self.iters.peek_mut() {
if *current < *inner_iter {
std::mem::swap(&mut *inner_iter, current);
}
}
Ok(())
-
先把和當前
current
迭代器key相同的迭代器移除。 -
當前
current
迭代器取下一個元素 -
當前
current
迭代器非法,從堆頂pop
出一個迭代器 -
當前
current
迭代器合法,和堆頂迭代器比較,若小於則交換
Task 3-LSM Iterator + Fused Iterator
在此任務中,您需要修改:
src/lsm_iterator.rs
我們使用LsmIterator結構來表示內部的LSM迭代器。當系統中新增了更多的迭代器時,您將需要在整個教程中多次修改此結構。目前,因為我們只有多個memtable,所以它應該定義為:
型別
LsmIteratorInner=MergeIterator<MemTableIterator>
;您可以繼續實現LsmIterator結構,它呼叫相應的內部迭代器,並且也跳過刪除的鍵。
我們不在此任務中測試LsmIterator。在任務4中,將有一個整合測試。
然後,我們希望在迭代器上提供額外的安全性,以避免使用者誤用它們。當迭代器無效時,不應呼叫key、value或next。同時,如果next返回錯誤,則不應該再使用迭代器。FusedIterator是一個圍繞迭代器的包裝器,用於規範化所有迭代器的行為。你可以自己去實現它。
LsmIterator
主要還是直接呼叫inner
,就是在空value
的時候需要跳到下一個鍵值對:
impl LsmIterator {
pub(crate) fn new(iter: LsmIteratorInner) -> Result<Self> {
let mut lsm = Self { inner: iter };
if lsm.is_valid() && lsm.value().is_empty() {
lsm.next();
}
Ok(lsm)
}
}
impl StorageIterator for LsmIterator {
type KeyType<'a> = &'a [u8];
fn is_valid(&self) -> bool {
self.inner.is_valid()
}
fn key(&self) -> &[u8] {
self.inner.key().raw_ref()
}
fn value(&self) -> &[u8] {
self.inner.value()
}
fn next(&mut self) -> Result<()> {
self.inner.next();
if self.inner.is_valid() && self.inner.value().is_empty() {
return self.next();
}
Ok(())
}
}
FusedIterator
判斷是否有異常、是否合法,否則報錯
impl<I: StorageIterator> StorageIterator for FusedIterator<I> {
type KeyType<'a> = I::KeyType<'a> where Self: 'a;
fn is_valid(&self) -> bool {
!self.has_errored && self.iter.is_valid()
}
fn key(&self) -> Self::KeyType<'_> {
if !self.is_valid() {
panic!("invalid access to the underlying iterator");
}
self.iter.key()
}
fn value(&self) -> &[u8] {
if !self.is_valid() {
panic!("invalid access to the underlying iterator");
}
self.iter.value()
}
fn next(&mut self) -> Result<()> {
if self.has_errored {
bail!("the iterator is tainted");
}
if self.iter.is_valid() {
if let Err(e) = self.iter.next() {
self.has_errored = true;
return Err(e);
}
}
Ok(())
}
}
Task 4-Read Path - Scan
在此任務中,您需要修改:
src/lsm_storage.rs
我們終於實現了——有了所有已經實現的迭代器,您終於可以實現LSM引擎的掃描介面了。你可以簡單地用memtable迭代器構造一個LSM迭代器(記得把最新的memtable放在merge迭代器的前面),然後你的儲存引擎就可以處理掃描請求了。
將此前寫的迭代器用於scan介面:
let snapshot = {
let guard = self.state.read();
Arc::clone(&guard)
};
let mut memtable_iters = Vec::with_capacity(snapshot.imm_memtables.len() + 1);
memtable_iters.push(Box::new(snapshot.memtable.scan(_lower, _upper)));
for memtable in snapshot.imm_memtables.iter() {
memtable_iters.push(Box::new(memtable.scan(_lower, _upper)));
}
Ok(FusedIterator::new(LsmIterator::new(
MergeIterator::create(memtable_iters),
)?))