深入解析数据库索引并发控制:从闩锁原理到B+树与哈希表实战
一.为什么索引需要并发控制
假设两个线程同时向一B+树叶子节点插入数据,若没有同步机制,可能出现以下问题
一个线程的修改覆盖另一个线程;节点中的键失去顺序;页面分裂执行两次;父节点保存错误的分隔键;线程读到修改一半的数据;指针指向无效页面;B+树结构永久损坏
二.事务锁(Lock)与闩锁(Latch)的区别

事务锁:防止另一个事务违反隔离性
闩锁:防止多个线程同时破坏B+树页面
三.闩锁的两种模式
1.读闩锁
多个线程可以同时持有读闩锁;持有读闩锁时不能进行结构修改
2.写闩锁
同一时间只能有一个线程持有;与其他读闩锁、写闩锁互斥
兼容关系

四.闩锁的常见实现
1.阻塞式互斥锁:线程无法获得闩锁时进入休眠,等待操作系统唤醒。
优点:等待时不持续消耗CPU;适合临界区较长的场景
缺点:线程休眠和唤醒成本较高;涉及操作系统调度
2.自旋锁:线程无法获得锁时进行循环检查
while (无法获得锁) {
继续尝试
}
优点:不需要线程休眠和唤醒;适合持锁时间非常短的场景
缺点:等待期间持续消耗CPU;竞争激烈时性能较差
3.读写锁:
读写锁分别支持:多个并发读者;一个独占写者
4.设计闩锁时候需要考虑的问题
(1)公平性
(2)缓存一致性
(3)闩锁粒度
粒度过大:一把闩锁保护整个B+树。实现简单,但是几乎所有操作都是串行
粒度过小:每个键一个闩锁。并发度高,但是复杂度高
因此,实际系统通常采用节点级闩锁。
五.闩锁耦合
1.核心过程
锁住父节点
↓
锁住子节点
↓
释放父节点
↓
继续向下
线程移动时,短暂同时持有相邻两层节点的闩锁,就像沿树向下移动。
2.优点:可以避免
线程刚读取子节点指针
另一个线程就分裂或删除了该子节点
六.安全节点
是否能够提前释放祖先节点的闩锁取决于当前节点是否安全
1.查询节点:不会修改树结构,属于安全节点
2.插入操作:如果插入节点后子节点不会分裂,就是安全节点
3.删除操作:如果删除后不会低于最低占有率,就是安全节点
七.基本的B+树插入流程
保守的插入流程:
- 从根节点开始获得写闩锁;
- 向下找到目标子节点;
- 获得子节点写闩锁;
- 如果子节点安全,释放所有祖先闩锁;
- 到达叶子节点后插入;
- 必要时执行分裂并向上更新;
- 释放剩余闩锁。
注:大多数插入不会引发分裂,但保守协议一路获取写闩锁,会阻塞大量只读操作。
八.乐观闩锁协议
为了提高并发效率,可以先假设本次操作不会引起节点的分裂
执行过程:
- 从根节点向下时只获得读闩锁;
- 到达目标叶子节点后获得写闩锁;
- 如果叶子节点安全,直接完成操作;
- 如果发现需要结构修改,则释放闩锁;
- 使用保守协议重新执行。
九.常见问题以及处理
1.根节点并发问题
如果每次操作都长时间持有根节点闩锁,即使下层访问的是不同分支,也会被串行化。
优化方向包括:尽快释放根节点闩锁;使用乐观协议;单独保护根指针;’仅在根节点分裂或树高变化时使用独占保护
2.叶子结点扫描问题
通常按从左到右的顺序获得闩锁,但删除或合并操作可能需要以另一个方向访问兄弟节点。如果两个操作的获取顺序相反,就可能发生死锁。
因此,范围扫描通常采用尝试获取下一片叶子的闩锁的方式
如果失败:不无限等待;释放当前闩锁;重新开始或稍后重试。
十.哈希表的并发控制
1.保护的内容:桶内容;槽位状态;溢出页;全局扩容状态;目录结构
2.闩锁的类型
(1)整表闩锁
(2)页面级或桶级闩锁
(3)槽位级保护
3.为什么哈希表容易并发
(1)不同键大概率映射到不同的桶,普通哈希操作只访问一个桶或者少量槽位
(2)B+树从根节点开始操作,还可能沿父子关系传播分裂或者合并,所以更加困难
十一.物理正确性和逻辑正确性
1.物理正确性:由闩锁保护
(1)页面没有被破坏
(2)指针关系有效
(3)键保持有效
(4)分裂合并处于同一个状态
2.逻辑正确性:由事务锁,MVCC等机制保护
(1)事务能否看到某条记录
(2)是否允许两个事务修改同一行
(3)查询结果是否满足隔离级别
(4)是否出现不可重复读或幻读
附.性能优化原则,易错点


openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)