{T}

工作面试老大难 —— 锁

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 UPDATEUPDATEDELETEINSERT
  • 行锁基于索引记录加锁:没有索引的 WHERE 条件会退化为锁全表(所有记录 + 间隙)。

3. 三种区间锁(面试核心)

以索引记录 10, 11, 13, 20 为例:

锁定范围目的使用场景
记录锁单个索引记录(如 11)行级互斥主键/唯一键等值查询
间隙锁记录之间的开放区间(如 (11,13)),不含记录本身阻止间隙内插入等值查询但记录不存在;范围查询
临键锁记录 + 前一个间隙(如 (11,13])防幻读(阻止插入到区间)RR 级别范围查询/扫描

关键规则

  1. 临键锁 = 记录锁 + 间隙锁,是 RR 级别默认的"范围锁";
  2. 间隙锁之间不冲突:两个事务可以同时持有同一间隙的间隙锁(它们的冲突对象是"插入者");
  3. RC 级别binlog_format=ROW 时)关闭间隙锁,只保留记录锁——牺牲防幻读换并发;
  4. 唯一键等值查询:记录存在 → 只加记录锁(临键锁退化为记录锁,因为唯一确定);记录不存在 → 加间隙锁。
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 段

排查步骤

  1. sys.innodb_lock_waits 找到阻塞者与等待者(及各自的 SQL);
  2. SHOW PROCESSLIST 确认事务状态(trx_query 显示当前 SQL);
  3. 对阻塞者:KILL <trx_mysql_thread_id> 或等待其提交;
  4. 根治:优化 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 或等提交。