锁机制

锁是数据库并发控制的核心,理解锁机制是掌握 MySQL 的关键。

锁的基本概念

什么是锁?

锁是用来管理对共享资源的并发访问的一种机制。在数据库中,锁用于:

  • 保证数据一致性:防止多个事务同时修改同一数据

  • 实现隔离性:控制事务之间的可见性

  • 协调并发访问:在并发和一致性之间取得平衡

为什么需要锁?

-- 场景:两个事务同时给账户加 100 元
-- 初始余额:1000 元

-- 事务 A                        事务 B
BEGIN;                           BEGIN;
SELECT balance FROM account      
WHERE id = 1;  -- 读到 1000
                                 SELECT balance FROM account 
                                 WHERE id = 1;  -- 读到 1000
UPDATE account SET balance = 1100
WHERE id = 1;  -- 1000 + 100
                                 UPDATE account SET balance = 1100
                                 WHERE id = 1;  -- 1000 + 100
COMMIT;                          COMMIT;

-- 结果:余额是 1100,而不是 1200!(丢失更新)

使用锁后


锁的分类

MySQL 的锁可以从三个维度分类:

1. 按锁的粒度分类

1.1 全局锁(Global Lock)

加锁方式

特点

  • 锁定整个数据库实例

  • 所有表都变成只读

  • 其他线程的写操作、DDL 操作都会被阻塞

使用场景

  • 全库逻辑备份(mysqldump)

  • 不推荐在生产环境使用

更好的替代方案

1.2 表级锁

1.2.1 表锁(Table Lock)

加锁方式

读锁(共享锁)特点

  • 当前会话:可以读,不能写

  • 其他会话:可以读,写会阻塞

写锁(排他锁)特点

  • 当前会话:可以读写

  • 其他会话:读写都会阻塞

锁兼容性矩阵

读锁
写锁

读锁

✅ 兼容

❌ 互斥

写锁

❌ 互斥

❌ 互斥

示例

1.2.2 元数据锁(MDL,Metadata Lock)

特点

  • MySQL 5.5 引入

  • 自动加锁,无需显式使用

  • 保护表结构的一致性

加锁规则

MDL 锁兼容性

MDL 读锁
MDL 写锁

MDL 读锁

✅ 兼容

❌ 互斥

MDL 写锁

❌ 互斥

❌ 互斥

经典问题:长事务持有 MDL 读锁导致 DDL 阻塞

原因

  1. 会话 A 持有 MDL 读锁

  2. 会话 B 的 DDL 等待 MDL 写锁(被 A 阻塞)

  3. 会话 C 的查询也要申请 MDL 读锁,但排在 B 后面,所以也被阻塞

解决方案

  1. 避免长事务

  2. 在业务低峰期执行 DDL

  3. 使用 ALTER TABLE ... NOWAITWAIT N(MySQL 8.0.1+):

1.2.3 意向锁(Intention Lock)

作用

  • 表明事务想要在表的某些行上加锁

  • 协调表锁和行锁的冲突

  • 提高加表锁的效率

类型

  • 意向共享锁(IS):表明事务想要对某些行加共享锁

  • 意向排他锁(IX):表明事务想要对某些行加排他锁

加锁规则

为什么需要意向锁?

没有意向锁时,加表锁需要:

有意向锁时,加表锁只需:

锁兼容性矩阵

IS
IX
S
X

IS

IX

S

X

注意:意向锁之间兼容,不会互相阻塞。

1.2.4 AUTO-INC 锁

作用:保证自增主键的连续性

加锁机制

参数控制innodb_autoinc_lock_mode

  • 0(traditional):传统模式

    • 所有 INSERT 都用表级 AUTO-INC 锁

    • 语句执行完才释放

    • 并发性能差

  • 1(consecutive,默认):连续模式

    • 简单插入(INSERT):使用轻量级互斥量

    • 批量插入(INSERT ... SELECT):使用表级锁

    • 保证连续性,性能较好

  • 2(interleaved):交错模式

    • 所有 INSERT 都用轻量级互斥量

    • 不保证连续性(ID 可能有空洞)

    • 并发性能最好

    • binlog 必须是 row 格式

2. 按锁的模式分类

2.1 共享锁(Shared Lock,S 锁)

特点

  • 也叫读锁

  • 多个事务可以同时持有

  • 持有 S 锁的事务可以读数据

  • 阻止其他事务获得 X 锁

加锁方式

2.2 排他锁(Exclusive Lock,X 锁)

特点

  • 也叫写锁

  • 只有一个事务可以持有

  • 持有 X 锁的事务可以读写数据

  • 阻止其他事务获得 S 锁和 X 锁

加锁方式

锁兼容性

S 锁
X 锁

S 锁

✅ 兼容

❌ 互斥

X 锁

❌ 互斥

❌ 互斥

3. 按锁的算法分类(InnoDB 行锁)

这是 MySQL 锁机制的核心,也是面试的重点

3.1 记录锁(Record Lock)

定义:锁定单个索引记录。

特点

  • 总是锁定索引记录,而不是数据行本身

  • 如果表没有索引,InnoDB 会创建隐藏的聚簇索引

示例

在 EXPLAIN 中的表现

3.2 间隙锁(Gap Lock)

定义:锁定索引记录之间的"间隙",但不包括记录本身。

作用

  • 防止其他事务在间隙中插入数据

  • 防止幻读的关键机制

特点

  • 只在 REPEATABLE READ 隔离级别下有效

  • READ COMMITTED 级别下没有间隙锁

  • 间隙锁之间不互斥(都是为了防止插入)

示例

间隙锁不互斥的例子

3.3 临键锁(Next-Key Lock)

定义:记录锁 + 间隙锁,锁定一个左开右闭区间 (a, b]

特点

  • 这是 InnoDB 的默认行锁算法

  • REPEATABLE READ 隔离级别下的标配

  • 彻底解决幻读问题

示例

临键锁的组成

为什么是左开右闭?

  • 为了避免扫描时的重复加锁

  • 简化加锁逻辑

3.4 插入意向锁(Insert Intention Lock)

定义:一种特殊的间隙锁,在插入时加的锁。

特点

  • 插入意向锁之间不互斥

  • 但会被间隙锁阻塞

示例


InnoDB 行锁详解

行锁的本质

核心原则:InnoDB 的行锁是加在索引上的,而不是加在数据行上。

不同索引类型的加锁

1. 主键索引(聚簇索引)

2. 唯一索引

3. 普通索引

4. 无索引

优化建议

  • ✅ 必须为 WHERE 条件的列建立索引

  • ✅ 避免全表扫描锁全表


加锁规则分析

这是面试的重点,必须掌握!

加锁规则总结

两个原则

  1. 加锁的基本单位是 Next-Key Lock(临键锁)

  2. 查找过程中访问到的对象才会加锁

两个优化

  1. 等值查询,如果找到了唯一记录,Next-Key Lock 退化为 Record Lock

  2. 等值查询,没有找到记录,Next-Key Lock 退化为 Gap Lock

一个 bug(MySQL 8.0 已修复):

  • 唯一索引的范围查询会访问到不满足条件的第一个值为止

场景 1:唯一索引等值查询(记录存在)

加锁分析

  1. 查询使用主键索引

  2. 等值查询,找到了 id = 10 的记录

  3. 优化 1:Next-Key Lock 退化为 Record Lock

加锁结果

  • 主键索引:id = 10 的记录锁(X 锁)

验证

场景 2:唯一索引等值查询(记录不存在)

加锁分析

  1. 查询使用主键索引

  2. 等值查询,没有找到 id = 7 的记录

  3. 扫描到 id = 10(第一个大于 7 的记录)

  4. 优化 2:Next-Key Lock 退化为 Gap Lock

加锁结果

  • 主键索引:间隙锁 (5, 10)

验证

场景 3:唯一索引范围查询

加锁分析

  1. 查询使用主键索引

  2. 范围查询,扫描 id = 10, 15

  3. 对扫描到的记录加 Record Lock 及 Gap Lock

加锁结果

  • 主键索引:

    • 10 的记录锁

    • (10, 15) 的间隙锁

验证

场景 4:非唯一索引等值查询

加锁分析

  1. 查询使用二级索引 c

  2. 等值查询,找到 c = 10 的记录

  3. 对 c 索引加 Next-Key Lock

  4. 继续向右扫描,直到第一个不满足条件的记录

  5. 对主键索引加记录锁(回表)

加锁结果

  • c 索引:

    • (5, 10] 的临键锁

    • (10, 15) 的间隙锁

  • 主键索引:

    • id = 10 的记录锁

为什么要锁 (10, 15) 的间隙?

  • 防止其他事务插入 c = 10 的新记录

  • 保证可重复读

验证

场景 5:非唯一索引范围查询

加锁分析

  1. 查询使用二级索引 c

  2. 范围查询,扫描 c = 10, 15

  3. 对 c 索引加 Next-Key Lock

  4. 对主键索引加记录锁

加锁结果

  • c 索引:

    • (5, 10] 的临键锁

    • (10, 15] 的临键锁

  • 主键索引:

    • id = 10, 15 的记录锁

场景 6:无索引条件

加锁分析

  1. 全表扫描

  2. 所有记录都加 Next-Key Lock

  3. 退化为表锁

加锁结果

  • 主键索引:所有记录的 Next-Key Lock

验证

优化建议

  • 必须为 WHERE 条件的列建立索引

  • ❌ 避免无索引条件的加锁查询

加锁规则速查表

索引类型
查询类型
记录是否存在
加锁情况

唯一索引

等值查询

存在

记录锁

唯一索引

等值查询

不存在

间隙锁

唯一索引

范围查询

-

临键锁

普通索引

等值查询

-

临键锁 + 间隙锁

普通索引

范围查询

-

临键锁

无索引

任何查询

-

全表锁


死锁

什么是死锁?

死锁是指两个或多个事务互相持有对方需要的锁,导致所有事务都无法继续执行。

死锁的四个必要条件

  1. 互斥条件:资源不能被多个事务同时使用

  2. 请求与保持条件:事务持有资源的同时请求新资源

  3. 不可剥夺条件:已获得的资源不能被强制剥夺

  4. 循环等待条件:存在循环等待链

死锁案例 1:简单的两个事务

死锁图示

死锁案例 2:间隙锁与插入意向锁

原因

  • 间隙锁之间不互斥

  • 但插入意向锁会被间隙锁阻塞

  • 两个事务都持有间隙锁,都想插入,形成循环等待

死锁检测与处理

1. 死锁检测

参数innodb_deadlock_detect

检测机制

  • InnoDB 有专门的死锁检测线程

  • 当事务等待锁时,检测是否形成循环等待

  • 时间复杂度:O(n),n 是事务数

高并发下的问题

  • 死锁检测本身消耗 CPU

  • 1000 个并发事务,检测一次需要 100 万次计算

  • 可能导致 CPU 使用率飙升

2. 超时机制

参数innodb_lock_wait_timeout

机制

  • 如果等待锁的时间超过设置值,事务回滚

  • 不够智能(可能回滚了重要的事务)

3. 死锁处理策略

InnoDB 的处理

  1. 检测到死锁

  2. 选择回滚代价最小的事务

  3. 回滚整个事务

  4. 释放该事务持有的所有锁

  5. 其他事务继续执行

回滚代价计算

  • 持有行级写锁最少的事务

  • 事务权重(可通过 innodb_trx.trx_weight 查看)

4. 查看死锁信息

死锁日志示例

如何避免死锁

1. 按相同顺序访问资源

2. 尽量使用索引访问数据

3. 减小事务粒度,缩短事务时间

4. 使用较低的隔离级别

5. 为表添加合理的索引

6. 避免大事务

7. 使用 SELECT ... FOR UPDATE 要慎重


锁的监控与诊断

1. 查看当前锁等待

MySQL 5.7

MySQL 8.0+

2. 查看详细的锁信息

3. 分析锁等待时间

4. 杀死阻塞的会话


面试高频问题

Q1: MySQL 有哪些锁?

回答要点

按粒度分

  • 全局锁:FLUSH TABLES WITH READ LOCK

  • 表级锁:表锁、MDL、意向锁、AUTO-INC 锁

  • 行级锁:记录锁、间隙锁、临键锁

按模式分

  • 共享锁(S 锁):LOCK IN SHARE MODE / FOR SHARE

  • 排他锁(X 锁):FOR UPDATE

按算法分(InnoDB 行锁):

  • 记录锁:锁单个索引记录

  • 间隙锁:锁索引记录之间的间隙

  • 临键锁:记录锁 + 间隙锁,左开右闭区间

Q2: 什么是间隙锁和临键锁?

间隙锁(Gap Lock)

  • 锁定索引记录之间的"间隙"

  • 防止其他事务在间隙中插入数据

  • 防止幻读

  • 只在 RR 隔离级别下有效

  • 间隙锁之间不互斥

临键锁(Next-Key Lock)

  • 记录锁 + 间隙锁

  • 锁定一个左开右闭区间 (a, b]

  • InnoDB 的默认行锁算法

  • 彻底解决幻读问题

示例

Q3: 不同 SQL 语句如何加锁?

唯一索引等值查询

唯一索引范围查询

非唯一索引查询

无索引查询

Q4: 如何避免死锁?

7 个方法

  1. 按相同顺序访问资源

  2. 尽量使用索引(避免全表锁)

  3. 减小事务粒度(缩短持锁时间)

  4. 使用较低的隔离级别(RC 没有间隙锁)

  5. 合理设计索引

  6. 避免大事务(分批处理)

  7. 慎用 FOR UPDATE

Q5: 为什么 InnoDB 的锁是加在索引上的?

原因

  1. 效率:通过索引快速定位数据

  2. 一致性:索引是有序的,便于范围锁定

  3. 设计:InnoDB 的数据是按索引组织的

后果

  • 没有索引 → 全表扫描 → 锁全表

  • 有索引 → 快速定位 → 只锁部分行

示例

Q6: 意向锁的作用是什么?

作用:协调表锁和行锁的冲突,提高加表锁的效率。

没有意向锁时

有意向锁时

机制

Q7: RC 和 RR 隔离级别的锁有什么区别?

READ COMMITTED(RC)

  • ✅ 没有间隙锁

  • ✅ 只有记录锁

  • ✅ 并发性能更好

  • ❌ 有不可重复读和幻读问题

REPEATABLE READ(RR)

  • ✅ 有间隙锁和临键锁

  • ✅ 防止幻读

  • ✅ 保证可重复读

  • ❌ 并发性能相对较差

  • ❌ 更容易死锁(间隙锁冲突)

使用建议

  • 大多数场景:RC 更好(性能 + 死锁少)

  • 需要可重复读:RR


总结

核心要点

  1. 锁的分类

    • 粒度:全局锁、表锁、行锁

    • 模式:共享锁(S)、排他锁(X)

    • 算法:记录锁、间隙锁、临键锁

  2. 行锁的本质

    • 锁加在索引

    • 没有索引 → 锁全表

  3. 临键锁

    • 记录锁 + 间隙锁

    • 左开右闭区间 (a, b]

    • 防止幻读的关键

  4. 加锁规则

    • 基本单位:临键锁

    • 两个优化:退化为记录锁或间隙锁

  5. 死锁

    • 循环等待

    • 按相同顺序访问资源

    • 减小事务粒度

学习建议

  1. 动手实验

    • 搭建测试环境

    • 验证每种加锁场景

    • 观察锁等待和死锁

  2. 画图理解

    • 画出间隙锁的区间

    • 画出临键锁的范围

    • 画出死锁的循环等待图

  3. 分析真实案例

    • 查看生产环境的死锁日志

    • 分析慢查询是否因为锁等待

    • 优化加锁策略

  4. 掌握监控命令

    • SHOW ENGINE INNODB STATUS

    • SELECT * FROM information_schema.INNODB_TRX

    • SELECT * FROM performance_schema.data_locks

记住这些关键点

  • 锁加在索引上

  • 没有索引会锁全表

  • 临键锁 = 记录锁 + 间隙锁

  • 间隙锁防止幻读

  • 按相同顺序避免死锁

  • RC 级别没有间隙锁


下一步:学习 MySQL 事务,理解 ACID、隔离级别和 MVCC 原理。

Last updated