MySQL 锁机制
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。
- InnoDB按照锁的粒度分为以下三类:
- 全局锁:锁定数据库中的所有表,粒度最大
- 表级锁:每次操作锁住整张表,粒度大,并发低,但开销小加锁快
- 行级锁:每次操作锁住一条行数据,粒度小,并发大,但开销大加锁慢
- MyISAM、Memory等引擎仅支持表级锁,难以支持高并发的场景,且不支持事务,因此适用于只读或以读为主的场景
- InnoDB 支持行级锁、表级锁,默认使用行级锁
- 几乎每种锁在实现上都会分共享锁(读锁)和排他锁(写锁),共享锁允许多个线程持有,但只能读数据,排他锁只有一个线程可以获取,且只有该线程可以读写数据。
全局锁
全局锁就是对整个数据库实例加锁,是一个重操作。加锁后整个实例就处于只读状态,后续的 DML、DDL语句,以及更新类事务提交语句都将被阻塞。典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
语法:
-- 加全局锁(FTWRL)
flush tables with read lock;
-- 数据备份
mysqldump -uroot -p****** db_name > backup_path
-- 释放锁
unlock tables;
特点:
- 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。
- 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的 binlog,会导致主从延迟。
- 如果是 InnoDB 引擎,可以在 mysqldump 备份时加上
--single-transaction
参数,启动一个事务,确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
表级锁
表级锁每次操作锁住整张表,实现简单,加锁速度快,但锁定粒度大,锁冲突的概率高,因此并发度低。表级锁分为两类:表锁、元数据锁。
表锁
表锁分为两种:
- 表共享锁(表S锁 / 读锁): 所有事务(包括当前加锁的客户端)都只能读,不能写
- 表独占锁(表X锁 / 写锁): 当前加锁的客户端可读可写,其他客户端不可读也不可写。
语法:
-- 加读锁
lock tables tb_name read;
-- 加写锁
lock tables tb_name write;
-- 释放锁
unlock tables;
-- 查看当前数据库中的加锁情况:
select object_type, object_schema, object_name,lock_type, lock_duration from performance_schema.metadata_locks;
另外,对于不支持行锁的 MyISAM 等引擎来说,默认的就是表锁,所以 DML 语句会自动加解写锁,DQL 语句自动加解读锁。还有连接断开时也会自动释放锁。
元数据锁
当查询一个表中的数据时,如果另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构就不一致了,这肯定是不合理的,这时就需要用到元数据锁。
Meta Data Lock(MDL)位于 Server 层,MDL 加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL锁主要作用是维护表元数据的一致性,避免 DML 与 DDL 冲突,保证读写的正确性。也就是说表上有活动事务的时候,不可以对元数据进行写入操作。
MDL 也分共享锁和排他锁:
- DML 和 DQL 语句会自动申请 MDL 共享锁,多个线程可同时 CRUD
- DDL 语句执行期间可能申请排它锁,也可能申请共享锁,具体取决于 MySQL 版本和执行的 DDL 语句。当申请的是排他锁时,其它线程不能读写。TODO: 待补充文档@chanper
行级锁
每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高,但开销大,加锁速度慢,而且可能出现死锁。
InnoDB 数据基于索引组织,因此行级锁在实现上是对索引上的索引项进行加锁,如果一条 SQL 语句操作了主键索引,MySQL 就会锁住这条主键索引,如果一条语句操作了非主键索引,MySQL 会先锁定该非主键索引,再锁定关联的主键索引。
行级锁实现上分为记录锁、间隙锁、临键锁,每种实现也分共享锁和排他锁,DQL 语句读取记录时需要先获取该记录的共享锁,DML 语句需要先获取该记录的排他锁。
记录锁
Record Lock(LOCK_REC_NOT_GAP),锁定单个行记录,防止其他事务对该行进行修改。在 RC(Read Committed)、RR(Repeatable Read)隔离级别下都支持。

间隙锁
Gap Locks(LOCK_GAP)间隙锁对索引记录间隙(不含该记录)加锁,确保索引记录间隙不变,防止其他事务在这个间隙插入新记录。RR 隔离级别支持。
- 间隙锁唯一目的是为了防止产生幻读
- 间隙锁可以共存,不会限制其它事务在同一间隙上加任何行锁
- 如果要对索引末尾加间隙锁,可以对 Supremum 伪记录加间隙锁

临键锁
Next-Key Locks(LOCK_ORDINARY)临键锁是记录锁和间隙锁的组合,同时锁住数据和数据前面的间隙(左开右闭)。RR 隔离级别支持。
- 默认情况下 InnoDB 使用临键锁进行搜索和索引扫描,以防止幻读
- 临键锁只与非唯一索引列有关,唯一索引上的查询不会存在临键锁,退化为记录锁
- 索引上的等值查询(普通索引),向右遍历,最后一个值不满足查询需求时,临键锁退化为间隙锁

加锁总结
SQL | 记录锁类型 | 说明 |
---|---|---|
SELECT ... | 不加锁 | InnoDB 引擎采用 MVCC 机制实现非阻塞读 |
SELECT ... LOCK IN SHARE MODE | 共享临键锁 | 扫描如果有唯一索引,则降级为记录锁 |
SELECT ... FOR UPDATE | 排他临键锁 | 扫描如果有唯一索引,则降级为记录锁 |
UPDATE ... | 排他临键锁 | 自动加锁,扫描如果有唯一索引,则降级为记录锁 |
DELETE ... | 排他临键锁 | 自动加锁,扫描如果有唯一索引,则降级为记录锁 |
INSERT ... | 排他记录锁 | 在将要插入的那一行设置 |
注:行级锁的实现是依靠其对应的索引,如果没用到索引的查询,就会走表锁
其它锁
意向锁
假设,事务A获取了某一行记录的排它锁,事物A尚未提交,事务B想要获取表锁时,则事物B必须要确认表的每一行都不存在排他锁,需要进行全表扫描,效率很低,此时就引入意向锁。
意向锁是 InnoDB 引擎自动维护的,当执行加行锁的语句时会自动尝试获取意向锁,无法手动操作。从范围上属于表级锁,分两种:
- 意向共享锁(IS):Intention Shared Lock,当一个事务准备在某条记录上加行级共享锁时,需要先获取该表的 IS 锁
- 意向排他锁(IX):Intention Exclusive Lock,当一个事务准备在某条记录上加行级排他锁时,需要先获取该表的 IX 锁
意向锁特点:
- 从锁粒度角度:InnoDB 允许行级锁与表级锁共存,而意向锁是表锁
- 从锁模式角度:意向锁是一种独立类型,辅助解决记录锁效率不及的问题
- 从兼容性角度:意向锁包含了共享/排他两种。
兼容互斥性:
- 意向锁之间是互相兼容的,不管是共享意向锁还是排他意向锁
- 共享意向锁和表级共享锁兼容,和表级排他锁互斥
- 排他意向锁和表级共享锁、表级排他锁互斥
注:从意向锁加锁时机来理解,如果表上有意向排他锁,说明有事务持有表内某个记录的行级排他锁,那么该表自然也不能加表级排他锁。而意向排他锁只能表明表里的某一行被加了行级排他锁,所以另一个事务也可以在该表上加意向排他锁,并对表内另一行加行级排他锁,行记录的锁由行级锁来控制。
插入意向锁
插入意向锁(Insert Intention Lock)是在插入数据行之前,由 INSERT 操作设置的一种间隙锁。插入意向锁表示一种插入的意图,是一种特殊的间隙锁,如果插入到相同间隙中的多个事务没有插入相同位置,则不需要互相等待。假设存在索引记录 4 和 7。两个事务分别尝试插入 5 和 6,它们在获取行排他锁之前,分别使用插入意向锁来锁定 4 到 7 之间的间隙;但是不会相互阻塞,因为插入的是不同的行。
AUTO_INC锁
用于AUTO_INCREMENT修饰的列递增赋值,innodb_autoinc_lock_mode控制赋值方案是使用AUTO_INC锁还是采用一个轻量级锁生成列值。
页级锁
页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。仅 BDB 引擎支持页级锁。
锁的内存结构
对记录加锁的本质就是在内存中创建一个锁结构与之关联,如果符合以下条件,则对这些记录的锁可以放到一个锁结构中:
- 在同一个事务中进行加锁操作
- 被加锁的记录在同一个页面中
- 加锁的类型一致
- 等待状态一致

- 锁所在的事务信息:指向关于生成该锁事务的相关信息
- 索引信息:对于行锁需要记录加锁的记录属于哪个索引
- 表锁/行锁信息:
- 表锁:记录对哪个表加锁,以及一些其它信息
- 行锁:记录表空间Id、所在页号、以及结构末尾的比特位数。每条记录对应一个比特位,记录是否加锁
- type_mode: 32位数,其中
- 低四位 - lock_mode: 锁的模式,包括LOCK_IS, LOCK_IX, LOCK_S, LOCK_X, LOCK_AUTO_INC;
- 5~8位 - lock_type: 锁的类型,分表锁LOCK_TABLE和行锁LOCK_REC
- 其余位 - rec_lock_type: 行锁具体类型,如LOCK_ORDINARY, LOCK_GAP, LOCK_REC_NOT_GAP, LOCK_INSERT_INTENTION等。并且其中的第9个比特位标志is_waiting,即是否获取到锁
- 其它信息
- 一堆比特位:针对行锁结构,每个比特位对应一条记录,映射一个heap_no