一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
sled 是一个用Rust编写的嵌入式数据库。最近在工作中频繁使用它,本篇文章打算好好了解它的工作原理。它的底层结构是 Log Structure Merge
(并且暴露给client),同时具有 B+Tree
的读性能。(顺便还可以复习一些 LSM 的知识,不错不错)
通常来说,在阅读源码的时候,会选取一个较早的版本:
- 尽快理解作者开发的初衷
- 初代设计更为直接和简洁明了
- 可以观察项目的演进
这里选取该 commit 。我们可以看到 sled 的最初组织结构:
我们需要关注正是这个 crates
下的核心代码。
crates/pagecache
→ 从文件夹名已经给出sled很多的存储细节:
- 使用
page
作为数据组织单元 (比如 levedb 就不是) - 而且对其在内存中有 cache (所以可能不会使用 mmap)
这个推论或许对后面理解源码会比较重要。
上述显示:有N个分片;
fn traverse<'g, T: 'static + Send>(
head: Shared<'g, Node1<T>>,
k: PageId,
guard: &'g Guard,
) -> &'g Atomic<T> {
let (l1k, l2k) = split_fanout(k);
debug_delay();
let l1 = unsafe { &head.deref().children };
debug_delay();
let mut l2_ptr = l1[l1k].load(SeqCst, guard);
if l2_ptr.is_null() {
let next_child = Owned::new(Node2::default()).into_shared(guard);
debug_delay();
let ret = l1[l1k].compare_and_set(l2_ptr, next_child, SeqCst, guard);
match ret {
Ok(_) => {
// CAS worked
l2_ptr = next_child;
}
Err(e) => {
// another thread beat us, drop unused created
// child and use what is already set
l2_ptr = e.current;
}
}
}
debug_delay();
let l2 = unsafe { &l2_ptr.deref().children };
&l2[l2k]
}
基本流程:
- 遍历节点tree → 找到key对应的leaf node
- 修改这个leaf node,设置新的<k, v>键值对
在 <l2_ptr>
中:如果在CAS中没有成功,究竟是谁要放弃未使用的创建值?答案在Guard和Epoch身上。我们在调用 into_shared
时将该值与它们相关联,由于我们没有将它从 Guard
中取出,所以当 Epoch
结束时,它将被删除。
traverse()的最终结果是直接返回第二层的值:
debug_delay();
let l2 = unsafe { &l2_ptr.deref().children };
&l2[l2k]
TODO!!!