不可重复读与幻读一直有人分不清,而且网上对这两者的区别和解释的也不太到位,有的甚至会误人子弟。
我以我的理解解释一下幻读和不可重复读,若有错误还望指正。
希望文章中的例子可以自己实验一样,对事务的理解会更深刻。
首先看一下对不可重复读和幻读的解释
不可重复读(幻影问题):指在同一事务内,多次相同的SQL返回的结果却不同
幻读:事务A前后查询多次的结果条数是一样的,此时事务A查询的记录是不包括事务B新增的那条记录的,但是此时事务A对事务B插入的那条记录是可以执行更新操作的,操作完之后再次查询就会发现记录条数更加了一条。难以用语言描述,我这实际上是在用一个例子解释。
问题是
为什么innodb的repeatable read的隔离级别下能解决不可重复读问题而不能解决幻读问题?
因为innodb利用Next-key Locking解决不可重复读问题,每次查询会锁定记录和一个范围,以隔离本事务中已经查询过的行记录来避免其他事务的干扰,实现可重复读。但是Next-key Locking 无法解决幻读问题,因为其他事务可以修改或插入本事务中没有被锁定的范围内的数据,且本事务感知不到这种修改或插入,即原来的查询语句仍返回原来的结果。但是,本事务是可以对这种插入做修改的!因为事务B已经成功提交了,这种感知不到却能做出修改的行为就好像幻觉一样。
接下来会用实验演示,为什么会产生不可重复读,如何实现可重复读,为什么会出现幻读
数据准备如下:
create table t (a int primary key)
insert into t select 1;
insert into t select 2;
insert into t select 5;
不可重复读如下:
时间 | 事务A | 事务B |
---|---|---|
1 | set session transaction_isolation=‘read-committed’(设置隔离级别为读已提交) | |
2 | begin; | |
3 | select * from t where a >2 for update (结果返回5) | |
4 | begin; | |
5 | insert into t select 4; | |
6 | commit; | |
7 | select * from t where a >2 for update (结果返回4,5) | |
10 | commit; |
这里产生了不可重复读问题,在read-committed下,innodb会采用record Lock的方式锁定行记录,即select * from t where a >2 for update只会锁住5这行记录,所以事务B插入4成功了。
但是如果在repeatable read下,innodb会采用Next-key Lock的方式,除了锁住5这行记录以外,还会锁住a>2这个范围,因此事务B无法插入4这行记录,实现了可重复读。
但是这又产生了一个问题,只锁住了已经查询到的行记录,这里是5,还有一个范围,这里是a>2,但是还有没有被锁住的地方,比如a<2这个范围,这里就是会产生幻读的地方!
先接着恢复表中的数据还是1,2,5这三条
幻读如下:
时间 | 事务A | 事务B |
---|---|---|
1 | begin | |
2 | select * from t where a >2 for update (结果返回5) | |
3 | begin; | |
4 | insert into t select 0; | |
5 | commit; | |
6 | select * from t where a >2 for update (结果还是返回5,但t表已经有4条记录) | |
7 | update t set a=-1 where a=0;(成功了) | |
8 | select * from t;(返回-1,1,2,5) | |
9 | commit; |
事务A中select * from t where a >2 for update 会锁住a>2的所有范围,但是a<2的范围没有被锁住,因此事务B插入0会成功,
事务B提交后,t表已经有4个记录了,但是事务A可以修改a=0的记录!
幻读的命令行演示如下:
事务A先查询所有a>2的记录,只有5这一行
事务B插入值为0的行
事务A再次执行刚才的SQL语句,结果与之前的一致,满足可重复读。
事务A可以修改事务B插入的行记录
事务B提交后,事务A的update才可以继续执行
事务A查询所有行数据,发现事务B插入的数据在事务A中显示出来了
由此,幻读的解决方式是得锁整个表了
如果对幻读还不明白,大家一定要开命令行自己试一试哦