首頁 > 軟體

spring 中事務註解@Transactional與trycatch的使用

2021-06-21 16:00:18

spring事務註解@Transactional與trycatch

在專案中 @service層中 我們會經常在做一些增刪改操作的方法上看到 spring 的事務註解 @transaction 已知@transaction 是讓spring 幫我們實現事務的控制。

但是在專案中會經常看到 有的方法中 會存在trycatch塊包括的方法上註解著@transaction

eg:

 @Override
    @Transactional
    public Json addOrder(TOrderAddReq tOrderAddReq) {
  try{
    //增刪改方法
        } catch (Exception e) {
            .....
            e.printStackTrace();}
//        }
        return json;
    }

上述的方法執行後可以看到事務並沒有執行,接下來再看一個例子eg:

 @Override
    @Transactional
    public Json addOrder(TOrderAddReq tOrderAddReq) {
  try{
    //增刪改方法
        } catch (Exception e) {
         // 手動寫死開啟spring事務管理
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            e.printStackTrace();}
//        }
        return json;
    }

上述方法執行後我們可以看到事務最後執行了,但實際上 事務 執行只是因為手動寫死開啟spring事務管理起了作用 而方法上的註解並沒有起作用

接下來再看一個例子eg

 @Override
    @Transactional
    public Json addOrder(TOrderAddReq tOrderAddReq) {
  try{
    //增刪改方法
        } catch (Exception e) {
         
            throw new RuntimeException();
            }
//        }
        return json;
    }

上述方法執行後我們可以看到事務是執行了的,但這裡有個小細節:@Transactional不做任何設定 預設是對丟擲的unchecked異常回滾,checked異常不會回滾,為了讓所有異常都會讓事務啟動可以將 @Transactional設定為 @Transactional(rollbackFor = Exception.class)

解釋:

spring的事務邊界是在呼叫業務方法之前開始的,業務方法執行完畢之後來執行commit or rollback(spring預設取決於是否丟擲runtime異常).

如果丟擲runtime exception 並在你的業務方法中沒有catch到的話,事務會回滾。

一般不需要在業務方法中catch異常,如果非要catch,在做完你想做的工作後(比如關閉檔案等)一定要丟擲runtime exception,否則spring會將你的操作commit,這樣就會產生髒資料.所以你的catch程式碼是畫蛇添足。

@Transactional回滾問題(try catch、巢狀)

Spring 事務註解 @Transactional 本來可以保證原子性,如果事務內有報錯的話,整個事務可以保證回滾,但是加上try catch或者事務巢狀,可能會導致事務回滾失敗。測試一波。

準備

建兩張表,模擬兩個資料操作

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `age` smallint(3) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

測試

根據排列組合原理,我們進行四種測試:1、無try catch、無巢狀;2、有try catch、無巢狀;3、無try catch、有巢狀;4、都有。

最簡單測試

如果我們單純@Transactional,事務可以正常回滾嗎?

    @GetMapping("/saveNormal0")
    @Transactional
    public void saveNormal0() throws Exception {
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:"+age);
        userService.save(user);
        throw new RuntimeException();
    }

如果事務內報了RuntimeException錯誤,事務可以回滾。

    @GetMapping("/saveNormal0")
    @Transactional
    public void saveNormal0() throws Exception {
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:"+age);
        userService.save(user);
        throw new Exception();
    }

如果事務內報了Exception錯誤(非RuntimeException錯誤),事務不可以回滾。

    @GetMapping("/saveNormal0")
    @Transactional( rollbackFor = Exception.class)
    public void saveNormal0() throws Exception {
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:"+age);
        userService.save(user);
        throw new Exception();
    }

如果是Exception錯誤(非RuntimeException),加上 rollbackFor = Exception.class 引數也可以實現回滾。

結論一:對於@Transactional可以保證RuntimeException錯誤的回滾,如果想保證非RuntimeException錯誤的回滾,需要加上rollbackFor = Exception.class 引數。

try catch 影響

經過博主多種情況測試,發現try catch對回滾這個事本身沒有什麼影響,結論一照樣成立。try catch只是對異常是否可以被@Transactional 感知 到有影響。如果錯誤拋到切面可以感知到的地步,那就可以起作用。

    @GetMapping("/saveTryCatch")
    @Transactional( rollbackFor = Exception.class)
    public void saveTryCatch() throws Exception{
        try{
            int age = random.nextInt(100);
            User user = new User().setAge(age).setName("name:"+age);
            userService.save(user);
            throw new Exception();
        }catch (Exception e){
            throw e;
        }
    }

比如上面一段程式碼就回滾了。

    @GetMapping("/saveTryCatch")
    @Transactional( rollbackFor = Exception.class)
    public void saveTryCatch() throws Exception{
        try{
            int age = random.nextInt(100);
            User user = new User().setAge(age).setName("name:"+age);
            userService.save(user);
            throw new Exception();
        }catch (Exception e){
        }
    }

然而,將catch中的錯誤不繼續網上拋,切面無法感知到錯誤,無法進行處理,那麼事務就無法回滾了。

結論二:try catch只是對異常是否可以被@Transactional 感知 到有影響。如果錯誤拋到切面可以感知到的地步,那就可以起作用。

事務巢狀 影響

首先經過實驗,結論一仍然成立,即,當不加上rollbackFor = Exception.class 的時候,無論內外報RuntimeException,都會回滾;無論內外報 非RuntimeException 錯誤,都不會回滾。如果加上rollbackFor = Exception.class,無論內外怎麼報錯,都會回滾。這些程式碼就不給出了。接下來,試下下面兩種情況:

    @GetMapping("/out")
    @Transactional( rollbackFor = Exception.class)
    public void out() throws Exception{
        innerService.inner();
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:" + age);
        userService.save(user);
        throw new Exception();
    }
    @Transactional
    public void inner() throws Exception{
        Role role = new Role();
        role.setRoleName("roleName:"+new Random().nextInt(100));
        roleService.save(role);
//        throw new Exception();
    }

情況一,外面事務加上rollbackFor = Exception.class,裡面事務不加,測試內外分別報錯的情況(為了簡化程式碼量,只給出了外面報錯的程式碼),都可以回滾。因為,無論如何,錯誤都拋給了外面那個事務進行處理,而外面那個加上了rollbackFor = Exception.class,具備處理非RuntimeException錯誤的能力,所以都可以讓事務進行正常回滾。

下面看情況二,裡面的事務加上rollbackFor = Exception.class,外面不加,外面報錯。

    @GetMapping("/out")
    @Transactional
    public void out() throws Exception{
        innerService.inner();
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:" + age);
        userService.save(user);
        throw new Exception();
    }
    
    @Transactional( rollbackFor = Exception.class)
    public void inner() throws Exception{
        Role role = new Role();
        role.setRoleName("roleName:"+new Random().nextInt(100));
        roleService.save(role);
    }

事務都無法回滾,這是我們有個疑問,裡面的事務明明有很強的處理能力啊,為什麼和外面一起回滾失敗呢,彆著急,等等聊這個。

然後試下里面報錯:

    @GetMapping("/out")
    @Transactional
    public void out() throws Exception{
        innerService.inner();
        int age = random.nextInt(100);
        User user = new User().setAge(age).setName("name:" + age);
        userService.save(user);
    }
     @Transactional( rollbackFor = Exception.class)
    public void inner() throws Exception{
        Role role = new Role();
        role.setRoleName("roleName:"+new Random().nextInt(100));
        roleService.save(role);
        throw new Exception();
    }

咦,這回都進行了正常的回滾。我的天,這回外面沒有處理能力,為什麼接受裡面丟擲來的錯誤,也進行了回滾!!!看上去,就好像裡外事務總是同生共死的對不對?原來,@Transactional還有個引數,看下原始碼,這個註解還有預設值:

Propagation propagation() default Propagation.REQUIRED;

REQUIRED的意思是說,事務巢狀的時候,如果發現已經有事務存在了,就加入這個事務,而不是新建一個事務,所以根本就不存在兩個事務,一直只有一個!至於,此引數其他值,本文不進行測試。回到上面的問題,當外面報錯的時候,此時檢視事務,沒有增加rollbackFor = Exception.class引數,即沒有處理非RuntimeException能力,所以程式碼走完,貌似「兩個事務」,都回滾失敗了。當裡面報錯的時候,事務已經新增上了處理非RuntimeException能力,所以,程式碼走完就回滾成功了。

結論三:由於REQUIRED屬性,「兩個事務」其實是一個事務,處理能力看報錯時刻,是否新增了處理非RuntimeException的能力。

try catch和事務巢狀 共同影響

在結論一二三成立的條件下,探索共同影響的問題就簡單多了,由於情況太多,就不進行過多的程式碼展示了。

結論

結論一:

對於@Transactional可以保證RuntimeException錯誤的回滾,如果想保證非RuntimeException錯誤的回滾,需要加上rollbackFor = Exception.class 引數。

結論二:

try catch只是對異常是否可以被@Transactional 感知 到有影響。如果錯誤拋到切面可以感知到的地步,那就可以起作用。

結論三:

由於REQUIRED屬性,「兩個事務」其實是一個事務,處理能力看報錯時刻,是否新增了處理非RuntimeException的能力。

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com