在第 14 篇文章中,我们介绍了 MySQL 数据库中常用的几种隔离机制,并且介绍了由于隔离机制所产生的几种读现象。
大部分的读现象我们可以通过提高事务的隔离机制来解决,但是有一种读现象很特殊,它就是幻读。
今天,我们用一篇文章,来具体介绍一下幻读是怎样产生的、幻读会导致什么问题以及如何解决幻读所产生的问题。
幻读是怎么产生的
在了解幻读是怎么产生的之前,我们首先创建一个数据表,方便用来测试:
CREATE TABLE `t1`.`test` (
`id` int NOT NULL AUTO_INCREMENT,
`a` int(11) NULL,
`b` int(11) NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci;
该数据表有三个字段,分别是 id、a 和 b。
顺便,我们在该数据表中插入几条测试数据。
INSERT INTO test VALUES (10,10,10);
INSERT INTO test VALUES (20,20,20);
INSERT INTO test VALUES (30,30,30);
INSERT INTO test VALUES (40,40,40);
INSERT INTO test VALUES (50,50,50);
INSERT INTO test VALUES (60,60,60);
首先,我们回顾一下幻读,它体现出来的最主要的一个现象是:同一个事务中,多次查询同一条数据时显示的结果不一致。具体如下:
-- 在事务1中查询
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where a = 20 for update;
+----+------+------+
| id | a | b |
+----+------+------+
| 20 | 20 | 20 |
+----+------+------+
1 row in set (0.00 sec)
此时,我们可以看到的是,查询出来一条相关的数据。
然后,我们在一个新的事务中修改一条数据,具体如下:
更新数据之前不要使用 select … for update,修改数据之后再使用 select … for update ,模拟修改数据的场景。
-- 在事务 2 中修改一条数据
mysql> update test set a = 20 where b = 50;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
此时,可以看到的是我们将 b = 50
的这条数据的a
字段的值修改成了20
,那么在事务 1 中查询相关的数据会是什么结果呢?具体如下:
-- 在事务1中查询
mysql> select * from test where a = 20 for update;
+----+------+------+
| id | a | b |
+----+------+------+
| 20 | 20 | 20 |
| 50 | 20 | 50 |
+----+------+------+
2 rows in set (0.00 sec)
此时,我们可以看到的是,同一个事务、同一条 SQL ,最终的结果却是不同;除此之外,我们新增一条数据是不是也是相同的结果呢?具体如下:
-- 在事务 3 中新增一条数据
mysql> insert into test values (80,20,80);
Query OK, 1 row affected (6.88 sec)
-- 在事务 1 中查询
mysql> select * from test where a = 20 for update;
+----+------+------+
| id | a | b |
+----+------+------+
| 20 | 20 | 20 |
| 50 | 20 | 50 |
| 80 | 20 | 80 |
+----+------+------+
3 rows in set (0.00 sec)
跟我们设想的结果相同,新增一条数据之后,我们可以使用排他锁立即查询相关的数据,这个时候我们形象地将其称为:幻读。
幻读导致了什么问题
上面,我们通过给 select 加上排他锁(for update)的方式模拟修改数据的场景;也就是说,在当时的情况下,如果我们采用相同条件的 SQL 修改相关的数据的话,那么修改的是最新的数据。
此时,我们会问有什么问题吗?确实有问题!
不过,在介绍具体问题之前,我们首先通过一个案例,来给你介绍幻读会导致哪些问题。
我们将上述的 SQL 变一下,在事务 1 中修改部分数据,具体如下:
-- 在事务1中我们修改部分数据
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set b = 11 where b = 10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
然后在事务 2 中修改部分数据,具体如下:
mysql> update test set b = 10 where b = 80;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
再然后,我们在事务 3 中新增部分数据,具体如下:
mysql> insert into test values (100,100,10);
Query OK, 1 row affected (0.00 sec)
此时,我们再来提交事务 1,具体如下:
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
从这流程中,我们可能会产生一个疑问,感觉这个过程似乎是没有问题的,对吗?
OK,那么我们来查询一下该数据表中的数据。具体如下:
mysql> select * from test;
+-----+------+------+
| id | a | b |
+-----+------+------+
| 10 | 10 | 11 |
...... -- 此处省略部分数据
| 80 | 20 | 10 |
| 100 | 100 | 10 |
+-----+------+------+
10 rows in set (0.00 sec)
如果,此时你是操作事务 1 的同学,你会不会感觉到很奇怪呢?明明你执行的 SQL 的主要作用是将所有b = 10
的数据的 b
字段的数值修改成了 11
,那么怎么还会出现两个 b
字段的数值仍然是 10
?
好,为了解释这个问题,我们把上方的 SQL 简单描述一下。
- 第一个步骤:执行
update test set b = 11 where b = 10;
,但是该执行的结果在步骤二和三执行之后才提交。 - 第二个步骤:执行
update test set b = 10 where b = 80;
。 - 第三个步骤:执行
insert into test values (100,100,10);
。 - 第四个步骤:提交第一个步骤执行结果。
为了方便理解,我用普通的 SQL
再来解释一遍,具体如下:
update test set b = 11 where b = 10;
update test set b = 10 where b = 80;
insert into test values (100,100,10);
注意:这个时候,有朋友就会问了,它不是在第四个步骤才提交吗?应该会修改所有的数据的呀?这个其实是受 MVCC 的影响,步骤一会先修改快照中的值,然后再将快照中的值 merge 过来。
此时,幻读问题就给我们造成了数据不一致的问题,这个问题对于 MySQL 这类关系型的数据库来说是非常严重的问题。
MySQL 数据库为了解决这个问题,提供了一个新的锁机制,即:间隙锁。
如何解决幻读所产生的影响
我们都知道,为了解决 MySQL 数据库在并发时的数据安全问题,它为我们提供了一个锁机制,我们之前学习了行锁。行锁:顾名思义就是操作一行锁定一行。
结合上面我们解释的幻读现象。我们来一起分析一下,出现幻读现象最主要的原因是当我们在一个事务中操作部分内容时,有其他事务在我们本次操作的范围之内添加了新的操作,进而导致了幻读。
那么,换句话说,如果我们想解决幻读现象所导致的问题时,那么就必须保证在一个事务中操作部分数据的时候,其他事务禁止修改该部分数据。
如果我们能够达到这一效果,就能够彻底解决幻读现象。
MySQL 数据库为了解决这个问题,给我们提供了一个新的锁机制,即间隙锁。
在详细介绍间隙锁之前,我们说明一下,为什么不能使用行锁?
行锁只锁定部分数据,而不能锁定某一个范围,例如:我们修改b=10
的相关数据时,行锁无法控制其他事务添加b=10
的相关数据,所以不可取。
那么什么是间隙锁呢?它是怎么解决幻读的呢?
间隙锁,顾名思义就是会锁定某一个范围,这个范围中的数据全部都归该锁控制,以至于其他事务无法操作相关范围的数据,以此达到解决幻读的目的。
为了方便理解,我们给大家详细解释一下(将数据表中的数据还原成最初的数据)。
回到上面,如果我们操作的 SQL 为:update test set b = 11 where b = 10;
注意:此时,MySQL 数据库会将相关的数据分为几个区间,分别是:(-∞, 10]、(10, 20]、(10, 30]、(10, 40]、(10, 50]、(10, 60] 以及 (60, +∞)。MySQL 间隙锁是一个左开右闭的区间。
那么,MySQL 不仅会锁定b=10
的相关数据,还会锁定相关的 7
区间,此时如果你想修改相关数据和新增相关的数据时,都是阻塞状态。
总结
今天,我们学习了幻读相关的知识。首先我们介绍了幻读是怎么形成的,其次我们又分析了幻读可能会导致的问题,最后我们解释了如何解决幻读所导致的问题。
在实际应用过程中,我们应该尽量避免过多地使用间隙锁,因为间隙锁的性能消耗比较高,如果想要提高性能就少用。