开篇:为什么你该认真理解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-changegh-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=2id=4

简单,直接,锁行不锁间隙。

间隙锁(Gap Lock)
SELECT * FROM user WHERE id BETWEEN 4 AND 6 FOR UPDATE;

注意:id=4id=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 预防死锁的实战建议

  1. 统一加锁顺序:多个事务访问多张表时,按相同的顺序加锁,避免循环等待。

  2. 尽早加锁:在事务一开始就通过 SELECT ... FOR UPDATE 拿到所有需要的锁,缩短锁持有时间。

  3. 缩小事务范围:事务越长,持有的锁越多,死锁概率越高。把不需要强一致性的操作放到事务外。

  4. 确保查询走索引:不走索引会导致行锁变表锁,死锁概率指数上升。

  5. 考虑降级隔离级别:如果业务允许,从RR降级到RC,消除间隙锁带来的死锁风险。

终章、总结:一张图看懂MySQL锁体系

MySQL锁体系
│
├── 全局锁(FTWRL)
│   └── 全库备份,生产环境慎用
│
├── 表级锁
│   ├── 表锁(LOCK TABLES)—— 手动控制,InnoDB很少用
│   └── MDL锁(元数据锁)—— 自动维护,DDL的大坑
│
└── 行级锁(InnoDB专有)
    ├── 记录锁(Record Lock)—— 锁行
    ├── 间隙锁(Gap Lock)—— 锁间隙,防止幻读
    └── 临键锁(Next-Key Lock)—— 行+间隙,RR默认

最后的思考:

锁的本质,是用并发性能换取数据一致性

理解了这个权衡,你就不会在任何场景都无脑使用RR级别,也不会在出现死锁时手足无措。你能根据业务的实际需求,在一致性和性能之间找到最佳的平衡点。

希望这篇文章能让你对MySQL的锁机制有一个质的理解。如果觉得有收获,欢迎点赞、收藏、转发,让更多人看到。

更多推荐