数据库两阶段锁
我们需要一种方法来保证所有执行计划都是正确的(即可序列化),而无需提前知道整个计划。
所以要用Lock来保护数据库对象
Locks VS. Latches
locks
- 保护数据库的逻辑内容免受其他事务的影响。
- 保证事务的持久性
- 需要能够回滚
Latches
- 保护 DBMS 内部数据结构的关键部分免受其他线程的影响。
- 保证操作的持久性
- 并不需要能够回滚
为什么Lock可以回滚而Latches不可以回滚?。
Locks是为了保护数据的一致性和完整性,当事务失败或需要回滚时,系统需要确保事务对数据的修改能够被撤销,从而维护数据库的一致性状态。
Latches主要是为了保护内部数据结构的完整性,不直接涉及用户数据的修改,因此不需要支持回滚操作。
Basic Lock Types
S-Lock : Shared lock for reads
X-Lock: Exclusive locks for writes
Executing With Locks
Txn请求锁定(或升级)。
锁管理器授予或阻止请求。
事务释放锁。
锁管理器更新其内部锁表。
- 它跟踪哪些事务持有哪些锁以及哪些事务正在等待获取锁。
Concurrency Control Protocol
两阶段锁定(2PL)是一种并发控制协议,它确定 txn 是否可以在运行时访问数据库中的对象。
协议不需要提前知道 txn 将执行的所有查询。
Two-Phase Lock
Phase 1:Growing
- 每个 txn 都会向 DBMS 的锁管理器请求所需的锁。
- 锁管理器授予/拒绝锁请求。
Phase 2 : Shrinking
- txn只能释放/降级它之前获取的锁。 它无法获取新的锁。
增长阶段结束后,txn 不允许获取/升级锁。
2PL 本身足以保证冲突可串行化,因为它生成优先级图是非循环的调度。但它会受到级联中止的影响。
存在可序列化的潜在调度,但 2PL 不允许,因为锁定限制了并发性。
- 大多数 DBMS 更看重正确性而不是性能。
可能仍然有“脏读”。
- 解决方案:强严格 2PL(又名严格 2PL)
可能会导致僵局。
- 解决方案:检测或预防
大多数 DBMS 更看重正确性而不是性能
Strong Strict Two-Phase Lock
txn 仅在结束后才允许释放锁
仅允许冲突的可序列化计划,但它通常比某些应用程序所需的更强大。
如果某个 txn 写入的值在该 txn 完成之前不会被其他 txn 读取或覆盖,则调度是严格的。
优点:
- 不会导致级联中止(cascading aborts)。
- 中止的 txns 可以通过恢复修改元组的原始值来撤消。
Deadlock Detection or Prevention
死锁是一个事务等待锁被对方释放的循环
死锁的两个解决方法
- 死锁检测(Detection)
- 死锁预防(Prevention)
Deadlock Detection
死锁是一个事务等待锁被对方释放的循环
死锁检测的两个方法:
- 超时检测:这种方法是设置一个阈值,如果一个事务在等待锁的时间超过了这个阈值,就认为该事务陷入了死锁,并将其回滚。这种方法的优点是简单易实现,缺点是可能造成误判和性能损失。
- 等待图检测:这种方法是维护一个等待图,其中每个结点表示一个事务,每条边表示一个等待关系。如果一个事务T等待另一个事务U持有的锁,就在T和U之间画一条从T指向U的边。如果等待图中存在环路,就说明存在死锁,并选择环路中的一个或多个事务进行回滚。这种方法的优点是能够准确地发现死锁,缺点是需要额外的空间和时间开销。
系统定期检查等待图中的环,然后决定如何中断它。
Deadlock Handling
当DBMS检测到死锁时,它将选择一个“受害者(victim)”txn进行回滚以打破循环。受害者txn将重新启动或中止(更常见),具体取决于它的调用方式。在检查死锁的频率和txns在死锁被打破之前等待的时间之间存在权衡。
Victim Selection:
选择合适的Victim取决于诸多因素
- By age(lowest timestamp)
- 事务的优先级:如果一个事务具有较高的优先级,例如由于紧急性、重要性或用户等级等原因,那么它应该尽量避免被回滚。
- By progress(least/most queries executed)
- 事务的进度:如果一个事务已经执行了很长时间,或者已经修改了很多数据,那么它应该尽量避免被回滚,以减少浪费和恢复的开销。
- By the # of items already locked
- 事务的资源占用:如果一个事务占用了很多资源,例如内存、磁盘或网络等,那么它应该尽量被回滚,以释放资源给其他事务。
- By the # of txn that we have to rollback with it
-事务的影响范围:如果一个事务影响了很多其他事务,例如通过加锁或触发级联等方式,那么它应该尽量被回滚,以减少阻塞和冲突。
还应该考虑过去重新启动txn的次数,以防止饥饿。
Rollback Length
在选择要中止的受害者txn后,DBMS还可以决定回滚txn更改的程度。这个标准是确定一个事务回滚的长度,通常有以下两种选择
- 全部回滚:这种方法是将一个事务完全回滚到开始状态,并释放所有占用的资源。这种方法的优点是简单清晰,缺点是可能造成大量的开销和浪费。
- 部分回滚:这种方法是将一个事务回滚到某个中间状态,并保留部分占用的资源。这种方法的优点是可以减少回滚的代价,缺点是可能造成复杂性和不一致性。
Deadlock Prevention
当一个txn试图获取由另一个txn持有的锁时,DBMS会杀死其中一个txn以防止死锁。这种方法不需要等待图或检测算法。
这种策略是在设计时静态地避免死锁发生的可能性,并在运行时保证不会出现死锁。常用的死锁预防方法有:
- 有序加锁:这种方法是按照一定的顺序对资源进行加锁,例如按照资源名称的字典序或者资源编号的升序。这样可以保证不会出现循环等待的情况,从而避免死锁。这种方法的优点是简单有效,缺点是可能降低并发性能和增加加锁开销。
- 时间戳检测:这种方法是给每个事务分配一个唯一的时间戳,表示其开始执行的时间。当一个事务T请求另一个事务U持有的锁时,根据时间戳的大小可以采用以下两种方案之一:
- 等待-死亡方案:如果T的时间戳小于U(T先开始),则让T等待U释放锁;如果T的时间戳大于U(U先开始),则让T“死亡”,即回滚并重启。这样可以保证最早开始的事务总能完成,避免饥饿现象。
- 伤害-等待方案:如果T的时间戳小于U(T先开始),则让T“伤害”U,即强制U回滚并释放锁;如果T的时间戳大于U(U先开始),则让T等待U释放锁。这样可以保证最晚开始的事务总能完成,避免饥饿现象。 这两种方案都可以保证不会出现循环等待的情况,从而避免死锁。它们的优点是不需要额外的空间和时间开销,缺点是可能造成过度回滚和重启。
lock granularities
锁粒度(lock granularity)是指数据库系统在进行并发控制时,对数据资源进行加锁的最小单位。不同的锁粒度会影响数据库系统的并发性能和开销。一般来说,锁粒度越小,能够加锁的数据单元就越多,发生锁冲突的概率就越低,从而提高并发性能;但是,锁粒度越小,也意味着需要更多的锁资源和管理开销,从而降低系统效率。因此,选择合适的锁粒度是数据库系统设计和优化的重要问题。
当txn想要获取“锁”时,DBMS可以决定该锁的粒度(即范围)。理想情况下,DBMS应该获得txn所需的最少数量的锁。
这是并行性与开销之间的权衡:更少的锁,更大的粒度与更多的锁,更小的粒度。
常见的锁粒度有以下几种:
- 表级锁(table-level lock):表级锁是指对整个表进行加锁,是最大的锁粒度。表级锁的优点是简单易实现,占用的内存和CPU资源较少;缺点是并发性能较低,容易造成数据访问的阻塞和饥饿。例如,MyISAM存储引擎只支持表级锁。
- 页级锁(page-level lock):页级锁是指对数据页进行加锁,是中等的锁粒度。页级锁的优点是在并发性能和系统开销之间取得了一定的平衡;缺点是可能出现数据浪费和死锁的情况。例如,BDB存储引擎支持页级锁。
- 行级锁(row-level lock):行级锁是指对单个行记录进行加锁,是最小的锁粒度。行级锁的优点是并发性能最高,可以支持更多的事务同时执行;缺点是需要更多的内存和CPU资源,以及更复杂的死锁检测和恢复机制。例如,InnoDB存储引擎支持行级锁。
选择合适的锁粒度是一个涉及到数据库系统的并发性能和开销的重要问题。一般来说,锁粒度越小,能够加锁的数据单元就越多,发生锁冲突的概率就越低,从而提高并发性能;但是,锁粒度越小,也意味着需要更多的锁资源和管理开销,从而降低系统效率。因此,选择合适的锁粒度需要根据不同的场景和需求进行权衡和调整。
因此,在选择合适的锁粒度时,需要考虑以下几个方面:
- 数据库系统使用的存储引擎:不同的存储引擎支持不同的锁粒度,因此需要根据存储引擎的特点和限制来选择合适的锁粒度。
- 数据库系统执行的事务类型:不同类型的事务对数据访问和修改有不同的需求,因此需要根据事务类型来选择合适的锁粒度。例如,如果事务涉及到大量数据或者频繁修改数据,则可以选择较大的锁粒度来减少开销;如果事务只涉及到少量数据或者主要查询数据,则可以选择较小的锁粒度来提高并发性能。
- 数据库系统面临的并发压力:数据库系统在高并发场景下需要处理更多的事务请求,因此需要根据并发压力来选择合适的锁粒度。例如,如果数据库系统面临着高并发压力,则可以选择较小的锁粒度来减少冲突;如果数据库系统面临着低并发压力,则可以选择较大的锁粒度来降低开销。
database lock hierarchy
database lock hierarchy是指数据库系统在进行并发控制时,对数据资源进行加锁的不同层次。不同层次的加锁可以影响数据库系统的并发性能和开销。一般来说,加锁层次越高,能够加锁的数据单元就越少,发生锁冲突的概率就越高,从而降低并发性能;但是,加锁层次越高,也意味着需要更少的锁资源和管理开销,从而提高系统效率。因此,选择合适的加锁层次是数据库系统设计和优化的重要问题。
Table Page Tuple Attr是指四种不同的加锁层次,分别对应于表、页、元组(行)和属性(列)这四种数据单元。它们的特点和区别如下:
- 表级(Table):表级是指对单个表进行加锁,是较高的加锁层次。表级的加锁通常用于执行涉及到大量数据或者频繁修改数据的事务,例如在创建或删除索引时。表级的加锁可以减少开销和复杂性,但也会影响其他事务对同一表中任何数据进行访问或修改。
- 页级(Page):页级是指对单个数据页进行加锁,是中等的加锁层次。页级的加锁通常用于执行涉及到中等数量数据或者偶尔修改数据的事务,例如在更新或删除部分记录时。页级的加锁可以在并发性能和系统开销之间取得一定的平衡,但也可能出现数据浪费和死锁的情况。
- 元组级(Tuple):元组级是指对单个行记录进行加锁,是最低的加锁层次。元组级的加锁通常用于执行涉及到少量数据或者主要查询数据的事务,例如在插入或查询单条记录时。元组级的加锁可以提高并发性能和精确性,但也需要更多的内存和CPU资源,以及更复杂的死锁检测和恢复机制。
- 属性级(Attr):属性级是指对单个列属性进行加锁,比元组级更细致。属性级的加锁可以进一步提高并发性能和灵活性,但也增加了系统开销和复杂性。目前,大多数数据库系统不支持属性级的加锁。
Intention Locks
意图锁是一种表级锁,用于表示事务对表中的某些行有加锁的意图,但并不实际加锁。意图锁有两种类型:意向共享锁(IS)和意向排他锁(IX)。意向共享锁表示事务想要对表中的某些行加共享锁(S),而意向排他锁表示事务想要对表中的某些行加排他锁(X)。
意图锁的作用是为了提高并发性和效率,避免在检测表级锁时扫描所有的行级锁。如果一个事务想要对一个表加共享锁或排他锁,它只需要检查该表上是否有相应的意图锁,而不需要检查每一行上是否有冲突的锁。
意图锁允许在共享或独占模式下锁定更高级别的节点,而不必检查所有后代节点。如果节点在意图模式下被锁定,则某些txn正在树中的较低级别执行显式锁定。
- Intention-Shared(IS)表示使用共享锁在较低级别上的显式锁定
- Intention-Execlusive(IX) 使用独占锁在较低级别指示显式锁定。
- Shared+Intention-Execlusive(SIX) 该节点根植的子树在共享模式下被显式锁定,而显式锁定是在较低级别使用独占模式锁进行的。
Locking Protocol
每个txn在数据库层次结构的最高级别获得适当的锁。
要在节点上获得S或IS锁,txn必须在父节点上至少持有IS。
要在节点上获取X、IX或SIX,必须在父节点上至少保留IX。
Multiple Lock Granularities
分层锁在实践中很有用,因为每个txn只需要几个锁。
意向锁有助于提高并发性
- 意向共享(IS):意图以更细粒度获得S锁(s)。
- 意向-独占(IX):意图以更细的粒度获得X锁(s)。
- 共享+意向独家(SIX):同时像S和IX。
Lock Escalation
当txn获取太多低级锁时,DBMS可以自动切换到粗粒度锁。这减少了锁管理器必须处理的请求数量。