一文探讨!MySQL锁

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用版 2核4GB 50GB
简介: 0. 简介锁,是在计算机执行多线程(或协程)并发访问时,用于对同一共享资源的一致性同步机制。MySQL中的锁是在存储引擎中实现的,保证了数据访问的一致性。1. MySQL中的锁在InnoDB中,锁分为全局锁,表级锁和行级锁。1.1 全局锁全局锁主要用于全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。加全局锁:arduino复制代码flush tables with read lock这时候,意味着整个数据库都处于只读状态。不过在InnoDB引擎下,因为支持MVCC,所以在使用 mysqldump 时加上 –single

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_type是RECORD是记录锁;lock_mode是X表示是排他锁;lock_index是PRIMARY,表示锁的对象是主键索引;且锁住的记录是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_mode是X,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=15的Record 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 Lock,mina表示a所在间隙最左边的值,比如以上例子,mina = 15;
  • id >= a: 当id = a这条记录不存在时,形成(mina, b]、(b, c]、...、(z, +∞]的Next-Key Lock; 当id = a这条记录存在时,形成id = a的Record 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 Lock,maxz表示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 Lock,maxz表示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_data为39, 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. 总结

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

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
SQL 关系型数据库 MySQL
MySQL数据库进阶第五篇(锁)
MySQL数据库进阶第五篇(锁)
|
2月前
|
存储 SQL 关系型数据库
MYSQL--锁机制*
MYSQL--锁机制*
|
4天前
|
存储 SQL 关系型数据库
深入MySQL锁机制:原理、死锁解决及Java防范技巧
深入MySQL锁机制:原理、死锁解决及Java防范技巧
|
4天前
|
存储 SQL 关系型数据库
深入解析MySQL事务机制和锁机制
深入解析MySQL事务机制和锁机制
|
9天前
|
SQL 存储 关系型数据库
"MySQL增列必锁表?揭秘InnoDB在线DDL,让你的数据库操作飞一般,性能无忧!"
【8月更文挑战第11天】在数据库领域,MySQL凭借其稳定高效的表现深受开发者喜爱。对于是否会在给数据表添加列时锁表的问题,MySQL的行为受版本、存储引擎等因素影响。从5.6版起,InnoDB支持在线DDL,可在改动表结构时保持表的可访问性,避免长时间锁表。而MyISAM等则需锁表完成操作。例如,在使用InnoDB的表上运行`ALTER TABLE users ADD COLUMN email VARCHAR(255);`时,通常不会完全锁表。虽然在线DDL提高了灵活性,但复杂操作或大表变更仍可能暂时影响性能。因此,进行结构变更前应评估其影响并择机执行。
32 6
|
8天前
|
存储 SQL 关系型数据库
MySQL中的update操作与锁机制
本文探讨MySQL中`UPDATE`操作的自动加锁机制及其对数据一致性的保障作用。尤其在InnoDB存储引擎下,系统会在涉及索引的更新操作中加行锁或间隙锁,防止多事务并发修改同一条记录。通过福利码兑换系统的实例展示,当线程A开启事务更新库存时,线程B试图更新相同记录会被阻塞,直至线程A提交。此外,文章还介绍了乐观锁及版本号控制等策略进一步提升并发性能的方法。作者:小明爱吃火锅,来源:稀土掘金。
|
20天前
|
SQL 关系型数据库 MySQL
(八)MySQL锁机制:高并发场景下该如何保证数据读写的安全性?
锁!这个词汇在编程中出现的次数尤为频繁,几乎主流的编程语言都会具备完善的锁机制,在数据库中也并不例外,为什么呢?这里牵扯到一个关键词:高并发,由于现在的计算机领域几乎都是多核机器,因此再编写单线程的应用自然无法将机器性能发挥到最大,想要让程序的并发性越高,多线程技术自然就呼之欲出,多线程技术一方面能充分压榨CPU资源,另一方面也能提升程序的并发支持性。
|
2月前
|
存储 SQL 关系型数据库
【MySQL技术内幕】6.3-InnoDB中的锁
【MySQL技术内幕】6.3-InnoDB中的锁
169 57
|
20天前
|
SQL 算法 关系型数据库
(十)全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析
经过《MySQL锁机制》、《MySQL-MVCC机制》两篇后,咱们已经大致了解MySQL中处理并发事务的手段,不过对于锁机制、MVCC机制都并未与之前说到的《MySQL事务机制》产生关联关系,同时对于MySQL锁机制的实现原理也未曾剖析,因此本篇作为事务、锁、MVCC这三者的汇总篇,会在本章中补全之前空缺的一些细节,同时也会将锁、MVCC机制与事务机制之间的关系彻底理清楚。
|
27天前
|
存储 关系型数据库 文件存储
面试题MySQL问题之简单的SELECT操作在MVCC下加锁如何解决
面试题MySQL问题之简单的SELECT操作在MVCC下加锁如何解决
31 2