1. 总体分类
MySQL 的锁可以按锁定范围和使用思想来理解:
| 分类角度 | 类型 | 说明 |
|---|---|---|
| 按锁范围 | 全局锁 | 锁住整个 MySQL 实例中的所有库、所有表 |
| 按锁范围 | 表级锁 | 锁住一张表,或者锁住表结构的元数据 |
| 按锁范围 | 行级锁 | 锁住某些行或某些索引范围,主要由 InnoDB 提供 |
| 按并发控制思想 | 悲观锁 | 先加锁,再操作 |
| 按并发控制思想 | 乐观锁 | 不先加锁,更新时检查数据是否被别人改过 |
在实际开发中,InnoDB 是 MySQL 最常用的事务型存储引擎,它主要依靠MVCC + 行锁 + 间隙锁 / 临键锁来保证并发安全和事务隔离。
2. 全局锁
2.1 什么是全局锁
MySQL 里最常见的全局锁是:
FLUSH TABLES WITH READ LOCK;它会关闭所有打开的表,并对所有数据库中的所有表加一个全局读锁。加锁后,其他会话通常可以继续读数据,但不能修改数据。释放锁使用:
UNLOCK TABLES;官方文档中说明,FLUSH TABLES WITH READ LOCK获取的是 global read lock,而不是普通表锁,它会锁住所有数据库中的所有表,常用于文件系统快照或备份场景。
2.2 全局锁的典型场景:全库备份
全局锁常用于全库逻辑备份或物理快照。原因是:如果备份过程中先导出 A 表,再导出 B 表,而此时 B 表被修改,就可能导致 A、B 两张表的数据不是同一个时间点的数据。
例如订单系统中:
A 表是
ordersB 表是
order_items备份
orders后,用户又新增了订单明细再备份
order_items
这样备份出来的数据就可能出现主表和明细表不一致的问题。
所以,全局锁的作用是让整个实例在备份期间保持一个相对静止的数据状态。
2.3 InnoDB 为什么可以不依赖全局锁完成一致性备份?
InnoDB 支持 MVCC,也就是多版本并发控制。普通SELECT在REPEATABLE READ或READ COMMITTED隔离级别下通常是一致性非锁定读。它不是去等最新数据,而是基于某个时间点的 Read View 读取快照数据。
因此,InnoDB 做逻辑备份时,可以使用:
mysqldump --single-transaction这个参数会在一个事务中创建一致性快照,从而尽量避免用全局锁阻塞业务写入。MySQL 官方文档也说明,mysqldump --single-transaction可以在不锁住其他客户端的情况下创建一致性快照。,仍然可能破坏快照一致性或导致异常。
3. 表级锁
3.1 表级锁是什么
表级锁是锁住整张表。相比全局锁,它的范围更小;相比行锁,它的粒度更大,开销较低,但并发能力较弱。
MySQL 可以显式使用:
LOCK TABLES table_name READ; LOCK TABLES table_name WRITE; UNLOCK TABLES;其中:
| 表锁类型 | 含义 |
|---|---|
READ锁 | 当前会话可以读,不能写;其他会话也可以读,但不能写 |
WRITE锁 | 当前会话可以读写;其他会话不能读也不能写 |
官方文档说明,持有READ锁的会话可以读表但不能写表,多个会话可以同时持有READ锁;持有WRITE锁的会话可以读写表,并且只有持有该锁的会话可以访问这张表,其他会话会被阻塞。
3.2 哪些存储引擎常用表级锁
MyISAM、MEMORY、MERGE 等存储引擎主要使用表级锁,同一时间通常只允许一个会话更新表,因此更适合读多写少、只读或单用户场景。官方文档也指出,MySQL 对 MyISAM、MEMORY、MERGE 表使用 table-level locking,这会降低写并发。
InnoDB 主要使用行级锁,但并不代表完全没有表级锁。InnoDB 中也存在一些表级相关的锁,例如:
意向锁:
IS、IX自增锁:
AUTO-INC Lock元数据锁:
Metadata Lock显式表锁:
LOCK TABLESDDL 场景下的表结构锁
4. 行级锁
4.1 行级锁是什么
行级锁是 InnoDB 的核心锁机制。它锁的不是整张表,而是某些行,准确地说,InnoDB 的行锁大多数情况下锁的是索引记录。即使表没有显式索引,InnoDB 也会创建隐藏聚簇索引用于记录锁。如果没有合适索引,扫描范围变大,锁的范围也可能变大。
行级锁的优点是并发能力强,不同行之间可以并发修改;缺点是锁管理开销更大,也更容易出现死锁、锁等待等问题。
4.2 共享锁 S Lock
共享锁,又叫读锁,英文是 Shared Lock,简称S Lock。
特点:
持有共享锁的事务可以读取该行;
多个事务可以同时对同一行持有共享锁;
如果某行已经有共享锁,其他事务不能对它加排他锁。
MySQL 8.0 以后推荐写法:
SELECT * FROM user WHERE id = 1 FOR SHARE;旧写法是:
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;4.3 排他锁 X Lock
排他锁,又叫写锁,英文是 Exclusive Lock,简称X Lock。
特点:
持有排他锁的事务可以更新或删除该行;
如果某行已经有排他锁,其他事务不能再对它加共享锁或排他锁;
UPDATE、DELETE、SELECT ... FOR UPDATE通常会涉及排他锁。
示例:
SELECT * FROM user WHERE id = 1 FOR UPDATE; UPDATE user SET name = 'Tom' WHERE id = 1; DELETE FROM user WHERE id = 1;5. 当前读与快照读
5.1 快照读
快照读就是普通SELECT:
SELECT * FROM user WHERE id = 1;在 InnoDB 中,普通SELECT通常不加锁,而是通过 MVCC 读取历史版本快照。
快照读的特点:
不读取未提交数据;
不阻塞其他事务修改;
其他事务修改数据也通常不阻塞当前快照读;
在 InnoDB 默认的REPEATABLE READ隔离级别下,同一个事务中的一致性读会读取第一次读建立的快照。
5.2 当前读
当前读是读取数据的最新版本,并且通常需要加锁,防止读到后马上被别人改掉。
常见当前读包括:
SELECT * FROM user WHERE id = 1 FOR UPDATE; SELECT * FROM user WHERE id = 1 FOR SHARE; UPDATE user SET name = 'Tom' WHERE id = 1; DELETE FROM user WHERE id = 1; INSERT INTO user(id, name) VALUES(1, 'Tom');SELECT ... FOR UPDATE、SELECT ... FOR SHARE、UPDATE、DELETE这类 locking read 或写操作,会根据是否使用唯一索引、是否是范围条件来加记录锁、间隙锁或临键锁。
6. 记录锁、间隙锁、临键锁
这三个是理解 InnoDB 行锁的重点。
6.1 记录锁 Record Lock
记录锁锁住的是某一条索引记录。
例如:
SELECT * FROM user WHERE id = 10 FOR UPDATE;如果id是主键或唯一索引,那么 InnoDB 通常只锁住id = 10这一条索引记录。
注意:InnoDB 的记录锁本质上是锁索引记录,而不是直接锁“表中的物理行”。
6.2 间隙锁 Gap Lock
间隙锁锁住的是两个索引值之间的空隙,不是某条具体记录。
例如表中有索引值:
10, 20, 30如果锁住(10, 20)这个间隙,那么其他事务不能在这个区间插入新的索引值,比如 15。
间隙锁的主要作用是:防止其他事务在范围中插入新数据,从而解决幻读问题。
6.3 临键锁 Next-Key Lock
临键锁也叫 Next-Key Lock,它可以理解为:
临键锁 = 记录锁 + 该记录前面的间隙锁例如索引中有:
10, 11, 13, 20那么可能的临键锁范围包括:
(-∞, 10] (10, 11] (11, 13] (13, 20] (20, +∞)6.4 为什么非唯一索引容易产生临键锁?
假设有表:
CREATE TABLE user ( id INT PRIMARY KEY, age INT, KEY idx_age(age) );数据如下:
age: 10, 20, 20, 30执行:
SELECT * FROM user WHERE age = 20 FOR UPDATE;如果age是非唯一索引,InnoDB 不能只锁一条记录,因为可能有多条age = 20,而且还要防止其他事务继续插入新的age = 20。所以它可能会锁住相关的索引记录以及附近间隙。
如果使用的是唯一索引并且查询条件也是唯一等值查询,例如:
SELECT * FROM user WHERE id = 10 FOR UPDATE;这种情况下通常只需要记录锁,不需要锁前面的间隙。使用唯一索引查找唯一行时不需要 gap lock;如果没有索引或使用非唯一索引,就可能锁住前面的间隙。
7. 意向锁
7.1 什么是意向锁
意向锁是 InnoDB 中的一种表级锁,用于支持“表锁和行锁共存”。
它不是真的要锁整张表的数据,而是用来声明:
我这个事务准备在这张表的某些行上加锁。
意向锁分为两类:
| 类型 | 含义 |
|---|---|
IS | Intention Shared Lock,表示事务准备对某些行加共享锁 |
IX | Intention Exclusive Lock,表示事务准备对某些行加排他锁 |
例如:
SELECT * FROM user WHERE id = 1 FOR SHARE;会先加IS意向共享锁。
SELECT * FROM user WHERE id = 1 FOR UPDATE;会先加IX意向排他锁。
7.2 为什么需要意向锁
假设事务 A 已经锁住了表user中id = 1的这一行。
此时事务 B 想对整张表加写锁:
LOCK TABLES user WRITE;如果没有意向锁,MySQL 可能要一行一行检查表里是否存在行锁,这个代价很高。
有了意向锁后,事务 A 在加行锁之前会先在表上加IX锁。事务 B 想加整表写锁时,只需要检查表上是否有冲突的意向锁即可,不用扫描所有行。
一句话总结:
意向锁是为了让表锁和行锁能够高效判断冲突。
8. 乐观锁与悲观锁
8.1 悲观锁
悲观锁的思想是:
我认为数据很可能被别人修改,所以我先加锁,再操作。
常见实现:
BEGIN; SELECT * FROM product WHERE id = 1 FOR UPDATE; UPDATE product SET stock = stock - 1 WHERE id = 1; COMMIT;适合场景:
写冲突多;
库存扣减、余额扣减等强一致性场景;
不允许多个事务同时修改同一份数据。
缺点:
容易出现锁等待;
并发性能可能下降;
事务写得不好容易死锁。
8.2 乐观锁
乐观锁的思想是:
我先不加锁,提交更新时再检查数据有没有被别人改过。
常见实现是增加version字段:
SELECT id, stock, version FROM product WHERE id = 1; UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 3;如果更新影响行数为 0,说明版本号已经变化,当前事务更新失败,需要重试或提示用户。
适合场景:
读多写少;
冲突概率低;
希望减少数据库锁等待。
9. 死锁与锁等待
行锁粒度小,但也更容易出现死锁。
死锁例子:
事务 A:
BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; COMMIT;事务 B:
BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 2; UPDATE account SET balance = balance + 100 WHERE id = 1; COMMIT;事务 A 先锁id = 1,事务 B 先锁id = 2,然后双方都等待对方释放锁,就会产生死锁。
死锁可能发生在多个事务以相反顺序锁住多张表或多个范围时;减少死锁的方法包括缩短事务、多个事务按相同顺序访问表和行、为SELECT ... FOR UPDATE和UPDATE ... WHERE中的条件列建立索引等。
排查死锁可以使用:
SHOW ENGINE INNODB STATUS;10. 总结
MySQL 的锁可以分为全局锁、表级锁和行级锁。全局锁常用于全库备份,但 InnoDB 可以通过 MVCC 和一致性快照减少对全局锁的依赖。表锁粒度大、开销小、并发差,MyISAM 等引擎主要使用表级锁;InnoDB 主要使用行锁,同时也有意向锁、元数据锁、自增锁等表级锁。InnoDB 行锁本质上是索引记录锁,普通SELECT是快照读,不加锁;SELECT ... FOR UPDATE、UPDATE、DELETE属于当前读,会加锁。为了防止幻读,InnoDB 在REPEATABLE READ下会使用间隙锁和临键锁,其中临键锁可以理解为记录锁加前一个间隙锁。