跳到主要内容

锁机制

问题

MySQL 有哪些锁?InnoDB 行锁是怎么实现的?什么是间隙锁和 Next-Key Lock?如何排查和解决死锁?

答案

锁分类全景

按粒度分类

全局锁

锁住整个数据库实例,所有表只读:

-- 加全局读锁
FLUSH TABLES WITH READ LOCK;

-- 解锁
UNLOCK TABLES;

典型场景:全库逻辑备份。但对 InnoDB 可以使用 mysqldump --single-transaction 利用 MVCC 实现一致性备份,无需全局锁。

表级锁

锁类型说明命令
表锁显式锁定整张表LOCK TABLES t READ/WRITE
元数据锁(MDL)防止 DDL 和 DML 并发冲突自动加锁,无需手动
意向锁表明事务打算对行加 S/X 锁InnoDB 自动加
自增锁(AUTO-INC)保证自增列值的连续分配INSERT 时自动加
MDL 锁导致的阻塞

执行 DML(SELECT/INSERT/UPDATE/DELETE)时会自动获取 MDL 读锁,执行 DDL(ALTER TABLE)时需要获取 MDL 写锁。如果有长事务持有 MDL 读锁,DDL 会被阻塞;而 DDL 的 MDL 写锁请求又会阻塞后续所有 DML,最终导致整张表不可用

线上执行 DDL 前应先检查是否有长事务,或使用 pt-online-schema-changegh-ost 等工具。

意向锁

意向锁是表级锁,InnoDB 自动维护,用于快速判断表中是否有行锁:

意向锁含义
IS(意向共享锁)事务打算对某些行加 S 锁
IX(意向排他锁)事务打算对某些行加 X 锁

意向锁之间互不冲突,它的作用是:当要加表级锁时,不需要逐行检查是否有行锁,只需检查意向锁即可。

兼容性矩阵:

ISIXSX
IS
IX
S
X

行级锁(InnoDB 特有)

InnoDB 的行锁是通过锁住索引记录来实现的,不是锁住数据行。如果查询没有命中索引,会退化为表锁

三种行锁算法

锁类型锁定范围示例(WHERE id = 10)作用
Record Lock(记录锁)锁定单条索引记录锁住 id=10 这条记录防止其他事务修改/删除该行
Gap Lock(间隙锁)锁定索引记录之间的间隙锁住 (5, 10) 间隙防止其他事务在间隙中插入
Next-Key LockRecord Lock + Gap Lock锁住 (5, 10]防止修改和插入,解决幻读
Next-Key Lock 是默认行为

在 RR 隔离级别下,InnoDB 默认使用 Next-Key Lock。但在以下情况会退化:

  • 等值查询唯一索引,命中记录 → 退化为 Record Lock(唯一性保证不会有间隙插入)
  • 等值查询,未命中记录 → 退化为 Gap Lock
  • 等值查询非唯一索引,命中记录 → Next-Key Lock + 下一个间隙的 Gap Lock

加锁规则详解

以索引值 5, 10, 15, 20 为例:

等值查询唯一索引,命中
-- 记录锁:只锁 id=10
SELECT * FROM t WHERE id = 10 FOR UPDATE;
等值查询唯一索引,未命中
-- 间隙锁:锁 (10, 15)
SELECT * FROM t WHERE id = 12 FOR UPDATE;
范围查询
-- Next-Key Lock:锁 (5, 10], (10, 15]
SELECT * FROM t WHERE id >= 10 AND id < 15 FOR UPDATE;
非唯一索引等值查询,命中
-- 假设 name 列有普通索引,值为 '张三' 的 id 是 10
-- Next-Key Lock:锁住命中记录前后的间隙
SELECT * FROM t WHERE name = '张三' FOR UPDATE;

共享锁与排他锁

共享锁(S Lock)——其他事务可以读,不能写
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;  -- MySQL 5.7
SELECT * FROM user WHERE id = 1 FOR SHARE; -- MySQL 8.0+
排他锁(X Lock)——其他事务不能读也不能写
SELECT * FROM user WHERE id = 1 FOR UPDATE;
操作加锁类型
SELECT ... LOCK IN SHARE MODE / FOR SHARES 行锁
SELECT ... FOR UPDATEX 行锁
INSERTX 行锁(插入意向锁 + 记录锁)
UPDATEX 行锁
DELETEX 行锁
普通 SELECT不加锁(MVCC 快照读)

死锁

死锁产生的条件

  1. 互斥:锁是排他的
  2. 持有并等待:持有一个锁的同时等待另一个锁
  3. 不可抢占:锁只能由持有者释放
  4. 循环等待:事务之间形成等待环路

死锁示例

死锁处理策略

InnoDB 提供两种死锁处理方式:

策略参数说明
死锁检测innodb_deadlock_detect = ON(默认)主动检测死锁环路,回滚代价最小的事务
锁等待超时innodb_lock_wait_timeout = 50(默认 50 秒)超时后回滚等待的事务
高并发场景的死锁检测开销

死锁检测的时间复杂度为 O(n²),在高并发热点行更新场景下(如秒杀),检测本身会消耗大量 CPU。可以考虑:

  1. 临时关闭死锁检测,依赖超时
  2. 将热点行拆分为多行,减少锁冲突
  3. 使用队列 + 串行处理

死锁排查

查看最近的死锁信息
SHOW ENGINE INNODB STATUS;
-- 查看 LATEST DETECTED DEADLOCK 部分
查看当前锁等待情况(MySQL 8.0+)
-- 查看当前持有的锁
SELECT * FROM performance_schema.data_locks;

-- 查看锁等待关系
SELECT * FROM performance_schema.data_lock_waits;
查看 InnoDB 锁信息(MySQL 5.7)
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

如何减少死锁

  1. 按固定顺序访问表和行:所有事务以相同的顺序操作资源
  2. 大事务拆小事务:减少锁持有时间
  3. 合理使用索引:避免行锁升级为表锁
  4. 尽量使用等值查询:减少 Next-Key Lock 的范围
  5. 降低隔离级别到 RC:RC 没有间隙锁,锁范围更小

常见面试问题

Q1: InnoDB 行锁是怎么实现的?

答案

InnoDB 的行锁是通过给索引记录加锁实现的,而不是给数据行加锁。这意味着:

  1. 只有通过索引条件查询时,InnoDB 才使用行级锁;否则退化为表锁
  2. 即使查询条件中有索引,如果 MySQL 优化器判断全表扫描更优,也不会使用索引,此时行锁退化为表锁

例如 UPDATE user SET name='test' WHERE age=25

  • 如果 age 列有索引 → 行锁(锁住 age=25 对应的索引记录)
  • 如果 age 列无索引 → 表锁(锁住所有记录)

Q2: 什么是间隙锁?它解决了什么问题?

答案

间隙锁(Gap Lock) 锁住索引记录之间的间隙(不包含记录本身),阻止其他事务在这个间隙中插入新记录。

它的核心目的是解决幻读:在 RR 隔离级别下,事务执行范围查询时,通过间隙锁锁住查询范围内的间隙,防止其他事务插入新行。

特点:

  • 只在 RR 隔离级别下存在,RC 级别没有间隙锁
  • 间隙锁之间不互斥(两个事务可以同时持有同一个间隙的 Gap Lock)
  • 间隙锁只阻止插入操作

Q3: Record Lock、Gap Lock、Next-Key Lock 的区别?

答案

以索引值 10, 15, 20 为例:

锁类型锁定范围说明
Record Lock单条记录,如 {10}精确锁定一条索引记录
Gap Lock记录间的间隙,如 (10, 15)开区间,不含两端记录
Next-Key Lock左开右闭区间,如 (10, 15]Record Lock + Gap Lock

Next-Key Lock 是 InnoDB 在 RR 级别下的默认行锁算法,它在等值查询命中唯一索引时会退化为 Record Lock(因为唯一性保证间隙中不会有重复值,不需要锁间隙)。

Q4: 什么情况下行锁会升级为表锁?

答案

严格说 InnoDB 没有锁升级机制(不像 SQL Server),但以下情况效果类似表锁:

  1. 查询条件没有命中索引:WHERE 条件的字段没有索引,InnoDB 会对聚簇索引的所有记录加 Next-Key Lock,效果等同于表锁
  2. 索引失效:查询条件虽然有索引,但因为类型转换、函数等原因导致索引失效
  3. 全表扫描:优化器认为全表扫描效率更高而放弃索引

因此,合理设计索引是控制锁粒度的关键

Q5: 如何分析和解决死锁?

答案

分析步骤

  1. 执行 SHOW ENGINE INNODB STATUS,查看 LATEST DETECTED DEADLOCK 部分
  2. 分析两个事务分别持有什么锁、等待什么锁
  3. 找出形成循环等待的原因(通常是访问行的顺序不一致)

解决方案

  1. 统一访问顺序:所有事务按照相同的顺序(如主键升序)访问行
  2. 减少锁范围:使用精确的索引条件,避免大范围的间隙锁
  3. 缩短事务:减少事务中的操作,降低锁持有时间
  4. 降低隔离级别:RC 级别没有间隙锁,死锁概率大幅降低
  5. 应用层重试:捕获死锁异常(错误码 1213),自动重试事务

相关链接