MySQL锁系统全解
约 1959 字大约 7 分钟
mysqllock
2025-05-04
MySQL 的锁系统是保证数据一致性和并发控制的核心机制。本文从全局锁、表锁到行锁,逐层剖析 InnoDB 的锁体系。
锁的层级结构
全局锁
全局锁对整个数据库实例加锁,主要用于全库备份。
-- 加全局读锁(所有表只读)
FLUSH TABLES WITH READ LOCK;
-- 释放
UNLOCK TABLES;
-- 推荐:使用 mysqldump 的 single-transaction 替代全局锁
mysqldump --single-transaction --all-databases > backup.sqlFLUSH TABLES WITH READ LOCK 的问题:阻塞所有写入和 DDL,对业务影响极大。InnoDB 的 --single-transaction 利用 MVCC 获取一致性快照,无需全局锁。
表级锁
表锁
-- 手动加表锁(极少使用)
LOCK TABLES t1 READ, t2 WRITE;
UNLOCK TABLES;元数据锁 (MDL)
MDL 在 MySQL 5.5 引入,自动加锁,保护表结构不在查询执行期间被修改。
MDL 的危险:一个 DDL 被长查询阻塞后,会导致该表所有后续请求(包括 SELECT)排队等待。
-- 查看 MDL 锁等待
SELECT * FROM performance_schema.metadata_locks;
-- 查看阻塞的会话
SELECT * FROM sys.schema_table_lock_waits;意向锁 (Intention Lock)
意向锁是表级锁,用于表明事务打算对表中的行加什么类型的锁。
| IS | IX | S | X | |
|---|---|---|---|---|
| IS | 兼容 | 兼容 | 兼容 | 不兼容 |
| IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
| S | 兼容 | 不兼容 | 兼容 | 不兼容 |
| X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
-- 意向锁自动加锁
-- SELECT ... FOR SHARE → 先加 IS 锁,再加行级 S 锁
-- SELECT ... FOR UPDATE → 先加 IX 锁,再加行级 X 锁
-- 意向锁的作用:
-- 当需要加表级锁时,无需扫描所有行是否有行锁
-- 只需检查意向锁即可判断是否有行级锁冲突AUTO-INC 锁
用于保证自增列(AUTO_INCREMENT)的值唯一且连续。
-- AUTO-INC 锁模式
SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';
-- 0: 传统模式,语句级锁
-- 1: 连续模式(默认,MySQL 8.0 前),简单 INSERT 用轻量锁,批量用表锁
-- 2: 交叉模式(MySQL 8.0 默认),最高并发,但批量插入的自增值可能不连续行级锁详解
InnoDB 的行锁是加在索引上的,不是加在行数据上。如果查询不使用索引,会退化为表锁。
共享锁 (S) 和排他锁 (X)
-- 共享锁:允许其他事务读,不允许写
SELECT * FROM t1 WHERE id = 1 FOR SHARE;
-- MySQL 8.0 前使用 LOCK IN SHARE MODE
-- 排他锁:不允许其他事务读写(指加锁读)
SELECT * FROM t1 WHERE id = 1 FOR UPDATE;
-- 普通 SELECT 是快照读(MVCC),不加锁
SELECT * FROM t1 WHERE id = 1;记录锁 (Record Lock)
锁定索引中的一条记录。
-- 精确等值查询,唯一索引
-- 仅锁定 id=5 这条记录
SELECT * FROM t1 WHERE id = 5 FOR UPDATE;间隙锁 (Gap Lock)
锁定索引记录之间的间隙,防止其他事务在间隙中插入数据。仅在 REPEATABLE READ 隔离级别下存在。
-- 假设表中 id 有值:1, 5, 10, 15
-- 以下查询会锁定 (5, 10) 这个间隙
SELECT * FROM t1 WHERE id = 8 FOR UPDATE;
-- id=8 不存在,加间隙锁
-- 间隙锁之间不互斥(都是防止插入)
-- Session A: SELECT * FROM t1 WHERE id = 8 FOR UPDATE; -- Gap Lock (5,10)
-- Session B: SELECT * FROM t1 WHERE id = 7 FOR UPDATE; -- Gap Lock (5,10) OK!
-- 但 INSERT INTO t1 VALUES (7, ...) 会被阻塞临键锁 (Next-Key Lock)
Next-Key Lock = Record Lock + Gap Lock,锁定一条记录及其前面的间隙。这是 InnoDB 默认的行锁类型。
-- 索引值: 1, 5, 10, 15
-- Next-Key Locks 划分:
-- (-∞, 1]
-- (1, 5]
-- (5, 10]
-- (10, 15]
-- (15, +∞)
-- 范围查询
SELECT * FROM t1 WHERE id >= 10 AND id < 15 FOR UPDATE;
-- 加锁:Next-Key Lock (5,10], (10,15)加锁规则(MySQL 8.0 RR 隔离级别)
插入意向锁 (Insert Intention Lock)
一种特殊的间隙锁,在 INSERT 操作前获取。多个事务向同一个间隙插入不同位置的记录时,互不阻塞。
-- 假设间隙 (5, 10)
-- Session A: INSERT INTO t1 VALUES (7, ...); -- Insert Intention Lock at 7
-- Session B: INSERT INTO t1 VALUES (8, ...); -- Insert Intention Lock at 8
-- 两者不冲突!
-- 但如果间隙有 Gap Lock
-- Session A: SELECT * FROM t1 WHERE id = 8 FOR UPDATE; -- Gap Lock (5,10)
-- Session B: INSERT INTO t1 VALUES (7, ...); -- 被 Gap Lock 阻塞!死锁
死锁产生条件
死锁检测与处理
-- 启用死锁检测(默认开启)
SET GLOBAL innodb_deadlock_detect = ON;
-- 锁等待超时
SET GLOBAL innodb_lock_wait_timeout = 50; -- 秒
-- 查看最近一次死锁
SHOW ENGINE INNODB STATUS\G
-- 关注 LATEST DETECTED DEADLOCK 部分
-- MySQL 8.0 查看当前锁
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;常见死锁场景
-- 场景1:间隙锁导致的死锁
-- Session A:
SELECT * FROM t1 WHERE id = 8 FOR UPDATE; -- Gap Lock (5,10)
-- Session B:
SELECT * FROM t1 WHERE id = 8 FOR UPDATE; -- Gap Lock (5,10) OK!
-- Session A:
INSERT INTO t1 VALUES (7, 'a'); -- 等待 B 的 Gap Lock
-- Session B:
INSERT INTO t1 VALUES (9, 'b'); -- 等待 A 的 Gap Lock → 死锁!
-- 场景2:不同索引顺序
-- 表有 id (PK) 和 idx_name (普通索引)
-- Session A: 先通过 idx_name 加锁,再通过 PK 加锁
-- Session B: 先通过 PK 加锁,再通过 idx_name 加锁避免死锁的实践
- 固定加锁顺序:多表操作时按固定顺序访问
- 缩短事务:减少持锁时间
- 使用低隔离级别:RC 级别没有 Gap Lock
- 合理使用索引:避免行锁退化为表锁
- 尽量用等值查询:减少 Next-Key Lock 范围
锁监控
-- 查看当前所有锁
SELECT
engine_lock_id,
engine_transaction_id,
object_name,
index_name,
lock_type,
lock_mode,
lock_status,
lock_data
FROM performance_schema.data_locks;
-- 查看锁等待关系
SELECT
r.trx_id AS waiting_trx,
r.trx_mysql_thread_id AS waiting_thread,
b.trx_id AS blocking_trx,
b.trx_mysql_thread_id AS blocking_thread
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_engine_transaction_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_engine_transaction_id = b.trx_id;总结
| 锁类型 | 粒度 | 冲突范围 | 使用场景 |
|---|---|---|---|
| Record Lock | 单行 | 仅该行 | 唯一索引等值查询 |
| Gap Lock | 间隙 | 间隙内的插入 | 防止幻读(RR 级别) |
| Next-Key Lock | 记录+间隙 | 记录和间隙 | InnoDB 默认行锁 |
| Insert Intention | 间隙 | 与 Gap Lock 冲突 | INSERT 操作 |
理解 InnoDB 的加锁规则是优化并发性能和排查死锁问题的基础。核心原则:行锁加在索引上,加锁范围取决于查询条件和索引类型。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于