@Transactional的四种隔离级别测试

不知道大家有没有想过数据库的事务隔离级别和@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的一级缓存,其实就已经帮我们实现了可重复读的隔离级别;

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
@TransactionalSpring框架中用于管理事务的注解之一。它可以应用在方法或类级别上,用于指定方法或类中的操作需要在一个事务中执行。 @Transactional注解具有一个isolation属性,用于指定事务的隔离级别。默认情况下,Spring使用数据库的默认隔离级别作为@Transactional的默认隔离级别数据库隔离级别定义了事务之间的可见性和并发控制的程度。常见的隔离级别包括: 1. READ_UNCOMMITTED(读未提交):事务可以读取其他事务未提交的数据。这个隔离级别会导致脏读、不可重复读和幻读问题。 2. READ_COMMITTED(读已提交):事务只能读取其他事务已经提交的数据。这个隔离级别可以避免脏读问题,但仍可能出现不可重复读和幻读问题。 3. REPEATABLE_READ(可重复读):事务在整个过程中可以多次读取相同的数据,并且保证其他事务不能修改这些数据。这个隔离级别可以避免脏读和不可重复读问题,但仍可能出现幻读问题。 4. SERIALIZABLE(串行化):事务按照顺序依次执行,相当于对所有操作加了锁。这个隔离级别可以避免脏读、不可重复读和幻读问题,但会降低并发性能。 如果没有显式指定@Transactional的isolation属性,Spring将使用数据库的默认隔离级别。不同的数据库可能有不同的默认隔离级别,例如MySQL的默认隔离级别是REPEATABLE_READ。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wait疯man

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值