上一篇博客提到了脏读、不可重复读、幻读的含义,也知道了是因为什么情况导致出现的这些问题,这篇博客就带大家一起来了解一下他们的解决办法~
脏读:脏读出现的原因主要是因为一个事务读取了另外一个事务未提交的数据,就可能出现脏读,解决办法可以通过“读已提交”这种形式进行解决。
“读已提交”这种方法会在事务读取数据的时候生成一个全局性的ID,这个ID代表了所有已提交事物的最新状态,Innodb会去检查每一条数据行,如果其中的id信息是否比当前ID小或者相等,且是已经提交的事务,则这个版本信息是可见的,这就保证了当前事务只能读取到在他开始之前且已经提交的数据。
不可重复读:不可重复读主要是因为事务两次读取到的数据不一样,通过MVCC版本控制可以进行解决。
MVCC:翻译过来就是多版本并发控制,通过对每一个数据行添加版本信息,来解决不可重复读的问题,在RR隔离级别下,使用快照读,这种读取方式只会读取数据一次,后续的所有快照读都是用的同一个快照,所以就不会发生不可重复读的问题了。
幻读:幻读主要是因为事务两次读取到的数据行不一致,有种产生了幻觉的感觉,可以通过在RR隔离级别下使用MVCC+间隙锁的方式进行解决,但是这种方法没法完全避免所有幻读情况的发生;
我们刚刚说的在RR隔离级别下,使用的是快照读,读取的是第一次读取到的数据,后续读取的数据都是通过获取第一次快照读读取的数据,所以在一定程度上可以解决部分幻读的情况;然后通过间隙锁的方式,锁住条件内的数据,对于其他事物新增的数据,直接新增失败
MVCC解决幻读
我们知道,在MVCC中有两种读,一种是快照读、一种是当前读。
所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的普通的SELECT语句在不加锁情况下就是快照读。
在 RC 中,每次读取都会重新生成一个快照,总是读取行的最新版本。
在 RR 中,快照会在事务中第一次SELECT语句执行时生成,只有在本事务中对数据进行更改才会更新快照。
那么也就是说,如果在RR下,一个事务中的多次查询,是不会查询到其他的事务中的变更内容的,所以,也就是可以解决幻读的。
如果我们把事务隔离级别设置为RR,那么因为有了MVCC的机制,就能解决幻读的问题:
有这样一张表:
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');
执行如下事务时序:
可以看到,同一个事务中的两次查询结果是一样的,就是在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 ...
举一个下面的例子:
像上面这种情况,在RR的级别下,当我们使用SELECT … FOR UPDATE的时候,会进行加锁,不仅仅会对行记录进行加锁,还会对记录之间的间隙进行加锁,这就叫做间隙锁。
因为记录之间的间隙被锁住了,所以事务2的插入操作就被阻塞了,一直到事务1把锁释放掉他才能执行成功。
因为事务2无法插入数据成功,所以也就不会存在幻读的现象了。所以,在RR级别中,通过加入间隙锁的方式,就避免了幻读现象的发生。
解决不了的幻读
前面我们介绍了快照读(无锁查询)和当前读(有锁查询)下是如何解决幻读的问题的,但是,上面的例子就是幻读的所有情况了吗?显然并不是。
我们说MVCC只能解决快照读的幻读,那如果在一个事务中发生了当前读,并且在另一个事务插入数据前没来得及加间隙锁的话,会发生什么呢?
那么,我们稍加修改一下上面的SQL代码,通过当前读的方式进行查询数据:
在上面的例子中,在事务1中,我们并没有在事务开启后立即加锁,而是进行了一次普通的查询,然后事务2插入数据成功之后,再通过事务1进行了2次查询。
我们发现,事务1后面的两次查询结果完全不一样,没加锁的情况下,就是快照读,读到的数据就和第一次查询是一样的,就不会发生幻读。但是第二次查询加了锁,就是当前读,那么读取到的数据就有其他事务提交的数据了,就发生了幻读。
那么,如果你理解了上面的这个例子,并且你也理解了当前读的概念,那么你很容易就能想到,下面的这个CASE其实也是会发生幻读的
这里发生幻读的原理,和上面的例子其实是一样的,那就是MVCC只能解决快照读中的幻读问题,而对于当前读(SELECT FOR UPDATE、UPDATE、DELETE等操作)还是会产生幻读的现象的。即,在同一个事务里面,如果既有快照读,又有当前读,那是会产生幻读的、
UPDATE语句也是一种当前读,所以它是可以读到其他事务的提交结果的。
为什么事务1的最后一次查询和倒数第二次查询的结果也不一样呢?
是因为根据快照读的定义,在RR中,如果本事务中发生了数据的修改,那么就会更新快照,那么最后一次查询的结果也就发生了变化。
如何避免幻读
那么了解了幻读的解决场景,以及不能解决的几个CASE之后,我们来总结一下该如何解决幻读的问题呢?
首先,如果想要彻底解决幻读的问题,在InnoDB中只能使用Serializable这种隔离级别。
那么,如果想在一定程度上解决或者避免发生幻读的话,使用RR也可以,但是RC、RU肯定是不行的。
在RR级别中,能使用快照读(无锁查询)的就使用快照读,这样不仅可以减少锁冲突,提升并发度,而且还能避免幻读的发生。
那么,如果在并发场景中,一定要加锁的话怎么办呢?那就一定要在事务一开始就立即加锁,这样就会有间隙锁,也能有效的避免幻读的发生。
但是需要注意的是,间隙锁是导致死锁的一个重要根源~所以,用起来也需要慎重。