想象一座巨大的图书馆,几万本书按编号规律藏在几十排书柜里。每排书柜上方都贴着一张指引牌:小于 100 号往左区,大于等于 100 号往右区,再往下还有更细的分区。管理员想把一本新书放到正确位置,必须从入口开始,一层层跟着指引牌找下去。可图书馆里不止一个管理员,如果两个人同时整理同一个书柜,编号顺序就会乱套。于是大家约定:动哪个书柜之前,先在上面放一块“临时占位牌”,用完立刻拿走。问题来了——怎么放占位牌才不会把路堵死?B+树给出的方案像“螃蟹走路”:一只手扶住上一块指引牌,另一只手去够下一块,拿到新牌子才松开旧牌子,保证找书路径不断。B-Link Tree 则更聪明:给相邻书柜之间加一条“滑梯”,如果目标书柜正被占着,管理员可以直接从左边书柜沿滑梯滑过去,不用站在原地傻等。这就是 B+树与 B-Link Tree 并发控制的核心故事。

岗位关联

对 AI 从业者来说,理解这套“占位牌 + 滑梯”机制,是搞懂高并发数据库、向量检索系统里“多线程同时读写索引会不会崩”的底层功力。面试聊索引优化、系统设计时,它往往是压轴题。

目录
  1. 可视化演示
  2. B+树并发挑战
  3. 锁耦合协议
  4. 右移锁策略
  5. Latch与Lock
  6. 乐观悲观并发
  7. 性能差异对比
  8. PostgreSQL应用
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示B+树锁耦合crabbing协议的逐层加锁释放、B-Link Tree的link指针分裂优化、以及悲观与乐观并发控制策略对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

锁耦合Crabbing:逐层加锁释放 红=当前游标 获取R-latch向下遍历 绿=R-latch持有 子节点加锁后释放父节点 latch child -> unlatch parent 锁耦合减少持锁时间提升并发

B+树并发挑战

管理员小B所在的老式图书馆,用的是一棵巨大的 B+树:最上层是总目录,中间层是一排排分区指引牌,最下层才是真正放书的叶子书柜。平时一个人找书很轻松,只要按指引逐层往下走。可一旦十几个管理员同时上架、下架,麻烦就来了。一个人刚把“300 号以后”的牌子往右挪了半米,另一个人正盯着旧牌子往左走,两个人就会在同一排书柜前撞车,甚至有人会把书塞进错误的柜子。

在数据库里,这种“撞车”叫并发写冲突。B+树为了保持有序,插入数据时可能让叶子节点满员,于是要分裂,把一半书挪到新书柜,还要改父节点的指引牌。如果多个线程同时做这件事,树的层次结构就会“漂移”:有的线程看到旧指引牌,有的看到新指引牌,读路径就不再稳定。更危险的是,一个线程分裂节点时,另一个线程正在这个节点上找数据,轻则返回错误结果,重则直接让进程崩溃。

于是计算机科学家引入一个指标来衡量找书的步数:搜索深度。树越矮,找书越快;树越高,每层都要停一下,并发出错的概率也越高。

所以并发挑战的本质是:树越深、路径越长,多个线程在同一段路上相遇的概率就越高。老图书馆必须想出一套规矩,让管理员们在共享书柜时既不打架,又不迷路。

搜索深度=⌈log⁡fN⌉\displaystyle \text{搜索深度} = \left\lceil \log_f N \right\rceil
好比图书馆楼层越多,管理员每层都要看指引牌,走的步数就是“以每层分支数为底、对藏书总数取对数”。
  1. N 代表什么整棵 B+树能容纳的记录总数,也就是图书馆藏书量。藏书越多,树越深。
    N=叶子书柜能装下的记录总数\displaystyle N = \text{叶子书柜能装下的记录总数}
  2. f 代表什么每个指引牌最多指向几个下一层牌子,专业叫“扇出”。扇出越大,一层能辐射的书柜越多,树就越矮。
    f=内部节点的最大分支数\displaystyle f = \text{内部节点的最大分支数}
  3. 整体公式搜索深度就是“把 N 本书记住,每次按 f 路分流,需要问几次路”。问路次数越少,并发窗口越小,出乱子的机会也越少。
    搜索深度=⌈log⁡fN⌉\displaystyle \text{搜索深度} = \left\lceil \log_f N \right\rceil

锁耦合协议

小B开始用“螃蟹走路”的规矩:他左手先扶住入口的总目录牌,右手伸向下一层的分区牌;右手刚摸到分区牌,左手才松开总目录牌。接着右手扶住分区牌,左手再去够更下一层的书柜牌。整条路上,他始终至少有一只手扶着某块牌子,既不会让后面的人撞上他,也不会让前面的人悄悄换掉指引牌。

这个规矩在数据库里叫“锁耦合”,也叫 crabbing 协议。它的核心思想是:在往下走的每一层,先给当前节点加锁,再给子节点加锁,确认子节点锁拿稳后,立刻释放当前节点锁。这样每个节点被锁的时间都很短,但任意时刻搜索路径上总有一把“接力锁”保护着方向,不会被并发的节点分裂带偏。

更具体地说,锁耦合有三个硬性约定:第一,只能从上往下加锁,不能跳层;第二,父节点锁必须保留到子节点锁成功获取才能释放;第三,释放顺序永远比获取顺序晚一层。这样就不会出现“两个线程各拿一把锁、互相等对方”的死锁局面,因为锁的方向永远是单向的。

锁耦合的好处是锁的粒度极小,每个节点只被占用一瞬间,多个管理员可以在树的不同分支上并行工作。但它的缺点是:如果目标书柜正好被别人占着,小B只能在原地等,等到对方放完牌子才能接上去。图书馆里人多时,这种“傻等”会排成一条长队。

L父→L子→U父\displaystyle L_{\text{父}} \rightarrow L_{\text{子}} \rightarrow U_{\text{父}}
这串符号就是螃蟹走路的完整动作:先锁父,再锁子,最后解锁父。
  1. L父给当前层的父节点加“临时占位牌”,表示这里正在被使用,别人不能改。
    L父=对父节点获取 latch\displaystyle L_{\text{父}} = \text{对父节点获取 latch}
  2. L子在父节点锁还握在手里时,去获取目标子节点的锁。这一步保证找路方向不会变。
    L子=对子节点获取 latch\displaystyle L_{\text{子}} = \text{对子节点获取 latch}
  3. U父子节点锁成功拿到后,才释放父节点锁。先松手的人会迷路,所以顺序不能反。
    U父=释放父节点 latch\displaystyle U_{\text{父}} = \text{释放父节点 latch}

右移锁策略

工程师小L看不下去了。他给每排书柜的右侧加了一条窄窄的滑梯,滑梯入口贴着当前书柜的编号,出口就是隔壁书柜。当小B想去的书柜被人占着时,小L不用站在原地干等,而是从左边书柜的滑梯“唰”一下滑到目标书柜——虽然目标书柜还锁着,但他至少已经站到它门口,申请一块新占位牌就行。

这个“滑梯”就是 B-Link Tree 的 link 指针。B+树原本只在父子之间连线,叶子节点之间是“断头路”。B-Link Tree 给每个叶子节点增加一个指向右兄弟的指针,当某个节点在并发分裂时,老节点会保留一个 link 指针,指向分裂出来的新右节点。搜索线程如果循着旧路径来到老节点,发现数据可能在新节点,就沿 link 指针直接滑到右边,不需要回头重新走父节点。

右移锁策略进一步规定:所有加锁、分裂、搜索都“向右看”。比如一个节点分裂时,先锁住原节点,再锁住新右节点,把数据搬过去,然后通过 link 指针把原节点和右节点串起来,最后释放锁。正在搜索的线程即使撞上分裂现场,也能顺着 link 找到新家。这就是“右移锁”名字的来历:锁的传递方向永远是向右的,绝不向左回头。

对比锁耦合,右移锁策略并没有破坏“父子接力”的安全底线,而是额外开辟了一条水平通道。它让读线程在分裂瞬间不再被堵死,而是灵活“滑”到正确位置,这正是 B-Link Tree 在高并发下吞吐量更高的关键。

L前⟶linkL后\displaystyle L_{\text{前}} \overset{\text{link}}{\longrightarrow} L_{\text{后}}
当目标节点被占或刚分裂完,搜索线程不必回父节点重走,直接从左边邻居的滑梯滑到右边。
  1. L前当前搜索线程所在的原节点,它已经被锁住,是滑梯的起点。
    L前=持有左邻居节点的 latch\displaystyle L_{\text{前}} = \text{持有左邻居节点的 latch}
  2. link 箭头沿着叶子节点之间的右兄弟指针移动,这一步只花一次指针跳转,不用爬到父节点再下来。
    ⟶link=通过右兄弟指针转移\displaystyle \overset{\text{link}}{\longrightarrow} = \text{通过右兄弟指针转移}
  3. L后到达目标右节点后,对它加锁。由于已经站到门口,等待时间比从根节点重新走一遍短得多。
    L后=获取目标右节点的 latch\displaystyle L_{\text{后}} = \text{获取目标右节点的 latch}

Latch与Lock

图书馆里其实有两种完全不同的“牌子”。一种是管理员手里快速传递的“临时占位牌”,只贴几毫秒,用来防止别人同时动同一个书柜,用完就扔。另一种是读者借书的“登记本”,记录谁借走了什么书、什么时候还,要跟着整个借阅事务一直保留,可能长达几秒甚至几分钟。小B以前把它们统称“锁”,结果总是搞混:明明占位牌很快就能拿走,为什么登记本要一直占着?

在数据库里,前者叫 latch,后者叫 lock。latch 是低层机制,保护内存里的 B+树结构本身,比如节点指针、页分裂、页合并;它不关心事务逻辑,谁拿到谁用,用完马上释放,一般微秒级。lock 是高层机制,保护业务数据,比如某一行记录“被事务A修改”,其他事务必须等事务A提交或回滚才能动;它跟事务的声明周期绑定,可能持续毫秒甚至秒级。

两者的层级也不同:lock 由事务管理器管理,需要能恢复、能死锁检测;latch 由存储引擎直接管理,不考虑事务回滚,只求快。打个比方,latch 是“我正在挪这个书架,别人别碰”,lock 是“这本书已经借给我了,在我还书之前你不能借”。挪书架最多几秒钟,借书却可能是一整个下午。

理解 latch 与 lock 的区别,就能看懂很多数据库日志:索引页竞争看 latch 等待,行级冲突看 lock 等待。把两者混为一谈,是并发优化入门时最常见的翻车点。

Tlatch≪Tlock\displaystyle T_{\text{latch}} \ll T_{\text{lock}}
latch 的持有时长通常比 lock 小几个数量级,就像“顺手贴个标签”和“填一张借书单”的差别。
  1. Tlatchlatch 的持有时间,以微秒计。它保护的是内存页结构,操作完立即释放。
    Tlatch=节点结构操作的持锁时间\displaystyle T_{\text{latch}} = \text{节点结构操作的持锁时间}
  2. Tlocklock 的持有时间,以毫秒甚至秒计。它跟事务边界绑定,要等提交或回滚。
    Tlock=事务级锁的持有时间\displaystyle T_{\text{lock}} = \text{事务级锁的持有时间}
  3. 整体对比两个时间相差悬殊,所以数据库设计时把二者分开管理,避免用昂贵的事务锁去保护廉价的内存页操作。
    Tlatch≪Tlock⇒内存页用 latch,业务行用 lock\displaystyle T_{\text{latch}} \ll T_{\text{lock}} \Rightarrow \text{内存页用 latch,业务行用 lock}

乐观悲观并发

小L又发现,管理员们处理冲突有两种性格。悲观的阿稳,每次进书柜区之前都要先举起占位牌,把整条路都“圈起来”再走,绝不允许任何人插队。乐观的小急,不举牌直接跑进书柜区,拿到书就走,只有在撞到别人的瞬间才停下来处理冲突。

这两种性格对应数据库里的悲观并发控制和乐观并发控制。悲观策略“先拿锁再干活”,适用于冲突率高的场景,比如同一本书被多人抢着借,先到先得最安全。乐观策略“先干活再验证”,适用于冲突率低的场景,比如冷门书区几乎没人,如果每次进去都举牌,反而浪费大量时间在拿牌、还牌上。

在 B+树并发里,锁耦合属于偏悲观的策略:它假定每一步都可能遇到结构修改,所以必须接力加锁。B-Link Tree 在某些场景下可以更乐观:依靠 link 指针,搜索线程可以一路滑到叶子,如果发现叶子版本没变就成功,若变了再重试一次。这很像图书馆读者先看书的目录页,发现页数对就放心读,不预先申请整排书柜的独占权。

\displaystyle \text{重试成本} = \text{从起点重新搜索或重放}

重试成本<等待成本⇒选择乐观\displaystyle \text{重试成本} < \text{等待成本} \Rightarrow \text{选择乐观}
如果“撞到人再道歉”比“提前封路等所有人走完”更便宜,那就该用乐观策略。
  1. 重试成本乐观策略失败后要重新执行一遍操作,代价是重复计算和重复读路径。

性能差异对比

\displaystyle \text{吞吐量} \propto \frac{1}{t_{\text{hold}} + t_{\text{wait}}}

> 平均每个操作占用的时间越短、排队等待越少,单位时间能完成的事就越多。1. t_hold:每个线程真正持有 latch 的时间,包括定位、比较、修改节点的耗时。$$\displaystyle t_{\text{hold}} = \text{平均持锁时间}$$2. t_wait:每个线程因为目标节点被占而等待的时间,等待越久,吞吐量越低。$$\displaystyle t_{\text{wait}} = \text{平均锁等待时间}$$3. 整体公式:吞吐量与“持锁时间 + 等待时间”之和成反比。B-Link 用 link 指针压缩了 t_wait,因此分母变小,吞吐量上升。$$\displaystyle \text{吞吐量} \propto \frac{1}{t_{\text{hold}} + t_{\text{wait}}}$$性能差异的本质不是“要不要锁”,而是“锁的粒度、时长、路径能不能更聪明”。B-Link Tree 牺牲一点实现复杂度,换来的是高并发下更短的等待队列——这是一笔非常划算的工程交易。

PostgreSQL应用

如果说前面是在讲理论图纸,PostgreSQL 就是真正把 B-Link 思想装进图书馆的一家公司。它的 B-tree 索引实现(nbtree)借鉴了 Lehman-Yao 的 B-Link Tree 设计:在叶子页和内部页之间保留右链接指针,并在页分裂时采用“先锁原页、再锁新右页、最后接上 link”的流程。结果就是,即使在索引页分裂的瞬间,并发的搜索也不会读到“半成品”,而是顺着右链接找到新页里的数据。

在 PostgreSQL 中,每个索引页都有 btpo_next 这样的字段,本质上就是书柜间的滑梯。当一个事务插入数据导致索引页满时,系统会裂出一个新右页,旧页的 link 指针指向新页。正在执行索引扫描的线程,走到旧页后如果发现目标 key 不在本页范围内,就会自动跳到右边继续找,而不需要返回父节点重新定位。这让 PG 在高并发写入时依然能保持稳定的读延迟。

从工程取舍看,PostgreSQL 并没有发明完全无锁的索引,而是把锁的“接力棒”从父节点传到了水平右邻居。它仍然用 latch 保证每个页结构不被撕裂,但不再让搜索线程在一个被锁的节点前死等。这正是 B-Link Tree 的精髓:并发控制不是消灭锁,而是让锁不成为路障。

PostgreSQL 的实践告诉我们,经典数据结构不必推倒重来。只需要在关键路径上加一条“滑梯”,把锁的路径从垂直变成水平,就能在保持正确性的同时大幅提升并发度。这也是 B-Link Tree 能成为数据库教科书常客的原因。

B-LinkPG=B+树+link 指针+右移锁\displaystyle B\text{-Link}_{\text{PG}} = B^+\text{树} + \text{link 指针} + \text{右移锁}
PostgreSQL 的 B-tree 不是全新的树,而是在经典 B+树上加了一条向右的滑梯,并规定锁只能向右接力。
  1. B+树基底PostgreSQL 仍然使用多路平衡查找树,保证单线程搜索复杂度为 O(log n)。
    B+树=有序叶子页 + 多层内部指引页\displaystyle B^+\text{树} = \text{有序叶子页 + 多层内部指引页}
  2. link 指针每个页保存指向右兄弟的指针,分裂时用它把新旧页连起来。
    link 指针=右兄弟页的地址\displaystyle \text{link 指针} = \text{右兄弟页的地址}
  3. 右移锁加锁顺序永远是从左到右,保证不会两个线程互相等待形成死锁。
    右移锁=先左后右的单向加锁规则\displaystyle \text{右移锁} = \text{先左后右的单向加锁规则}

AI 应用场景

PostgreSQL 在线索引扩展

阶段:数据库索引维护 / 高并发写入

解决的问题:传统 B+树在索引页分裂时必须锁住父节点,导致写放大和读阻塞。B-Link 的 link 指针让分裂过程对搜索线程几乎透明,读线程不会因为看到“半分裂”状态而等待或报错。

具体案例:电商大促期间,订单表以秒级速度插入新记录。订单号作为主键索引不断写入,索引页频繁分裂。PostgreSQL 的 nbtree 借助右链接指针,使得正在执行订单范围查询的会话,在索引分裂瞬间依然能无缝读到新数据,不必回滚重查。

内存 KV 引擎的并发页面合并

阶段:存储引擎并发控制

解决的问题:内存数据库用 B+树组织热点数据时,多线程读写同一页会造成严重 latching 竞争。借鉴 B-Link 思想,为叶子页增加右邻指针,让读线程在页被合并或分裂时可以直接跳转,减少不必要的锁等待。

具体案例:一个 AI 特征存储系统要同时服务几百路在线推理请求,每路请求都会更新用户特征。特征索引页频繁分裂,采用右移锁后,读线程平均等待时间下降 60%,推理 p99 延迟稳定在 20ms 以内。

Python 演示

python66 行
from bisect import bisect_right  class Node:    def __init__(self, keys, children=None):        self.keys = keys        self.children = children        self.next_leaf = None        self.latched = False     @property    def is_leaf(self):        return self.children is None  def latch(node, name):    node.latched = True    print(name, "locks", node.keys)  def unlatch(node, name):    node.latched = False    print(name, "unlocks", node.keys)  def search_with_coupling(root, key, name):    latch(root, name)    node = root    while not node.is_leaf:        i = bisect_right(node.keys, key)        child = node.children[i]        latch(child, name)        unlatch(node, name)        node = child    return node  def search_with_right_link(leftmost, key, name):    cur = leftmost    while cur is not None:        latch(cur, name)        if cur.keys[0] <= key <= cur.keys[-1]:            return cur        unlatch(cur, name)        cur = cur.next_leaf    return None  def main():    leaf1 = Node([1, 3])    leaf2 = Node([5, 7])    leaf3 = Node([9, 11])    leaf1.next_leaf = leaf2    leaf2.next_leaf = leaf3     root = Node([7, 9], [leaf1, leaf2, leaf3])     leaf = search_with_coupling(root, 8, "crab")    print("coupling found:", leaf.keys)     leaf = search_with_right_link(leaf1, 10, "link")    print("right-link found:", leaf.keys)  if __name__ == "__main__":    main()
目的:用极简数据结构演示两种并发索引搜索策略——锁耦合需要从根节点逐层接力,而 B-Link 可以直接沿叶子层 link 指针向右滑。
  1. property is_leaf用 children is None 判断当前节点是内部指引牌还是叶子书柜,避免引入复杂类型字段。
  2. latch(child, name); unlatch(node, name)这是锁耦合的核心:先锁子节点,再释放父节点,顺序一旦颠倒就会迷路。
  3. bisect_right(node.keys, key)在指引牌列表中找“第一个大于目标 key”的位置,决定往哪个子树走。
  4. cur = cur.next_leaf这就是 B-Link 的滑梯:不满足范围时直接跳到右兄弟,不用回父节点重新爬树。
  5. if cur.keys[0] <= key <= cur.keys[-1]判断当前叶子书柜的编号区间是否覆盖目标 key,覆盖则说明找到了家。

常见误区

误区我一开始以为 latch 就是数据库里的锁,事务没提交之前都要一直持有,所以索引操作都很慢。

正解latch 不是业务锁。它更像管理员手里临时贴的“占位牌”,只保护内存里某个节点不被并发拆改,拿到钥匙打开门就立刻放回去,微秒级完成。业务锁 lock 才跟事务绑定,要等提交或回滚才释放。两者面向的层级完全不同。

为何常见:因为中文里 latch 和 lock 都被翻译成“锁”,再加上面试题总把二者放在一起,初学者很容易把它们当成一回事。

误区我听说 B-Link Tree 用 link 指针解决了锁等待,以为它本质上是无锁并发索引,任何操作都不用加锁。

正解B-Link Tree 并没有消灭锁,它只是把锁的竞争路径从“垂直重走父节点”改成“水平滑向右邻居”。节点分裂时仍然要锁住原节点和新右节点,搜索时也仍要拿 latch,只不过在目标节点被占或刚分裂时,可以沿 link 指针直接到达正确位置,而不是阻塞在父节点门口。

为何常见:因为“减少等待”的宣传让人产生“无锁”的直觉,忽略了 link 指针只是优化等待路径,不是替代锁。

误区我认为锁耦合就是从上到下把所有路径上的节点都锁住,一直锁到叶子节点,然后一口气释放,这样最安全。

正解锁耦合的精髓恰恰是“用了就放”。拿到子节点锁后,父节点锁立刻释放,整条路径上任意时刻只保留少量锁。这样才能让不同线程在不同分支上并行工作。如果一直持有所有父锁,本质上变成了全局锁,并发度会断崖式下跌。

为何常见:因为“锁住整条路径”听起来更安全,初学者容易忽略 B+树设计者真正追求的是在安全与并发之间找平衡。

误区乐观并发听起来很高级,以为就是不让线程互相等,谁都不拿锁,直接读写,肯定最快。

正解乐观并发控制仍然需要版本号或时间戳来做冲突检测,失败后还要重试。它的优势只是“假设冲突少、先干活再校验”,而不是真的无锁。冲突率一旦变高,重试成本会迅速超过悲观锁的等待成本,反而更慢。选择乐观还是悲观,本质上是在算一笔概率账。

为何常见:因为“乐观”这个词自带“宽松自由”的暗示,让人误以为它把锁完全去掉了。

面试题

Q1. B+树中 latch(闩锁)的主要作用是?

单选题 · easy

  • A.记录事务是否提交
  • B.保护内存中的索引结构不被并发修改← 正确答案
  • C.管理磁盘文件的读写权限
  • D.生成唯一主键

答案:B。latch 是低层保护机制,像临时占位牌一样保护内存页结构。A 是事务日志的职责,C 是文件系统职责,D 是自增主键职责。记住“latch 护树形,lock 护业务”就不会错。

Q2. 锁耦合(crabbing)的释放原则是?

单选题 · easy

  • A.一次性持有所有锁直到事务结束
  • B.拿到子节点锁后即可释放父节点锁← 正确答案
  • C.只释放叶子锁
  • D.永远不释放锁

答案:B。锁耦合像螃蟹走路:右手抓住子节点,左手才松开父节点。因此拿到子节点锁后马上释放父节点锁,保证路径不断又不长期占用。A 是全局锁,C 和 D 都会造成死锁或无法并发。

Q3. B-Link Tree 的 link 指针通常指向?

单选题 · mid

  • A.父节点
  • B.左兄弟
  • C.右兄弟← 正确答案
  • D.根节点

答案:C。B-Link 的关键是给每个节点加一条“向右的滑梯”,让搜索线程在分裂现场能水平移动。link 指针指向右兄弟,所以加锁顺序也是从左到右,避免死锁。

Q4. 在 B+树搜索路径上,读线程进入叶子前必须做什么?

单选题 · mid

  • A.释放所有父锁
  • B.获取该叶子的 latch,同时保持父节点 latch 直到获取成功← 正确答案
  • C.直接修改树结构
  • D.等所有线程退出

答案:B。锁耦合要求先拿父节点锁,再拿子节点锁,拿到子节点后才放开父节点。如果先释放父锁再拿叶子锁,叶子可能已被分裂或移动,路径就会断。C 和 D 违反了 B+树并发的基本规则。

Q5. 关于 latch 与 lock 的区别,以下说法正确的是?

多选题 · mid

  • A.latch 保护内存中的索引结构,lock 保护业务数据← 正确答案
  • B.latch 可以跨事务长时间持有
  • C.lock 通常与事务生命周期相关← 正确答案
  • D.latch 和 lock 完全等价

答案:AC。latch 是临时占位牌,微秒级释放,不跨事务;lock 是借书登记本,跟事务提交回滚绑定。因此 A、C 正确,B 错误,D 把二者混为一谈。答错的人多半没看清“latch 快、lock 慢”的本质。

Q6. B-Link Tree 的右移锁策略主要解决什么问题?

单选题 · hard

  • A.减少磁盘 IO
  • B.消除写锁
  • C.让搜索不用回父节点,直接从右邻居继续找← 正确答案
  • D.增加索引高度

答案:C。右移锁的核心是“向右看”:节点分裂后,旧节点保留 link 指针指向新右节点。搜索线程从旧节点沿 link 滑到新节点,不必回父节点重新定位。它不减少磁盘 IO,也不消除写锁,反而增加了一点指针维护开销。

Q7. 乐观并发控制适合哪些场景?

多选题 · hard

  • A.冲突率低← 正确答案
  • B.冲突率高
  • C.每次操作都需要长时间持锁
  • D.重试成本低于锁等待成本← 正确答案

答案:AD。乐观策略赌“冲突少”,先干活再验证,失败重试。冲突率高时重试会连环爆炸,所以 B 错。长时间持锁是悲观策略的典型代价,C 反而说明悲观更差。核心判断是 D:重试比等待便宜,才选乐观。

Q8. PostgreSQL 的 B-tree 借鉴 B-Link 思想后,页分裂时对并发搜索的影响是?

单选题 · extreme

  • A.搜索必须等待分裂完成
  • B.分裂后通过右链接仍能找到数据← 正确答案
  • C.分裂会导致索引失效
  • D.搜索只能走旧的父节点

答案:B。PostgreSQL nbtree 在分裂后,旧页通过 link 指针指向新页,正在搜索的线程沿右链接就能找到数据,无需阻塞等待。A 是普通 B+树最坏情况,C 是设计事故,D 不明白右链接的作用。