天天看點

@Transactional復原問題-try

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和事務嵌套 共同影響

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

結論