一行神奇的代碼引發的思考——異步事務如何控制先后提交順序


不了解事務,推薦閱讀本人另一篇博客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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM