深入剖析MySQL锁机制:从全局锁到行锁,一文读懂
开篇:为什么你该认真理解MySQL的锁?
大多数开发者在生产环境遇到这两个问题时的反应:
-
锁等待超时:一脸茫然地执行
SHOW PROCESSLIST,然后重启应用。 -
死锁报错:把错误日志扔给DBA,说"数据库又出问题了"。
这些问题,本质上都是对MySQL锁机制理解不够造成的。
锁不是MySQL故意设计出来折磨我们的。它的存在只有一个目的:在并发环境下,保证数据的一致性和完整性。
本文将沿着 锁的粒度 这条主线,从全局锁一路讲到行锁,不仅告诉你"是什么",更讲清楚"为什么"以及"怎么用"。
第一章、全局锁:最狠的一把锁
1.1 什么是全局锁?
全局锁,顾名思义,是MySQL中粒度最大的锁。一旦加锁,整个数据库进入只读状态。
执行这条命令:
FLUSH TABLES WITH READ LOCK; -- 简称 FTWRL
效果立竿见影:
-
所有写操作(INSERT、UPDATE、DELETE)被阻塞
-
所有DDL操作(ALTER TABLE、DROP TABLE)被阻塞
-
所有带更新操作的提交被阻塞
解锁也很简单:
UNLOCK TABLES;
1.2 什么时候用?——全库备份
这是全局锁最经典的场景。
假设你凌晨3点要对一个100GB的数据库做全量备份。如果备份过程中,有数据在持续写入,那么你备份出来的数据是不一致的——可能表A是凌晨3:00的状态,表B是凌晨3:05的状态,表C是凌晨3:10的状态。
这样的备份,恢复出来是废的。
所以,传统的备份方案是:加全局锁 → 开始备份 → 备份完成 → 释放全局锁。这样备份出来的数据,一定是某个时间点的一致性快照。
1.3 为什么生产环境很少用全局锁?
因为太狠了。
在高并发的生产环境,哪怕只锁1秒钟,也可能导致:
-
数百个写请求堆积
-
应用连接池被占满
-
雪崩式的服务不可用
所以现在主流做法是使用 mysqldump --single-transaction,利用InnoDB的MVCC机制实现一致性备份,不加全局锁。
一句话总结全局锁:武器库里的核弹,知道它存在就行,轻易别用。
第二章、表级锁:轻量但粗糙
从全局锁往下走,粒度细化一级,就是表级锁。
2.1 表锁:手动控制的表级锁
加锁语法:
LOCK TABLES user READ; -- 读锁 LOCK TABLES user WRITE; -- 写锁
表锁的规则:
-
读锁(共享锁):其他事务可以读,但不能写
-
写锁(排他锁):其他事务既不能读也不能写
表锁的优点:开销小,加锁快。
表锁的缺点:并发度低,锁的粒度太粗。
在InnoDB中,表锁几乎被行锁完全替代。你几乎不需要手动使用 LOCK TABLES。
2.2 元数据锁(MDL):自动的表级锁
这才是你需要重点关注的东西。
MDL是MySQL 自动维护 的表级锁,不需要我们手动加。它的规则很简单:
-
对表执行 DML操作(SELECT、INSERT、UPDATE、DELETE):加 MDL读锁
-
对表执行 DDL操作(ALTER TABLE、DROP TABLE):加 MDL写锁
读锁之间不互斥,多个事务可以同时读一张表。
但读写互斥,写锁之间也互斥。
一个隐藏的生产事故场景
假设你有一张千万级数据的表,执行:
ALTER TABLE user ADD COLUMN age INT;
这条DDL执行期间,需要加MDL写锁。如果表很大,执行时间很长,所有对该表的DML操作都会被阻塞。
更可怕的是:如果DDL在等待MDL写锁,而前面有一个长事务持有MDL读锁没释放,那么DDL会一直等。更更可怕的是:DDL后面排队的DML操作,全都会被堵住。
![MDL阻塞示意:读锁→写锁→读锁排队,后面的读锁全部等待]
这就是很多生产环境"数据库突然变慢"的原因之一。
应对方案:
-
使用
pt-online-schema-change或gh-ost等工具做在线DDL -
避免在业务高峰期执行DDL
-
监控长事务,及时kill
第三章、行级锁:InnoDB的王牌
行级锁是InnoDB最核心的竞争力。它让多个事务可以同时修改同一张表的不同行,极大提升了并发能力。
但是要真正理解行锁,必须先从索引说起。
3.1 核心结论:行锁加在索引上,而不是数据行上
这是理解行锁一切行为的基础。
InnoDB的表是索引组织表,数据和主键索引存储在一起。当你执行:
SELECT * FROM user WHERE id = 1 FOR UPDATE;
InnoDB会在主键索引的B+树上定位到 id=1 这个索引项,然后在这个索引项上加锁。
索引决定了锁的粒度:
-
走唯一索引/主键索引:精确定位一行 → 行锁
-
走普通索引:定位到多行 → 多行记录被锁
-
不走索引:全表扫描,所有扫描到的行都加锁 → 行锁退化为表锁
最后一条极其重要。UPDATE user SET name='a' WHERE name='张三',如果 name 字段没有索引,这条语句会锁住整张表的所有行。
3.2 三种行锁:Record、Gap、Next-Key
用一个具体案例来理解。假设 user 表的 id 列已有数据:1,3,5,10
记录锁(Record Lock)
SELECT * FROM user WHERE id = 3 FOR UPDATE;
锁住 id=3 这一条记录。其他事务不能修改 id=3,但可以插入 id=2 或 id=4。
简单,直接,锁行不锁间隙。
间隙锁(Gap Lock)
SELECT * FROM user WHERE id BETWEEN 4 AND 6 FOR UPDATE;
注意:id=4 和 id=6 在表中都不存在。
那InnoDB锁什么?锁的是索引记录之间的间隙:
-
间隙
(3, 5) -
间隙
(5, 10)
在这两个间隙范围内,其他事务不能插入新数据。
所以,另一个事务执行 INSERT INTO user VALUES (4) 会被阻塞,执行 INSERT INTO user VALUES (7) 也会被阻塞。
间隙锁锁的是"空隙",不是记录本身。它的唯一目的:防止幻读。
临键锁(Next-Key Lock)
临键锁 = 记录锁 + 间隙锁,锁定范围是 左开右闭 (上一个值, 当前值]。
在RR隔离级别下,InnoDB默认使用临键锁。
SELECT * FROM user WHERE id = 5 FOR UPDATE;
在RR级别下,锁定的范围是 (3, 5]:
-
锁住
id=5这条记录(记录锁) -
同时锁住
(3, 5)这个间隙(间隙锁)
3.3 一个案例吃透幻读与间隙锁
场景重现:
表中数据:id = 1, 3, 5, 10
事务A:
BEGIN; SELECT * FROM user WHERE id > 2 FOR UPDATE; -- 返回 3, 5, 10
事务B(在事务A提交前):
INSERT INTO user VALUES (4);
如果没有任何锁保护,事务B插入成功。然后事务A再次查询,会看到4条记录——幻读发生了。
间隙锁如何阻止?
RR级别下,事务A的 SELECT ... WHERE id > 2 FOR UPDATE 会加临键锁:
-
扫描到
3:锁定(2, 3] -
扫描到
5:锁定(3, 5] -
扫描到
10:锁定(5, 10] -
扫描到末尾:锁定
(10, +∞)
事务B想插入 id=4,但 4 在 (3, 5] 区间内,被锁住了。插入阻塞。
这就是RR级别用间隙锁解决幻读的底层原理。
代价是什么? 锁定范围变大,并发性能下降,死锁风险升高。
第四章、事务隔离级别:锁的"总开关"
隔离级别决定了锁的行为和使用范围。理解隔离级别,才能真正理解锁在不同场景下的表现。
4.1 四种隔离级别对
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 间隙锁 | 并发性能 |
| 读未提交(RU) | 可能 | 可能 | 可能 | 无 | 最高 |
| 读已提交(RC) | 不可能 | 可能 | 可能 | 无 | 高 |
| 可重复读(RR) | 不可能 | 不可能 | 特定场景下可能 | 有 | 中 |
| 串行化 | 不可能 | 不可能 | 不可能 | 有 | 最低 |
4.2 RC vs RR:互联网公司的选择题
大部分互联网公司(包括阿里、美团等)将隔离级别降级为RC,用更少的锁冲突换取更高的并发性能。如果你的业务对幻读不敏感(大多数业务确实不敏感),RC是更优的选择。
-
选择RC的理由:RC级别没有间隙锁,锁的范围最小,死锁概率最低,并发性能最高。对于绝大多数OLTP业务(如订单、用户信息),通过业务逻辑(如唯一索引、分布式ID)来处理幻读是完全可接受的。
-
不选RR的理由:虽然RR通过间隙锁解决了大部分幻读问题,但间隙锁本身就是一把双刃剑。它不仅会降低并发性能,还会显著增加死锁风险。为了那一点在特定场景下才有可能出现的幻读问题,付出如此大的性能代价,对很多互联网业务来说并不划算。
4.3 MVCC与锁的关系
这里必须要提一下MVCC(多版本并发控制)。
很多人以为RR级别下所有查询都不会有幻读,这是一个误解。
-
普通SELECT:走MVCC,读取的是快照数据,不加锁,不会有幻读
-
当前读(SELECT ... FOR UPDATE / UPDATE / DELETE):走的是最新数据,会加锁,可能有幻读,需要间隙锁来保护
MVCC和锁是互补关系:
-
MVCC负责无锁的一致性读(提高并发)
-
锁负责有锁的当前读(保证强一致性)
第五章、锁的实战:如何排查和解决锁问题
5.1 查看当前锁信息
-- 查看当前正在执行的事务
SELECT * FROM information_schema.INNODB_TRX;
-- 查看当前锁的等待关系(MySQL 8.0)
SELECT * FROM performance_schema.data_locks;
-- 查看锁等待信息(MySQL 5.7)
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
5.2 死锁的排查
执行 SHOW ENGINE INNODB STATUS,在输出中找到 LATEST DETECTED DEADLOCK 部分,可以看到最近一次死锁的详细信息和被回滚的事务。
5.3 预防死锁的实战建议
-
统一加锁顺序:多个事务访问多张表时,按相同的顺序加锁,避免循环等待。
-
尽早加锁:在事务一开始就通过
SELECT ... FOR UPDATE拿到所有需要的锁,缩短锁持有时间。 -
缩小事务范围:事务越长,持有的锁越多,死锁概率越高。把不需要强一致性的操作放到事务外。
-
确保查询走索引:不走索引会导致行锁变表锁,死锁概率指数上升。
-
考虑降级隔离级别:如果业务允许,从RR降级到RC,消除间隙锁带来的死锁风险。
终章、总结:一张图看懂MySQL锁体系
MySQL锁体系
│
├── 全局锁(FTWRL)
│ └── 全库备份,生产环境慎用
│
├── 表级锁
│ ├── 表锁(LOCK TABLES)—— 手动控制,InnoDB很少用
│ └── MDL锁(元数据锁)—— 自动维护,DDL的大坑
│
└── 行级锁(InnoDB专有)
├── 记录锁(Record Lock)—— 锁行
├── 间隙锁(Gap Lock)—— 锁间隙,防止幻读
└── 临键锁(Next-Key Lock)—— 行+间隙,RR默认
最后的思考:
锁的本质,是用并发性能换取数据一致性。
理解了这个权衡,你就不会在任何场景都无脑使用RR级别,也不会在出现死锁时手足无措。你能根据业务的实际需求,在一致性和性能之间找到最佳的平衡点。
希望这篇文章能让你对MySQL的锁机制有一个质的理解。如果觉得有收获,欢迎点赞、收藏、转发,让更多人看到。
更多推荐


所有评论(0)