openGauss B-tree 索引在实现时,没有完全遵循 L & Y 的理论,本文主要关注openGauss 中 B-tree 索引的实现。
实现与理论的差异
-
读锁
介绍B-tree索引并发读写原理时讲到,L & Y 的理论中读操作完全不加锁,而是假设 B-tree 节点在内存中的拷贝是非共享的。但实际在 PG 和 openGauss 的实现中,内存中的 buffer 是共享的。所以在实现中,实际读操作需要对节点加读锁,来保证读期间没有修改操作。加锁动作降低了并发性,但保证了数据正确性。
-
写锁
L & Y 的理论总是将加锁和 moveright 耦合,最多同时会对三个节点加锁(分裂的子节点、父节点、父节点的右兄弟节点)。实现中,同一层的节点不需要都加锁。L & Y 的理论中通过记录从根节点开始查找路径上的节点及 key 的信息来记录父子节点关系,理论中假设所有的 key 都是不同的。实现中,父节点通过记录子节点的 Block Number 来实现父节点指向子节点的指针。理论中如果发生分裂需要更新父节点时,父节点所在层的加锁的顺序是:先对父节点加锁,如果父节点发生了分裂,则对父节点的右兄弟节点加锁,然后释放父节点的锁。而实现中,在对父节点的右兄弟节点加锁前,可以释放父节点的锁,因为 link ptr 实现中是记录的右兄弟节点的 block number, 即使右兄弟节点又发生分裂,依然可以通过 block number 找到分裂后的左侧节点,然后继续向右查找。
-
link ptr
L & Y 的理论中每一层节点都有指向右侧节点的 link ptr,当执行 scan 的操作时,通过 link ptr 只能支持自左向右的 scan。为支持逆向 scan,OpenGauss中每个节点都有指向左右两侧的两个指针,将指向同层右侧节点的称为 right link ptr,指向左侧节点的称为 left link ptr。这个修改,导致 L & Y 的理论中节点分裂需要额外的动作。在锁定分裂节点的同时,还需要锁定该节点分裂前的右兄弟,以修改其 left link ptr,如图-1所示。实际逆向 scan 比正向 scan 要复杂一些,因为如果逆向scan的过程中节点发生分裂,可能还需要通过 right link ptr 向右查找(实际比更复杂,我们会在后面讨论)。
在 scan 过程中,只有在需要读一个节点时才会对其加锁,为减少加锁的时长,读操作会一次把所有满足条件的项全部读出来,在处理这些项时就可以释放锁了。有时,为了防止并发的 delete 操作,需要 pin 住某个叶子节点(后面会讲到)。scan过程中,需要记住当前在扫描节点的 right link ptr,通过 right link ptr 向右scan不会遗漏数据;但如果是向左scan,需要获取最新的 left link ptr。
大多数情况下,在对下一个节点进行 pin 和加锁前,会释放前一个节点的pin 和锁;但有些情况下会先对下一个节点进行 pin 和加锁,再释放前一个节点的 pin 和锁。之前讲并发原理时证明过,流程中可以避免死锁的出现,但现在由于实现没有完全按照 L & Y 的理论,加了 left link ptr,似乎有死锁的可能。
L & Y 的理论中假设 key 的大小是固定的,实际需要处理变长的 key。因此对每一个 page 而言,key 的个数没有一个固定的最大值,实现中假设一个 page 内至少要有3个 item,因此 tuple 的大小不能超过一个 page 最大可用空间的1/3。节点发生分裂时,按照占用空间来平均分配分裂后的左右节点,而不是按照 item 的个数均分。同时分裂需要将已经在执行中的事务占用的空间考虑在内,否则事务提交时可能出现 page 内没有足够空间的问题。
L & Y 的理论中假设key没有重复的,实际是对于非 unique index 可能存在重复 key。
插入
在执行数据插入的流程中,需要更新 heap tuple 和 index tuple,更新index tuple 执行 ExecInsertIndexTuples 方法,处理的主要流程如下:
{
遍历表上的每一个索引 {
如果索引的 indisready = false,表示索引不可写,则跳过此索引;
如果是分区表的 local 索引,且 pg_partiton 中 indisusable = false,跳过此索引;
调用索引插入函数 index_insert,完成 index tuple 插入
检查索引插入是否违反约束 unique / exclusion constraint
返回结果
}