以 InnoDB、MySQL 8.0 为基准。核心结论:InnoDB 行锁加在索引记录上;RR 下用 Next-Key Lock 防幻读;RC 下基本只用记录锁;无索引查询会锁大量记录和间隙,效果接近表锁。
一、锁分类
按粒度:
- 全局锁:锁定整个数据库实例。
- 表级锁:锁定整张表,包括表锁、元数据锁、意向锁、AUTO-INC 锁。
- 行级锁:锁定索引记录,包括记录锁、间隙锁、临键锁、插入意向锁。
按模式:
- 共享锁(S):读锁,
SELECT ... FOR SHARE。 - 排他锁(X):写锁,
SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT。
按算法:
- Record Lock:记录锁。
- Gap Lock:间隙锁。
- Next-Key Lock:记录锁 + 间隙锁。
- Insert Intention Lock:插入意向锁。
- AUTO-INC Lock:自增锁。
InnoDB 支持行级锁,MyISAM 只支持表级锁。
二、全局锁
2.1 加锁与释放
sql
FLUSH TABLES WITH READ LOCK;
UNLOCK TABLES;
加锁后整个实例处于只读状态:
- 所有 DML 被阻塞。
- 所有 DDL 被阻塞。
- 更新事务无法提交。
- 普通读可以执行。
典型用途:全库逻辑备份。
2.2 一致性数据备份
MyISAM 不支持事务,备份通常需要全局锁:
bash
mysqldump --lock-all-tables -uroot -p db > db.sql
InnoDB 可以使用事务快照,无需长时间全局锁:
bash
mysqldump --single-transaction --source-data=2 -uroot -p db > db.sql
--single-transaction 利用 MVCC 一致性快照,只对 InnoDB 有效。备份期间不能有 DDL,否则快照可能不一致。
如果必须使用全局锁备份,建议在从库执行,避免阻塞主库写入。流程:
- 主库或从库执行
FLUSH TABLES WITH READ LOCK; - 记录 binlog 位点。
- 执行
mysqldump。 - 执行
UNLOCK TABLES;
三、表级锁
3.1 表锁
手动加锁:
sql
LOCK TABLES t READ;
LOCK TABLES t WRITE;
UNLOCK TABLES;
兼容性:
| 锁类型 | 本会话 | 其他会话 |
|---|---|---|
| 读锁 | 可读,不可写 | 可读,写阻塞 |
| 写锁 | 可读,可写 | 读、写均阻塞 |
InnoDB 通常不手动加表锁,MyISAM 常用表锁。
3.2 元数据锁(MDL)
系统自动加锁,无需手动。
- DML:自动加 MDL 读锁,事务提交后释放。
- DDL:自动加 MDL 写锁,语句结束后释放。
- MDL 读锁之间兼容。
- MDL 读锁与写锁互斥。
- MDL 写锁与写锁互斥。
作用:保证表结构在事务执行期间不被修改。
问题:
- 长事务持有 MDL 读锁,会阻塞 DDL。
- DDL 等待 MDL 写锁时,后续 DML 的 MDL 读锁也会排队,导致连接堆积。
查看 MDL 等待:
sql
SELECT * FROM performance_schema.metadata_locks;
SELECT * FROM sys.schema_table_lock_waits;
处理:找到长事务并 kill。
sql
SELECT * FROM information_schema.innodb_trx;
3.3 意向锁
InnoDB 自动维护的表级锁,表示事务准备在表中某些行上加 S 或 X 锁。
- 意向共享锁(IS):准备加行 S 锁。
- 意向排他锁(IX):准备加行 X 锁。
作用:加表锁时无需逐行检查行锁,只需检查表上的意向锁。
兼容矩阵:
| 当前 \ 请求 | IS | IX | S | X |
|---|---|---|---|---|
| IS | 兼容 | 兼容 | 兼容 | 冲突 |
| IX | 兼容 | 兼容 | 冲突 | 冲突 |
| S | 兼容 | 冲突 | 兼容 | 冲突 |
| X | 冲突 | 冲突 | 冲突 | 冲突 |
3.4 意向锁测试
事务 A:
sql
BEGIN;
SELECT * FROM t WHERE id = 1 FOR UPDATE;
此时事务 A 对 id=1 加行 X 锁,同时自动加表级 IX 锁。
事务 B:
sql
LOCK TABLES t WRITE;
阻塞,因为表写锁与 IX 冲突。
事务 B:
sql
LOCK TABLES t READ;
也阻塞,因为表读锁与 IX 冲突。
如果事务 A 使用:
sql
SELECT * FROM t WHERE id = 1 FOR SHARE;
则加表级 IS 锁。此时事务 B 加表读锁兼容,加表写锁冲突。
四、行级锁
4.1 行级锁介绍
InnoDB 行锁基于索引项实现,不是直接锁物理行。
如果查询未命中索引,InnoDB 会扫描聚簇索引并锁大量记录和间隙,效果接近表锁。
普通 SELECT 是快照读,不加锁。
当前读加锁:
SELECT ... FOR SHARE:加 S 锁。SELECT ... FOR UPDATE:加 X 锁。UPDATE、DELETE、INSERT:加 X 锁。
4.2 行锁(记录锁)
示例表:
sql
CREATE TABLE t (
id INT PRIMARY KEY,
a INT,
b INT,
KEY idx_a (a)
) ENGINE=InnoDB;
唯一索引等值命中:
sql
SELECT * FROM t WHERE id = 5 FOR UPDATE;
加 Record Lock,仅锁 id=5 这条记录。
测试:
事务 A:
sql
BEGIN;
SELECT * FROM t WHERE id = 5 FOR UPDATE;
事务 B:
sql
UPDATE t SET a = 10 WHERE id = 5;
阻塞。
事务 B:
sql
UPDATE t SET a = 10 WHERE id = 6;
不阻塞。
非唯一索引等值:
sql
SELECT * FROM t WHERE a = 5 FOR UPDATE;
对匹配记录加 Next-Key Lock,并在下一个间隙加 Gap Lock。
无索引查询:
sql
SELECT * FROM t WHERE b = 5 FOR UPDATE;
b 无索引,全表扫描,锁所有记录和间隙,插入任何行都可能阻塞。
4.3 间隙锁与临键锁
- Gap Lock:锁定索引记录之间的间隙,不锁记录本身,防止其他事务插入。
- Next-Key Lock:Record Lock + Gap Lock,左开右闭区间
(prev, current]。 - RR 隔离级别下,Next-Key Lock 是默认加锁单位,用于防止幻读。
- RC 隔离级别下,间隙锁基本关闭,仅外键约束和唯一键冲突检查可能使用。
4.4 RR 下加锁规则
- 唯一索引等值命中:Next-Key Lock 退化为 Record Lock。
- 唯一索引等值未命中:加 Gap Lock。
- 非唯一索引等值:对匹配记录加 Next-Key Lock,并在下一个间隙加 Gap Lock。
- 范围查询:对扫描到的记录加 Next-Key Lock,直到不满足条件的第一个记录。
- 无索引查询:锁全表记录和间隙。
4.5 间隙锁与临键锁测试
数据:
sql
INSERT INTO t (id, a) VALUES (1,1),(3,3),(5,5),(7,7),(9,9);
唯一索引等值命中:
事务 A:
sql
BEGIN;
SELECT * FROM t WHERE id = 5 FOR UPDATE;
事务 B:
sql
INSERT INTO t (id, a) VALUES (4, 4);
不阻塞,因为只加 Record Lock,不锁间隙。
唯一索引等值未命中:
事务 A:
sql
BEGIN;
SELECT * FROM t WHERE id = 6 FOR UPDATE;
id=6 不存在,加 Gap Lock,锁住 (5,7) 间隙。
事务 B:
sql
INSERT INTO t (id, a) VALUES (6, 6);
阻塞。
非唯一索引等值:
事务 A:
sql
BEGIN;
SELECT * FROM t WHERE a = 5 FOR UPDATE;
对 a=5 加 Next-Key Lock,锁 (3,5],并在下一个间隙加 Gap Lock,锁 (5,7)。
事务 B:
sql
INSERT INTO t (a) VALUES (4);
阻塞,因为 4 在 (3,5) 间隙内。
事务 B:
sql
INSERT INTO t (a) VALUES (6);
阻塞,因为 6 在 (5,7) 间隙内。
事务 B:
sql
INSERT INTO t (a) VALUES (8);
不阻塞。
范围查询:
事务 A:
sql
BEGIN;
SELECT * FROM t WHERE a >= 5 FOR UPDATE;
锁范围包括:
(3,5](5,7](7,9](9,+∞)
插入 a=4、a=6、a=10 都可能阻塞。
无索引:
事务 A:
sql
BEGIN;
SELECT * FROM t WHERE b = 5 FOR UPDATE;
全表扫描,所有记录和间隙被锁,插入任何行都阻塞。
五、死锁与排查
死锁:两个或多个事务互相持有对方需要的锁并等待。
InnoDB 行为:
- 自动死锁检测,
innodb_deadlock_detect默认 ON。 - 检测到死锁后,回滚代价较小的事务。
- 锁等待超时由
innodb_lock_wait_timeout控制,默认 50 秒。
查看死锁:
sql
SHOW ENGINE INNODB STATUS\G
开启全量死锁日志:
sql
SET GLOBAL innodb_print_all_deadlocks = ON;
查看当前锁:
sql
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM sys.innodb_lock_waits;
查看事务:
sql
SELECT * FROM information_schema.innodb_trx;
减少死锁:
- 事务尽量短。
- 多表或多行访问保持一致的加锁顺序。
UPDATE、DELETE的WHERE必须命中索引。- 业务允许时使用
READ COMMITTED。 - 应用层对死锁回滚事务做重试。
六、总结
| 锁类型 | 粒度 | 加锁方式 | 主要作用 |
|---|---|---|---|
| 全局锁 | 实例 | 手动 | 全库备份 |
| 表锁 | 表 | 手动 | 简单并发控制 |
| MDL | 表 | 自动 | 保护表结构 |
| 意向锁 | 表 | 自动 | 辅助行锁与表锁冲突检测 |
| 记录锁 | 行 | 自动/手动 | 锁单条记录 |
| 间隙锁 | 间隙 | 自动 | 防插入,防幻读 |
| 临键锁 | 记录+间隙 | 自动 | RR 默认加锁单位 |
核心要点:
- InnoDB 行锁基于索引,无索引会锁全表。
- RR 用 Next-Key Lock 防幻读,RC 仅记录锁。
- 长事务、不一致访问顺序、缺失索引是锁冲突和死锁的主要来源。
- 事务尽量短,
UPDATE/DELETE的WHERE必须命中索引。 - 监控锁等待:
performance_schema.data_locks、data_lock_waits、sys.innodb_lock_waits。