一、为什么MemTable是存储引擎的核心枢纽

1.1 MemTable的真实作用

我们平时用的数据库,比如LevelDB,要把数据存在硬盘上,可硬盘的“脾气”很慢,每次读或写都要等好久,要是把用户的每一条写请求都直接存到硬盘,那系统肯定卡得不行。所以工程师想了个办法:搞一块内存区域,叫MemTable,用来临时存刚收到的数据,等攒够一定数量,再一起转成有序的硬盘文件(也就是SSTable)。

MemTable的要求特别高:第一,写要快,每秒得扛得住几万甚至几十万条请求;第二,读也要快,用户查数据的时候,不能等太久;第三,数据还要有序,因为转成硬盘文件的时候,有序的话合并起来更省时间,不然乱七八遭的,合并的时候要扫好几遍,浪费磁盘IO。

二、跳表 vs 红黑树:大白话讲清两者是什么

很多人选MemTable的底层结构时,会纠结跳表和红黑树,其实两者都是“有序的、可以快速增删查的数据结构”,但实现起来差得特别多。

2.1 红黑树:带颜色的平衡二叉树

你可以把红黑树想象成一棵“左右平衡的二叉树”,每个节点还带个颜色(红色或黑色),它的规则很简单:根节点是黑色,相邻节点不能都是红色,所有从根到叶子的路径上,黑色节点的数量要一样。这么设计的目的是,不管怎么插入删除,树都不会长得歪歪扭扭,找数据的时候最多走几十步,比普通链表快多了。

举个简化的例子,用Python写的红黑树核心结构(不含完整平衡逻辑,只看样子):

# 技术栈:Python 3.8
# 红黑树简化实现,仅展示核心结构,不包含完整平衡逻辑
class RBNode:
    RED = True
    BLACK = False
    def __init__(self, key):
        self.key = key  # 数据的键,比如用户的ID
        self.color = RBNode.RED  # 新节点默认红色,这是红黑树的基础规则
        self.left = None  # 左子节点
        self.right = None # 右子节点
        self.parent = None # 父节点

class SimpleRBTree:
    def __init__(self):
        self.root = None  # 树根节点

    # 简化版插入,没有做平衡,只是把节点放到正确位置
    def insert(self, key):
        new_node = RBNode(key)
        if not self.root:
            self.root = new_node
            self.root.color = RBNode.BLACK  # 根必须是黑色
            return
        current = self.root
        parent = None
        # 找插入位置
        while current:
            parent = current
            if key < current.key:
                current = current.left
            elif key > current.key:
                current = current.right
            else:
                # 重复键,忽略,简化处理
                return
        # 把新节点挂到父节点上
        new_node.parent = parent
        if key < parent.key:
            parent.left = new_node
        else:
            parent.right = new_node

    # 查找数据
    def search(self, key):
        current = self.root
        while current:
            if key < current.key:
                current = current.left
            elif key > current.key:
                current = current.right
            else:
                return True  # 找到
        return False  # 没找到

# 测试代码
rbt = SimpleRBTree()
for k in [1,3,5,2,4]:
    rbt.insert(k)
print("查找key=3是否存在:", rbt.search(3))  # 输出True

2.2 跳表:带“电梯”的有序链表

跳表就聪明多了,它是个“加了多层索引的有序链表”。普通的有序链表,找数据要从头走到尾,比如找第100个元素,要走100步。但跳表搞了几层“电梯”:最高层的电梯跨100个节点,中间层跨10个,最底层就是普通链表。找数据的时候,先坐最高层电梯,快到目标了再换低层,这样最多只需要走几十步,和红黑树的速度差不多,但实现起来简单太多。

用Python写的简化跳表,核心增删查逻辑如下:

# 技术栈:Python 3.8
# 跳表简化实现,展示核心增删查逻辑,和LevelDB的核心结构一致
import random

class SkipNode:
    def __init__(self, key, level):
        self.key = key  # 数据的键
        self.forward = [None]*(level+1)  # 每层的指针,比如level=2有3个指针(0、1、2层)

class SimpleSkipList:
    def __init__(self, max_level=12, p=0.25):
        self.max_level = max_level  # 最高层级,LevelDB用的是12层
        self.p = p  # 节点晋升高层的概率,LevelDB用0.25
        self.level = 0  # 当前跳表的最高层级,初始是0
        self.head = SkipNode(-1, self.max_level)  # 头节点,所有指针都指向空

    # 随机生成新节点的层级,符合概率规则
    def random_level(self):
        level = 0
        while random.random() < self.p and level < self.max_level:
            level +=1
        return level

    # 插入操作,从高层往下找,每层更新前驱节点
    def insert(self, key):
        update = [None]*(self.max_level+1)  # 每层需要更新的前驱节点
        current = self.head
        # 从最高层走到0层,找到插入位置
        for i in range(self.level, -1, -1):
            while current.forward[i] and current.forward[i].key < key:
                current = current.forward[i]
            update[i] = current  # 记录每层的前驱
        current = current.forward[0]
        # 如果key已存在,简化处理不做更新
        if not current or current.key != key:
            new_level = self.random_level()
            # 新层级比当前高,要更新高层的前驱为头节点
            if new_level > self.level:
                for i in range(self.level+1, new_level+1):
                    update[i] = self.head
                self.level = new_level
            # 创建新节点,每层插入到对应位置
            new_node = SkipNode(key, new_level)
            for i in range(new_level+1):
                new_node.forward[i] = update[i].forward[i]
                update[i].forward[i] = new_node

    # 查找操作,和插入的搜索逻辑一致
    def search(self, key):
        current = self.head
        for i in range(self.level, -1, -1):
            while current.forward[i] and current.forward[i].key < key:
                current = current.forward[i]
        current = current.forward[0]
        return current and current.key == key

# 测试代码
sl = SimpleSkipList()
for k in [1,3,5,2,4]:
    sl.insert(k)
print("查找key=3是否存在:", sl.search(3))  # 输出True

三、两者在MemTable场景下的真实差异

现在回到MemTable的场景,我们从三个维度对比:应用场景、技术优缺点、注意事项。

3.1 应用场景的适配

红黑树适合什么?适合需要严格平衡、对最坏情况要求高的场景,比如内存数据库的复杂数据结构,但它对操作的“稳定性”要求不高,比如偶尔有几次慢操作也没关系。但MemTable不一样,它需要每秒处理大量请求,每一次写都要快,不能突然卡一下。跳表的操作是固定的,不管什么数据,插入删除最多改几个指针,不会出现突然要旋转几十次的情况,更稳定。

3.2 核心操作的性能差异

我们来算个简单的账:假设要插入10万条数据,红黑树的插入,每一次都要检查颜色、旋转,平均要做几十次操作;而跳表的插入,平均只需要做十几次操作,常数更小,速度更快。而且,跳表的遍历特别简单,只要从0层的头节点往后走就行,而红黑树的遍历要按中序,虽然也是O(logn),但代码逻辑复杂,遍历的时候也容易出问题。

3.3 注意事项

红黑树的坑特别多:比如根节点必须是黑色,插入后如果出现红色相邻节点,要马上变色或旋转,要是漏了一步,树就会不平衡,后续的查找速度会变慢,调试的时候很难找到问题,比如某个插入后树歪了,你得一行行查代码,花几个小时。而跳表的坑只有两个:一是层级的设置,LevelDB用的max_level=12,p=0.25,这个参数是算出来的,足够应付100万条数据;二是随机生成层级的概率,不能随便改,不然空间或性能会受影响,这些参数只要调对了,就很少出问题。

四、LevelDB最终选跳表的真实原因

很多人以为LevelDB选跳表是因为性能更好,其实不是,是因为它“适合工程实现”,是务实的选型:

4.1 实现复杂度低,代码量少

LevelDB是用C++写的,要快还要开发成本低。红黑树的完整实现,包括插入、删除、平衡、旋转,要写几百行代码,还要处理各种边界情况,比如删除节点的时候,颜色调整、指针移动,很容易写错。而跳表的实现,只有几百行,核心逻辑就是插入、删除、层级随机,代码量少,出bug的概率也低,后期维护起来特别轻松。

4.2 操作性能稳定,无突发高峰

跳表的每一次操作,不管是插入、删除还是查找,都是固定的步骤,最多改几个指针,不会出现“突然要旋转十几圈”的情况,所以延迟特别稳定,对于MemTable来说,延迟稳定比绝对的性能高更重要,因为MemTable是写请求的第一道关卡,要是延迟忽高忽低,用户体验会特别差。

4.3 适配MemTable的有序性需求

MemTable转成SSTable的时候,需要有序的结构,跳表天然有序,遍历的时候直接按顺序来,不用额外处理排序,省了很多事。红黑树也有序,但遍历的逻辑复杂,而且转成硬盘文件的时候,要是结构乱了,还要重新排序,浪费时间。

五、总结

MemTable的选型,从来不是选“最先进的数据结构”,而是选“最适合场景的结构”。跳表和红黑树都能满足MemTable的性能要求,但跳表实现简单、稳定,适合LevelDB这种追求工程效率的存储引擎。这就是为什么LevelDB最终选了跳表,而不是红黑树的真实原因。