Featured image of post MySQL - 锁

MySQL - 锁

全局锁

一般用于全库逻辑备份,以保证在备份期间不会出现数据或表结构的变更

表级锁

表锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 表级别的共享锁
// 允许当前会话读取被锁定的表,阻止其他会话对表进行写操作
lock tables students read;

// 表级别的独占锁
// 允许当前会话对表进行读写,阻止其他会话的任何操作
lock tables students write;

// 释放表锁
unlock tables;

元数据锁

元数据锁(MDL)不需要显式的使用,我们对数据库表进行操作时,会自动加上 MDL

  • 对一张表进行 CRUD 操作时,加的是 MDL 读锁
  • 对一张表做表结构变更时,加的是 MDL 写锁

MDL 是为了保证用户对表执行 CRUD 操作时,不会有其他线程对这个表结构做变更

MDL 会在事务提交之后释放,在事务执行期间,MDL 是一直持有的

意向锁

  • 在对某些记录加上共享锁之前,需要先在表级别加一个「意向共享锁」
  • 在对某些记录加上独占锁之前,需要先在表级别加一个「意向独占锁」

在执行插入、更新、删除操作时,需要先对表加上「意向独占锁」,然后再对该记录加独占锁

普通的 select 语句是不会加行级锁的,是通过 MVCC 实现的一致性读,是无锁的

不过也可以通过以下方式加锁

1
2
3
4
// 先在表级别加意向共享锁,然后对读取的记录加共享锁
select ... lock in share mode;
// 先在表级别加意象独占锁,然后对读取的记录加独占锁
select ... for update;

意向共享锁和意象独占锁是表级锁,不会和行级的锁发生冲突,意向锁之间也不会发生冲突,只会和共享表锁和独占表锁发生冲突

如果没有意向锁,那么加独占表锁时,需要遍历表中所有记录查看是否存在独占锁。

有了意向锁,就可以直接在表级别判断是否有记录被加锁

AUTO-INC 锁

通常来说表中的主键都会设置为自增的,自增主要是通过 AUTO-INC 锁实现的

在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为 AUTO_INCREMENT 的字段赋值在增值,在插入语句执行完成后,才会把锁释放掉

显然这样在高并发下有些低效,因此 InnoDB 还提供了一个轻量级的锁来解决这一点: 当系统变量 innodb_autoinc_lock_mode 的值为:

  • 0: 使用 AUTO-INC 锁,语句执行完毕才释放锁
  • 1:
    • 普通 insert 语句:申请之后就释放
    • 类似 insert … select 这样的批量插入数据的语句,还是要等到语句结束才会释放锁
  • 2:采用轻量级锁,申请自增主键之后就释放锁

行级锁

首先需要明确共享锁(S锁)和独占锁(X 锁):

行级锁主要有三个类型:

  • Record Lock: 记录锁,也就是仅仅把一条记录锁上
  • Gap Lock: 间隙锁,锁定一个范围,不包含记录本身
  • Next-Key Lock: 临键锁,以上两者的结合,锁定一个范围,并锁定记录本身

记录锁

Record Lock 称为记录锁,锁住的是一条记录

间隙锁

间隙锁只存在于可重复读隔离级别,是为了解决可重复读级别下幻读的问题

假设表中存在一个范围 id 为 (3,5) 的间隙锁,那么其他事务就无法插入 id = 4 的记录了

间隙锁虽然存在 S 锁和 X 锁两种类型,但并没有什么区别。

间隙锁之间是兼容的,两个事务可以同时持有包含共同间隙范围的间隙锁,不存在互斥关系。因为间隙锁的目的是防止插入幻影记录 而提出的

临键锁

临键锁是记录锁和间隙锁的结合,锁定一个范围和记录本身

假设表中存在一个范围 id 为 (3, 5] 的临键锁,那么其他事务既不能插入 id = 4 的记录,也不能修改 id = 5 的记录

前文提到间隙锁之间是互相兼容的,但由于临键锁包含记录锁,因此 X 类型的临键锁之间是不兼容的

插入意向锁

一个事务在插入记录时,需要判断插入位置是否已经被其他事务加了间隙锁

如果有,插入操作就会发生阻塞,直到间隙锁释放。在此期间,会生成一个插入意向锁,表明事务想在某个区间插入新纪录,但现在处于等待状态

例如,假设事务已经对表加了一个 (3,5) 的间隙锁,之后事务 B 向该表插入一条 id = 4 的记录。这时会判断此位置已经被事务 A 加了间隙锁,于是事务 B 会生成一个插入意向锁,然后置于等待状态。此时事务 B 就会发生阻塞,直到事务 A 提交了事务。

插入意向锁虽然名字叫意向锁,但其实是一种特殊的间隙锁,属于行级别锁

行级锁是如何加锁的

行级锁加锁的规则比较复杂,在不同场景下,加锁的形式是不同的

  • 加锁的对象是索引
  • 默认的加锁方式是临键锁
  • 在使用记录锁或者间隙锁时就能避免幻读的场景下,临键锁会退化成记录锁或者间隙锁

假设存在以下表结构和数据

1
2
3
4
5
6
7
CREATE TABLE `user` (
    `id` bigint NOT NULL AUTO_INCREMENT,
    `name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
    `age` int NOT NULL,
    PRIMARY KEY (`id`),
    KEY `index_age` (`age`) USING BTREE
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

唯一索引等值查询

  • 当记录存在时,在索引树上定位到这一条记录后,将该记录的索引中的临键锁退化为记录锁
  • 当记录不存在时,在索引树上找到第一条大于该查询记录的记录后,将该记录的索引中的临键锁退化为间隙锁

我们可以通过 select * from performance_schema.data_locks\G; 语句来查看事务执行 SQL 过程中加了什么锁

语句执行后的 LOCK_TYPE 取值如下:

  • LOCK_TYPE: ‘X’,说明是临键锁
  • LOCK_TYPE: ‘X, REC_NOT_GAP’,说明是记录锁
  • LOCK_TYPE: ‘X, GAP’,说明是间隙锁

记录存在时

1
2
3
begin; -- 开启事务

select * from user where id = 1 for update;

那么此时事务会为 id=1 的这条记录加上 X 型的记录锁。接下来如果有其他事务对记录进行更新或者删除,这些操作都会被阻塞

记录不存在时

1
2
3
begin; -- 开启事务

select * from user where id = 2 for update;

此时通过上述语句可以看到共加了两个锁:

  • 表锁:X 类型的意向锁
  • 行锁:X 类型的间隙锁

因此,此时事务在 id = 5 记录的主键索引加的是间隙锁 (1, 5)

唯一索引范围查询

范围查询与等值查询的枷锁规则是不同的。当唯一索引进行范围查询时,会对每一个扫描到的索引加临键锁,在遇到下面这些情况时,会退化成记录锁或间隙锁:

  • 针对大于等于的情况,因为存在等值查询,如果等值查询的记录存在,那么该记录的临键锁会退化为记录锁
  • 针对小于或者小于等于的范围查询:
    • 当条件值的记录不存在表中,那么扫描到终止范围查询的记录时,该记录的临键锁会退化为间隙锁
    • 当条件值的记录存在于表中:
      • 如果是小于条件的范围查询,扫描到终止范围查询的记录时,该记录的临键锁会退化为间隙锁
      • 如果小于等于的范围查询,扫描到终止范围查询的记录时,高架路的临键锁不会退化

详见 唯一索引范围查询

Licensed under CC BY-NC-SA 4.0
最后更新于 Nov 07, 2020 00:00 UTC