工作面试老大难 —— 锁
0. 引言
锁是数据库并发控制的核心,也是面试的"老大难":既有 S/X 锁的兼容矩阵,又有间隙锁/临键锁的区间语义,还有自增锁、元数据锁这些"边角料"。本文按"类型 → 兼容矩阵 → 加锁流程 → 监控排查"的脉络,一次讲透 InnoDB 锁体系(基于 MySQL 8.0)。
1. 锁粒度与类型总览
图表渲染中…
2. 行级锁:S/X 与兼容矩阵
| S(共享) | X(排他) | |
|---|---|---|
| S | ✅ 兼容(读读可并发) | ❌ 冲突 |
| X | ❌ 冲突 | ❌ 冲突 |
- S 锁:
SELECT ... LOCK IN SHARE MODE(8.0 也可用SELECT ... FOR SHARE); - X 锁:
SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT; - 行锁基于索引记录加锁:没有索引的 WHERE 条件会退化为锁全表(所有记录 + 间隙)。
3. 三种区间锁(面试核心)
以索引记录 10, 11, 13, 20 为例:
| 锁 | 锁定范围 | 目的 | 使用场景 |
|---|---|---|---|
| 记录锁 | 单个索引记录(如 11) | 行级互斥 | 主键/唯一键等值查询 |
| 间隙锁 | 记录之间的开放区间(如 (11,13)),不含记录本身 | 阻止间隙内插入 | 等值查询但记录不存在;范围查询 |
| 临键锁 | 记录 + 前一个间隙(如 (11,13]) | 防幻读(阻止插入到区间) | RR 级别范围查询/扫描 |
关键规则:
- 临键锁 = 记录锁 + 间隙锁,是 RR 级别默认的"范围锁";
- 间隙锁之间不冲突:两个事务可以同时持有同一间隙的间隙锁(它们的冲突对象是"插入者");
- RC 级别(
binlog_format=ROW时)关闭间隙锁,只保留记录锁——牺牲防幻读换并发; - 唯一键等值查询:记录存在 → 只加记录锁(临键锁退化为记录锁,因为唯一确定);记录不存在 → 加间隙锁。
sql
-- 例 1:主键等值且记录存在 → 仅记录锁
SELECT * FROM t WHERE id = 11 FOR UPDATE;
-- 例 2:主键等值但记录不存在 → 间隙锁(id=15 不存在,锁 (13,20) 间隙)
SELECT * FROM t WHERE id = 15 FOR UPDATE;
-- 例 3:范围查询 → 临键锁 (10,11] (11,13] (13,20] (20,∞)
SELECT * FROM t WHERE id > 10 FOR UPDATE;4. 意向锁与表级锁
- 意向锁(IS/IX):事务想对表内行加锁前,先对表加意向锁——表级"意图声明",让表锁判断无需逐行检查;
- 兼容性:IS 与 IX 互相兼容(行锁互不阻塞),但与表级 S/X 冲突(
LOCK TABLES场景); - 元数据锁(MDL):DML 自动持有 MDL 共享锁,DDL 需要 MDL 排他锁 → 长事务阻塞 DDL 的根源(
Waiting for table metadata lock); - 自增锁(AUTO-INC):
innodb_autoinc_lock_mode=2(8.0 默认交错模式)——只保证单语句内自增值连续,statement 格式 binlog 下主从自增值可能不一致(→ 8.0 建议binlog_format=ROW)。
5. 加锁流程与一致读
图表渲染中…
- 两阶段锁(2PL):锁在事务中"随用随加",统一在提交/回滚时释放——因此事务内最后一条语句加锁后尽快提交,能缩短持锁时间;
- 快照读不加锁(普通 SELECT 走 MVCC),只有当前读(FOR UPDATE/LOCK IN SHARE MODE/DML)才加锁。
6. 锁监控与排查(8.0)
sql
-- 8.0 性能表:当前持有/等待的锁
SELECT * FROM performance_schema.data_locks\G
SELECT * FROM performance_schema.data_lock_waits\G
-- 阻塞关系查询(配合 sys 库)
SELECT * FROM sys.innodb_lock_waits\G
-- 经典:死锁信息
SHOW ENGINE INNODB STATUS\G -- LATEST DETECTED DEADLOCK 段排查步骤:
sys.innodb_lock_waits找到阻塞者与等待者(及各自的 SQL);SHOW PROCESSLIST确认事务状态(trx_query显示当前 SQL);- 对阻塞者:
KILL <trx_mysql_thread_id>或等待其提交; - 根治:优化 SQL(走索引)、缩短事务、统一加锁顺序。
7. 面试高频问答题
Q1:RR 级别下 SELECT ... FOR UPDATE 一定加临键锁吗?
不一定。主键/唯一键等值且记录存在 → 退化为记录锁;记录不存在 → 间隙锁;范围/全表扫描 → 临键锁。
Q2:间隙锁冲突吗? 间隙锁之间互相兼容,间隙锁与插入意向锁冲突(间隙被锁时无法插入)。
Q3:为什么 RC 级别下 binlog 要 ROW 格式? RC 关闭间隙锁 → 可能幻读 → statement 格式 binlog 回放结果与主库不一致;ROW 格式记录行变更,与隔离级别无关。
Q4:如何避免死锁? 统一加锁顺序、短事务、走索引、RC 级别、捕获 1213 重试(详见《深入理解事务与锁机制》)。
8. 小结
- 锁体系两条线:行级(S/X + 记录/间隙/临键) 与 表级(意向锁/MDL/自增锁);
- 临键锁是 RR 防幻读的武器,RC 关闭间隙锁换并发(必须 ROW binlog);
- 加锁路径依赖索引:无索引 UPDATE = 全表锁,生产大忌;
- 排查用 8.0 的
performance_schema.data_locks/sys.innodb_lock_waits,定位阻塞后 KILL 或等提交。