面試官:聊聊 spring 的七種事務傳播行爲?

spring 的七種事務傳播行爲

以下事務傳播屬性都是打在 B 方法上的事務註解

我們看完了每個傳播屬性的一些解釋,腦子裏應該是還是濛濛的,下面來看下真正的代碼

實戰

Propagation.REQUIRED

A 接口

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
        // 調用B接口的insertTest方法事務方法
        BTestService.insertTest(test);
        return i;
    }

我們再來看下 B 接口

 @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer insertTest(Test updateVO) {
        System.out.println("B insertTest方法");
        int i = testMapper.insert(updateVO);
        //  拋出異常
        int a = 1 / 0 ;
        return i;
    }

假如說,我代碼這麼寫的話,那麼這個數據庫裏最終是會有什麼數據呢?

@PostMapping("/update")
    public Object updateTest(@RequestBody Test updateVO){
        Integer flag = ATestService.updateTest(updateVO);
        return flag;
    }

原數據庫內容

postman 來一發看看

可以看到控制檯的結果是這樣的,他們共用一個事務(sqlSession 是一樣的)

此時數據庫的內容也並沒有發生變化,說明 A,B 接口都回滾了

這個時候就會出現一個常見的面試題:如果 B 方法拋出的異常被**「A 方法 try catch」** 捕獲了,那麼 A 方法的操作還會回滾嗎?

答案是:「會回滾」

來看下測試代碼,我們在 A 方法中添加了捕獲 B 方法拋出異常的代碼

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
          try {
               // 調用insertTest方法事務方法
               BTestService.insertTest(test);
           }catch (Exception e){
               System.out.println("A方法補獲了異常"+e.getMessage());
           }
        return i;
    }

再次來一發 postman,控制檯輸出測試結果如下

我們看數據庫數據也沒有變

那麼問題又來了, 如果**「A 沒有捕獲,B 方法自己捕獲了異常」**,那麼事務還會回滾嗎?答案是:**「不會」**

把 B 接口的代碼改一下

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer insertTest(Test updateVO) {
        System.out.println("B insertTest方法");
        int i = 0;

        try {
             i = testMapper.insert(updateVO);
            //  拋出異常
            int a = 1 / 0 ;
        }catch (Exception e){
            System.out.println("B方法補獲了異常"+e.getMessage());
        }

        return i;
    }

同時把 A 方法的捕獲異常去掉

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
        // 調用insertTest方法事務方法
        BTestService.insertTest(test);
        return i;
    }

這個時候的結果是

數據庫的數據是

由此可見,A 和 B 兩個接口都生效了都操作數據庫了,都沒有回滾

A 方法捕獲和 B 方法捕獲有什麼區別嗎(指捕獲異常)

只要理解了上面這個例子,我們以後各種異常 / 傳播屬性到底回滾不回滾就好分析啦!

Propagation.SUPPORTS

我們把 B 接口的事務傳播屬性換成 Propagation.SUPPORTS

@Transactional(rollbackFor = Exception.class,propagation = Propagation.SUPPORTS)
    @Override
    public Integer insertTest(Test updateVO) {
        System.out.println("B insertTest方法");
        int i = testMapper.insert(updateVO);
        //  拋出異常
        int a = 1 / 0 ;
        return i;
    }

A 方法

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
        // 調用insertTest方法事務方法
        BTestService.insertTest(test);
        return i;
    }

測試結果是

數據庫的值也沒有被改變 , 所以兩個操作都被回滾了 那我們把 A 方法的事務註解去掉後再看一下

 @Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
        // 調用insertTest方法事務方法
        BTestService.insertTest(test);
        return i;
    }

測試結果是是

數據庫的值是

由此可見,兩個操作都沒有被回滾,B 方法是以非事務方式進行的操作

Propagation.MANDATORY

只能在存在事務的方法中被調用,A 方法調用 B 方法,如果 A 方法沒事務,則 B 方法會拋出異常

A 接口如下

@Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
        // 調用insertTest方法事務方法
        BTestService.insertTest(test);
        return i;
    }

B 接口如下

@Transactional(rollbackFor = Exception.class,propagation = Propagation.MANDATORY)
    @Override
    public Integer insertTest(Test updateVO) {
        System.out.println("B insertTest方法");
        int i = testMapper.insert(updateVO);
        //  拋出異常
        int a = 1 / 0 ;
        return i;
    }

數據庫的值也沒有變,由此可見,B 方法的事務註解爲 Propagation.MANDATORY 當 A 方法沒事務時,則直接報錯。

Propagation.REQUIRES_NEW

A 方法

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
        // 調用insertTest方法事務方法
        BTestService.insertTest(test);
        return i;
    }

B 方法

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    @Override
    public Integer insertTest(Test updateVO) {
        System.out.println("B insertTest方法");
        int i = testMapper.insert(updateVO);
        //  拋出異常
        int a = 1 / 0 ;
        return i;
    }

結果是其中可以發現 兩個接口的 DefaultSqlSession  不一樣,那麼就表明,這兩個不是一個事務,所以就是,當 A 接口存在事務的時候,B 接口將其掛起並且重新開啓一個新的事務

  • B 方法拋出了異常,那麼 A 方法沒有捕獲的話,則 A,B 方法都會回滾

  • A 方法捕獲了異常,則 A 方法不回滾

「還是那句話,如果在方法內捕獲了異常,則此方法上的事務註解就感知不到這個異常的存在了,那麼此方法的操作就不會回滾!」

Propagation.NOT_SUPPORTED

A 方法調用 B 方法,如果 A 方法有事務,則 B 方法掛起 A 方法中的事務中,否則 B 方法自己使用非事務方式執行

A 接口

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
        // 調用insertTest方法事務方法
        BTestService.insertTest(test);
        return i;
    }

B 接口

@Transactional(rollbackFor = Exception.class,propagation = Propagation.NOT_SUPPORTED)
    @Override
    public Integer insertTest(Test updateVO) {
        System.out.println("B insertTest方法");
        int i = testMapper.insert(updateVO);
        //  拋出異常
        int a = 1 / 0 ;
        return i;
    }

測試結果是

數據庫的結果是

我們可以看到,B 接口生效了,確實插入了一條數據,A 接口沒有生效,沒有更改數據,這是因爲,異常在 B 接口內拋出來了,由於 B 接口的事務傳播行爲是 Propagation.NOT_SUPPORTED 則會掛起 A 接口的事務,B 接口以非事務情況操作(所以報錯也不回滾),異常刨到了 A 接口內,A 接口是有事務的,則會回滾,所以就沒有更改數據

Propagation.NEVER

A 接口

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
        // 調用insertTest方法事務方法
        BTestService.insertTest(test);
        return i;
    }

B 接口

@Transactional(rollbackFor = Exception.class,propagation = Propagation.NEVER)
    @Override
    public Integer insertTest(Test updateVO) {
        System.out.println("B insertTest方法");
        int i = testMapper.insert(updateVO);
        //  拋出異常
        int a = 1 / 0 ;
        return i;
    }

結果是

數據庫也沒有被改變, 可見,當 A 接口有事務的情況下調用 B 接口,直接報錯

Propagation.NESTED

A 接口

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateTest(Test updateVO) {
        System.out.println("A updateTest方法");
        int i = testMapper.updateByPrimaryKey(updateVO);
        Test test =new Test("小杰",24);
          try {
               // 調用insertTest方法事務方法
               BTestService.insertTest(test);
           }catch (Exception e){
               System.out.println("A方法補獲了異常"+e.getMessage());
           }
        return i;
    }

B 接口

@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
    @Override
    public Integer insertTest(Test updateVO) {
        System.out.println("B insertTest方法");
        int i = testMapper.insert(updateVO);
        //  拋出異常
        int a = 1 / 0 ;
        return i;
    }

結果是

數據庫的變化如下

A 接口的操作沒有回滾,B 操作的回滾了,這就是因爲 “savePoint” 安全點,在進行 B 接口操作時,當前的狀態(A 接口已經操作完了)被保存至安全點,B 接口失敗的話,回滾只會回滾到這個安全點

注:需要在 A 接口裏 try catch B 接口的異常

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Xaf5xaHJ7d6AbOy6Gh1ZZQ