一句话总结
InnoDB 锁机制分为行锁(Record Lock、Gap Lock、Next-Key Lock)和表锁(意向锁、AUTO-INC 锁)。默认使用 Next-Key Lock(行锁+间隙锁),在 RR 级别下解决幻读。加锁规则:等值查询唯一索引命中 → 退化为 Record Lock;等值查询未命中 → Gap Lock;范围查询 → Next-Key Lock。死锁排查用 SHOW ENGINE INNODB STATUS。
初级理解
InnoDB 锁分类
| 锁类型 | 粒度 | 加锁方式 | 冲突 |
| Record Lock(记录锁) | 单行记录 | 锁住索引记录 | S 与 X 互斥,S 与 S 兼容 |
| Gap Lock(间隙锁) | 索引间隙 | 锁住索引记录之间的间隙 | 间隙锁之间不冲突 |
| Next-Key Lock(临键锁) | 记录+间隙 | Record Lock + Gap Lock | InnoDB 默认行锁 |
| 意向锁(IS/IX) | 表级 | 加行锁前先加意向锁 | 表锁与意向锁冲突 |
| AUTO-INC 锁 | 表级 | 自增列插入时 | 插入完成后释放 |
共享锁(S)与排他锁(X)
# 共享锁(S Lock):允许其他事务读,不允许写
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; # MySQL 8.0 前
SELECT * FROM users WHERE id = 1 FOR SHARE; # MySQL 8.0+
# 排他锁(X Lock):不允许其他事务读和写
SELECT * FROM users WHERE id = 1 FOR UPDATE;
# 兼容矩阵
# | S | X |
# S | ✅ | ❌ |
# X | ❌ | ❌ |
一句话总结:InnoDB 默认 Next-Key Lock = Record Lock + Gap Lock,既锁行又锁间隙,解决幻读。
中级深入
加锁规则(丁奇总结)
两个原则、两个优化、一个 bug(MySQL 5.x):
原则1:加锁的基本单位是 Next-Key Lock(前开后闭区间)
原则2:查找过程中访问到的对象才会加锁
优化1:等值查询唯一索引命中 → Next-Key Lock 退化为 Record Lock
优化2:等值查询向右遍历到第一个不满足条件的值 → Next-Key Lock 退化为 Gap Lock
# 假设表 t,id 主键:1, 5, 10, 15
# 间隙:(负无穷,1], (1,5], (5,10], (10,15], (15,正无穷)
# 场景1:等值查询唯一索引命中 → Record Lock
SELECT * FROM t WHERE id = 5 FOR UPDATE;
# 只锁 id=5 这一行(Next-Key Lock 退化为 Record Lock)
# 场景2:等值查询未命中 → Gap Lock
SELECT * FROM t WHERE id = 7 FOR UPDATE;
# 锁住 (5, 10) 间隙(防止插入 id=6,7,8,9)
# 场景3:范围查询 → Next-Key Lock
SELECT * FROM t WHERE id >= 10 AND id < 15 FOR UPDATE;
# 锁住 (5,10] + (10,15](Next-Key Lock)
# 场景4:非唯一索引等值查询
# 假设 age 有索引,值:10, 20, 20, 30
SELECT * FROM t WHERE age = 20 FOR UPDATE;
# 锁住 age=20 的两行(Record Lock)
# + age=20 前后的间隙 (10,20) 和 (20,30)(Gap Lock)
# 防止插入 age=20 的新行
意向锁 — 表锁与行锁的桥梁
意向锁是表级锁,表示事务想要在表中的行上加锁。作用是让表锁(如 LOCK TABLES)不需要遍历每一行检查是否有行锁。
# 意向共享锁(IS):事务想要获取表中某些行的 S 锁
# 意向排他锁(IX):事务想要获取表中某些行的 X 锁
# 加行锁前自动加意向锁:
SELECT * FROM t WHERE id = 1 FOR UPDATE;
# 1. 先加表级 IX 锁
# 2. 再加行级 X 锁
# 表锁与意向锁兼容矩阵:
# | IS | IX | S | X |
# IS | ✅ | ✅ | ✅ | ❌ |
# IX | ✅ | ✅ | ❌ | ❌ |
# S | ✅ | ❌ | ✅ | ❌ |
# X | ❌ | ❌ | ❌ | ❌ |
中级要点:等值唯一命中 → Record Lock;等值未命中 → Gap Lock;范围查询 → Next-Key Lock;意向锁是表锁与行锁的桥梁。
高级拓展
死锁的四个条件
1. 互斥:资源只能被一个事务持有
2. 持有并等待:事务持有资源的同时等待其他资源
3. 不可剥夺:资源不能被强制释放
4. 循环等待:事务之间形成等待环
# 死锁示例
# 事务A:先锁 id=1,再锁 id=2
# 事务B:先锁 id=2,再锁 id=1
# 两个事务互相等待对方释放锁 → 死锁
# 事务A
BEGIN;
SELECT * FROM t WHERE id = 1 FOR UPDATE; # 锁 id=1
# 事务B 此时执行:
# BEGIN;
# SELECT * FROM t WHERE id = 2 FOR UPDATE; # 锁 id=2
SELECT * FROM t WHERE id = 2 FOR UPDATE; # 等待 id=2(事务B持有)
# 事务B 此时执行:
# SELECT * FROM t WHERE id = 1 FOR UPDATE; # 等待 id=1(事务A持有)
# → 死锁!InnoDB 检测到后回滚其中一个事务
死锁排查
# 1. 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS\G
# 找到 LATEST DETECTED DEADLOCK 部分
# 2. 开启死锁日志(MySQL 5.7+)
SET GLOBAL innodb_print_all_deadlocks = ON;
# 死锁信息会输出到 error log
# 3. 查看当前锁等待
SELECT * FROM information_schema.innodb_lock_waits; # MySQL 5.7
SELECT * FROM performance_schema.data_lock_waits; # MySQL 8.0
# 4. 查看当前持有的锁
SELECT * FROM performance_schema.data_locks; # MySQL 8.0
避免死锁的最佳实践
1. 按相同顺序访问资源:所有事务都先锁 id=1 再锁 id=2
2. 尽量缩短事务:减少锁持有时间
3. 使用低隔离级别:RC 级别没有 Gap Lock,死锁概率更低
4. 合理使用索引:避免锁升级为表锁
5. 一次锁定所有资源:用 SELECT ... FOR UPDATE 提前锁定
# 死锁预防:按主键顺序更新
# ❌ 容易死锁
UPDATE t SET val = val + 1 WHERE id IN (2, 1); # 加锁顺序不确定
# ✅ 按主键排序
UPDATE t SET val = val + 1 WHERE id IN (1, 2) ORDER BY id;
实战场景
场景:线上死锁排查
# 1. 查看死锁日志
SHOW ENGINE INNODB STATUS\G
# 关键信息解读:
# ------------------------
# LATEST DETECTED DEADLOCK
# ------------------------
# *** (1) TRANSACTION: # 事务1
# UPDATE t SET val=val+1 WHERE id=2 # 持有 id=2 的锁,等待 id=1
# *** (1) HOLDS THE LOCK(S):
# RECORD LOCKS ... # 持有的锁
# *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
# RECORD LOCKS ... # 等待的锁
# *** (2) TRANSACTION: # 事务2
# UPDATE t SET val=val+1 WHERE id=1 # 持有 id=1 的锁,等待 id=2
# *** WE ROLL BACK TRANSACTION (2) # InnoDB 回滚了事务2
# 2. 解决方案:统一加锁顺序
# 所有事务都按 id 升序加锁
UPDATE t SET val = val + 1 WHERE id IN (1, 2) ORDER BY id;
面试模拟
面试官:什么是 Next-Key Lock?
你:Next-Key Lock = Record Lock + Gap Lock,是 InnoDB 默认的行锁算法。它锁住索引记录和记录之间的间隙(前开后闭区间),在 RR 级别下解决幻读问题。等值查询唯一索引命中时会退化为 Record Lock。
面试官:Gap Lock 之间会冲突吗?
你:不会。Gap Lock 的唯一目的是防止其他事务在间隙中插入数据,多个事务可以在同一个间隙上加 Gap Lock,互不冲突。这与 Record Lock 不同,Record Lock 的 S 和 X 是互斥的。
面试官:如何排查和解决死锁?
你:排查用 SHOW ENGINE INNODB STATUS 查看死锁日志,找到互相等待的事务和 SQL。解决:1)统一加锁顺序(按主键排序);2)缩短事务时间;3)降低隔离级别到 RC(无 Gap Lock);4)死锁重试机制(捕获 DeadlockLoserDataAccessException 后重试)。