锁机制
问题
MySQL 有哪些锁?行锁、间隙锁、Next-Key Lock 分别是什么?死锁如何排查和预防?
答案
一、锁的分类全景
二、按粒度分类
| 锁类型 | 粒度 | 并发度 | 死锁 | 引擎 |
|---|---|---|---|---|
| 表锁 | 锁整张表 | 低 | 不会 | MyISAM |
| 行锁 | 锁单行 | 高 | 可能 | InnoDB |
| 页锁 | 锁数据页 | 中 | 可能 | BDB(已弃用) |
InnoDB 的行锁是通过 在索引上加锁 实现的。如果 SQL 没有走索引,行锁会退化成表锁。
行锁退化为表锁
-- 假设 name 列没有索引
UPDATE users SET age = 30 WHERE name = '张三';
-- InnoDB 需要全表扫描 → 对每一行都加锁 → 等同于表锁
这是生产环境常见的性能问题根源。UPDATE/DELETE 一定要确保 WHERE 条件走索引。
三、共享锁与排他锁
| 锁模式 | 简写 | 说明 | 兼容性 |
|---|---|---|---|
| 共享锁 (S) | Shared | 读锁,多个事务可同时持有 | S 与 S 兼容 |
| 排他锁 (X) | Exclusive | 写锁,独占 | X 与任何锁不兼容 |
-- 加共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- MySQL 8.0 之前
SELECT * FROM users WHERE id = 1 FOR SHARE; -- MySQL 8.0+
-- 加排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
兼容矩阵:
| S 锁 | X 锁 | |
|---|---|---|
| S 锁 | ✅ 兼容 | ❌ 冲突 |
| X 锁 | ❌ 冲突 | ❌ 冲突 |
四、意向锁(表级)
意向锁是 InnoDB 自动添加的 表级锁,用于快速判断表中是否有行锁:
- 意向共享锁 (IS):事务准备给某行加 S 锁前,先在表上加 IS
- 意向排他锁 (IX):事务准备给某行加 X 锁前,先在表上加 IX
有了意向锁,加表锁时不需要逐行检查是否有行锁。
五、InnoDB 行锁的三种算法
这是 MySQL 锁机制的 最核心内容:
1. Record Lock(记录锁)
锁住索引上的 一条记录。
-- 精确命中索引记录
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 锁住 id=5 这一条记录
2. Gap Lock(间隙锁)
锁住索引记录之间的 间隙,防止其他事务在间隙中插入新行。间隙锁之间不冲突,它们只阻塞 INSERT。
假设表中有 id = 5, 10, 15:
SELECT * FROM users WHERE id = 8 FOR UPDATE;
-- id=8 不存在,锁住间隙 (5, 10)
-- 其他事务无法在这个范围内 INSERT
3. Next-Key Lock(临键锁)
Record Lock + Gap Lock 的组合,锁住 记录 + 记录前面的间隙。这是 InnoDB 在 RR 隔离级别下的 默认行锁算法。
假设表中有 id = 5, 10, 15,Next-Key Lock 的锁定范围:
(-∞, 5] (5, 10] (10, 15] (15, +∞)
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 加 Next-Key Lock:锁住 (5, 10] 这个范围
加锁规则总结
InnoDB 加锁规则(丁奇总结版)
- 原则 1:加锁的基本单位是 Next-Key Lock
- 原则 2:查找过程中访问到的对象才加锁
- 优化 1:等值查询,唯一索引命中 → Next-Key Lock 退化为 Record Lock
- 优化 2:等值查询,最后一个不满足条件的记录 → Next-Key Lock 退化为 Gap Lock
- Bug(已修复):唯一索引的范围查询会访问到不满足条件的第一个值
六、死锁
死锁产生的条件
- 互斥:锁是排他的
- 持有并等待:持有一把锁的同时请求另一把锁
- 不可抢占:锁不能被强制释放
- 循环等待:A 等 B,B 等 A
死锁示例
-- 事务 A -- 事务 B
BEGIN; BEGIN;
UPDATE users SET age=20 UPDATE orders SET status='paid'
WHERE id=1; -- 锁住 users.id=1 WHERE id=1; -- 锁住 orders.id=1
UPDATE orders SET status='done' UPDATE users SET age=25
WHERE id=1; -- 等待 orders.id=1 WHERE id=1; -- 等待 users.id=1
-- 💀 死锁! -- 💀 死锁!
死锁处理机制
InnoDB 有两种策略:
| 策略 | 配置 | 说明 |
|---|---|---|
| 超时回滚 | innodb_lock_wait_timeout=50(秒) | 等待超时后回滚 |
| 死锁检测 | innodb_deadlock_detect=ON(默认) | 主动检测死锁,回滚代价最小的事务 |
-- 查看最近的死锁信息
SHOW ENGINE INNODB STATUS\G
-- 查看 LATEST DETECTED DEADLOCK 部分
死锁预防
| 方法 | 说明 |
|---|---|
| 固定加锁顺序 | 所有事务以相同顺序访问表和行 |
| 缩小事务范围 | 减少事务持锁时间 |
| 合理使用索引 | 避免行锁升级为表锁 |
| 降低隔离级别 | RC 级别没有间隙锁,死锁概率降低 |
常见面试问题
Q1: 乐观锁和悲观锁的区别?
答案:
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 思路 | 先加锁再操作 | 不加锁,提交时检查冲突 |
| MySQL 实现 | SELECT ... FOR UPDATE | 版本号 / CAS |
| 适用场景 | 写多读少、冲突频繁 | 读多写少、冲突少 |
-- 悲观锁
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
UPDATE products SET stock = stock - 1 WHERE id = 1;
-- 乐观锁(版本号机制)
SELECT stock, version FROM products WHERE id = 1;
-- 假设读到 stock=10, version=3
UPDATE products SET stock = 9, version = 4
WHERE id = 1 AND version = 3; -- 版本号不匹配则更新失败
Q2: 如何分析加锁过程?
答案:
-- 1. 开启 InnoDB 锁监控
SET GLOBAL innodb_status_output_locks = ON;
-- 2. 查看当前锁等待
SELECT * FROM performance_schema.data_lock_waits;
-- 3. 查看当前持有的锁
SELECT * FROM performance_schema.data_locks;
-- 4. 查看 InnoDB 状态
SHOW ENGINE INNODB STATUS\G
Q3: INSERT 语句的加锁过程?
答案:
INSERT 加锁比较特殊:
- 先检查插入位置是否有 Gap Lock → 有则等待
- 加 插入意向锁(一种特殊的 Gap Lock,不与其他插入意向锁冲突)
- 插入成功后,在新记录上加 Record Lock
插入意向锁之间互相兼容,所以多个事务可以同时向同一间隙中插入不同的记录。