跳到主要内容

锁机制

问题

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. 原则 1:加锁的基本单位是 Next-Key Lock
  2. 原则 2:查找过程中访问到的对象才加锁
  3. 优化 1:等值查询,唯一索引命中 → Next-Key Lock 退化为 Record Lock
  4. 优化 2:等值查询,最后一个不满足条件的记录 → Next-Key Lock 退化为 Gap Lock
  5. 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 加锁比较特殊:

  1. 先检查插入位置是否有 Gap Lock → 有则等待
  2. 插入意向锁(一种特殊的 Gap Lock,不与其他插入意向锁冲突)
  3. 插入成功后,在新记录上加 Record Lock

插入意向锁之间互相兼容,所以多个事务可以同时向同一间隙中插入不同的记录。


相关链接