CMU-15445数据库系统导论笔记<七>—并发控制

386 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第20天,点击查看活动详情

并发控制导引

大部分的数据库是并发的,我们需要允许多个线程安全地访问我们的数据结构。但是也有一些数据库不是多线程的,例如redis,因此不需要额外的线程调度开销,因此十分快速。我们主要讨论支持并发的数据库。

数据库的并发控制保证数据库本身的逻辑正确性和数据正确性,首先我们根据当前所学,讨论一下最基础的索引如何支持并发

并发在表现上为数据库的以下两种正确性

逻辑正确性:这意味着线程能够读取它应该读取的值,例如线程应该读回它之前写入的值。

物理正确性:这意味着对象的内部表示是可靠的,例如,数据结构中没有会导致线程读取无效内存位置的指针。

索引并发

锁总览

首先是Lock和Latche的区别和联系,如下图所示。

更具体的,Lock用于保护数据库(例如元组、表、数据库)的内容不受其他事务的影响。事务将在其整个期间保持锁定。数据库系统可以向用户公开在查询运行时持有的锁。锁需要能够回滚更改。

Latches是用于DBMS内部数据结构(例如,数据结构、内存区域)与其他线程之间的关键部分的低级保护原语。

对于具体的数据结构,我们规定有一对锁为读锁写锁,它们的概念定义如下。

Latches有三种实现的方式

Blocking OS Mutex

锁的一个可能实现是操作系统的内建锁。Linux提供futex(快速用户空间互斥),它由

  • 用户空间中的旋转锁存器
  • 操作系统级互斥器

如果DBMS可以获取用户空间锁存器,则设置锁存器。它在DBMS中显示为一个锁存器,即使它包含两个内部锁存器。如果DBMS无法获取用户空间锁存器,那么它将进入内核并尝试获取更昂贵的互斥锁。如果DBMS无法获取第二个互斥锁,那么线程会通知操作系统它在互斥体上被阻塞,然后它会进入睡眠状态。这种锁不能规模化,无法应对大规模并发。

Test-And-Set Spin Latch

自旋锁是操作系统互斥锁的更有效的替代方案,它由DBMS控制。它不可扩展,也不利于缓存,因为对于多个线程,CAS指令将在不同的线程中执行多次。这会导致缓存一致性问题,因为线程正在轮询其他CPU上的缓存线。

注:Linux创始人同样建议用户不要用自旋锁。

Reader-Writer Latches

依赖于自旋锁的实现,但是应用程序有大量读取时将具有更好的性能,因为读线程之间可以共享资源,而不是等待。

并且读写锁是相对公平的,写线程介入后无法再加更多的读线程。

哈希表锁

在静态哈希表中很容易支持并发访问,因为对哈希表的访问总是相同顺序的,这意味着永远不会有死锁。动态哈希表则会更复杂一些,但是总体上相同。哈希表锁分为两种访问方式的锁。

page-latches

每个页面都有自己的读写锁,可以保护其全部内容。线程在访问页面之前获取读锁或写锁,这会降低并行性,因为一次可能只有一个线程可以访问一个页面,但对于单个线程来说,访问页面中的数据会更快一些,因为页内是没有锁的。

slot-latches

每个slot都有自己的读写锁,因为两个线程可以访问同一页上的不同slot。但它增加了访问表的存储和计算开销,因为线程必须为其访问的每个slot获取一个锁存器,每个slot必须为读写锁存储数据。

注:也有用COMPARE-AND-SWAP(CAS)实现的无锁并发,不过这是后话了。

B+树锁

B+树同样是需要保持并发性的数据结构,B+树的锁策略主要是crabbing/coupling。它的基本描述是,当操作到达到子节点时

  1. 获得父节点的锁
  2. 获得子节点的锁
  3. 如果对子节点的操作是安全的,就释放父节点的锁。

安全指的是子节点的更新不会拆分(插入时未满)或合并(删除时超过一半满),从而造成父节点的结构发生改变。

具体的,当对B+树进行搜索时,从根部开始,然后向下,反复获取子节点上的锁,然后解锁父结点的锁。当对B+树进行插入和删除时,可能会造成很多写锁遗留,如下图所示,直到D结点都不能确认是否能够解锁根节点的写锁。

因为根节点被锁,其它读线程也无法进行查询,这是很浪费资源的。所以一种优化的方法是无论是读还是写都上读锁,如果写操作会造成B+树的结构改变,那么重新进行这次写操作,并且在这次上写锁。

从正确性的角度来看,锁的释放顺序并不重要。但是,从性能角度来看,最好释放树中较高位置的锁存器,因为它们会阻止对更大部分叶节点的访问。

Leaf Node Scans

叶节点扫描容易出现死锁,因为假如有线程试图同时在两个不同的方向(即从左到右和从右到左)获取锁时,索引锁是不支持死锁检测的,因此很可能造成无法解决的死锁。因此,程序员处理这个问题的唯一方法是通过定义冲突规则,来调整锁的获取顺序。

一个常见的解决方案是不允许相反方向的索引,例如MySQL在很长一段的时间里根本不支持反向扫描叶结点(倒序遍历)。

\