一行神奇的代码引发的思考——异步事务如何控制先后提交顺序

不了解事务,推荐阅读本人另一篇博客MySql的四种事务隔离级别

一、说说场景

 先写下伪代码

V1版本

service层代码

 1 public class DemoService {
 2     @Autowired
 3     private DemoTask demoTask;
 4     @Autowired
 5     private DemoDao demeDao;
 6 
 7     @Transactional
 8     public void save(){
 9         System.out.println("事务执行开始");
10         List<DemoEntity> list = new ArrayList();//假设list长度很长。
11         demeDao.saveAll(list);//insert 一大堆数据
12         System.out.println("开始执行异步程序");
13         demoTask.task();
14         System.out.println("事务执行完毕");
15     }
16 }

 异步任务

 1 @Component
 2 public class DemoTask {
 3     @Autowired
 4     private DemoDao demeDao;
 5     
 6     @Async
 7     public void task(){
 8         try {//为了方便演示,让当前异步线程睡一会
 9             Thread.sleep(1000);
10         } catch (InterruptedException e) {
11             e.printStackTrace();
12         }
13         List<DemoEntity> list = demeDao.findAll();
14         //伪代码,这个异步任务中会根据上一步中提交的结果做大量运算。
15         System.out.println("处理异步事务");
16     }
17 }

 

如果设置打印sql。

 1 事务执行开始
 2 sql insert into .......
 3 sql insert into .......
 4 sql insert into .......
 5 sql insert into .......
 6 ........
 7 sql insert into .......
 8 sql insert into .......
 9 sql insert into .......
10 sql insert into .......
11 事务执行完毕
12 处理异步事务

 

打印的日志如上所示,没问题吧?

可是结果并不是这样的,而是下面这样。

事务执行开始
sql insert into .......
sql insert into .......
sql insert into .......
sql insert into .......
........
事务执行完毕
处理异步事务
........
sql insert into .......
sql insert into .......
sql insert into .......
sql insert into .......

 

呃(⊙o⊙)…为什么saveAll()方法为什么会出现异步的效果?

V2版本

public class DemoService {
    @Autowired
    private DemoTask demoTask;
    @Autowired
    private DemoDao demeDao;

    //@Transactional
    public void save(){
        System.out.println("事务执行开始");
        List<DemoEntity> list = new ArrayList();//假设list长度很长。
        demeDao.saveAll(list);//insert 一大堆数据
        System.out.println("开始执行异步程序");
        demoTask.task();
        System.out.println("事务执行完毕");
    }
}

 

一行关键的代码是注释掉了 @Transactional 继续看打印的日志

事务执行开始
sql insert into .......
sql insert into .......
sql insert into .......
sql insert into .......
........
sql insert into .......
sql insert into .......
sql insert into .......
sql insert into .......
事务执行完毕
处理异步事务

 

呃成同步的了,没错, @Transactional 就是造成saveAll()方法成为异步的关键,至于为什么会成为异步的我们有时间再研究其源码。

本文关注的重点是什么呢?继续往下看

 

二、从一行神奇的代码开始

再贴一遍代码

//service业务层
public class DemoService {
    @Autowired
    private DemoTask demoTask;
    @Autowired
    private DemoDao demeDao;

    @Transactional
    public void save(){
        System.out.println("事务执行开始");
        List<DemoEntity> list = new ArrayList();//假设list长度很长。
        demeDao.saveAll(list);//insert 一大堆数据,异步执行 insert
        System.out.println("开始执行异步程序");
        demoTask.task();
        System.out.println("事务执行完毕");
    }
}
//异步任务类
@Component
public class DemoTask {
    @Autowired
    private DemoDao demeDao;
    
    @Async//异步执行
    public void task(){
        try {//为了方便演示,让当前异步线程睡一会
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        List<DemoEntity> list = demeDao.findAll();
        //伪代码,这个异步任务中会根据上一步中提交的结果做大量运算。
        System.out.println("处理异步事务");
    }
}

先注意一下标红的关键代码

首先是 @Transactional 造成 demeDao.saveAll(list);//insert 一大堆数据 saveAll()方法成了异步执行的。

其次 @Async 是task()方法是异步的。

需要明确的一点 save() 是一个事务, task() 方法中也是一个独立的事务。这些就不解释了,不信可以执行这行sql看看事务的ID show transaction_isolation 你会明确的看到的确是两个独立的事务。

再捋一遍场景,service层的save()方法会保存一堆数据,然后task()方法中会查出save()方法保存的结果,进行下一步的运算。

这时候问题就出来了,save()和task()方法都是异步的,我们如何控制两个事务,保证save()事务提交了(commit)要保存的数据,在task()事务中能查到save()方法中保存的最新结果?

因为用的是postgresql数据库,默认的隔离级别是read-committed读已提交。read-committed的意思也就说当save()方法中的事务提交了,在task()方法中就能读到该表中的最新插入的数据。所以现在问题来到如何保证save()方法的事务能在task异步方法执行前提交。

这时你需要明白,事务提交的时机。在各种数据库中,事务都是默认自动提交的,先不说如何手动控制事务提交。

事务提交的时机:当一个事务中顺利执行完所有sql,该事务会自动提交。

如果明白了以上问题,那就不难想到那行神奇的代码是什么了。

 1 public class DemoService {
 2     @Autowired
 3     private DemoTask demoTask;
 4     @Autowired
 5     private DemoDao demeDao;
 6 
 7     @Transactional
 8     public void save(){
 9         System.out.println("事务执行开始");
10         List<DemoEntity> list = new ArrayList();//假设list长度很长。
11         demeDao.saveAll(list);//insert 一大堆数据
12         demoDao.countByXXX();//这就是那行神奇的代码,其实仅仅需要执行一个查询的sql
13         System.out.println("开始执行异步程序");
14         demoTask.task();
15         System.out.println("事务执行完毕");
16     }
17 }

 

那行神奇的代码就是上面那行,仅仅是执行一个select语句即可(严格来说这个select应该覆盖刚刚插入的数据,最简单的就是select count(*) from xxxtable)。为什么?

因为这行代码的sql会让改表生成一个最新的快照,也就是说该select语句之前的sql即使是异步的,也会被select阻塞到这里,直到被 @Transactional 导致的 saveAll(list) 异步执行完成。当前线程(对于springboot,对于数据库来说就是当前事务)才会继续往下执行。

有了那行神奇的代码,日志打印如下:

事务执行开始
sql insert into .......
sql insert into .......
sql insert into .......
sql insert into .......
........
sql insert into .......
sql insert into .......
sql insert into .......
sql insert into .......
事务执行完毕
处理异步事务

 

结合上面事务提交的时机继续,save()方法中的事务,到此执行完毕,也就该提交(commit)了。

这样task()方法中的事务就能顺利看到刚刚插入的数据了。

除了这行神奇的代码还有什么解决方案吗?有的,就是手动控制事务的提交。当然手动控制事务是不推荐的,因为手动控制事务,意味着要手动控制事务的回滚。没有回滚事务也就没意义了。

1 execute("start transaction")//开启事务
2 //业务代码
3 execute("commit")//提交事务

 

三、PostGreSql与MySQL的事务区别

不了解事务可以读读本人的另一篇博客:MySql的四种事务隔离级别

PostGreSql与MySQL的事务区别在于PostGreSql默认的事务隔离级别是read-committed,而MySQL默认级别是repeatable-read。

其内部机制都是一样的,包括锁,MVCC,快照读,当前读等等。

 

四、引申

如果将事务隔离级别设置为:repeatable-read,如何控制两个异步事务的提交顺序。

如果用:ReentrantLock或者,synchronized当然也可以解决这个问题。

感兴趣的可以留言讨论。

 

 

 

  不忘初心

  如有错误的地方还请留言指正。

  原创不易,转载请注明原文地址:https:////www.cnblogs.com/hello-shf/p/12543766.html

 

posted @ 2020-03-22 02:13  超级小小黑  阅读(1966)  评论(0编辑  收藏  举报