【原文参考https://github.com/sdg-sysdev/bdb-study/blob/master/btree_locking.txt】 | |
B-TREE locking需要区分两种情况: | |
1. 并发的数据库事务在查询或修改数据库内容时对B-TREE索引访问的并发控制 | |
2. 并发的线程对内存中的B-TREE数据结构进行访问时的并发控制 | |
事务并发控制使用Lock,线程并发控制使用Latch | |
Lock和Latch的区别: | |
Lock Latch | |
分离... 用户事务 线程 | |
保护... 数据库内容 内存中的数据结构 | |
存活周期... 整个事务 临界区 | |
模式... Shared, exclusive, update, 读,写, | |
intention, escrow, schema, etc. (可能)更新 | |
死锁... 检测和分解 避免 | |
...通过... 分析waits-for图,超时,事务中止, 编码规范,"lock leveling" | |
部分回滚,lock降级 | |
保存于... Lock管理器的哈希表 被保护的数据结构 | |
在分布式事务中,当等待全局事务协调器的决策时,需要保持Locks,以保证本地事务可以 | |
遵守全局协调器的最终决策。但Latches在这时不需要被保持。 | |
在没有新事务并发执行的崩溃恢复阶段,不需要Locks。因为在系统崩溃之前的并发控制已经 | |
保证活跃事务之间不会冲突。而Latches则总是被需要。 | |
physiological日志是根据页面中记录的标识来引用它,而不是根据字节位置。因此,在页面 | |
内移动记录是为数不多的不需要写恢复日志的操作。 | |
在页面内整理空闲空间和记录空间不需要写日志,除非在移除非法记录之后改变了某些记录的标识, | |
例如它们在页面内的slot numbers | |
从系统崩溃中恢复时,如果在日志分析的同时获取Lock,那么可以允许在重做操作及进行补偿操作 | |
的同时继续事务处理。在此过程中获取的Lock可以不同于原始事务执行过程中获取的Lock。 | |
Latch则仅限于保护那些修改B-TREE结构,但不改变其逻辑内容的操作。典型的例子就是分拆B-TREE节点 | |
以及在B-TREE相邻节点间均衡负载。其它影响整个B-TREE索引的例子包括压缩,碎片整理,和其它 | |
形式的重组。影响范围小于一整个节点的例子则是创建和移除幽灵记录。 | |
存在的问题: | |
1. 当一个线程在读取缓冲池中的某个页面时,不能被另外一个线程修改。 | |
2. 当跟随指针(页面标识符)从一个页面到另一个时,例如从B-TREE索引中的一个父节点到一个子节点, | |
该指针必须不能被另外一个线程废止(invalidate) | |
3. 不仅父-子指针之间有"指针追踪"的问题,在相邻指针之间也存在。不同的线程可能根据升序或降序 | |
进行索引扫描,这会引起死锁。 | |
4. 在B-TREE中执行插入操作时,一个子节点可能会因溢出而需要在父节点中进行插入。在极端情况下, | |
B-TREE的旧根节点会溢出,而不得不进行分拆,并使用一个新的根节点代替。从叶子节点往根节点 | |
访问在单线程环境下不会出问题,但在多线程环境下有死锁的可能。 | |
对于第一个问题,数据库实现了只读latch和读-写latch。 | |
解决第二个问题的方法是使用"lock coupling",即保留父节点的latch直到获取了子节点的latch。 | |
如果子节点当前不在缓冲池中,因而需要相对缓慢的磁盘I/O,则在从磁盘读取子节点的过程中 | |
应该释放父节点上的latch。这种情况下,可以通过获取缓冲池中所选页面对应的描述符结构的latch | |
来实现lock coupling。 | |
另外,为了防止B-TREE在此期间的变化,该I/O应该重新遵循从根到叶子的遍历方式。这么做看起来会 | |
比较昂贵,但是通常可以直接基于之前的搜索结果。例如,验证在将子节点页面读取到缓冲池中时 | |
所有父页面上的LSN都没有变化。 | |
第三个问题和第二个类似,只有两点不同。从积极的一面来说,异步read-ahead可以缓解该问题的发生。 | |
由于B-TREE索引中的大规模read-ahead通常不能依赖于相邻指针,而必须依赖于叶子节点父节点中的指针 | |
来预读,甚至是祖父节点。从消极的一面来说,要避免相反方向扫描的死锁,latch代码必须提供一种立即 | |
失败模式。当在向前或向后遍历时发生了获取latch失败的情况,则必须释放所有叶子节点所对应页面的 | |
latch。 | |
第四个问题是最复杂的。它影响所有的更新,包括插入,删除,甚至记录的更新。 | |
一个方法是在查找受影响的叶子节点时,将从根节点到叶子节点的遍历过程中碰到的所有节点都加上排他(exclusive)latch。 | |
这种方法显然的问题是会碰到并发上的瓶颈,尤其在根节点。 | |
另一个方法是在从根到叶子的搜索过程中使用共享(shared)latch,当有需要的时候将共享latch升级成排他latch。 | |
在能够允许升级并且没有死锁风险的情况下这种方法很好,但是实际上由于它可能会失败,因此在实现中不能只 | |
使用该方法。 | |
第三个方法是在通常的共享和排他latch之外,使用"更新"("update")或"升级"("upgrade") latch。"更新"latch兼容共享 | |
latch,但是彼此之间不兼容,这导致对于多个更新来说B-TREE根节点还是一个瓶颈。 | |
以上三个方法可以有个改进,就是当碰到一个更低层的未满节点,如果它能保证拆分操作不会传递到更高层的节点,则可以释放 | |
从根到父节点这条路径上的所有latch。另一方面,变长B-TREE记录和变长键值会使得决定需要多少空闲空间才能做出该保证 | |
变得很困难甚至不可能。有趣的是,该问题可以通过以下方法来解决:在释放父节点的latch之前,可以知道如果子节点因为该 | |
插入操作而需要拆分,则哪个键值会被添加到父节点中去。 | |
第四个方法在为插入操作从根到叶子的遍历过程中主动拆分节点。该方法避免了第一个方法的瓶颈问题和第二个方法的升级 | |
失败问题。它的不利之处是在真正需要之前拆分,浪费了一些空间,更重要的是在变长记录和键值的情况下,可能不可能在任何 | |
情况下都能够主动拆分。 | |
第五个方法是在初次根到叶子的搜索过程中使用共享latch,当一个节点需要拆分的时候中止该过程。然后启动一个新的过程, | |
并且在到达需要拆分的节点时,获取一个排他latch然后拆分。该方法能够受益于之前讨论过的基于之前路径的快速搜索机制。 | |
BLINK-TREE [Lehman and Yao 1981].BLINK-TREE的优势是分配一个新的节点并初始接入到树中是一个本地步骤,仅仅影响一个 | |
已存在的节点。劣势是搜索可能会稍微慢一点点。在密集插入的情况下,需要有方法来防止形成过长的相邻节点列表。 | |
(PostgreSQL实现的也是BLINK-TREE索引) | |
[Jaluta et al. 2005] 中有更详细的描述。 | |
"key range locking", next-key locking" | |
在许多B-TREE实现中,当用户事务请求删除记录时实际上并没有实际删除。它只是将该记录标记为非法记录,称为幽灵记录。 | |
因此,每次查询过程中如果碰到记录头中设置了"ghost bit"的记录,需要将它们从查询结果中剔除。 | |
在插入一个新的B-TREE键值时,如果一个拥有相同键值的幽灵记录已经存在,则该操作会转化为一个对已有(幽灵)记录的更新操作。 | |
对幽灵记录的回收在一个异步的清理事务中进行。在被清理之前,它们的键值像通常记录的键值一样参与并发控制和key rangelocking。 | |
Locking in non-unique indexes | |
Increment lock modes |
latch与lock区别
最新推荐文章于 2024-06-04 14:45:23 发布