MySQL—锁分析

0. 简介

锁,是在计算机执行多线程(或协程)并发访问时,用于对同一共享资源的一致性同步机制。MySQL中的锁是在存储引擎中实现的,保证了数据访问的一致性。

1. MySQL中的锁

在InnoDB中,锁分为全局锁,表级锁和行级锁。

1.1 全局锁

全局锁主要用于全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

加全局锁:

arduino 复制代码
flush tables with read lock

这时候,意味着整个数据库都处于只读状态。不过在InnoDB引擎下,因为支持MVCC,所以在使用 mysqldump 时加上 --single-transaction 参数的时候,就会在备份数据库之前先开启事务。在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。

释放全局锁,执行这条命令:

unlock tables

当然,当会话断开了,全局锁会被自动释放。

1.2 表级锁

表锁

表锁可以加读或者写锁:

arduino 复制代码
//表级别的共享锁,也就是读锁;
lock tables t_student read;

//表级别的独占锁,也就是写锁;
lock tables t_stuent write;

需要注意的是,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。

要释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁:

unlock tables

另外,当会话退出后,也会释放所有表锁。 不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,InnoDB 牛逼的地方在于实现了颗粒度更细的行级锁

元数据锁(MDL)

我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:

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

意向锁

在InnoDB中:

  • 对行级数据加共享锁 之前,需要先在表级别上加一个意向共享锁
  • 对行级数据加独占锁 之前,需要现在表级别上加一个意向独占锁

也就是,当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。

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

不过,select 也是可以对记录加共享锁和独占锁的,具体方式如下:

sql 复制代码
//先在表上加上意向共享锁,然后对读取的记录加共享锁
select ... lock in share mode;

//先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;

意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read )和独占表锁(lock tables ... write)发生冲突。

因为表锁和行级锁的读写、写写互斥,意向锁的目的是使得加表级锁时无需遍历表中的表锁,只需判断意向锁。

AUTO-INC 锁

参考AUTO-INC 锁

1.3 行级锁

InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。

行级锁有共享锁(S)独占锁(X) 之分,从类型上分为:

  • Record Lock:记录锁,也就是仅仅把一条记录锁住;
  • Gap Lock:间隙锁,锁定一个范围,不包括记录本身,是一个开区间;
  • Next-Key LockRecord Lock + Gap Lock,锁定一个范围,并且锁定记录本身,是左开右闭区间。

Record Lock

Record Lock称为记录锁,锁住的是一条记录,且记录锁有S和X之分:

  • 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
  • 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。

Gap Lock

Gap Lock称为间隙锁,只存在于可重复度隔离级别,目的是为了解决可重复读隔离级别下的幻读。

假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。

间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的

Next-Key Lock

Next-Key Lock称为临键锁,是Record Lock + Gap Lock的组合,锁定一个范围,并且锁定记录本身。

假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。

所以,next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。

next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的

比如,一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。

虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系,X 型的记录锁与 X 型的记录锁是冲突的。

插入意向锁

一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。

如果有的话,插入操作会发生阻塞,直到持有间隙锁的事务提交为止。但是在等待期间,会生成一个插入意向锁,表明有事务想要在区间内插入新记录,但是现在处于等待状态。

隐式锁

在内存中生成锁结构并不是没有成本的,所以一般情形下执行INSERT语句不会生成锁结构,假如说此时有其他事务执行SELECT ... FOR UPDATE之类的语句,如果对INSERT事务不加锁的话,很可能出现脏读。

这个时候,InnoDB引擎:

  1. 对于聚簇索引记录而言,隐藏的trx_id列记录着事务id,当其他事务想获取这条记录的锁时,会判断该记录事务是否是当前的活跃事务,不是的话可以正常读取,是的话就帮助该事务创建一个锁结构,iswating属性为false;然后给自己创建一把锁,iswating属性为true,进入等待状态
  2. 如果是二级索引,先判断页的Page Header部分的PAGE_MAX_TRX_ID属性值小于当前活跃的事务id,说明该页面的修改事务都已经提交;都则定位到对应的二级索引,再执行步骤1。

隐式锁起到了延迟生成锁结构的用处。但是这对用户是透明的。

2. MySQL加锁分析

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

加锁的对象是索引,加锁的基本单位是 next-key lock ,它是由记录锁和间隙锁组合而成的,next-key lock 是前开后闭区间,而间隙锁是前开后开区间

但是,next-key lock 在一些场景下会退化成记录锁或间隙锁。

那到底是什么场景呢?总结一句,在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁

我们使用下表进行实验说明:

sql 复制代码
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` USING BTREE (`age`)
) ENGINE = INNODB CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

然后插入以下数据:

sql 复制代码
INSERT INTO `user` (id, `name`, age)
VALUES (1, '路飞', 19),
	(5, '索隆', 21),
	(10, '山治', 22),
	(15, '乌索普', 20),
	(20, '香克斯', 39);

SELECT * FROM `user`;
+----+-----------+-----+
| id | name      | age |
+----+-----------+-----+
|  1 | 路飞      |  19 |
|  5 | 索隆      |  21 |
| 10 | 山治      |  22 |
| 15 | 乌索普    |  20 |
| 20 | 香克斯    |  39 |
+----+-----------+-----+
5 rows in set (0.00 sec)

2.1 唯一索引等值查询

记录存在的情况

首先,事务A使用当前读读取id=1的数据:

sql 复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id = 1 for update;
+----+--------+-----+
| id | name   | age |
+----+--------+-----+
|  1 | 路飞   |  19 |
+----+--------+-----+
1 row in set (0.00 sec)

然后事务B更新当前记录,被阻塞:

sql 复制代码
mysql> update user set age = 20 where id = 1;

这时候查看锁的使用情况:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 1084099:256:3:2 | 1084099     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        2 | 1         |
| 1084096:256:3:2 | 1084096     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        2 | 1         |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rows in set, 1 warning (0.00 sec)

可以发现,此时的lock_typeRECORD是记录锁;lock_modeX表示是排他锁;lock_indexPRIMARY,表示锁的对象是主键索引;且锁住的记录是lock_data = 1,表示锁住的是第一条记录。

为什么唯一索引等值查询并且查询记录存在的场景下,该记录的索引中的 next-key lock 会退化成记录锁?

原因就是在唯一索引等值查询并且查询记录存在的场景下,仅靠记录锁也能避免幻读的问题。

记录不存在的情况

假设事务A执行了这条等值查询语句,且记录并不在表中:

sql 复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id = 2 for update;
Empty set (0.03 sec)

这时候,事务B执行以下插入:

sql 复制代码
mysql> insert user (id, name, age) values (3, "iguochan", 18);

可以发现事务B被阻塞,这时候查看锁信息如下:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 1084104:256:3:3 | 1084104     | X,GAP     | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        3 | 5         |
| 1084102:256:3:3 | 1084102     | X,GAP     | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        3 | 5         |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rows in set, 1 warning (0.01 sec)

可以发现,lock_modeX,GAP,表示是排他间隙锁;lock_data = 5表示间隙锁范围是(1, 5),即从前面一个记录到lock_data这条记录之间。

为什么唯一索引等值查询并且查询记录「不存在」的场景下,在索引树找到第一条大于该查询记录的记录后,要将该记录的索引中的 next-key lock 会退化成「间隙锁」?

原因就是在唯一索引等值查询并且查询记录不存在的场景下,仅靠间隙锁就能避免幻读的问题。

  • 为什么 id = 5 记录上的主键索引的锁不可以是 next-key lock?如果是 next-key lock,就意味着其他事务无法删除 id = 5 这条记录,但是这次的案例是查询 id = 2 的记录,只要保证前后两次查询 id = 2 的结果集相同,就能避免幻读的问题了,所以即使 id =5 被删除,也不会有什么影响,那就没必须加 next-key lock,因此只需要在 id = 5 加间隙锁,避免其他事务插入 id = 2 的新记录就行了。
  • 为什么不可以针对不存在的记录加记录锁?锁是加在索引上的,而这个场景下查询的记录是不存在的,自然就没办法锁住这条不存在的记录。

2.2 唯一索引范围查询

"大于"的范围查询

事务A执行以下语句:

sql 复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id > 16 for update;
+----+-----------+-----+
| id | name      | age |
+----+-----------+-----+
| 20 | 香克斯    |  39 |
+----+-----------+-----+
1 row in set (0.00 sec)

假设事务B希望向15~20之间插入数据:

sql 复制代码
mysql> insert user (id, name, age) values (19, "iguochan", 18);

此时查看锁情形是:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 1084129:256:3:6 | 1084129     | X,GAP     | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        6 | 20        |
| 1084124:256:3:6 | 1084124     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        6 | 20        |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rows in set, 1 warning (0.00 sec)

此时,发现事务A(也就是1084124)加了X锁,从分析来看应该是Next-Key Lock,应该锁住了范围,为了验证,我们尝试修改id=20的值。

假设事务C执行如下:

sql 复制代码
mysql> update `user` set age = 20 where id = 20;

发现也被阻塞,然后查看锁分析是:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 1084130:256:3:6 | 1084130     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        6 | 20        |
| 1084124:256:3:6 | 1084124     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        6 | 20        |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rows in set, 1 warning (0.00 sec)

即给20这个记录加上了Record Lock,结合以上分析,可以认为在(15, 20]加上了Next-Key Lock(即Record Lock + Gap Lock)。

然后我们再尝试向20以上的空隙插入一条:

sql 复制代码
mysql> insert user (id, name, age) values (22, "iguochan", 18);

再分析加锁:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+------------------------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data              |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+------------------------+
| 1084131:256:3:1 | 1084131     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        1 | supremum pseudo-record |
| 1084124:256:3:1 | 1084124     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        1 | supremum pseudo-record |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+------------------------+
2 rows in set, 1 warning (0.00 sec)

可以发现,加了(20, +∞]的Next-Key Lock

"大于等于"范围查询

我们修改一下事务A如下:

sql 复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id >= 15 for update;
+----+-----------+-----+
| id | name      | age |
+----+-----------+-----+
| 20 | 香克斯    |  39 |
+----+-----------+-----+
1 row in set (0.00 sec)

这时候,除了以上(15, 20]的Next-Key Lock、(20, +∞)的Next-Key Lock外,还包含id=15Record Lock

比如,事务B此时执行以下语句:

sql 复制代码
mysql> update `user` set age = 20 where id = 15;

锁分析如下:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 1084138:256:3:5 | 1084138     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        5 | 15        |
| 1084133:256:3:5 | 1084133     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        5 | 15        |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rows in set, 1 warning (0.00 sec)

所以总结下来就是,主键的范围查询时:

  • id > a:在所在值间隙形成(mina, b]、(b, c]、...、(z, +∞]Next-Key Lockmina表示a所在间隙最左边的值,比如以上例子,mina = 15;
  • id >= a
    • id = a这条记录不存在时,形成(mina, b]、(b, c]、...、(z, +∞]Next-Key Lock
    • id = a这条记录存在时,形成id = aRecord Lock(a, b]、(b, c]、...、(z, +∞]Next-Key Lock

其实很简单,就是从所在间隙往后推,记录包含在内的上Record Lock,记录不在的上Gap Lock,所在值不在记录中,则需要往前推到上一条记录。

小于或者小于等于

其实明白了以上的逻辑,我们很好分析小于的逻辑。

主键的范围查询时:

  • id < z
    • 若z的记录不存在,在所在值间隙形成(-∞, a]、(a, b]、...、(x, y]Next-Key Lock,以及形成(y, maxz)Gap Lockmaxz表示z所在间隙往右存在的记录值;
    • 若z的记录存在,则形成(-∞, a]、(a, b]、...、(x, y]Next-Key Lock,以及形成(y, z)Gap Lock
  • id <= z
    • id = z这条记录不存在时,形成(-∞, a]、(a, b]、...、(x, y]Next-Key Lock,以及形成(y, maxz)Gap Lockmaxz表示z所在间隙往右存在的记录值;
    • id = a这条记录存在时,形成(-∞, a]、(a, b]、...、(y, z]Next-Key Lock

其实还是遵循这个原理,在所在间隙往前推,记录包含在内的上Record Lock,记录不在的上Gap Lock,所在值不在记录中,则需要往后推到下一条记录。

2.3 非唯一索引等值查询

记录不存在的情况

事务A对非唯一索引age进行了等值查询,且表中不存在age = 25的记录:

sql 复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where age = 25 for update;
Empty set (0.00 sec)

此时事务B想插入一条数据:

sql 复制代码
mysql> insert user (id, name, age) values (16, "iguochan", 27);

这时候进行加锁分析如下:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 1084151:256:4:6 | 1084151     | X,GAP     | RECORD    | `test`.`user` | index_age  |        256 |         4 |        6 | 39, 20    |
| 1084149:256:4:6 | 1084149     | X,GAP     | RECORD    | `test`.`user` | index_age  |        256 |         4 |        6 | 39, 20    |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rows in set, 1 warning (0.00 sec)

可以看到,给二级索引index_age加上了(22, 39)的Gap Lock。此时,如果其他事务插入的年龄在23~38之间的话,都会被阻塞,比如事务B。

但是对于年龄22和39这两个特殊的点,我们需要特殊讨论,这里我们给出一个表示,即(age-id, age-id)来表示Gap Lock的范围,那么事务A锁住的区间就是(22-10, 39-20),这也就是以上表中lock_data39, 20的原因。

那也就是说,会有以下场景:

sql 复制代码
-- 成功
mysql> insert user (id, name, age) values (9, "iguochan", 22);
Query OK, 1 row affected (0.00 sec)

-- 阻塞
mysql> insert user (id, name, age) values (11, "iguochan", 22);
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted

-- 阻塞
mysql> insert user (id, name, age) values (19, "iguochan", 39);
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted

-- 成功
mysql> insert user (id, name, age) values (21, "iguochan", 39);
Query OK, 1 row affected (0.00 sec)

其实也很好理解,因为二级索引的索引对象就是一级索引。

记录存在的情况

我们先看下此时的表(有些记录的年龄被修改,怕无法同步,我们这里看一眼):

sql 复制代码
mysql> select * from `user`;
+----+-----------+-----+
| id | name      | age |
+----+-----------+-----+
|  1 | 路飞      |  19 |
|  5 | 索隆      |  20 |
| 10 | 山治      |  22 |
| 15 | 乌索普    |  20 |
| 20 | 香克斯    |  39 |
+----+-----------+-----+
5 rows in set (0.00 sec)

假设事务A对非唯一索引age进行了等值查询,且表中存在age = 22的记录:

sql 复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where age = 22 for update;
+----+--------+-----+
| id | name   | age |
+----+--------+-----+
| 10 | 山治   |  22 |
+----+--------+-----+
1 row in set (0.00 sec)

事务A的加锁如下:

  • 由于不是唯一索引,所以肯定存在值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,最开始要找的第一行是 age = 22,于是对该二级索引记录加上范围为 (20-15, 22] 的 next-key 锁。同时,因为 age = 22 符合查询条件,于是对 age = 22 的记录的主键索引加上记录锁,即对 id = 10 这一行加记录锁。
  • 接着继续扫描,扫描到的第二行是 age = 39,该记录是第一个不符合条件的二级索引记录,所以该二级索引的 next-key 锁会退化成间隙锁,范围是 (22-10, 39-20)。

接下来我们就验证以上三把锁:

(20-15, 22-10] 的 next-key 锁

事务B执行以下语句:

sql 复制代码
-- 成功
mysql> insert user (id, name, age) values (14, "iguochan", 20);
Query OK, 1 row affected (0.00 sec)

-- 阻塞
mysql> insert user (id, name, age) values (16, "iguochan", 20);

阻塞时的锁分析:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 1084177:256:4:4 | 1084177     | X,GAP     | RECORD    | `test`.`user` | index_age  |        256 |         4 |        4 | 22, 10    |
| 1084170:256:4:4 | 1084170     | X         | RECORD    | `test`.`user` | index_age  |        256 |         4 |        4 | 22, 10    |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rows in set, 1 warning (0.00 sec)
(22-10, 39-20)的 Gap Lock

事务C执行以下语句:

sql 复制代码
-- 阻塞
mysql> insert user (id, name, age) values (19, "iguochan", 39);
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted

-- 成功
mysql> insert user (id, name, age) values (21, "iguochan", 39);
Query OK, 1 row affected (0.00 sec)

阻塞时的锁分析如下:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 1084180:256:4:6 | 1084180     | X,GAP     | RECORD    | `test`.`user` | index_age  |        256 |         4 |        6 | 39, 20    |
| 1084170:256:4:6 | 1084170     | X,GAP     | RECORD    | `test`.`user` | index_age  |        256 |         4 |        6 | 39, 20    |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rows in set, 1 warning (0.00 sec)

为什么需要这把间隙锁呢?那是因为(20-15, 22-10]的Next-Key Lock无法完全锁住age = 22这行。

id = 10 的主键锁

事务D执行以下语句:

sql 复制代码
mysql> update `user` set age = 20 where id = 10;

阻塞时分析锁:

sql 复制代码
mysql> select * from information_schema.innodb_locks;
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| lock_id         | lock_trx_id | lock_mode | lock_type | lock_table    | lock_index | lock_space | lock_page | lock_rec | lock_data |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
| 1084181:256:3:4 | 1084181     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        4 | 10        |
| 1084170:256:3:4 | 1084170     | X         | RECORD    | `test`.`user` | PRIMARY    |        256 |         3 |        4 | 10        |
+-----------------+-------------+-----------+-----------+---------------+------------+------------+-----------+----------+-----------+
2 rows in set, 1 warning (0.00 sec)

可以发现,对PRIMARY做了记录锁。

2.4 非唯一索引范围查询

假设事务A执行了以下搜索:

sql 复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where age >= 22  for update;
+----+-----------+-----+
| id | name      | age |
+----+-----------+-----+
| 10 | 山治      |  22 |
| 20 | 香克斯    |  39 |
+----+-----------+-----+
2 rows in set (0.01 sec)

那么锁会如下图所示:

也就是非唯一索引不会退化成记录锁,原因就是非唯一索引不可能唯一锁定记录,所以无法退化。同样的,所有满足的聚簇索引上加记录锁。

2.5 没有加索引的查询

如果锁定读查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞

不只是锁定读查询语句不加索引才会导致这种情况,update 和 delete 语句如果查询条件不加索引,那么由于扫描的方式是全表扫描,于是就会对每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表。

因此,在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。

3. 总结

其实前面分析了这么多,我们不要死记硬背这些加锁过程,只需要记住加锁过程和索引结构以及索引的搜索过程息息相关,而锁的设计就是为了让该搜索不会出现幻读。

4. 参考文档

MySQL 是怎么加锁的?

《MySQL是怎样运行的》从根儿上理解 MySQL

MySQL行锁表锁和语句加锁分析

相关推荐
卡西里弗斯奥4 小时前
【达梦数据库】dblink连接[SqlServer/Mysql]报错处理
数据库·mysql·sqlserver·达梦
杨俊杰-YJ6 小时前
MySQL 主从复制原理及其工作过程
数据库·mysql
一个儒雅随和的男子6 小时前
MySQL的聚簇索引与非聚簇索引
数据库·mysql
独泪了无痕7 小时前
MySQL查询优化-distinct
后端·mysql·性能优化
hadage2338 小时前
--- Mysql事务 ---
数据库·mysql
天天向上vir10 小时前
缓存三大问题及其解决方案
java·redis·mysql
初尘屿风10 小时前
vue3项目,旅游景点页面
java·javascript·vue.js·spring boot·后端·mysql·ecmascript
Cikiss11 小时前
图解MySQL【日志】——Buffer Pool
java·数据库·后端·mysql
又逢乱世12 小时前
Node.js 连接 mysql 数据库
数据库·mysql·node.js
君败红颜12 小时前
MySQL 使用 Performance Schema 定位和解决慢 SQL 问题
数据库·sql·mysql