了解数据库中常用存储引擎数据结构(2)

目录

深入了解B树及其变种

BTree

B+Tree

B*Tree

BTree并发机制


深入了解B树及其变种

先把我们要解释的B树变种都列出来,B树的变种主要有B+树、B*树、B-Link树、COW B树、惰性B树、Bw树等。

下面具体来分析这些变种的优势和发展趋势。

BTree

下图是原始 BTree 的结构,

可以注意到:在 BTree 中,每个数据只存储一份。

如果要进行全表扫描,则需要中序遍历整个 BTree,因此会产生大量的随机 IO,性能不佳。所以基本上没有直接使用 BTree 实现存储结构的。

BTree 早期有两个变种:

  • B+Tree
  • B*Tree

B+Tree

相比于 BTree,B+Tree 的数据按照键值大小顺序存放在同一层的叶子节点中(和上面 BTree 中在非叶子节点也存放数据不同),各个叶子节点按照指针连接,组成一个双向链表。

因此,对于 B+Tree 而言,其非叶子节点仅仅作为查找路径的判断依据,一个 key 值可能在 B+Tree 中存在两份(仅 Key 值)。

B+Tree 的结构解决了 BTree 中中序遍历扫描的痛点,在一定程度上也能降低层数。

B*Tree

B*Tree 是 BTree 的另一个变种,其最关键的一点是将节点的最低空间利用率从 BTree 和 B+Tree 的 1/2 提高到了 2/3,并由此改变了节点数据满时的处理逻辑。

我们知道,BTree 和 B+Tree 的空间利用率为 1/2,即:们的叶子节点满而分裂时,默认状态下会分裂为两个各占一半数据的节点;

而 B*Tree在一个节点满了却又有新的数据要插入进来时,它会将其部分数据搬迁到下一个兄弟节点,直到两个节点空间都满了,就在中间生成一个节点,三个节点平分原来两个节点中的数据。

B*Tree 的思想主要是:将当前节点和兄弟节点相关联。

B*Tree 的这种设计虽然可以提升空间利用率,对减少层数、提升读性能有一定的帮助,但这种模式增加了写入操作的复制度;

而且向右兄弟节点搬迁数据的过程也要视作为一种 SMO 操作,对写入和并发能力有极大的损耗!因此,B*Tree 并没有被大量使用。

BTree并发机制

这里以 MySQL InnoDB 存储引擎为例,讲述基于 B+Tree 的存储引擎是如何通过 Latch 进行并发控制的。

在 MySQL 5.6 之前的版本,只有写和读两种 Latch,被称为 X Latch 和 S Latch。

对于读的过程,

  • 首先要在整个索引上添加 Index S Latch。
  • 再从上至下找到要读的叶子节点的 Page,然后上叶子节点的 Page S Latch。
  • 这时就可以释放 Index S Latch了。
  • 然后进行查询并返回结果,最后释放叶子节点中的 Page S Latch,完成整个读操作。

对于写的过程,

  • 首先进行乐观的写入,即:假设写入操作不会引起索引结构的变更(不触发 SMO 操作)。
    • 要先上整个索引的 Index S Latch,再从上至下找到要修改的叶子节点的 Page,此过程和上面的读取步骤相同!
    • 接下来判断叶子节点是否安全,即:写入操作是否会触发分裂或者合并;
    • 如果叶子节点 Page 安全,就上 Page X Latch,并释放 Index S Latch,然后再修改数据即可。
    • 完成乐观写入过程。
  • 如果叶子节点 Page 不安全,那么就要重新进行悲观写入。

    • 释放一开始上的 Index S Latch,重新上 Index X Latch,阻塞对整棵 B+Tree 的所有操作。

    • 然后重新搜索,并找到要发生结构变化的节点,上 Page X Latch,再修改树结构,此时可以释放 Index X Latch。

    • 完成悲观写入过程。

从前面的分析可以看出来,上面加锁的缺点非常明显:在触发 SMO 操作过程时,由于会持有 Index X Latch 锁住整棵树;此时所有操作都无法进行,包括读操作。

因此,在 MySQL 5.7、8.0 版本中,针对 SMO 操作会阻塞读的问题,引入了 SX Latch。

SX Latch 介于 S Latch 和 X Latch 之间,和 X Latch、SX Latch 冲突,但是和 S Latch 不冲突(可以理解为类似RWLock)。

下面来看一下引入 SX Latch 之后的并发控制方案。

对于读操作而言,

  • 相比于 MySQL 5.6 之前,这时读步骤主要加上了对查找路径上节点的锁。这是因为在引入了 SX Latch 之后,发生 SMO 操作的时候,读操作也可以进行。
  • 此时为了保证读取的时候查找路径上的非叶子节点不会被 SMO 操作改变,因此就需要对路径上的节点也加上 S Latch。

写的过程和上面类似,

  • 一样是先进行乐观写,
    • 由于此时假设只会修改叶子节点,因此,乐观写的查找过程和读操作一致:添加整个索引的 Index S Latch 和读取路径上节点的 Page S Latch 即可!
    • 接下来判断叶子节点是否安全,如果叶子节点 Page 安全,则上 Page X Latch,同时释放索引和路径上的 S Latch,然后再修改即可。

  • 但是如果叶子节点的 Page 不安全,这需要重新进行悲观写入。

    • 释放一开始上的所有 S Latch,这时我们上 Index SX Latch,然后重新搜索,找到要发生结构变化的节点,上 Page X Latch,再修改树结构,此时就可以释放 Index SX Latch 和路径上的 Page X Latch。

    • 随后即可完成对叶子节点的修改,返回结果,并释放叶子节点的 Page X Latch。

    • 完成悲观写入过程。

我们可以知道,B+Tree 的问题在于:其自上而下的搜索过程决定了加锁过程也必须是自上而下的!哪怕只对一个小小的叶子节点做读写操作,也都必须首先对根节点上 Latch。并且一旦触发 SMO 操作,就需要对整个树进行加锁!

B-Link Tree 相比于 B+Tree 主要做了三点优化:

  • 非叶子节点也都有指向右兄弟节点的指针。
  • 分裂模式上,采用和 BTree 类似的做法:将当前层数据向兄弟节点中迁移。
  • 每个节点都增加一个 High Key 值,记录当前节点的最大 Key。

B-Link Tree 结构如下图,其中加下划线的 Key 为 High Key。

在前面提到,B+Tree 中一个严重的问题就是,在读写过程中都需要对整棵树、或一层层向下的加 Latch,从而造成 SMO 操作会阻塞其他操作。

而 B-Link Tree 通过对分裂和查找过程的调整,避免了这一点!

下图就是 B-Link Tree 树节点分裂的过程:先将老节点的数据拷贝到新节点,然后建立同一层节点的连接关系,最后再建立从父节点指向新节点的连接关系(此顺序非常重要!)。

那么上面的分裂过程是如何避免整棵树上的锁的呢?可以通过指向右兄弟节点的指针和 High Key 实现!

如下图, 当节点 y 分裂为 y 和 y+ 两个节点后,在 B+Tree 中就必须要提前锁住他们的父节点 x。

而 B-Link Tree 可以先不锁 x,这时查找 15,顺着 x 找到节点 y,在节点 y 中未能找到 15,但判断 15 大于其中记录的 high key,于是顺着指针就可以找到其右兄弟节点 y+,仍能找到正确的结果。

因此,B-Link Tree 中的 SMO 操作可以自底向上加锁,而不必像 B+Tree 那样自顶向下加锁!从而避免了 B+Tree 中并发控制瓶颈。

上面就是 B-Link Tree 的基本思路。

但是在实现 B-Link Tree 时需要考虑的还有很多:

  • 删除操作需要单独设计;
  • 原论文中对于一些原子化的假定也不符合现状;

但是 B-Link Tree 仍是一种非常优秀的存储结构,很大程度上突破了 B+Tree 的性能瓶颈。

  • 20
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值