note LSM Skiplist

318 阅读2分钟

这是我参与更文挑战的第3天,活动详情查看:更文挑战

本文开始分析 LevelDBLSM-Tree 的设计。本文聚焦内存数据结构设计

首先说结论(后面我们还会说 LSM-TreeB+Tree 聚焦场景不同点在哪):

  1. 加速写 -> 顺序写。所以会把所有写入直接按照发生顺序写入内存
  2. 最终还是要落盘 -> 内存中的数据块达到一个阈值,成块(批量刷)刷入磁盘
  3. 查询 -> 先查询内存;没有则再进入磁盘查询
  4. 磁盘查询尽可能快 -> 有序的Block,这样遍历起来比较快(可以用到二分,类似 InnoDB 中的 page Directory slot)

所有整体思路:牺牲查询性能,极致优化写,再优化部分查询功能。

个人观点来说,不管是优化什么,你的思路都应该有一个数据结构作为中心,也就是说为了实现优化,选择了某种数据结构,然后再在这个数据结构上进行扩展开发。

所以我们目前的诉求有哪些:

  1. 因为数据第一个存储的是地方是内存,内存数据结构组织保证尽可能高的读写性能(高效插入)
  2. 数据要从内存 -> 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()
}
  1. 整个中最重要就是给 key 划分一个合理的层数(如何划分下一个函数讲解)
  2. 分配的层数 > 目前最高的层数(就是 head 的层数),则从 findGeOrEq 返回的前驱数组中 [randHeight->maxHeight] 这个新生成的层需要指向 head;并更新最高层数
  3. 分配的层数 < 目前最高的层数,新建一个节点;从 randHeight 一层一层赋值给 findGeOrEq 返回的前驱数组的每一层

randomHeight

获得一个概率分布的高度

func (sk *SkipList) randomHeight() int {
    h := 1
    for h < kMaxHeight && (rand.Intn(kBranching) == 0) {
        h ++
    }
    return h
}