面試突擊88:加入事務和巢狀事務有什麼區別?

2022-10-07 18:00:14

加入事務和巢狀事務是指在 Spring 事務傳播機制中的加入事務(REQUIRED)和巢狀事務(NESTED)的區別,二者看似很像,實則截然不同,那麼它們有什麼區別呢?接下來我們一起來看。

Spring 事務傳播機制是指,包含多個事務的方法在相互呼叫時,事務是如何在這些方法間傳播的,Spring 事務傳播機制分為 3 大類,總共 7 種級別,如下圖所示:

其中,支援當前事務的 REQUIRED 是加入(當前)事務,而 NESTED 是巢狀(當前)事務,本文要討論的就是這二者的區別。

1.加入事務

加入事務 REQUIRED 是 Spring 事務的預設傳播級別。

所謂的加入當前事務,是指如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務。我們這裡重點要討論的是第一種情況,也就是當前存在事務的情況下,它和巢狀事務的區別,接下來我們通過一個範例來看加入事務的使用和執行特點。

我們要實現的是使用者新增功能,只不過在新增使用者時,我們需要給使用者表和紀錄檔表中分別插入一條資料,UserController 實現程式碼如下:

@Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/add")
public int add(UserInfo userInfo) {
    int result = 0;
    int userResult = userService.add(userInfo);
    System.out.println("使用者新增結果:" + userResult);
    if (userResult > 0) {
        LogInfo logInfo = new LogInfo();
        logInfo.setName("新增使用者");
        logInfo.setDesc("新增使用者結果:" + userResult);
        int logResult = logService.add(logInfo);
        System.out.println("紀錄檔新增結果:" + logResult);
        result = 1;
    }
    return result;
}

從上述程式碼可以看出,新增使用者使用了事務,並設定了事務傳播機制為 REQUIRED(加入事務),此控制器呼叫的 UserService 實現程式碼如下:

@Transactional(propagation = Propagation.REQUIRED)
public int add(UserInfo userInfo) {
    int result = userMapper.add(userInfo);
    return result;
}

從上述程式碼可以看出,它也是使用事務,並設定了事務的傳播機制為 REQUIRED,而 LogService 也是類似的實現程式碼:

@Transactional(propagation = Propagation.REQUIRED)
public int add(LogInfo logInfo) {
    int result = logMapper.add(logInfo);
    try {
        int number = 10 / 0;
    } catch (Exception e) {
        // 手動回滾事務
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return result;
}

從上述程式碼我們可以看出,在設定事務傳播機制的同時,我們也在程式中主動的設定了一個異常。

執行以上程式的執行結果如下圖所示:

從上述結果我們可以看出:當我們設定了加入事務的事務傳播機制之後,程式的執行結果是將使用者表和紀錄檔表的事務都回滾了

2.巢狀事務

巢狀事務指的是事務傳播級別中的 NESTED,所謂的巢狀當前事務,是指如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於 REQUIRED。當然,我們本文要研究的重點也是第一種情況,也就是當前存在事務的前提下,巢狀事務和加入事務的區別。

所以接下來我們將上面程式碼中的事務傳播機制改為 NESTED,它的實現程式碼如下。
UserController 實現程式碼如下:

@Transactional(propagation = Propagation.NESTED)
@RequestMapping("/add")
public int add(UserInfo userInfo) {
    int result = 0;
    int userResult = userService.add(userInfo);
    System.out.println("使用者新增結果:" + userResult);
    if (userResult > 0) {
        LogInfo logInfo = new LogInfo();
        logInfo.setName("新增使用者");
        logInfo.setDesc("新增使用者結果:" + userResult);
        int logResult = logService.add(logInfo);
        System.out.println("紀錄檔新增結果:" + logResult);
        result = 1;
    }
    return result;
}

UserService 實現程式碼如下:

@Transactional(propagation = Propagation.NESTED)
public int add(UserInfo userInfo) {
    int result = userMapper.add(userInfo);
    return result;
}

LogService 實現程式碼如下:

@Transactional(propagation = Propagation.NESTED)
public int add(LogInfo logInfo) {
    int result = logMapper.add(logInfo);
    try {
        int number = 10 / 0;
    } catch (Exception e) {
        // 手動回滾事務
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return result;
}

執行以上程式的執行結果如下圖所示:

從上述結果可以看出:當設定巢狀事務的事務傳播級別之後,程式執行了部分事務的回滾,使用者表新增的事務沒有回滾,只是紀錄檔表的事務回滾了。

3.加入事務 VS 巢狀事務

加入事務(REQUIRED)和巢狀事務(NESTED)都是事務傳播機制的兩種傳播級別,如果當前不存在事務,那麼二者的行為是一樣的;但如果當前存在事務,那麼加入事務的事務傳播級別在遇到異常之後,會將事務全部回滾;而巢狀事務在遇到異常時,只是執行了部分事務的回滾。

4.巢狀事務實現原理

事務全部回滾很好理解,這本來就是事務原子性的一種體現,而巢狀事務中的部分事務回滾是怎麼實現的呢?

巢狀事務只所以能實現部分事務的回滾,是因為在資料庫中存在一個儲存點(savepoint)的概念,以 MySQL 為例,巢狀事務相當於新建了一個儲存點,而滾回時只回滾到當前儲存點,因此之前的事務是不受影響的,這一點可以在 MySQL 的官方檔案彙總找到相應的資料:https://dev.mysql.com/doc/refman/5.7/en/savepoint.html

而 REQUIRED 是加入到當前事務中,並沒有建立事務的儲存點,因此出現了回滾就是整個事務回滾,這就是巢狀事務和加入事務的區別。

儲存點就像玩通關遊戲時的「遊戲存檔」一樣,如果設定了遊戲存檔,那麼即使當前關卡失敗了,也能繼續上一個存檔點繼續玩,而不是從頭開始玩遊戲。

總結

加入事務(REQUIRED)和巢狀事務(NESTED)都是事務傳播機制中的兩種傳播級別,如果當前不存在事務,那麼二者的行為是一致的;但如果當前存在事務,那麼加入事務的事務傳播級別當遇到異常時會回滾全部事務,而巢狀事務則是回滾部分事務。巢狀事務之所以能回滾部分事務,是因為資料庫中存在一個儲存點的概念,巢狀事務相對於新建了一個儲存點,如果出現異常了,那麼只需要回滾到儲存點即可,這樣就實現了部分事務的回滾。

是非審之於己,譭譽聽之於人,得失安之於數。

公眾號:Java面試真題解析

面試合集:https://gitee.com/mydb/interview