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 的操作,即:
在插入元素时,节点未满
在删除元素时,节点超过半满
Search
从 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 上的相关技术,但这些技术同样适用于其他数据结构。
参考
Last updated