在数据库并发控制领域,两阶段锁协议(Two-Phase Locking,简称2PL)是保证事务隔离性和可串行化调度的经典方案。MySQL的InnoDB存储引擎在实现行级锁时,正是以两阶段锁协议为基础的。理解2PL,关键在于弄清楚一个事务内部什么时候加锁、什么时候释放锁,以及为什么要这样设计。本文将从协议原理、MySQL的具体实现、严格两阶段锁的区别以及实际优化技巧几个方面展开讲解。

两阶段锁协议的基本原理
两阶段锁协议把一个事务的生命周期划分为两个阶段:扩展阶段(Growing Phase)和收缩阶段(Shrinking Phase)。在扩展阶段,事务只能获取锁,不能释放任何锁;一旦事务释放了第一个锁,就进入收缩阶段,此后只能释放锁,不能再申请新的锁。这两个阶段之间的分界点,就是事务第一次释放锁的时刻。
这个看似简单的规则,却能带来一个非常重要的理论保证:如果所有事务都遵守两阶段锁协议,那么它们的并发执行调度一定是冲突可串行化的。也就是说,并发执行的结果等价于某个串行执行顺序的结果,数据的最终一致性得以保证。这是2PL在数据库理论中地位如此重要的根本原因。
可以用一个直观的对比来理解:假设事务T1先锁住了行A,接着锁住行B,然后立刻释放了行A的锁;此时事务T2获得了行A的锁并修改了数据。如果T1随后又去申请行C的锁,就破坏了两阶段约束。因为T2看到的行A是"半成品"事务T1修改过的状态,而T1又依赖后续的加锁操作继续推进,这种交错可能导致T1和T2都无法串行化等价,进而产生数据不一致。
MySQL中InnoDB的两阶段锁实现
InnoDB的行级锁遵循严格两阶段锁协议(Strict 2PL)。具体表现为:锁是在SQL语句执行过程中按需获取的,比如执行UPDATE语句时,被扫描到的行会被加上排他锁;但这些锁并不会在语句执行完毕后立刻释放,而是要等到整个事务提交或回滚时,才一次性统一释放。
可以用下面的例子观察这一行为。开启一个会话执行事务但先不提交:
-- 会话1 BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 1; -- 此时不提交,行id=1上的X锁一直保持 -- 会话2 UPDATE account SET balance = balance + 100 WHERE id = 1; -- 该语句会被阻塞,直到会话1提交或回滚
会话2的更新会一直等待,直到会话1执行COMMIT。这清楚地说明:InnoDB中行锁的加锁时机是语句执行时,而释放时机是事务结束时。锁持有的时间跨越了事务中后续所有语句的执行期间,这就是两阶段中的扩展阶段被拉长到事务末尾。
需要注意的是,两阶段锁协议描述的是锁协议的行为约束,与事务隔离级别是两个不同维度的概念。即使在READ COMMITTED隔离级别下,InnoDB的行锁依然遵循严格两阶段协议,区别只在于间隙锁的使用范围不同。READ COMMITTED下没有了大部分间隙锁,锁冲突会减少,但可串行化的保证范围也随之变化。
严格两阶段锁与普通两阶段锁的区别
理论上普通的2PL允许事务在收缩阶段逐步释放锁,即释放第一个锁之后就不再申请新锁,但已经持有的锁可以陆续放掉。这种方式的并发度更高,因为其他事务可以更早获得被释放的资源。但它也有明显缺陷:如果事务最终回滚,那些提前释放锁所做的修改就需要级联回滚其他已经读到中间状态的事务,带来级联回滚问题。
严格两阶段锁(S2PL)要求所有排他锁必须保持到事务提交或回滚之后才能释放。这样保证了事务提交前,它修改的数据对其他事务不可见、不可修改,从根本上避免了级联回滚。InnoDB采用的正是S2PL,牺牲了一部分并发性能,换取了更简单可靠的正确性保证。
此外还有一种更强的强严格两阶段锁(SS2PL),要求所有锁包括共享锁都保持到事务结束。这种方案在读写混合场景下锁持有时间更长,实际系统中较少完全采用。对比来看,InnoDB的选择是在正确性与并发度之间取得了工程上的平衡。
利用两阶段锁特性优化事务设计
理解了"锁在语句执行时获取、在事务提交时统一释放"这一规律,就能推导出一个非常实用的优化原则:如果一个事务中需要锁定多个资源,应该把锁冲突概率最高的操作尽量放在事务的后面执行。这样热点行被锁住的时间窗口最短,其他事务的等待时间也相应减少。
举个典型例子,电商下单场景中如果事务开头就去锁定库存行,然后再执行一系列耗时的校验和日志写入,那么库存这把热点锁会被持有整个事务周期,高并发下大量请求会阻塞在这把锁上。更合理的做法是:
BEGIN;
-- 先执行不影响热点资源的操作
INSERT INTO order_log(order_no, remark) VALUES ('20240101001', '创建订单');
-- 把热点行的更新放在事务最后
UPDATE product_stock SET stock = stock - 1 WHERE product_id = 100;
COMMIT;
-- 提交时才释放stock行的锁,等待窗口被压缩到最短
另一个常见的应用是死锁规避。由于所有事务都在事务结束时才放锁,如果两个事务以相反顺序加锁,就非常容易形成死锁。保持事务内按统一顺序访问表和行,配合把热点操作后置,可以显著降低死锁发生率。同时,事务本身也应尽量短小,避免在事务中夹杂远程调用、文件IO等耗时操作,因为这些操作会拉长锁的持有时间,放大两阶段锁协议带来的阻塞效应。
总结来说,两阶段锁协议是InnoDB实现并发控制的理论基石,它以"先全部加锁、后统一释放"的方式保证可串行化调度。理解加锁与释放锁的时机,不仅能帮助你看懂锁等待和死锁现象,更能指导你写出高并发下表现更好的事务代码。
MySQL两阶段锁协议2PL事务加锁机制修改时间:2026-09-02 14:02:41