锁是数据库用来协调多个事务或会话对同一数据并发访问的机制。MySQL 的锁体系设计得相当灵活,不同的锁粒度、锁模式组合在一起,能覆盖从全库备份到高并发行级更新的各种场景。但这也意味着,如果你对锁的分类没有一个清晰的认识,线上出现锁等待或者死锁时排查起来会很头疼。
理解 MySQL 锁体系的最好方式是按粒度分层:全局锁、表级锁、行级锁,每一层又包含不同的锁模式。
6.3.1 按锁粒度分类
锁粒度决定了锁所覆盖的数据范围。粒度越粗,并发度越低,但实现简单;粒度越细,并发度越高,但锁管理的开销也更大。MySQL 支持三种粒度的锁。
1. 全局锁
全局锁会锁定整个数据库实例,让数据库处于只读状态。它的命令是 FLUSH TABLES WITH READ LOCK(简称 FTWRL)。一旦执行,其他会话的以下操作都会被阻塞:
- 所有表的写入操作(INSERT、UPDATE、DELETE)
- 所有 DDL 操作(ALTER、CREATE、DROP 等)
- 所有未提交事务的提交操作
典型的应用场景是全库逻辑备份:用 FTWRL 锁住全库,保证备份期间数据不会发生变化,然后通过文件系统快照或 mysqldump 导出数据。
但在实际生产环境中,这把锁非常“重”。任何写入都会被阻塞,哪怕是无关紧要的日志表,也会导致业务停滞。因此,更常用的做法是利用 mysqldump 的 --single-transaction 参数,它基于 MVCC 开启一个一致性快照事务,在不加全局锁的情况下完成备份(仅对 InnoDB 表有效)。如果你使用 XtraBackup 物理备份,甚至对 InnoDB 表完全不需要加锁。
全局锁除了备份,还有一个极端用途:搭配主从切换做全库只读以阻止写入。但无论哪种情况,全局锁的使用都应极度谨慎,并且在最短时间内释放。
2. 表级锁
表级锁锁定整张表,是 MySQL 最基本的锁策略,也是 MyISAM 引擎唯一支持的锁粒度。InnoDB 虽然主打行锁,但在某些场景下也会使用表级锁。
表级锁可细分为几种形式:
- 显式表锁:通过
LOCK TABLES t READ或LOCK TABLES t WRITE手动加锁。读锁(READ)下所有会话可读、本会话不可写;写锁(WRITE)下只有本会话可读写,其他会话完全无法访问。这类锁易用但并发度低,在 InnoDB 时代已很少使用,除非某些遗留系统或特殊维护操作。 - 元数据锁(Metadata Lock,MDL):这是 MySQL 5.5 引入的自动表级锁,用于保护表结构不被并发修改。任何对表的增删改查都会自动加 MDL 读锁,而 ALTER TABLE 等 DDL 操作会申请 MDL 写锁。MDL 不需要用户干预,但它导致的阻塞却非常常见:一个长事务一直在读某张表,就会持有 MDL 读锁,此时任何 DDL 想获取写锁都会被阻塞,而后续的所有读写请求因需要获取 MDL 读锁也会排队,造成整个表“假死”。通过
SHOW PROCESSLIST可以看到 “Waiting for table metadata lock” 状态。 - 意向锁(Intention Lock):这是 InnoDB 特有的一种表级锁,分意向共享锁(IS)和意向排他锁(IX)。它的作用不是直接保护行数据,而是作为一个快速判定的标记。当一个事务要给某行加共享锁(S)时,先在表上加 IS 锁;要加排他锁(X)时,先在表上加 IX 锁。这样,当另一个事务想加表级锁(比如
LOCK TABLES ... WRITE)时,不需要逐行检查是否有行锁冲突,只需要看表上有没有不相容的意向锁即可。意向锁是自动加、自动释放的,完全由 InnoDB 内部管理。
3. 行级锁
行级锁只锁定被访问的那些行,粒度最细,是 InnoDB 支持高并发写入的核心。行锁是加载在索引记录上的,如果表没有定义索引,InnoDB 会使用隐式创建的主键(row ID)来锁定,但这种情况极不推荐,因为会导致不可预期的锁范围。
行级锁按照模式主要分为两种:
- 共享锁(S 锁):允许持有锁的事务读取一行数据,其他事务也可以加 S 锁读这行,但不能加 X 锁修改。典型语句是
SELECT ... LOCK IN SHARE MODE(8.0 后改为SELECT ... FOR SHARE)。 - 排他锁(X 锁):允许持有锁的事务修改或删除一行数据,其他任何事务都不能同时持有 S 或 X 锁。普通
UPDATE、DELETE、INSERT都会自动加 X 锁,SELECT ... FOR UPDATE也会对读取的行加 X 锁。
行锁的具体加锁算法(记录锁、间隙锁、临键锁)将在下一节 6.4 展开,本节先把握其分类逻辑。
6.3.2 锁模式的兼容关系
理解锁的兼容性,有助于你预判哪些 SQL 可能互相阻塞。这里列出关键兼容矩阵:
| | IS | IX | S | X |
|-----------|-------|-------|-------|-------|
| IS | 兼容 | 兼容 | 兼容 | 冲突 |
| IX | 兼容 | 兼容 | 冲突 | 冲突 |
| S | 兼容 | 冲突 | 兼容 | 冲突 |
| X | 冲突 | 冲突 | 冲突 | 冲突 |
- 意向锁之间(IS/IX)总是兼容,因为它们只是一份“意图”,不代表真正的数据冲突。
- 意向共享锁(IS)与共享锁(S)兼容,因为都是读意向。
- 意向共享锁(IS)与排他锁(X)冲突,因为某人要写,你总该阻止表的全局兼容性检查通过。
- 意向排他锁(IX)与共享锁(S)冲突——这很关键:当一个事务要对某几行加 X 锁时,它先在表上加了 IX 锁。另一个事务如果想对整张表加 S 锁(比如
LOCK TABLES t READ),就会因为 IX 而阻塞。这防止了在行级写锁存在的情况下,另一个事务试图把整张表锁住。 - 行级 S 锁与 X 锁自然互斥,这是最基础的读-写冲突。
这个矩阵的实际意义在于:大部分情况下你不需要手工管理意向锁,但当你看到阻塞时,通过 SHOW ENGINE INNODB STATUS 中的锁信息,就能理解为什么一个 ALTER TABLE 会被一个简单的 SELECT ... FOR UPDATE 阻塞——因为后者持有 IX 锁和行 X 锁,而 DDL 需要表级 S 锁。
6.3.3 实际使用中的常见锁问题及排查
全局锁滥用:有人习惯在备份前执行 FTWRL,但如果备份工具已支持事务一致快照,应优先使用 --single-transaction 或物理备份。
MDL 锁等待:这是线上最常见的问题之一。典型表现:某个查询长时间运行(比如未提交事务中的一个大查询),突然 DBA 执行 ALTER TABLE,就会发现库卡死,所有请求都在等 MDL。解决方案:在 DDL 前检查是否有长时间运行的事务(information_schema.innodb_trx),或者在执行 DDL 时加上 LOCK=NONE / LOCK=SHARED 参数(MySQL 8.0 的 Online DDL 支持),以及使用 Percona 的 pt-online-schema-change 等工具。
行锁竞争:高并发下多个事务争抢同一行,会显著拖慢 TPS。可以通过 SELECT * FROM performance_schema.data_locks 查看当前持有的行锁,结合 sys.innodb_lock_waits 视图快速定位阻塞源。
死锁:两个或多个事务互相持有对方需要的锁资源,形成循环等待。InnoDB 有死锁检测机制,一旦发现会立刻回滚其中一个事务,并抛出错误 1213。你需要做的不是避免死锁检测(反而可能导致崩溃),而是减少长事务、保证一致的访问顺序、合理设计索引以减少锁范围。
开发实用建议:
- 永远不要在长事务中混入交互等待(如等待用户输入)。
- DDL 操作放在业务低峰期,并设置适当的等待超时
lock_wait_timeout,而非无限等待。 - 尽量使用主键或唯一索引精确锁定行,避免范围锁定扩大。
- 对于需要排队的热点行,考虑将并发改串行(如 Redis 队列)或在应用层用乐观锁重试。
掌握了锁体系的分类和基本兼容规则,你再遇到 “Waiting for table metadata lock” 或 “Lock wait timeout exceeded” 时,就能有方向性地去查询哪个事务持有锁、是哪种粒度的锁,从而快速解决问题,而不是盲目重启。