一句话定义
两阶段锁(2PL,Two-Phase Locking)规定事务必须先完成全部加锁再统一放锁,以此换取可串行化;其代价是死锁(Deadlock)成为常态事件,需要检测算法与工程治理。
为什么重要
- 锁是「串行化」与「吞吐」的交换器:理解锁的粒度与阶段,才能解释为什么把热点行挪出事务能立竿见影提性能。
- 死锁不是异常而是并发设计的反馈信号:
SHOW ENGINE INNODB STATUS里的 LATEST DETECTED DEADLOCK 是最重要的现场证据。 - 间隙锁是 MySQL RR 级抑制幻读的独门武器,也是插入死锁高发的元凶,跨库迁移必须知晓。
前置知识
kp-015(隔离级别与并发异常)。
核心概念
- 共享锁(S)/ 排他锁(X):读共享、写独占;兼容矩阵 S-S 兼容,其余互斥。
- 两阶段:扩展阶段只加不放、收缩阶段只放不加;严格 2PL(S2PL)把 X 锁持有到提交。
- 锁粒度:行锁、页锁、表锁、意向锁(IS/IX,表级「声明」行级意图)。
- 间隙锁(Gap Lock)与临键锁(Next-Key Lock):锁定索引记录之间的区间,阻止插入。
- 死锁:环等待(T1 等 T2 持有的锁、T2 反之)。
- 检测与解除:等待图找环、选「代价最小」的牺牲者回滚;或超时(
innodb_lock_wait_timeout)放弃。
原理与机制
2PL 证明可串行化的直觉:若两事务访问冲突项的加锁区间不重叠,则它们在真实时间上必然一先一后,可串行化序即加锁序。严格 2PL 把锁持有到 COMMIT,保证不出现级联回滚(读到的数据必是已提交的)。死锁检测在 InnoDB 中是活跃的:每次加锁等待都检查等待图,发现环即回滚代价小的事务并返回错误 1213,应用必须重试。间隙锁只存在于 MySQL RR:SELECT ... FOR UPDATE WHERE id BETWEEN 10 AND 20 不仅锁住存在的行,还锁住 (10,20) 的空隙,其他事务无法在该区间 INSERT——这就是幻读被抑制的机制,也是两个事务先后锁相邻区间再互相插入时的经典死锁模板。锁的获取顺序纪律(按固定顺序访问资源、事务尽量短、一次锁定所需全部资源)是预防死锁的三板斧。
实例或案例
经典 AB-BA 死锁:
-- T1: UPDATE account SET ... WHERE user_id = 1; 再 WHERE user_id = 2;
-- T2: UPDATE account SET ... WHERE user_id = 2; 再 WHERE user_id = 1;
-- 两事务各持一锁互等 → InnoDB 检测到环,回滚代价小者(错误 1213)
治理:统一所有代码路径按 user_id 升序加锁(T2 改为先 1 后 2),死锁即刻消失。批量导入场景同理:多线程按主键排序后加锁,避免乱序交叉。
常见误区
- 认为死锁应「彻底消灭」:目标是把频率压到可重试水平;应用侧对 1213 做指数退避重试是标配而非补丁。
- 把
SELECT当无锁操作:普通快照读确实无锁,但FOR UPDATE/FOR SHARE与 DML 是排他锁,混用语境不清就会误解阻塞来源。 - 用表锁「简单粗暴防并发」:表锁把并发度打到 1,行级意图锁机制存在就是为了表级与行级共存。
自测题
- 为什么严格 2PL 能避免级联回滚? 答:X 锁持有到提交意味着他人读到的修改必然来自已提交事务,任何事务回滚都不会牵连已读取它数据的事务。
- 间隙锁在 RC 级下存在吗? 答:MySQL InnoDB 在 RC 下基本不使用间隙锁(外键与唯一性检查除外),这是 RC 并发更好、但幻读回归的原因。
公式或模型
死锁判定:等待图 G=(V,E),V 为事务、E 为等待边;存在环 ⟺ 死锁。检测复杂度 O(V+E),InnoDB 在锁等待时增量执行。
图示
直观类比
两阶段锁像搬家规则:所有纸箱(资源)都要在出门前一次性封好钥匙、路上不再开箱拿东西;若两口子各拿一把钥匙却都需要对方手里的箱子,就僵住了——按「卧室→客厅→厨房」固定顺序装箱,永远不会僵持。
与其他知识点的关系
kp-017 是避免读锁的乐观路线;kp-015 的幻读抑制依赖间隙锁;kp-022 的 latch(内存闩锁)与事务锁是不同层次的并发结构。
延伸阅读
- 《高性能 MySQL》(Baron Schwartz 等)锁与死锁章节
- 《数据库系统概念》(Abraham Silberschatz 等)并发控制章节