PostgreSQL B+树索引---基本结构

PostgreSQL B+树索引—基本结构

基本概念

在这里插入图片描述

图1

PostgreSQL中使用的B+树索引结构如上图所示,其中有几个关键点需要说明。

  1. B link Tree

    PostgreSQL B+树索引的思想来源于lehman和yao的论文《Efficient Locking for Concurrent Operations on B-Trees》,该论文介绍了B+树的一个变种,B link Tree,也叫B*树。B*树和B+树的主要区别在于B*树的内部节点(非叶子节点)也用双向链表连接。B*树的主要优势在于具备更高的并发性。

  2. high key
    在叶子节点和内部节点中,有一个特殊的元素,称为high key(在上图中用绿色来标记),high key表示一个节点的upper bound,即节点所能存放key的最大值,一个节点中的high key必须等于其右兄弟的最小值,这一点需要尤为注意(PostgreSQL在分裂时就是将右节点的最小key作为左节点的high key)。

    每层的最右节点(rightmost)不存在high key,因为最右节点的最大key应该是无穷大(在上图中用蓝色来标记)。high key位于节点中的第一个元素,只是用于表示节点的upper bound。所以对于非最右节点来说,节点内的第一个data key应该是high key之后的那个key(即第二个key),对于最右节点来说,第一个data key才是节点内的第一个key。PostgreSQL中对high key和firstkey有如下定义:

    #define P_HIKEY				((OffsetNumber) 1)
    #define P_FIRSTKEY			((OffsetNumber) 2)
    #define P_FIRSTDATAKEY(opaque)	(P_RIGHTMOST(opaque) ? P_HIKEY : P_FIRSTKEY)
    

    注意

    这里有一个值得注意的问题:为什么high key是节点内的第一个key作为而非最后一个key?这是为了防止插入时频繁移动high key。high key是节点中最大的一个key,由于节点内部需要保持key的有序性,所以如果high key放在节点最后,那么不论插入什么值,都需要后移high key。所以为了防止这样移动,将high key放在节点开始位置。由于high key只在节点分裂时产生,所以不必担心high key发生变化导致key的大小顺序发生变化。

  3. 叶子节点与内部节点

    PostgreSQL的B+树索引中,叶子节点中的元素存放的是key+tid,tid为tuple id及元组的位置信息,和Oracle的rowid是同一个概念。内部节点中的元素存放的是下级节点的最小值(min key)+下级节点的节点编号。注意这个下级节点其实是子树的根节点。与最右节点的high key类似,最左节点的min key也是无意义的,因为这个key应该表示无穷小

B*树并发性概述

B*树最大的优势在于并发性。对于B+树的并发控制,传统的方式叫蟹行协议(carbbing step),蟹行协议遍历B+树的流程如下:

  1. 给当前节点加共享锁(初始时当前节点为根节点)。
  2. 需要访问下级节点时,首先给下级节点加共享锁,然后释放当前节点的锁
  3. 重复step1、step2,直到叶子节点。

如果是插入操作,那么需要对待插入的节点加互斥锁。如果插入导致这个节点分裂,那么需要对父节点做插入,所以需要对父节点加互斥锁。所以,如果一个查询进程正试图锁定下级节点,同时另一个插入进程试图锁定父节点,就会发生死锁,如下图所示:

在这里插入图片描述

图2

B*树的遍历流程如下:

  1. 给当前节点加共享锁(初始时当前节点为根节点)。
  2. 需要访问下级节点时,首先释放当前节点的锁,然后给下级节点加共享锁
  3. 重复step1、step2,直到叶子节点。

这个流程和蟹行协议唯一的区别就在于step2,由于先释放当前节点的锁,在给下级节点加共享锁,所以在遍历时,最多只有1个节点持有锁,所以当然就不会有死锁发生。但这个流程又有另外的问题:

在这里插入图片描述

图3

假设现在有一个查询操作需要获取44,在查询时B+树的结构如图3所示,这是一个分裂的中间状态,即block1已经发生了分裂,但还没有来得及将节点的min key和blockno写入父节点。查询进程先对block3加共享锁,然后从block3得知,44应该在block1中,于是释放block3上的共享锁,尝试锁定block1。此时插入进程对block3加互斥锁,然后完成分裂。查询进程获取block1的共享锁后,由于block1已经发生了分裂44被迁移到了block4中。在block1中,不难发现,block1的high key为38小于44,所以我们会沿着block1与block4之间的链访问block4。注意,对于block4的访问,也是先释放block1的共享锁,再对block4加共享锁。当访问block4时,block4也可能发生分裂,导致44再次迁移到右节点,所以我们需要向右遍历叶子节点,直到找到一个high key > 44的节点。在PostgreSQL中,有一个叫_bt_moveright的函数就是为了实现上述功能的。

B*树redo

B+的redo最难的一个问题是要保证在分裂过程中,即便发生崩溃,也不能导致索引的损坏(即出现正确性问题)。我们先来回顾下索引分裂的几个步骤:

  1. 获取分裂点。
  2. 创建一个新的节点。
  3. 将原始节点中一半的数据迁移到新节点中。
  4. 修改原始节点的high key。
  5. 将新节点加入链表。
  6. 将新节点的min key和节点编号写入父节点。

从“B*树并发性概述”中,我们不难得出一个结论,在B*树中即便对图3这样的树进行查询,也不会影响查询的正确性,只是会影响查询性能(因为不能直接通过block3定位到block4)。这意味着什么?这就以为上述步骤的第6步,不影响正确性。所以在上面6步中,前5步需要作为一个原子操作,与前5步的相关的操作都需要记录到redo log中,而第6步不需要。这句话非常重要,只保证前5步的原子性要比保证所有6步的原子性简单得多,且并发性也要高的多。这个我们在后面的文档中再来详细讨论。

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值