这是我参与更文挑战的第3天,活动详情查看:更文挑战
本文开始分析
LevelDB中LSM-Tree的设计。本文聚焦内存数据结构设计
首先说结论(后面我们还会说 LSM-Tree 和 B+Tree 聚焦场景不同点在哪):
- 加速写 -> 顺序写。所以会把所有写入直接按照发生顺序写入内存
- 最终还是要落盘 -> 内存中的数据块达到一个阈值,成块(批量刷)刷入磁盘
- 查询 -> 先查询内存;没有则再进入磁盘查询
- 磁盘查询尽可能快 -> 有序的Block,这样遍历起来比较快(可以用到二分,类似
InnoDB中的page Directory slot)
所有整体思路:牺牲查询性能,极致优化写,再优化部分查询功能。
个人观点来说,不管是优化什么,你的思路都应该有一个数据结构作为中心,也就是说为了实现优化,选择了某种数据结构,然后再在这个数据结构上进行扩展开发。
所以我们目前的诉求有哪些:
- 因为数据第一个存储的是地方是内存,内存数据结构组织保证尽可能高的读写性能(高效插入)
- 数据要从内存 -> dump到磁盘中;数据组织需要支持高效的遍历
综上所述:Skiplist,是一个比较好的选择。主要是代码好写。
skipList
一句话描述
skiplist:带有额外指针的链表,额外指针可以解决部分链表查询性能问题
skiplist 是怎么构建这些额外指针呢?一个思路:跳步采样,构建索引,逐层减少。
findGreaterOrEqual
查询不小于 key 的第一个值(returnValue >= key)
// 查询不小于 key 的第一个值(returnValue >= key)
// 返回的第二个参数:记下待查找节点在各层中的前驱节点
func (sk *SkipList) findGeOrEq(key interface{}) (*Node, [kMaxHeight]*Node) {
var prev [kMaxHeight]*Node
x := sk.head // 从head开始
level := sk.maxHeight - 1 // 从最高层开始查找
for true {
next := x.getNext(level) // 先看当前节点当前层的下一个节点
if sk.keyIsAfterNode(key, next) {
x = next // 如果key > next,说明可以往当前层继续查找
} else {
prev[level] = x // 记录经过的每一层值
if level == 0 { // 到底了,返回 nowValue
return next, prev
} else { // nowValue >= key 而且没有到底,下降一层继续查询
level --
}
}
}
return nil, prev
}
Insert
func (sk *SkipList) Insert(key interface{}) {
sk.mu.Lock()
_, prev := sk.findGeOrEq(key)
h := sk.randomHeight()
if h > sk.maxHeight {
for i := sk.maxHeight; i < h; i++ {
prev[i] = sk.head
}
sk.maxHeight = h
}
x := newNode(key, h)
for i := 0; i < h; i++ {
x.setNext(i, prev[i].getNext(i))
prev[i].setNext(i, x)
}
sk.mu.Unlock()
}
- 整个中最重要就是给
key划分一个合理的层数(如何划分下一个函数讲解) - 分配的层数 > 目前最高的层数(就是
head的层数),则从findGeOrEq返回的前驱数组中[randHeight->maxHeight]这个新生成的层需要指向head;并更新最高层数 - 分配的层数 < 目前最高的层数,新建一个节点;从
randHeight一层一层赋值给findGeOrEq返回的前驱数组的每一层
randomHeight
获得一个概率分布的高度
func (sk *SkipList) randomHeight() int {
h := 1
for h < kMaxHeight && (rand.Intn(kBranching) == 0) {
h ++
}
return h
}