一.索引并发控制整体技术背景
1.技术诞生背景
B + 树、哈希表这些索引结构,默认都是「只有一个人操作」的单线程理想情况—— 就像你独自用自己的办公桌,想怎么翻文件、怎么整理文件夹都随便,不会有人抢、不会有人干扰。但真实的数据库是多核 CPU、多用户同时访问的,必须让很多线程同时读写同一个索引。这么做一是为了榨干多核性能,二是为了 “隐藏磁盘 IO 的等待”—— 磁盘读数据很慢,与其让线程傻等,不如先让别的线程干活。
若缺乏并发控制机制,会出现两类核心正确性问题:
- 逻辑正确性异常:读到脏的、半截的数据
- 物理结构损坏:直接把索引本身给搞散架
2.并发控制的正确性要求
逻辑正确性:线程只能读取当前事务/操作可见的合法数据,无脏读、幻读等异常;
物理正确性:确保数据结构的内部表示始终处于有效状态,如 B + 树节点的指针不被破坏、链表无断链等,这正是 Latch 需要解决的核心问题
二、闩锁(Latch)核心基础体系
1. 闩锁与事务锁的核心区别
(1)技术背景
数据库有两套完全不一样的并发控制机制:事务锁 Lock 面向上层业务事务,闩锁 Latch 面向底层内核数据结构。它们的管控层次、存活周期、使用场景均不相同
(2)核心逻辑对比
锁(Lock)与闩锁(Latch)的对比是数据库内核并发的重点知识,二者关键区别整理如下:
- 管控对象:Lock 用于隔离多个并发事务;Latch 用于隔离数据库内部各个工作线程。
- 保护对象:Lock 防护磁盘上的业务逻辑数据;Latch 保护驻留内存的索引、缓冲页这类内核数据结构。
- 持有周期:Lock 的有效期覆盖完整事务,事务结束才释放;Latch 只在临界代码执行期间持有,操作结束马上释放。
- 回滚特性:Lock 可以跟随事务完成回滚;Latch 只负责维护结构完整性,不参与事务回滚。
- 死锁处理:Lock 通过死锁检测、超时机制、事务中止来化解死锁;Latch 依靠编码规范从设计上避免死锁发生。
- 存储位置:Lock 交由锁管理器集中维护;Latch 直接挂载在受保护的内核结构之上。
(3)优缺点与实际落地表现
优点:双层并发控制机制分工明确。事务锁 Lock 负责保证上层业务事务的逻辑一致性,闩锁 Latch 维护数据库内核底层结构的物理完整性,同时兼顾业务逻辑正确和系统运行稳定。
缺点:闩锁不提供死锁检测能力,只能依靠开发时的编码规范规避死锁问题,开发实现难度大,系统容错能力较弱。
通俗示例:事务锁相当于图书馆的借阅权限管控锁,约束各个读者(对应事务)的借书行为;闩锁则是书架整理时的防护锁,约束管理员(对应工作线程)调整整理书架(索引结构),防止整理操作中途造成书架结构损坏。
2.闩锁工作模式与兼容性
(1)技术背景
多线程访问索引结构分为读、写两类操作,读操作可并发执行,写操作需要独占资源。为最大化并发性能、减少不必要的阻塞,闩锁设计了读写两种工作模式及兼容机制。
(2)核心逻辑
读模式(Read):允许多个线程同时获取读闩锁,支持并发读取,线程之间不会互相阻塞。
写模式(Write):属于独占访问模式。只要已有线程持有读闩锁或者写闩锁,其他线程就不能成功加写闩锁,同一时刻只允许一个线程执行修改操作。
兼容性规则:读‑读可以共存;读‑写相互排斥;写‑写相互排斥。
(3)优缺点
优点:充分释放读并发能力,适配数据库读多写少场景,提高系统吞吐量;锁逻辑简单,执行效率高。
缺点:读锁若长时间不释放,会一直挡住写请求。
3.闩锁设计目标与主流实现方案
(1)技术背景
闩锁是数据库内核高频使用的基础组件,它的内存消耗和执行效率直接约束数据库并发能力,所以闩锁要做到轻量、无冲突时快速运行、去中心化。
(2)核心设计目标
- 内存开销低:闩锁依附于海量的索引节点,每个闩锁都要尽量少占内存。
- 无冲突快速路径:没有线程竞争是常态,加锁解锁逻辑必须飞快,不能造成性能瓶颈。
- 去中心化:闩锁跟着节点走,不需要全局管理器,消除单点瓶颈。
(3)主流实现方案
- 测试并设置自旋闩锁(TAS):基于CPU原子指令实现,加解锁仅需单指令,无冲突效率极高;但不支持缓存友好、高并发下自旋浪费CPU,无法扩容。
- 阻塞式OS互斥锁:基于系统futex实现,使用简单;但每次加解锁约25ns,并发扩展性差,高竞争下线程阻塞开销大。
- 读写闩锁:支持并发读,通过读写队列避免饥饿;可基于自旋锁二次封装,但需要维护队列状态,存在少量开销。
(4)优缺点总结
自旋锁适合低竞争、短临界区场景;OS互斥锁适合高竞争、长临界区场景;自适应闩锁综合性能最优,是现代数据库内核主流选型。
三.哈希表索引并发控制机制
1.技术背景
哈希表的访问模式天生利于并发优化:线程访问哈希表只做单向寻址,每次仅访问一个槽位或一页,没有跨页的复杂遍历。因此哈希表并发不容易出现死锁,开发难度比 B + 树小很多。不过全局锁会严重压制并发性能,于是出现了多种粒度的闩锁实现方案。
2.三类核心实现方式
(1)全局闩锁方案
整表共用一把锁。实现最简单,并发性能最差,内存开销极小;仅适用于小表、原型验证场景。
(2)页/块级闩锁方案
按数据页独立加锁。粒度适中,不同页可并行操作,贴合磁盘 IO 特性;是磁盘哈希索引最常用的折中方案。
(3)槽位级闩锁方案
每个哈希槽独立加锁。并发性能最强、锁竞争最小;但实现复杂、锁本身内存开销大;适用于内存哈希、高并发场景。
四.B+树索引并发控制核心协议
1.技术背景
B + 树作为数据库的核心索引结构,复杂度远高于哈希表。线程访问 B + 树需要自顶向下逐层遍历节点,且写入操作会触发节点分裂、合并与数据迁移等结构性变更,存在遍历操作与结构修改之间的并发冲突。若不加以管控,会出现遍历中途节点被拆分、数据丢失、树结构损坏等严重问题,因此需要设计专属的 B + 树并发闩锁协议。
需要解决两类核心问题:
- 多线程同时修改同一节点
- 线程遍历树时其他线程修改节点结构
2.闩锁耦合协议
这是 B + 树并发控制的核心协议,通过「边加锁边解锁」兼顾结构安全与并发性能,核心三点:
- 加锁规则:严格自上而下、先父后子加锁,所有线程遵循统一加锁顺序,从根源避免死锁。
- 安全节点判定:插入场景下节点未满、删除场景下节点数据过半即为安全;安全节点操作后不会触发分裂 / 合并,不会牵连上层节点。
- 释锁规则:获取子节点锁并确认其安全后,立即释放所有祖先节点的锁,大幅缩短锁持有时间,提升整体并发度。
3.悲观耦合协议缺陷与优化方案
悲观协议 = 默认写操作一定会触发结构变动,全程加写锁
乐观优化 = 默认大概率不会触发结构变动,先读锁遍历,不行再回退
(1)缺陷背景
传统悲观耦合协议存在两个核心问题:
- 根节点成为全局瓶颈:每次更新都从根节点开始加写锁,而根节点全局唯一,高并发下所有写操作都争抢这一把锁,直接卡死整体并发上限。
- 策略过度保守:实际业务中绝大多数 B + 树修改只改动叶子节点的数据,根本不会触发节点分裂、合并,完全不需要锁住上层节点。悲观策略全程持写锁,白白浪费了大量并发能力。
(2)乐观闩锁优化算法
核心逻辑:基于绝大多数修改不会引发结构变动的统计规律做乐观假设,先用低成本的读锁遍历到叶子节点,再升级写锁修改;只有真的触发结构变化时,才回退走完整的悲观写锁流程。