数据库管理系统允许多个用户同时访问数据,这种并发操作带来了效率的提升,但也引入了一个几乎贯穿整个数据库理论体系的核心问题——当两个事务同时读写同一份数据时,如何保证结果不出错。事务的原子性、一致性、隔离性和持久性共同构成了事务正确性的基本保障,而隔离性的实现,正是靠今天要深入展开的主题:封锁协议。
并发操作如果不加任何控制,会引发三类典型的不一致问题,任何一本数据库教材在讲到事务隔离时都会首先列出这三个场景。丢失修改发生在两个事务先后写入同一数据项,后写入的事务覆盖了前一个事务的修改,导致前一个事务的更新结果凭空消失。经典的例子是银行转账场景:事务 A 从账户余额中扣除了一笔转账金额,尚未提交;事务 B 读取余额并基于旧值进行了新的扣款操作并提交;事务 A 随后提交,将自己的扣款结果覆盖到数据库中,事务 B 的扣款操作结果便永久丢失了。这个场景揭示了写操作之间缺乏互斥保护时最直接的危害。
不可重复读指的是同一个事务内两次读取同一数据项,却得到了不同的值,原因是在两次读取之间另一个事务修改了该数据项并提交。典型的触发条件是事务需要先后多次读取同一数据来进行一致性校验,如果在两次读取之间别的事务修改了数据并提交,事务持有的数据快照便出现了裂痕。读脏数据则是一个事务读到了另一个未提交事务的中间结果,后者随后回滚,读到的数据便成了从未真正存在过的中间状态。在金融、库存管理等对数据准确性要求极高的场景中,读脏数据的危害尤其严重——基于未提交数据做出的业务决策,一旦数据回滚便完全失去依据。
这三类问题的共同根源在于并发事务之间的相互干扰。数据库系统需要一个机制来协调事务的读写顺序,让多个事务的执行结果等价于某种串行执行的顺序,这就是并发控制理论的核心目标——可串行化调度。封锁机制正是实现这一目标最经典、最广泛应用的手段,几乎所有主流关系型数据库的并发控制实现都建立在封锁理论之上。理解封锁协议,不仅是为了应对数据库考试中的并发控制题目,更是理解现代数据库内核的一次深度巡游。
封锁的基本操作只有两种:排他锁和共享锁。排他锁又称写锁,事务在对数据项进行修改操作之前必须先获得该数据项的排他锁,持有排他锁期间其他事务既不能读也不能写该数据项。共享锁又称读锁,事务在读取数据项之前需要先获得共享锁,持有共享锁期间其他事务可以继续加共享锁进行读取,但不能加排他锁进行写入。这两种基本锁类型之间的相容关系构成了封锁协议的基础矩阵:共享锁与共享锁相容,共享锁与排他锁不相容,排他锁与任何锁都不相容。这个简单而优雅的相容规则,是所有后续封锁理论推导的起点。
封锁本身只提供了互斥访问的原子操作,但光有锁还不够——何时加锁、何时释放锁,这些规则决定了并发控制的严格程度和系统吞吐量之间的平衡。于是就有了封锁协议的概念:一套明确定义的加锁和解锁规则集合,不同级别的协议对并发一致性的保护程度不同,对系统并发性能的影响也不同。封锁协议之所以重要,是因为它回答了并发控制设计中一个根本性的工程权衡问题:如何在保证数据正确性的前提下,尽可能少地牺牲系统的并发吞吐能力。下文将逐级展开封锁协议的三个层级,以及保证可串行化的两段锁协议。
三级封锁协议并非三个彼此独立互斥的方案,而是一个层层叠加、逐步增强的递进体系。每一级协议都在前一级的基础上增加新的约束,从而消除更多类型的并发不一致问题。理解这个递进关系,是软考命题中封锁协议题目的核心逻辑。
一级封锁协议的规则只有一条,却也是最基础的一条:事务在修改数据项之前必须先对其加排他锁,直到事务结束后才释放。规则规定了一个下限——修改操作必须全程持有写锁,锁的释放最早也要等到事务提交或回滚之后。
一级封锁协议可以解决丢失修改问题。原因很简单:当一个事务对某数据项加了排他锁并持续持有到事务结束,其他事务在整个期间都无法对该数据项加排他锁进行修改,自然就无法覆盖前一个事务的写入结果。丢失修改的本质是两个写操作之间的竞态条件,排他锁的全程持有恰好切断了这种竞态。从锁相容矩阵的角度分析,一个数据项上只能存在一个排他锁且排他锁与任何其他锁都不相容,因此只要写操作全程持有排他锁,就不会有第二个写操作同时进入。
但一级封锁协议无法解决不可重复读和读脏数据问题。读操作在一级封锁协议下不需要加任何锁,这意味着事务在读数据时完全不设防,其他事务可以在它读取的间隙修改或回滚数据,产生不一致的读取结果。一级封锁协议对读操作完全放任的策略,决定了它的保护范围止步于写写冲突,而无法触及读写冲突和写读冲突。在实际数据库系统中,一级封锁协议对应着最低的隔离级别,通常只在几乎没有并发读操作的批处理场景中单独使用。
二级封锁协议在一级的基础上增加了一条针对读操作的规则:事务在读取数据项之前必须先对其加共享锁,读完之后立即释放共享锁。这条规则堵上了一级封锁协议留下的读脏数据漏洞。
读脏数据的场景是这样的:事务 A 修改了某个数据项但尚未提交,事务 B 读取了这个修改后的值,随后事务 A 回滚,事务 B 读到的值便成了从未生效的中间状态。在二级封锁协议下,事务 A 修改数据时持有排他锁,事务 B 想要读取就需要先加共享锁,而共享锁与排他锁不相容——事务 B 的共享锁请求会被阻塞,直到事务 A 提交或回滚并释放排他锁之后才能获得。这样一来,事务 B 永远不可能读到未提交事务的中间数据。二级封锁协议通过给读操作加上短暂的共享锁保护,在读写冲突的层面上建立起了一道屏障。
但二级封锁协议仍然无法解决不可重复读问题。原因出在"读完之后立即释放"这条规则上。事务 B 第一次读取某数据项时加了共享锁,读完释放;之后另一个事务 C 修改了该数据项并提交;事务 B 再次读取时重新加共享锁,读到的是事务 C 修改后的新值。两次读取之间锁被释放了,数据的连续性保护也随之消失。这个机制上的漏洞是二级封锁协议与三级之间最关键的差异所在,也是软考命题中区分二级和三级封锁协议的核心考点。
三级封锁协议在二级的基础上修改了读锁的持有时间:事务在读取数据项之前加共享锁,但这次不再读完即放,而是一直持有到事务结束才释放。这个看似微小的改动,彻底解决了不可重复读问题。
当共享锁持有到事务结束,意味着在整个事务生命周期内,其他事务无法对该数据项加排他锁进行修改——共享锁与排他锁不相容,只要共享锁还在,排他锁就无法获取。事务 B 在整个执行期间持有的数据项上的共享锁,为它构筑了一道读取防线,保证了同一事务内多次读取的结果始终一致。从实现层面看,三级封锁协议相当于将读操作的保护周期拉长到了整个事务的生命跨度,这个设计决策的代价是降低了并发度,因为共享锁的长期持有会阻塞所有试图修改该数据项的事务。
三级封锁协议解决了丢失修改、不可重复读和读脏数据全部三类并发不一致问题,因此也被称为"最高级别的封锁协议"。需要特别注意的是,三级封锁协议虽然解决了三类并发不一致问题,但它并不直接等同于可串行化调度——三级封锁协议只保证单个数据项的读写一致性,而可串行化涉及多个数据项之间的操作顺序关系,这是两段锁协议要解决的问题。这个区别是软考中封锁协议相关题目的经典分界点:三级封锁协议管的是"一个数据项上的读写不出错",两段锁协议管的是"多个数据项之间的操作顺序能串行化"。
三级封锁协议解决了单数据项的隔离问题,但并发控制的终极目标不是逐项隔离,而是让并发事务的整体执行效果等价于某种串行顺序——这就是可串行化调度。实现可串行化调度最经典的手段,便是两段锁协议。
两段锁协议由数据库理论的先驱学者在二十世纪七十年代提出,它的核心理念是把每个事务的执行过程严格划分为两个阶段。第一个阶段称为增长阶段
本篇完!