latch与lock区别

  【原文参考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

你可能感兴趣的:(分布式系统)