不知道大家有没有想过数据库的事务隔离级别和@Transaction设置的隔离级别到底是什么关系?
数据库设置的高隔离级别,@Transaction设置低隔离级别,那么事务的隔离级别到底以谁的为主?
下面就让我们一起去用代码研究一下
首先我们mysql数据库的默认隔离级别是read-commit读已经提交;
一、READ_UNCOMMITTED 读未提交
我们知道,这种隔离级别会产生脏读;
那么我们先测试@Transactional(isolation = Isolation.READ_UNCOMMITTED) 读未提交;
先上测试代码
@Service
public class InfoService{
@Autowired
private Mapper mapper;
//测试事务的隔离级别 读未提交
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void getInfo() {
//传入id查询数据
while (true) {
Info info = mapper.selectInfo("jzcs123");
if (info != null) {
System.out.println(Thread.currentThread().getName()+"获取到值");
return;
}else {
//等2s继续查询
Thread.sleep(2000);
}
}
}
//新增数据的方法
@Transactional
public void addInfo() {
//添加数据
mapper.addInfo("jzcs123");
System.out.println(Thread.currentThread().getName()+"已经插入数据了,但是事务没有提交");
//睡眠一分钟后事务才会提交
Thread.sleep(60 * 1000);
System.out.println(Thread.currentThread().getName()+"事务已经提交了");
}
}
上面写了两个方法,都是有事务的,一个查询方法的事务隔离级别是读未提交,另外一个插入的方法就默认事务;
public class TestService {
@Autowired
private InfoService infoService;
public static void main(String[] args) {
//开启不同的线程去操作
ExecutorService executorService = Executors.newFixedThreadPool(3);
//第一个线程调用查询方法
executorService.execute(new Runnable() {
@Override
public void run() {
infoService.getInfo();
}
});
//第二个线程调用插入方法
executorService.execute(new Runnable() {
@Override
public void run() {
infoService.addInfo();
}
});
//主线程睡眠2s,再开启第三个线程
Thread.sleep(2000);
//第三个线程调用查询方法
executorService.execute(new Runnable() {
@Override
public void run() {
infoService.getInfo();
}
});
}
}
上面代码是开启三个线程,第一个线程调用查询方法,第二个线程调用插入方法,第三个线程调用查询方法;
我们来看结果:
pool-13-thread-1没有获取到值
pool-13-thread-2已经插入数据了,但是事务没有提交
pool-13-thread-1没有获取到值
pool-13-thread-3获取到值
pool-13-thread-1没有获取到值
pool-13-thread-1没有获取到值
pool-13-thread-1没有获取到值
...
pool-13-thread-2事务已经提交了
pool-13-thread-1没有获取到值
pool-13-thread-1没有获取到值
pool-13-thread-3 在pool-13-thread-2提交事务前,就直接查询出来了值,符合读未提交的隔离级别,也就发生了脏读;
是不是感觉有点奇怪,pool-13-thread-1全程没有获取到值,这是为什么呢?
因为mybatis有一级缓存,即在同一个事务中,sql相同,参数也相同,那么获取到的结果也是相同的,即使其他事务已经把该数据更改,该事务获取到的值仍然不会变;pool-13-thread-1在pool-13-thread-2插入数据之前访问了数据库,查询结果为空,也就把结果保存到了一级缓存,未获取到值,事务结束,所以也就一直不会得到结果了;
二、读已提交 READ_COMMITTED
我们知道这种隔离级别不会产生脏读,但是会产生不可重复读的问题
接下来测试@Transactional(isolation = Isolation.READ_COMMITTED) 读已提交
@Service
public class InfoService{
@Autowired
private Mapper mapper;
//测试事务的隔离级别 读已提交
@Transactional(isolation = Isolation.READ_COMMITTED)
public void getInfo() {
//传入id查询数据
while (true) {
Info info = mapper.selectInfo("jzcs123");
if (info != null) {
System.out.println(Thread.currentThread().getName()+"获取到值");
return;
}else {
//等2s继续查询
Thread.sleep(2000);
}
}
}
//新增数据的方法
@Transactional
public void addInfo() {
//添加数据
mapper.addInfo("jzcs123");
System.out.println(Thread.currentThread().getName()+"已经插入数据了,但是事务没有提交");
//不需要睡眠,直接结束方法,提交事务
System.out.println(Thread.currentThread().getName()+"事务已经提交了");
}
}
上面写了两个方法,都是有事务的,一个查询方法的事务隔离级别是读已提交,另外一个插入的方法就默认事务;
public class TestService {
@Autowired
private InfoService infoService;
public static void main(String[] args) {
//开启不同的线程去操作
ExecutorService executorService = Executors.newFixedThreadPool(2);
//第一个线程调用插入方法
executorService.execute(new Runnable() {
@Override
public void run() {
infoService.addInfo();
}
});
//主线程睡眠2s,再开启第二个线程
Thread.sleep(2000);
//第二个线程调用查询方法
executorService.execute(new Runnable() {
@Override
public void run() {
infoService.getInfo();
}
});
}
}
上面代码是开启两个线程,第二个线程调用插入方法,第二个线程调用查询方法;为什么我们插入方法不需要睡眠直接提交数据,因为mybatis的一级缓存,如果我们查询方法先执行,肯定查询未空,那么后续查询都会走一级缓存,也就始终查询不到数据,也就没有验证的意义了;
我们来看结果:
pool-13-thread-1已经插入数据了,但是事务没有提交
pool-13-thread-1事务已经提交了
pool-13-thread-2获取到值
显然查询的线程获取到插入线程提交了的数据;
三、重复读 REPEATABLE_READ
这种还没有好的方法去测试,因为一级缓存,不好测试;
四、序列化 SERIALIZABLE
我们知道,隔离级别设置成串行化是不会出现脏读,不可重复的,幻读这些问题的;
接下来测试@Transactional(isolation = Isolation.SERIALIZABLE) 串行化
@Service
public class InfoService{
@Autowired
private Mapper mapper;
//测试事务的隔离级别 序列化
@Transactional(isolation = Isolation.SERIALIZABLE)
public void getInfo() {
//传入id查询数据
while (true) {
Info info = mapper.selectInfo("jzcs123");
if (info != null) {
System.out.println(Thread.currentThread().getName()+"获取到值");
return;
}else {
//等2s继续查询
Thread.sleep(2000);
}
}
}
//新增数据的方法
@Transactional
public void addInfo() {
//添加数据
mapper.addInfo("jzcs123");
System.out.println(Thread.currentThread().getName()+"已经插入数据了,但是事务没有提交");
//不需要睡眠,直接结束方法,提交事务
System.out.println(Thread.currentThread().getName()+"事务已经提交了");
}
}
上面写了两个方法,都是有事务的,一个查询方法的事务隔离级别是串行化,另外一个插入的方法就默认事务;
public class TestService {
@Autowired
private InfoService infoService;
public static void main(String[] args) {
//开启不同的线程去操作
ExecutorService executorService = Executors.newFixedThreadPool(2);
//第一个线程调用查询方法,隔离级别是串行化
executorService.execute(new Runnable() {
@Override
public void run() {
infoService.getInfo();
}
});
//主线程睡眠2s,再开启第二个线程
Thread.sleep(2000);
//第二个线程调用插入方法
executorService.execute(new Runnable() {
@Override
public void run() {
infoService.addInfo();
}
});
}
}
上面代码是开启两个线程,第一个线程调用查询方法,第二个线程调用插入方法;
我们看结果:
pool-13-thread-1没有获取到值
pool-13-thread-1没有获取到值
pool-13-thread-1没有获取到值
pool-13-thread-1没有获取到值
pool-13-thread-1没有获取到值
...
Exception in thread "pool-13-thread-2" org.springframework.dao.CannotAcquireLockException:
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
### The error may involve mapper.InfoMapper.insertSelective-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO info ( id ) VALUES( ? )
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
pool-13-thread-1没有获取到值
pool-13-thread-1没有获取到值
看到没,查询的线程一直查不到数据,插入的线程因为等待时间过长,报错;这是为什么呢?
查询线程访问的方法的事务隔离级别是SERIALIZABLE串行化,该隔离级别会把该方法所涉及到的表全部锁起来,共享锁,其他事务只能查,不能写;所以,我们的另一个线程插入数据的方法,一直无法执行,查询方法事务一直没有结束,所以导致插入数据的线程等待超时报错;
总结:
其实事务的隔离级别网络上已经有很多说明和解析了,该篇文章旨在搞清楚声明式事务和数据库本身的事务隔离级别的关系;显然,如果我们使用@Transactional,不指定隔离级别,就会使用数据库的默认隔离级别,如果制定了,就会覆盖数据库的隔离级别;所以,每个事务的隔离级别是可以不一样的,因为其原子性;mybatis的一级缓存,其实就已经帮我们实现了可重复读的隔离级别;