Index Concurrency Control

在前两节中讨论的数据结构中,我们都只考虑单线程访问的情况。实践中,绝大多数情况下,我们需要在并发访问情况下保证我们的数据结构还能够正常工作。除了 VoltDB,它用一个线程处理所有请求,彻底排除了并发的需要。

Concurrency Control

通常我们会从两个层面上来理解并发控制的正确性:

  • Logical Correctness:(17 节)我是否能看到我应该要看到的数据?

  • Physical Correctness:(本节)数据的内部表示是否安好?

本节大纲:

  • Latch Modes

  • Index Crabbing/Coupling

  • Leaf Scans

  • Delayed Parent Updates

Latch Modes

Read Mode

  • 多个线程可以同时读取相同的数据

  • 针对相同的数据,当别的线程已经获得处于 read mode 的 latch,新的线程也可以继续获取 read mode 的 latch

Write Mode

  • 同一时间只有单个线程可以访问

  • 针对相同的数据,如果获取前已经有别的线程获得任何 mode 的 latch,新的线程就无法获取 write mode 的 latch

B+ Tree Concurrency Control

我们希望在最大程度上允许多个线程同时读取、更新同一个 B+ Tree index,主要需要考虑两种情况:

  • 多个线程同时修改同一个 node

  • 一个线程正在遍历 B+ Tree 的同时,另一个线程正在 splits/merges nodes

举例如下:

T1 想要删除 44,T2 想要查询 41。删除 44 时,DBMS 需要 rebalance D 节点,将 H 节点拆分成两个节点。若在拆分前,T2 读取到 D 节点,发现 41 在 H 节点,此时时间片轮转到了 T1,T1 把 D 节点拆分成 H、I 两个节点,同时把 41 转移到 I 节点,之后 CPU 交还给 T2,T2 到 H 节点就找不到 41,如下图所示:

如何解决该问题?由于 B+ Tree 是树状结构,有明确的访问顺序,因此很容易想到沿着访问路径加锁的方法,即 Latch Crabbing/Coupling

Latch Crabbing/Coupling

Latch Crabbing 的基本思想如下:

  • 获取 parent 的 latch

  • 获取 child 的 latch

  • 如果安全,则可以释放 parent 的 latch

这里的“安全”指的是,当发生更新操作时,该节点不会发生 split 或 merge 的操作,即:

  • 在插入元素时,节点未满

  • 在删除元素时,节点超过半满

从 root 往下,不断地:

  • 获取 child 的 read latch

  • 释放 parent 的 read latch

举例如下:Search 38

Insert/Delete

从 root 往下,按照需要获取 write latch,一旦获取了 child 的 write latch,检查它是否安全,如果安全,则释放之前获取的所有 write latch。

例 1 如下:Delete 38

例 2 如下:Insert 45

例 3:Insert 25

Better Latching Algorithm

在实际应用中:

  • 更新操作每次都需要在路径上获取 write latch 容易成为系统并发瓶颈

  • 通常 Node 的大小为一个 page,可以存储很多 keys,因此更新操作的出现频率不算高

我们能否在 Latch Crabbing 的基础上做得更好?

可以采用类似乐观锁的思想,假设 leaf node 是安全(更新操作仅会引起 leaf node 的变化)的,在查询路径上一路获取、释放 read latch,到达 leaf node 时,若操作不会引起 split/merge 发生,则只需要在 leaf node 上获取 write latch 然后更新数据,释放 write latch 即可;若操作会引起 split/merge 发生,则重新执行一遍,此时在查询路径上一路获取、释放 write latch,即 Latch Crabbing 原始方案。

举例:Delete 38

Better Latching Algorithm 小结

  • Search:与 Latch Crabbing 相同

  • Insert/Delete:

    • 使用与 Search 相同的方式在查询路径上获取、释放 latch,在 leaf node 上获取 write latch

    • 如果 leaf node 不安全,可能会引起其它节点的变动,则使用 Latch Crabbing 的策略再执行一遍

该方法乐观地假设整个操作只会引起 leaf node 的变化,若假设错误,则使用 Latch Crabbing 的原始方案。

Horizontal Scan

之前的分析中我们仅仅关注了从上到下的访问模式,而没有考虑到左右方向的访问模式,在 range query 中,常常需要横向访问相邻的 nodes。

例1:T1: Find Keys < 4

如果此时刚好有另一个线程作相反方向的访问:T2: Find Keys > 1:

由于 read latch 允许被多次获取,因此并发读的情况不会产生负面影响。

例 2:T1: Delete 4,T2: Find Keys > 1

当遇到横向扫描无法获取下一个节点的 latch 时,该线程将释放 latch 后自杀。这种策略逻辑简单,尽管有理论上的优化空间,但在实践中是常见的避免死锁的方式。

Delayed Parent Updates

从上文中,我们可以观察到:每当 leaf node 溢出时,我们都需要更新至少 3 个节点:

  • 即将被拆分的 leaf node

  • 新的 leaf node

  • parent node

修改的成本较高,因此 B-link Tree 提出了一种优化策略:每当 leaf node 溢出时,只是标记一下而暂时不更新 parent node,等下一次有别的线程获取 parnet node 的 write latch 时,一并修改。

举例如下:Insert 25

Delayed Parent Updates 仅用在 insert 操作中。

Conclusion

让一个数据结构具备并发安全的特点知难行易,尽管本节只是介绍 B+ Tree 上的相关技术,但这些技术同样适用于其他数据结构。

参考

slides