天道不一定酬所有勤
但是,天道只酬勤
Hollis出品的全套Java面试宝典不来了解一下吗?

Innodb的RR到底有没有解决幻读?

Hollis出品的全套Java面试宝典不来了解一下吗?

关于Innodb中的REPEATABLE READ这种隔离级别到底有没有解决幻读?好像众说纷纭,大家的说法都不一致。

有的人说,RR当然没解决幻读了,因为只有Serializable才能解决幻读。

也有人说,RR解决了幻读,因为RR中加了间隙锁,就能解决幻读的问题。

还有人说,只有间隙锁是没用的,还有MVCC也帮助RR解决了幻读的问题。

那到底真实情况是怎么样的呢?

我认为,InnoDB中的REPEATABLE READ这种隔离级别通过间隙锁+MVCC解决了大部分的幻读问题,只有一种特殊的幻读情况无法解决。

为什么这么说呢?这种特殊情况是怎么回事儿呢?本文就来把这个问题讲清楚。(本文中所有SQL的运行环境是MySQL 5.7.9 及MySQL 8.0.30)

什么是幻读

在介绍如何解决幻读之前,有必要再明确一下什么是幻读,确保大家理解是一致的。

幻读就是事务在做范围查询(SELECT)的过程中,有另外一个事务对范围内新增了记录(INSERT),导致范围查询的结果条数不一致的现象。

有这样一张表:

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT,
    gmt_create DATETIME NOT NULL,
    age INT NOT NULL,
    name VARCHAR(16) NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB;

INSERT INTO users(gmt_create,age,name) values(now(),18,'Hollis');
INSERT INTO users(gmt_create,age,name) values(now(),28,'HollisChuang');
INSERT INTO users(gmt_create,age,name) values(now(),38,'Hollis666');

接着我们进行如下操作:

事务1 事务2
set session tx_isolation='READ-COMMITTED';
BEGIN;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30;
BEGIN;
INSERT INTO users(gmt_create,age,name) values(now(),20,'Hollis999');
COMMIT;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30;

在这个例子中,在事务1中执行了两次相同的查询操作。但是两次操作中间事务2向数据库中增加了一条符合事务1的查询条件的数据,最终事务1的两次查询得到的结果是不一样的,这种现象就是幻读。

MVCC解决幻读

MVCC,是Multiversion Concurrency Control的缩写,翻译过来是多版本并发控制,和数据库锁一样,他也是一种并发控制的解决方案。它主要用来解决读-写并发的情况。关于MVCC的原理可以参考《再有人问你什么是MVCC,就把这篇文章发给他!

我们知道,在MVCC中有两种读,一种是快照读、一种是当前读。

所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的普通的SELECT语句在不加锁情况下就是快照读。

SELECT * FROM xx_table WHERE ...

在 RC 中,每次读取都会重新生成一个快照,总是读取行的最新版本。

在 RR 中,快照会在事务中第一次SELECT语句执行时生成,只有在本事务中对数据进行更改才会更新快照。

那么也就是说,如果在RR下,一个事务中的多次查询,是不会查询到其他的事务中的变更内容的,所以,也就是可以解决幻读的。

所以,针对上面的例子,如果我们把事务隔离级别设置为RR,那么因为有了MVCC的机制,就能解决幻读的问题:

事务1 事务2
set session tx_isolation='REPEATABLE-READ';
BEGIN;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30;
BEGIN;
INSERT INTO users(gmt_create,age,name) values(now(),20,'Hollis999');
COMMIT;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30;

可以看到,同一个事务中的两次查询结果是一样的,就是在RR级别下,因为有快照读,所以第二次查询其实读取的是一个快照数据。

间隙锁与幻读

上面我们讲过了MVCC能解决RR级别下面的快照读的幻读问题,那么当前读下面的幻读问题怎么解决呢?

当前读就是读取最新数据,所以,加锁的 SELECT,或者对数据进行增删改都会进行当前读,比如:

SELECT * FROM xx_table LOCK IN SHARE MODE;

SELECT * FROM xx_table FOR UPDATE;

INSERT INTO xx_table ...

DELETE FROM xx_table ...

UPDATE xx_table ...

举一个下面的例子:

事务1 事务2
set session tx_isolation='REPEATABLE-READ';
BEGIN;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30 FOR UPDATE
BEGIN;
INSERT INTO users(gmt_create,age,name) values(now(),20,'Hollis999');
阻塞!!!

向上面这种情况,在RR的级别下,当我们使用SELECT ... FOR UPDATE的时候,会进行加锁,不仅仅会对行记录进行加锁,还会对记录之间的间隙进行加锁,这就叫做间隙锁(参考:数据库的锁,到底锁的是什么?)。因为记录之间的间隙被锁住了,所以事务2的插入操作就被阻塞了,一直到事务1把锁释放掉他才能执行成功。

因为事务2无法插入数据成功,所以也就不会存在幻读的现象了。所以,在RR级别中,通过加入间隙锁的方式,就避免了幻读现象的发生。

解决不了的幻读

前面我们介绍了快照读(无锁查询)和当前读(有锁查询)下是如何解决幻读的问题的,但是,上面的例子就是幻读的所有情况了吗?显然并不是。

我们说MVCC只能解决快照读的幻读,那如果在一个事务中发生了当前读,并且在另一个事务插入数据前没来得及加间隙锁的话,会发生什么呢?

那么,我们稍加修改一下上面的SQL代码,通过当前读的方式进行查询数据:

事务1 事务2
set session tx_isolation='REPEATABLE-READ';
BEGIN;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30
BEGIN;
INSERT INTO users(gmt_create,age,name) values(now(),20,'Hollis999');
COMMIT;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30 FOR UPDATE;

在上面的例子中,在事务1中,我们并没有在事务开启后立即加锁,而是进行了一次普通的查询,然后事务2插入数据成功之后,再通过事务1进行了2次查询。

我们发现,事务1后面的两次查询结果完全不一样,没加锁的情况下,就是快照读,读到的数据就和第一次查询是一样的,就不会发生幻读。但是第二次查询加了锁,就是当前读,那么读取到的数据就有其他事务提交的数据了,就发生了幻读。

那么,如果你理解了上面的这个例子,并且你也理解了当前读的概念,那么你很容易就能想到,下面的这个CASE其实也是会发生幻读的:

事务1 事务2
set session tx_isolation='REPEATABLE-READ';
BEGIN;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30
BEGIN;
INSERT INTO users(gmt_create,age,name) values(now(),20,'Hollis999');
COMMIT;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30;
UPDATE users set name ="Hollis888" where AGE = 20;
SELECT * FROM users WHERE AGE > 10 AND AGE < 30;

这里发生幻读的原理,和上面的例子其实是一样的,那就是MVCC只能解决快照读中的幻读问题,而对于当前读(SELECT FOR UPDATE、UPDATE、DELETE等操作)还是会产生幻读的现象的。

UPDATE语句也是一种当前读,所以它是可以读到其他事务的提交结果的。

为什么事务1的最后一次查询和倒数第二次查询的结果也不一样呢?

是因为根据快照读的定义,在RR中,如果本事务中发生了数据的修改,那么就会更新快照,那么最后一次查询的结果也就发生了变化。

如何避免幻读

那么了解了幻读的解决场景,以及不能解决的几个CASE之后,我们来总结一下该如何解决幻读的问题呢?

首先,如果想要彻底解决幻读的问题,在InnoDB中只能使用Serializable这种隔离级别。

图源:MySQL 8.0 Reference Manual

那么,如果想在一定程度上解决或者避免发生幻读的话,使用RR也可以,但是RC、RU肯定是不行的。

在RR级别中,能使用快照读(无锁查询)的就使用快照读,这样不仅可以减少锁冲突,提升并发度,而且还能避免幻读的发生。

那么,如果在并发场景中,一定要加锁的话怎么办呢?那就一定要在事务一开始就立即加锁,这样就会有间隙锁,也能有效的避免幻读的发生。

但是需要注意的是,间隙锁是导致死锁的一个重要根源~所以,用起来也需要慎重。

总结

在RC级别中,幻读是没有办法解决的,因为RC中快照读是每一次都会重新生成快照,并且RC中也不会有间隙锁。

在RR级别中,因为有MVCC机制,对于普通的无锁查询,这种是属于快照读的,RR的快照读在同一个事务中只会读一次,所以在事务过程中,其他事务的变更不会影响到当前事务的查询结果。所以这种幻读是可以解决的。

当时,MVCC只能对快照读起作用,而对于加锁的读请求,这种属于当前读,当前读的话是可以查询到其他事务的变更的,所以会产生幻读。

想要解决幻读,可以使用Serializable这种隔离级别,或者使用RR也能解决大部分的幻读问题。

在RR级别下,为了避免幻读的发生,要么就是使用快照读,要么就是在事务一开始就加锁。

赞(7)
如未加特殊说明,此网站文章均为原创,转载必须注明出处。HollisChuang's Blog » Innodb的RR到底有没有解决幻读?
Hollis出品的全套Java面试宝典不来了解一下吗?

评论 抢沙发

HollisChuang's Blog

联系我关于我