网站首页 文章专栏 到底什么是幻读?Mysql到底能不能解决?怎么解决的
到底什么是幻读?Mysql到底能不能解决?怎么解决的

一. Mysql的隔离级别以及幻读的定义

众所周知,mysql有四个隔离级别,分别可以解决不同场景的问题,mysql默认隔离界别为RR(Repeatable Read)可重复读。

     

事务隔离级别

脏读不可重复读幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

上图应该是我们见过最多的图了,四种隔离级别可解决脏读,不可重复读等问题,但是对于幻读,就有争议了,有的文章说RR能解决,有的文章说不能,到底行不行呢,看下文!

不可重复读:

不可重复读:就是一个事务读到另一个事务修改后并提交的数据(update)。在同一个事务中,对于同一组数据读取到的结果不一致。比如,事务B 在 事务A 提交前读到的结果,和在 事务A 提交后读到的结果可能不同

幻读:

幻读:事务A 按照一定条件进行数据读取, 期间事务B 插入了相同搜索条件的新数据,事务A再次按照原先条件进行读取时,发现了事务B 新插入的数据 称为幻读


二. Mysql如何解决不可重复读

InnoDB中通过MVCC机制来解决不可重复读,每一行数据都会有隐藏的事务id等属性,记录数据的版本,在开始事务时,会创建一个事务id,且是按照申请时间严格自增的,每行数据也是有多个版本的,

每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。

也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。

test.png

如上图:每行数据可能有很多个版本,V1-V4,各个事务启动的时候创建一个自己的版本,语句的update操作会在undo log中记录下来,实际上V1,V2,V3数据实际并不存在,要想获得V1数据,可以通过V4版本,结合undo log向前计算得出V1。

按照可重复读的语义,每次在创建一个事务的时候,第一次查询会查询一个read view,该read view中只会看到比自己事务id小版本的数据,然后后续的查询就会从这个read view中获取数据,这么做的话,后续事务的操作也就不会对本次的查询造成影响,也就是多次查询的结构是一致的,也就是可重复读。

可重复读举例:

准备数据库

CREATE TABLE `test` (
  `id` int(12) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(16) DEFAULT NULL,
  `age` int(12) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入两条原始数据:

INSERT INTO test VALUES(1,'星尘',18),(2,'星尘',19);

RR隔离界别下,有如下3个事务分别操作数据:

时间顺序

事务A

事务B事务C
1
start transaction with consistent snapshot;

2
start transaction with consistent snapshot;
3

UPDATE `test` SET age = age + 1 WHERE id = 1;
4

SELECT * FROM `test` WHERE id = 1;

UPDATE `test` SET age = age + 1 WHERE id = 1;

SELECT * FROM `test` WHERE id = 1;


5

SELECT * FROM `test` WHERE id = 1;

commit;



6
commit;

注意:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表语句的时候,事务才真正启动。

如果想马上启动一个事务,可以使用start transaction with consistent snapshot 命令。

在上面的例子里面,事务C没有显示地使用begin/commit,表示这个update语句本身就是一个事务,事务完成的时候会自动提交。事务B在更新了之后查询;事务A在一个只读事务中查询,查询时间是在事务B的查询之后。


我们先看写结果是什么:

事务A:第5步,读取结果为(1,"星尘",18)

事务B:第4步,第一次查询结果:(1,"星尘",18),第二次查询结果为(1,"星尘",20)

想一想符合我们预期吗?

RR的隔离级别下,应该是快照读,每次查询读取事务开始创建的快照,那么事务A的结果是符合预期的,因为他的事务id最小,后续的事务B,事务C的操作怎么样与他无关,所以读取的是最早的初始化数据(1,"星尘",18)。

事务B的第一次查询结果也是符合预期的,因为事务C的操作也与他无关,但是执行了一次update后为什么就变为20了呢,如果不受事务C影响的话,自增一次应该是19才对,但是实际是20,说明受到了事务C的影响。

实际上update语句是当前读的操作,他在更新数据必须拿到最新的数据再去更新,不然就出现事务C的更新丢失了,所以事务B的update操作是在事务C的age为19的基础上,再update的,这里用到一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为当前读(current read)。


总结:普通查询语句是一致性读,一致性读会根据 row trx_id和一致性视图确定数据版本的可见性。

- 对于可重复读,查询只承认在事务启动前就已经提交完成的数据

- 对于读提交,查询只承认在查询语句启动前就已经提交完成的数据

- 而当前读,总是读取已经提交完成的最新版本

通过上面的测试,可以看出,MVCC的机制保证了RR级别下的可重复读。那么为什么还会存在幻读呢?


三. 什么是幻读,RR能不能解决幻读

先明确说一下,RR是能解决幻读的,我们先调整到RC的级别下,看下什么是幻读。

时间顺序

事务A

事务B
1
set session transaction isolation level read committed;set session transaction isolation level read committed;
2start transaction with consistent snapshot;
3
start transaction with consistent snapshot;
4

select * from test where id > 0;

结果为3条:(1,"星尘",20)(2,"星尘",19)(3,"星尘",21


5


INSERT INTO test VALUES(NULL,'星尘',22);

commit;

6

select * from test where id > 0;

结果为4条:(1,"星尘",20)(2,"星尘",19)(3,"星尘",21)(4,"星尘",22

commit;


第一步,先开启两个窗口,并且都设置会话隔离级别为RC

如上表可见,事务A的两次相同查询条件,却出现了不同数量的结果,这就是幻读。


我们切回RR,再用同样的流程再试一次;

时间顺序

事务A

事务B
1
set session transaction isolation level repeatable read;set session transaction isolation level repeatable read;
2start transaction with consistent snapshot;
3
start transaction with consistent snapshot;
4

select * from test where id > 0;

结果为4条:(1,"星尘",20)(2,"星尘",19)(3,"星尘",21(4,"星尘",22


5


INSERT INTO test VALUES(NULL,'星尘',23);

commit;

6

select * from test where id > 0;

结果为4条:(1,"星尘",20)(2,"星尘",19)(3,"星尘",21)(4,"星尘",22


7

update test set age = 18 where id >0;

显示:Query OK, 5 rows affected,影响了5行


8select * from test where id > 0;

结果为5条:(1,"星尘",18)(2,"星尘",18)(3,"星尘",18)(4,"星尘",18(5,"星尘",18

commit;


先设置为RR,再开启两个事务,事务A先查询 id > 0 的数据,结果为4条,此时事务B插入一条符合事务A查询条件的数据并commit;

第6步,事务A查询结果仍然是4条,对比RC的结果,发现确实幻读没出现,符合预期。

第7步,事务A执行了一次update,结果就出人意料了,影响了5行,明明上面查询到4条,说明事务B插入的数据也被修改了,再执行相同的查询语句,发现结果为5条,而这就又出现幻读了。


其实这应该就是为什么有的文章说RR能解决幻读,有的文章说不能解决幻读,至于原因是什么,从上面不可重复读的例子我们也能看出,update语句是当前读,普通的select。。。where。。。就是快照读,如果都是快照读的情况下,那么是不会产生幻读的,当出现update的当前读后,读取了最新的数据再update,后续的查询因为发现版本号没变,是自己的更新,那么就会出现这种情况下的幻读。

其实还有其他的当前读,这些当前读也会产生所谓的幻读,比如:

select * from ... where ... for update

select * from .... where ... lock in share mode

update .... set .. where ... 

delete from. . where ..


如果事务中都使用快照读,那么就不会产生幻读现象,但是快照读和当前读混用就会产生幻读。


如果都是用当前读呢?

时间顺序

事务A

事务B
1
set session transaction isolation level repeatable read;set session transaction isolation level repeatable read;
2start transaction with consistent snapshot;
3
start transaction with consistent snapshot;
4

select * from test where id > 0 for update;  -- 此处为当前读

结果为5条:(1,"星尘",18)(2,"星尘",18)(3,"星尘",18)(4,"星尘",18)(5,"星尘",18


5


INSERT INTO test VALUES(NULL,'星尘',23);

此时会被阻塞,等待锁。。。

6

select * from test where id > 0 for update;  -- 此处为当前读

结果为5条:(1,"星尘",18)(2,"星尘",18)(3,"星尘",18)(4,"星尘",18)(5,"星尘",18

结果还是5条


7

commit;


8

此时得到锁,插入成功

commit;

可以看到事务B 被阻塞了。需要等到事务A提交事务后才能完成。当我们在事务中每次读取都使用当前读,也就是人工把InnoDB变成了串行化。一定程度上降低了并发性,但是也同样避免了幻读的情况。

实际上,mysql使用的GAP锁 + 行锁,形成了next-key lock临间锁,锁住查询条件的区间,来保证不会出现幻读,在本例中也就是锁住了 [0,+ ∞ ),注意gap锁,和next-key锁都可能会锁住不存在的id


总结:

再来回顾我们最早的问题,mysql RR解决了幻读了吗,实际上解决了,但是某些当前读的操作,会导致再次出现幻读。

1). mysql通过MVCC解决不可重复读

2). 通过next-key lock解决幻读

3). 可重复读隔离级别下,一个事务中只使用当前读,或者只使用快照读都能避免幻读。

4). 当前读,与快照读的区别,分别会带来什么影响


本人拙见,欢迎一块讨论




版权声明:本文由星尘阁原创出品,转载请注明出处!

本文链接:http://www.52xingchen.cn/detail/85




赞助本站,网站的发展离不开你们的支持!
来说两句吧
大侠留个名吧,或者可以使用QQ登录。
: 您已登陆!可以继续留言。
最新评论