数据库隔离级别有四种,每种隔离级别网上的解释已经很多了,这里就不多说了,我们可以看一下spring中对四种隔离级别的定义,其中DEFAULT是默认隔离级别,使用数据库的默认隔离级别。这篇文章主要是想通过一些例子来直观感受一下各种隔离级别。
本文示例主要使用到springboot,mybatis-plus和lombok。
使用的工具:sqlyog, idea, ScreenToGif(录制屏幕动图)
编写测试代码
我们在service层编写如下代码,addUser方法用来添加用户,getUser方法用来获取用户,其中addUser方法让线程sleep 10秒(原谅我手速不行,多sleep一会),用来模拟事务长时间未提交。
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void addUser(User user) {
this.save(user);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public List<User> getUser() {
return this.lambdaQuery().eq(User::getName, "zzj").list();
}
controller层通过两个接口分别调用addUser和getUser
@RequestMapping("addUser")
public String addUser() {
userService.addUser(User.builder().name("zzj").age(18).build());
return "success";
}
@RequestMapping("getUser")
public List<User> getUser() {
return userService.getUser();
}
情况1:不加@Transactional
第一种情况,我们先不加@Transactional看看效果,也就是把上面service类中的@Transactional注解去掉。
可以看到,addUser方法还没返回时,数据库就已经有数据了,getUser也能查询到。这也很好理解,因为没有开启事物,addUser方法执行完就会立马提交,不会等后面的sleep执行完再提交。
情况2:Isolation.READ_UNCOMMITTED
这种情况是最低的隔离级别,读未提交,也就是一个事务还没有结束,另一个事务可以读取到事务没提交的数据。
从下图可以直观地看到,addUser事务还没结束,数据库是查不到数据的,但是我们的getUser方法却获取到了这条数据!也就是读到了另一个事务未提交的数据。
情况3:Isolation.READ_COMMITTED
我们再把隔离级别设置为读已提交,从名字就可以看出来,这种情况下只能读取到事物已经提交的数据。但是读已提交可能会出现不可重复读的情况。也就是说在同一个事务中两次读取到的数据可能不一样。为了模拟这种情况,我们需要把addUser和getUser方法改变一下。这次让addUser sleep5秒,getUser sleep 8秒。另外有一点要特别注意,因为我用的是mybatis-plus做的测试,要把mybatis-plus的一二级缓存都关闭!不然在同一个方法中用同一个mapper去读的时候会读取缓存,可能会看不到我们想要的效果。
@Transactional(isolation = Isolation.READ_COMMITTED)
public void addUser(User user) {
this.save(user);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public List<User> getUser(User user) {
List<User> list = this.lambdaQuery().eq(User::getName, "zzj").list();
System.out.println("第一次尝试读取数据");
System.out.println(list);
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list = this.lambdaQuery().eq(User::getName, "zzj").list();
System.out.println("第二次尝试读取数据");
System.out.println(list);
return list;
}
从下图可以明显看出结果,在读已提交隔离级别下,getUser在8秒内的同一个事务中读取两次,读取到的结果不一样,这种情况称之为不可重复读。
情况4:Isolation.REPEATABLE_READ
还是和情况3一样的例子,我们把隔离级别设置为可重复读再试验一下(REPEATABLE_READ)。
在可重复读的隔离级别下,可以看出,即使另一个事务已经提交了数据,并且数据库中可以查询到,当前事务在没结束前,两次的查询结果是一样的,从而实现了在重复读取数据时不会出现读取到不一致数据的情况。
不可重复读的隔离级别下会出现的问题是幻读,也就是说数据明明没有读到,但是却可以更新数据,并且更新数据后又可以读到这条数据了。为了试验这种情况,我们把getUser方法再修改一下。
@Transactional(isolation = Isolation.REPEATABLE_READ)
public List<User> getUser() {
List<User> list = this.lambdaQuery().eq(User::getName, "zzj").list();
System.out.println("第一次尝试读取数据,读取不到");
System.out.println(list);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list = this.lambdaQuery().eq(User::getName, "zzj").list();
System.out.println("第二次尝试读取数据,仍然读取不到");
System.out.println(list);
System.out.println("尝试把name为zzj的数据的age改为20");
this.lambdaUpdate().eq(User::getName, "zzj").set(User::getAge, 20).update();
list = this.lambdaQuery().eq(User::getName, "zzj").list();
System.out.println("第三次尝试读取数据,读到了刚才更新的数据");
System.out.println(list);
return list;
}
情况5:Isolation.SERIALIZABLE
这种情况就很简单了,在这种隔离级别下,所有事务串行执行,只有等一个事务结果了另一个事务才会开始执行。把getUser修改为如下,看一下试验。
@Transactional(isolation = Isolation.SERIALIZABLE)
public List<User> getUser() {
List<User> list = this.lambdaQuery().eq(User::getName, "zzj").list();
System.out.println("getUser读取数据,在addUser5秒结束后才执行,并且读取到了addUser添加的数据");
System.out.println(list);
}