1. 锁机制

1.1 模式

类型拥有功能适用
排他锁(Exclusive Lock, X-Lock)只允许一个事务拥有数据的排他锁拥有排他锁的事务可以对数据进行读和写操作,同时禁止其他事务对该数据进行任何操作需要修改数据,如UPDATE
共享锁(Shared Lock, S-Lock)允许多个事务拥有同一数据的共享锁拥有共享锁的多个事务都可以读取数据,但是任何事务都不能对数据进行更改只需要读取数据,如SELECT

相容性

  • 被共享锁锁住的数据,事务可以申请该数据的共享锁,但是不能申请该数据的排他锁
  • 被排他锁锁住的数据,事务不能申请该数据的任何锁

锁的授予流程

  1. 事务向并发控制管理器发出锁请求 lock-X/S(data)
  2. 并发控制管理器根据规则决定是否授予锁 grant-X/S(T,data)
  3. 如果成功获取锁,事务继续执行,否则事务被迫阻塞等待
  4. 事务执行完毕或者不再需要对数据加锁,则释放锁 unlock-X/S(data)

1.2 问题

  • 死锁(deadline):多个事务彼此等待对方的锁释放,形成了闭合的等待链,从而陷入了无限阻塞的状态(如下图,T4等待T3释放B上的X锁,T3等待T4释放A上的S锁)
  • 饥饿(starvation):某个数据一直被申请共享锁,导致某个对该数据申请排他锁的事务陷入了长期等待的状态
  • 破坏冲突可串行化:某些指令等待时间过长,导致调度的冲突操作顺序与某种串行调度的冲突操作顺序不一致

1.3 封锁协议

封锁点(lock point):调度中事务获得最后一个锁的位置,标志着事务从增长阶段进入缩减阶段

一旦事务进入缩减阶段,就只能释放锁,不能再获取锁,即无法返回增长阶段

协议定义性质
简单封锁协议事务在访问数据项之前加锁,在操作完成后立即解锁简单,但可能导致各种问题
两阶段封锁协议(2PL)在增长阶段事务只能获取锁,在缩减阶段事务只能释放锁保证冲突可串行化,但可能导致死锁
严格两阶段封锁协议(Strict 2PL)在2PL的基础上,要求排他锁必须在事务提交或回滚后才能释放保证冲突可串行化,避免了级联回滚
强两阶段封锁协议(Rigorous 2PL)在2PL的基础上,要求排他锁和共享锁必须在事务提交或回滚后才能释放保证冲突可串行化,避免了级联回滚
带锁转换的两阶段封锁协议(Lock Conversions)允许在增长阶段将共享锁升级为排他锁,允许在缩减阶段将排他锁降级为共享锁提高了锁的灵活性,减少了锁的冲突

2PL的意义:封锁点顺序能够反映事务的依赖关系,与某种串行调度的事务执行顺序一致,保证了调度的冲突可串行化

1.4 锁表

锁表:以数据项标识为键的哈希表,每个键对应一个链表,链表连接了若干数据项,每个数据项又对应一个事务链表,包含以下信息

  • 提出锁请求的事务
  • 事务请求的锁类型
  • 锁被授予还是处于等待

锁表的操作

  1. 加锁:在对应数据项的链表末尾增加一条新的请求记录,如果请求的锁模式与当前授予的锁模式兼容,则立即授予锁,并标记为授予状态,如果不兼容,则将锁请求标记为等待状态
  2. 解锁:从对应数据项的链表中删除事务的请求记录,然后检查链表中随后的请求记录,判断是否可以授予锁
  3. 事务中止:会删除该事务所有的持有锁记录和等待锁记录,然后检查链表中随后的请求记录,判断是否可以授予锁

1.5 树型协议

数据库图:是一个有向无环图(DAG),用来表示数据项之间的部分顺序约束关系

  • 节点代表数据库中的数据项
  • 有向边didjd_i \to d_j意味着如果一个事务访问了did_idjd_j,它必须先访问did_i,然后才能访问djd_j
  • 可以选择一个没有入边的节点作为树的根节点,将有向无环图转换为树

规则

  1. 事务只能对数据项加排他锁 -> 所有数据访问都以写操作为主
  2. 事务的首次封锁可以在任何数据项上进行 -> 没有固定的起点限制
  3. 事务对一个数据项Q加锁的前提是事务当前持有Q的父节点的锁 -> 事务必须按树自顶向下的顺序访问数据项
  4. 事务对数据项解锁可以随时进行 -> 不局限于两阶段,没有扩展阶段和收缩阶段的限制
  5. 一个数据项被T加锁并解锁之后,T不能随后再对该数据项加锁 -> 确保事务只访问一次每个数据项

性质

  • 优点:保证了冲突可串行化,不会产生死锁,可以随时解锁不需要再访问的的数据项
  • 缺点:不保证可恢复性,无法避免级联回滚,需要给根本不访问的父亲数据项加锁来访问子数据项,不支持动态顺序

2. 死锁处理

2.1 死锁避免

策略定义缺点
一次性封锁在每个事务开始执行之前必须封锁它所需要的全部数据项降低并发性
优先级封锁为所有数据项设定一个固定的次序/优先级,只能按照次序来封锁数据项灵活性受限
等待死亡/自杀新事务请求一个被老事务持有的锁,则新事务会被回滚产生频繁或不必要的回滚
伤害等待/他杀老事务请求一个被新事务持有的锁,则新事务会被回滚产生频繁或不必要的回滚
超时申请锁的事务至多等待一段指定的时间,否则会自动回滚并重启无法区分死锁和长等待,需要合理设置超时时间

最简单的策略就是一次性封锁和锁超时,但由于前者会破坏并发性,因此实际情况下很少使用,而后者通常作为一种辅助配合其他策略使用

2.2 死锁检测与恢复

等待图:顶点集是事务集合,边Ti->Tj表示事务Ti正在等待事务Tj释放封锁的数据项

死锁检测:系统维护等待图,并周期性地在等待图中执行搜索环路算法,当且仅当等待图中包含环路时,系统中存在死锁

死锁恢复

  1. 根据以下代价因素,来选择破解环路需要牺牲/回滚哪一个或哪一些事务
  • 事务已经执行了多久,还需要执行多久
  • 事务当前封锁了多少数据项,事务还需要封锁多少数据项
  • 回滚会牵扯到多少个事务需要级联回滚
  • 事务的优先级
  1. 不回滚整个事务,而是只回滚导致死锁的部分操作
  2. 防止有些事务总是被牺牲从而长时间无法执行

3. 多粒度

多粒度:数据项可以指代不同层次的数据,其中最粗粒度为整个数据库,最细粒度为一个元组/记录,粒度越粗,锁的范围越大,事务可以一次性锁住更多数据

粒度控制:如果事务显式对粗粒度封锁,那么事务会隐式对其下细粒度也封锁

意向锁(intention lock):当事务需要给某个细粒度节点显式加锁前,他会给该节点的所有祖先节点都加上意向锁,因此如果某个节点存在意向锁,就代表他的某个后代节点存在该锁,从而避免在给粗粒度节点加锁时遍历全树来检查是否可以封锁

意向锁只用于声明某个事务的意图,不会实际锁住数据项

多粒度封锁协议

  • 加锁顺序必须从根节点开始,按照自顶向下的顺序加锁
  • 解锁顺序必须按照自底向上的顺序加锁
  • 两阶段封锁,即一旦开始解锁就不能再加锁
  • 只有拥有父节点的IX或IS,才能对子节点加S或IS
  • 只有拥有父节点的IX或SIX,才能对子节点加X、SIX或IX
  • 锁必须满足相容性矩阵

4. 数据库操作

4.1 插入和删除

之前一直聚焦于read和write,相当于select和update,现在聚焦于insert和delete

冲突情况

  1. 删除数据项之后,不能对该数据项进行select、update和delete
  2. 插入数据项之前,不能对该数据项进行select、update和delete
  3. 不存在数据项,则不能进行delete
  4. 已存在数据项,则不能进行insert

锁规则

  • 在对某个数据项执行删除操作之前,事务必须获得该数据项的排他锁
  • 当事务向数据库插入新元组时,系统会自动为该新元组分配一个排他锁,并且在提交时才解锁

4.2 谓词读

谓词读(Predicate Read):实际上就是带有where条件的select操作

幻读(Phantom Read):谓词读期间,其他事务对查询范围内的数据进行了插入、更新和删除,导致查询结果的记录数发生了变化

  1. 基于信息:给所有具有谓词信息的数据项加共享锁
  2. 基于索引:利用索引查询,给所有索引叶节点加共享锁

锁定信息或索引不会与锁定数据项冲突