不可重复读及幻读区别及不可重复读实例

首先描述一下问题:
ManagerA,ManagerB,ManagerC都用了@Transaction注解,A插入了数据,在A中通过线程池新建线程异步调用C,主线程中继续调用B,B会往数据库中插入数据,同时C中有查B中插入的数据,但是即使过来好久,在C中也查不到B中插入的数据。通过打断点,可以用数据库客户端能查到B插入的数据。

伪代码如下

public void main(){
    managerA.insertId1();
    managerB.insertId2();
}


public void insertId1(){
    dao.insert(1);
    new Thread(){
        managerC.selectId2();//就是查不到数据
    }
}

问题分析:
首先分析事务的传播特性,@Transaction注解默认的传播级别是REQUIRED,业务方法需要在一个事务中运行,如果方法运行时,已处在一个事务中,那么就加入该事务,否则自己创建一个新的事务。通过在C中打断点debug发现,只有在异步线程C执行完毕,才进入TransactionAspectSupport中结束事务,说明,同一个事务中即使新开一个线程,也遵循REQUIRED,加入现有的事务。

再看事务的隔离级别。通过SELECT @@global.tx_isolation;可以发现数据库的隔离级别是REPEATABLE-READ。
REPEATABLE-READ是为了解决不可重复读。不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。REPEATABLE-READ模式下,多次查找的结果是一致的。

所以结论就是:主线程的A和异步的C处在同一个事务1中,B处在另一个事务2中,事务1开始的时候,事务2还没有开始,所以C看不到B插入的数据,即使B已经commit了。但是数据库客户端查询的时候,因为B已经commit了,新的事务就可以查到了。

测试伪sql如下:

事务1
SET AUTOCOMMIT = 0;//不自动提交
insert id =1;
事务2
insert id =2;
事务3
select id =2;//能够查到
事务1
select id =2;//查不到
事务1
COMMIT //把事务1先提交了
事务4
select id =2;//新开一个事务,能查到

将数据库隔离级别改成Read Commited,在事务1的select id =2;,可以查到数据

参考文章:
https://www.jianshu.com/p/21ae9e7cf28d
http://youyu4.iteye.com/blog/2339878

另记录一下不可重复读和幻读的区别:
不可以重复读,可以理解为读不到相同的查询结果,在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
以上文为例,假如数据库的隔离级别是read commited,managerA和managerC处于同一事务,当managerA执行完,是查不到id=2的数据的,因为异步线程,当managerB比managerC先执行,那么managerC能查到id=2的数据,这样,在同一个事务中,managerA和managerC的同一查找的结果是不一致的,出现了不可重复读的问题,当使用了repeatable read,就解决了不可重复读问题。

幻读:查看了很多参考文章,基本上的解释都是如下的样子,如此看来姑且可以理解为幻读发生在update和insert之间。下面就以update和insert的情况来举例说明。如果之后发现其他条件的幻读再补充。

例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。`这里写代码片`

伪代码如下:

public void main(){
    new Thread(){
        managerA.updateIdBigThan4();
        managerC.selectIdBigThan4();
    }
    new Thread(){
        managerB.insertId10();
    }
}

public void updateIdBigThan4(){
    dao.updateIdBigThan4();
    Thread.sleep(1000);//保证事务提交之前mangerB执行完了。
}

首先这段伪代码和前文的代码不一样,managerC不是在mangerA中调用,所以两者处在不同事务中,所以这段代码存在3个事务。
当数据库的隔离级别为repeatable read的时候,在mangerC看来,上一步managerA已经把所有id大于4的都更新了,但是因为异步线程和数据库隔离级别的关系,没想到managerB插了一条数据,所以在mangerC查找的时候多出了一条id=10的纪录,像是update操作没有更新完,这是就是幻读。相当于两条平行的时间线,mangerC永远不知道前面有个mangerB。

当数据库的隔离级别更上一层楼的时候,Serializable 级别的时候,是锁表的。相当于两条平行时间线在执行到数据库层面的时候并线了,就像车道并线一样。mangerA即使sleep,只要没commit,mangerB永远执行不了,那么因为并线的关系,mangerC知道自己前面有个mangerB,id=10这条记录并不是update没更新完,而是因为insert操作。

所以前面引用的幻读解释中的“操作事务T1的用户”,可以理解为与事务1处在同一时间线上的新事务,用这个新事务去查找,会发现未更新完成的情况。

以上就是我对不可重复,幻读,以及部分隔离级别的理解。

20180815更新
在最前文提到的问题,即managerC无法读到managerB提交的数据,如果不改变数据库的隔离级别,可以更改@Transaction的传播级别,经过测试REQUIRES_NEW,NESTED都可以实现,但是最后还是决定采用NESTED。两者区别如下:
两者都是新建事务,REQUIRES_NEW的子事务和父事务相互独立,各自异常回滚不会互相影响。NESTED的子事务和父事务存在依赖关系,子事务和父事务一起提交。NESTED子事务失败回滚,父事务可以提交子事务之前的数据。如果父事务失败回滚,子事务也会失败回滚。

猜你喜欢

转载自blog.csdn.net/zzp448561636/article/details/80917085