1. 问题:间隙锁导致死锁
最近由于想让代码快一点把一段代码逻辑从:查询->存在即删除->插入;改成了:根据条件删除->插入。
后来查看数据发现很多数据都不见了,不知道出现了什么bug,后来查看mysql日志才知道是出现了死锁。
根据排查是因为条件删除(会生成间隙锁)又插入,导致间隙锁发生了死锁,所以就来系统学习一下间隙锁以及innodb如何避免幻读。
两个事务对同一个数据(可能是行锁也可能是间隙锁)的操作逻辑:
- 在RR中,是根据快照读来进行操作的
- 当对一个数据或者数据块进行更新时,会加上行锁或者间隙锁甚至表锁
- 如果另一个事务想要来修改这同一个或者一块数据,就会等待之前的事务释放锁,拿到锁之后才能继续往下执行
2. 什么是MVCC
在之前的文章中详细的介绍了 MySQL 中的事务和隔离级别,在并发访问数据库造成的问题(脏读、不可重复读、幻读),而 MVCC 就是在尽量减少锁使用的情况下高效避免这些问题。
MySQL 四大隔离级别:
MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能。
同一行数据平时发生读写请求时,会上锁阻塞住。但 MVCC 用更好的方式去处理读写请求,做到在发生读写请求冲突时不用加锁。
这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。
那它到底是怎么做到读写不用加锁的,快照读和当前读是指什么?
3. 当前读和快照读定义
3.1 当前读:
读取的是最新数据,而不是历史数据。
当前读是基于临键锁next-key lock(行锁+间歇锁)来实现的
insert,update,delete, select ... for update, select ... lock in share mode 语句,加了锁的 select 语句
。都是当前读。
3.2 快照读
读取的是快照数据。
快照读是基于 MVCC 和 undo log 来实现的,适用于简单的非阻塞的select 语句。
3.2.1 快照读在RR和RC中的区别:
- 读已提交(RC):是事务中的每个 sql 语句生成一个 readView。那就是一个事务内多条 sql 语句,会生成多个 readView。而每条 sql 执行时,都是查询最新 readView 的值。
- 假如事务 A 有2个查询 sql 语句,在第一个查询 sql 生成一个 readView(事务视图 id = n),事务 B 对该数据做了操作,那么就会生成新的 readView(事务视图 id = n + 1),第二个查询 sql 语句获取该条数据时,就会去 readView(事务视图 id = n + 1)查询数据。
- 可重复读(RR):是在事务开始的时候生成一个 readView。所以一个事务内的多条查询 sql ,查询同一条数据时,读取到的 readView 都是同一个,那么查询某条数据的值,也是同一个值。
- 例如事务A开始查询主键 id = 1 的行数据的列 age = 10,不管其他事务是否对该 age 做改变,当前事务的多条查询 sql 语句,查询 age 的值一直都是 age = 10。
4. MVCC机制原理
它的实现原理主要是版本链,undo日志 ,Read View来实现的。
4.1 版本链(隐藏字段)
表中的聚簇索引都包含三个隐藏列:
- A 6-byte
DB_TRX_ID
:用来标识最近一次对本行记录做修改 (insert 、update) 的事务的标识符 ,即最后一次修改本行记录的事务 id。 如果是 delete 操作, 在 InnoDB 存储引擎内部也属于一次 update 操作,即更新行中的一个特殊位 ,将行标识为己删除,并非真正删除。 - A 7-byte
DB_ROLL_PTR
:回滚指针,指向该行的 undo log 。每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。如果该行未被更新,则为空. - A 6-byte
DB_ROW_ID
:如果没有设置主键且该表没有唯一非空索引时,InnoDB
会使用该 id 来生成聚簇索引,如果有则没有row_id列
现在有这样一张表:
CREATE TABLE `user` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(20) DEFAULT NULL COMMENT '姓名',
`sex` char(1) DEFAULT NULL COMMENT '性别',
`age` varchar(10) DEFAULT NULL COMMENT '年龄',
`url` varchar(40) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `suf_index_url` (`name`(3)) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
插入如下一条数据:
INSERT INTO `user` (`id`, `name`, `sex`, `age`, `url`)
VALUES ('1', 'ayue', '1', '18', 'https://javatv.net');
假设插入该记录的事务 id 为 60,那么此刻该条记录的示意图如下所示:
假设之后有两个事务 id 分别为 80、120 的事务对这条记录进行 UPDATE 操作,操作流程如下:
每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。
另外,每个版本中还包含生成该版本时对应的事务 id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制(MVCC)。
4.2 undo日志
undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。当事务进行回滚时可以通过 undo log 里的日志进行数据还原。(MySQL 中的日志)
4.2.1 Undo log 的用途:
- 保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
- 用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。
4.2.2 undo log主要分为两种:
- insert undo log
代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃。 - update undo log
事务在进行 update 或 delete 时产生的 undo log, 不仅在事务回滚时需要,在快照读时也需要。所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除。
4.3 ReadView
上面说到了,改动的记录都存在在 undo 日志中,那如果一个日志需要查询行记录,需要读取哪个版本的行记录呢?
- 对于使用 READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
- 对于使用 SERIALIZABLE 隔离级别的事务来说,InnoDB 使用加锁的方式来访问记录,不存在并发问题。
- 而对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的。
核心问题就是: READ COMMITTED 和 REPEATABLE READ 隔离级别在不可重复读和幻读上的区别在哪里?这两种隔离级别对应的不可重复读与幻读都是指同一个事务在两次读取记录时出现不一致的情况,这两种隔离级别关键是需要判断版本链中的哪个版本是当前事务可见的。
ReadView 就是用来解决这个问题的,可以帮助我们解决可见性问题。 事务进行快照读操作的时候就会产生 Read View,它保存了当前事务开启时所有活跃的事务列表。(注:这里的活跃指的是未提交的事务)
每一个事务在启动时,都会生成一个 ReadView,用来记录一些内容,ReadView 中主要包含 4 个比较重要的属性:
其中,max_trx_id并不是指m_ids中的最大值,因为事务 id 是递增分配的,假如现在有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4。
在有了 ReadView 之后,在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
- trx_id = creator_trx_id ,可访问
如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。 - trx_id < min_trx_id ,可访问
如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。 - trx_id >= max_trx_id ,不可访问
如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。 - min_trx_id <= trx_id < max_trx_id,存在 m_ids 列表中不可访问
如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。 - 某个版本的数据对当前事务不可见
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。
4.4 RC 和 ReadView
在 READ COMMITTED 级别下每次读取数据前都生成一个 ReadView。
假如现在系统里同时开启两个事务 id 分别为 80、120 的事务在执行,且使用 READ COMMITTED 隔离级别。
4.4.1 trx_id = 80
# 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
# 修改当前会话隔离级别为 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
# 开启事务
BEGIN
UPDATE `user` SET `name` = 'a' WHERE id = 1;
UPDATE `user` SET `name` = 'y' WHERE id = 1;
此刻,表 user 中 id 为 1 的记录得到的版本链表如下所示:
在不提交的情况下,使用 READ COMMITTED 隔离级别的事务去查询:
# 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
# 修改当前会话隔离级别为 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
# 开启事务
BEGIN
SELECT * FROM `user` WHERE id = 1;
很明显,结果为ayue。
对于这个查询语句的执行过程如下:
在执行 SELECT 语句时会先生成一个 ReadView,ReadView 的 m_ids 列表的内容就是[80, 120],min_trx_id 为 80,max_trx_id 为 121,由于这是一个 SELECT 语句,所以并不会产生 creator_trx_id ,因此我们可以默认它为 0。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是y,该版本的 trx_id = 80,在 m_ids 列表内,不符合可见性要求,因此根据 roll_pointer 跳到下一个版本。
而在下一个版本中,name 的值为a,该版本的 trx_id = 80,还是不符合可见性要求,因此根据 roll_pointer 跳到下一个版本。
在这个版本中,name 的值为ayue,该版本的 trx_id = 60,符合可见性要求trx_id < min_trx_id,所以返回给用户的版本就是这条列 name 为ayue的记录。
然后,我们把事务 trx_id = 80 的事务提交(COMMIT),然后再到事务 trx_id = 120的事务中更新一下表 user 中 id 为 1 的记录。
4.4.2 trx_id = 120
# 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
# 修改当前会话隔离级别为 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
# 开启事务
BEGIN
UPDATE `user` SET `name` = 'u' WHERE id = 1;
UPDATE `user` SET `name` = 'e' WHERE id = 1;
此刻,表 user 中 id 为 1 的记录得到的版本链表如下所示:
此时,trx_id = 80已提交,trx_id = 120未提交,使用 READ COMMITTED 隔离级别的事务去查询:
# 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
# 修改当前会话隔离级别为 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
# 开启事务
BEGIN
SELECT * FROM `user` WHERE id = 1;
很明显,结果为y。
此时,SELECT 语句又会单独生成一个 ReadView,且其中的 m_ids 的内容为[120],因为trx_id = 80这个事务已经提交,所以再次生成快照的时候就不存在了。
同理,此时按照可见性要求,在trx_id = 120的事务未提交的情况下,读到的数据为 name = y。
而当事务 trx_id = 120也提交之后,再次去查询得到的就是正确结果了。
总的来说就是,使用 READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView。
4.4.3 总结一下:
READ COMMITTED,可能产生不可重复读的现象,如上面测试的情况:事务trx_id = 80和trx_id = 120同时开启的情况下,trx_id = 80提交了
4.5 RR 和 ReadView
在 REPEATABLE READ 级别下在第一次读取数据时生成一个 ReadView。
还是在同样的条件下,开启两个事务 trx_id = 80和 trx_id = 120,流程同上,一模一样(SQL语句和步骤),只是隔离级别为RR。
当事务trx_id = 80执行修改操作并在提交之前,版本链表如下所示:
此时执行 SELECT 语句会先生成一个 ReadView,ReadView的m_ids 列表的内容就是[80,120],min_trx_id = 80,max_trx_id = 121,creator_trx_id = 0。
根据可见性原则,此时的 trx_id 在min_trx_id和max_trx_id之间且在m_ids 列表中,不符合,所以会随着版本链往下找,直到trx_id = 60,找出的值为 ayue。
然后提交。
提交之后在去事务trx_id = 120中执行上述更新操作,版本链表如下所示:
由于之前在执行 SELECE 语句时已经生成过 ReadView 了,所以此时直接复用之前的 ReadView,之前的 ReadView 的 m_ids 列表的内容就是[80, 120],min_trx_id = 80,max_trx_id = 121,creator_trx_id = 0。
而当前的事务 id 为 120,在min_trx_id和max_trx_id之间且在m_ids列表中,所以不符合可见性原则,则会继续随着版本链往下找,当找到事务 id 为 80时,同样不满足,所以继续往下直到trx_id = 60,满足条件,返回的值为 ayue。
所以说 RR 解决了不可重复读问题。
5. RR解决了幻读问题了吗?
(先说结论:只通过MVCC只能解决快照读下的幻读现象,无法解决当前读的幻读现象。)
通过上面分分析我们知道,REPEATABLE READ 隔离级别下 MVCC 可以解决不可重复读问题。
那么幻读呢?MVCC是怎么解决的?
5.1 幻读是什么?
幻读主要是指:
5.1.1 幻读出现的简单情况:(快照读的情况)
同一个事务中,同一条sql第一次查询到的数据,和第二次查询不一致,多了数据或者少了数据,称为幻读。
针对这种情况,MVCC是可以替我们解决的,因为快照读读取到的始终是事务开始时得到的数据。
5.1.2 幻读出现的复杂情况:(当前读的情况)
因为当前读是要去系统中读取最新数据,而最新数据和我们快照读的数据有冲突。
- 案例1:
事务A第一次查询出来的数据中,例如是没有id为1的数据,这时候事务b插入了一条id为1的数据,事务b提交事务,如果这时候事务A,插入id为1的数据,这时候就会报错主键冲突,导致数据插入不了。 - 案例2:
银行 A 开启了一个事务窗口,查询当前系统中有没有 “ayue” 用户,发现没有,银行 B 也开启了一个事务窗口,查询当前系统中也没有 “ayue” 用户,银行 A 先创建 “ayue” 用户并且提交,由于可重复读取,银行 B 在一次事务中必须保证查询的数据一致性,因此查询不到 “ayue”,结果银行 B 窗口认为 “ayue” 没有被注册想注册 “ayue” 用户,就创建 “ayue” 用户结果发现系统提示 “ayue” 用户已经被注册",但是在本次事务中又查询不到 “ayue”,就好像出现幻觉一样
即:事务A不仅仅只是读取数据,而是和事务2对同一个索引进行了类似的插入修改操作。
针对这种情况,我们需要加锁才能解决,MVCC不能解决这种情况下的幻读。
5.2 MVCC在快照读下解决幻读的情况
可重复读隔离级是由 MVCC(多版本并发控制)实现的,实现的方式是启动事务后,在执行第一个查询语句后,会创建一个 Read View,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好了避免幻读问题。
做个实验,数据库表 t_stu 如下,其中 id 为主键。
然后在可重复读隔离级别下,有两个事务的执行顺序如下:
从这个实验结果可以看到,即使事务 B 中途插入了一条记录,事务 A 前后两次查询的结果集都是一样的,并没有出现所谓的幻读现象。
通过这个案例,我们可以看到MVCC替我们解决了简单情况下(快照读)的幻读现象,那么他解决了当前读的幻读现象了吗?答案是没有的,我们接着往下看:
5. MVCC下的幻读现象
接下来举两个RR下出现幻读的情况:
5.1 第一个发生幻读现象的场景
以这张表作为例子:
事务 A 执行查询 id = 5 的记录,此时表中是没有该记录的,所以查询不出来。
# 事务 A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_stu where id = 5;
Empty set (0.01 sec)
然后事务 B 插入一条 id = 5 的记录,并且提交了事务。
# 事务 B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t_stu values(5, '小美', 18);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
此时,事务 A 更新 id = 5 这条记录,对没错,事务 A 看不到 id = 5 这条记录,但是他去更新了这条记录,这场景确实很违和,然后再次查询 id = 5 的记录,事务 A 就能看到事务 B 插入的纪录了,幻读就是发生在这种违和的场景。
# 事务 A
mysql> update t_stu set name = '小林coding' where id = 5;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t_stu where id = 5;
+----+--------------+------+
| id | name | age |
+----+--------------+------+
| 5 | 小林coding | 18 |
+----+--------------+------+
1 row in set (0.00 sec)
整个发生幻读的时序图如下:
在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView(且在 RR 下只会生成一个 RV),之后事务 B 向表中新插入了一条 id = 5 的记录并提交。
ReadView 并不能阻止事务 A 执行 UPDATE 或者 DELETE 语句来改动这个新插入的记录(由于事务 B 已经提交,因此改动该记录并不会造成阻塞)。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。
因为这种特殊现象的存在,所以我们认为 MySQL Innodb 中的 MVCC 并不能完全避免幻读现象。
5.2 第二个发生幻读现象的场景
除了上面这一种场景会发生幻读现象之外,还有下面这个场景也会发生幻读现象。
T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。
要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。
6. 如何解决幻读
在第 2 点中,我们知道数据库的读操作分为当前读和快照读,而在 RR 隔离级别下,MVCC 在解决了不可重复读的同时解决了在快照读的情况下的幻读,而在实际场景中,我们可能需要读取实时的数据,比如在银行业务等特殊场景下,必须是需要读取到实时的数据,此时就不能快照读。
那么有什么方法来解决这个问题呢?
毫无疑问,在并发场景下,我们可以通过加锁的方式来实现当前读,而在 MySQL 中则是通过Next-Key Locks来解决幻读的问题。
Next-Key Locks包含两部分:记录锁(行锁,Record Lock),间隙锁(Gap Locks)。记录锁是加在索引上的锁,间隙锁是加在索引之间的。
6.1 Record Lock
记录锁,单条索引记录上加锁。
Record Lock 锁住的永远是索引,不包括记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。
记录锁是有 S 锁和 X 锁之分的,当一个事务获取了一条记录的 S 型记录锁后,其他事务也可以继续获取该记录的 S 型记录锁,但不可以继续获取 X 型记录锁;当一个事务获取了一条记录的 X 型记录锁后,其他事务既不可以继续获取该记录的 S 型记录锁,也不可以继续获取 X 型记录锁。
6.2 Gap Locks
间隙锁,对索引前后的间隙上锁,不对索引本身上锁。前开后开区间。
MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC 方案解决(有时可能会有问题),也可以采用加锁方案解决。但是在使用加锁方案解决时有问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁。所以我们可以使用间隙锁对其上锁。
如存在这样一张表:
CREATE TABLE test (
id INT (1) NOT NULL AUTO_INCREMENT,
number INT (1) NOT NULL COMMENT '数字',
PRIMARY KEY (id),
KEY number (number) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
# 插入以下数据
INSERT INTO test VALUES (1, 1);
INSERT INTO test VALUES (5, 3);
INSERT INTO test VALUES (7, 8);
INSERT INTO test VALUES (11, 12);
如下:
开启一个事务 A:
BEGIN;
SELECT * FROM test WHERE number = 3 FOR UPDATE;
此时,会对((1,1),(5,3))和((5,3),(7,8))之间上锁。
如果此时在开启一个事务 B 进行插入数据,如下:
BEGIN;
# 阻塞
INSERT INTO test (id, number) VALUES (2,2);
结果如下:
为什么不能插入?因为记录(2,2)要 插入的话,在索引 number上,刚好落在((1,1),(5,3))和((5,3),(7,8))之间,是有锁的,所以不允许插入。 如果在范围外,当然是可以插入的,如:
INSERT INTO test (id, number) VALUES (8,8);
另外,既然涉及到索引,那么索引对间隙锁会产生什么影响?
- 对主键或唯一索引,如果当前读时,where 条件全部精确命中(=或in),这种场景本身就不会出现幻读,所以只会加行记录锁,也就是说间隙锁会退化为行锁(记录锁)。
- 非唯一索引列,如果 where 条件部分命中(>、<、like等)或者全未命中,则会加附近间隙锁。例如,某表数据如下,非唯一索引2,6,9,9,11,15。如下语句要操作非唯一索引列 9 的数据,间隙锁将会锁定的列是(6,11],该区间内无法插入数据。
- 对于没有索引的列,当前读操作时,会加全表间隙锁,生产环境要注意。
6.3 Next-Key Locks
next-key locks 是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合,包括记录本身,每个 next-key locks 是前开后闭区间(同样说明锁住的范围更大,影响并发度),也就是说间隙锁只是锁的间隙,没有锁住记录行,next-key locks 就是间隙锁基础上锁住右边界行。
默认情况下,InnoDB 以 REPEATABLE READ 隔离级别运行。在这种情况下,InnoDB 使用 Next-Key Locks 锁进行搜索和索引扫描,这可以防止幻读的发生。
7. 总结
7.1 RR级别用处:
我们说 MVCC 在可重复读(RR)的隔离级别解决了以下问题:
- 并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作;
- 解决脏读、幻读、不可重复读等事务隔离问题。
7.2 RR和RC的区别
而对于 RR 和 RC 这两个隔离级别的不同:
- 生成 ReadView 的时机不同:
RC 在每一次进行普通 SELECT 操作前都会生成一个 ReadView,而 RR 只在第一次进行普通 SELECT 操作前生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 就好了,从而基本上可以避免幻读现象。 - RR 隔离级别下才有间隙锁,RC 隔离级别下没有间隙锁;
7.3 对于幻读:
但是,对于幻读来说,还存在当前读和快照读的情况:
- RR级别下通过MVCC只能解决快照读(select)的幻读现象,当前读(insert、update等)的幻读现象还是需要加锁来解决
- 间隙锁和行锁合称 Next-Key Locks,每个 Next-Key Locks 是前开后闭区间;
- 间隙锁的引入,可能会导致同样语句锁住更大的范围,影响并发度。
参考:
https://www.jianshu.com/p/b7c53ee0ed0e
https://javatv.blog.youkuaiyun.com/article/details/121963884