面試突擊87:說一下 Spring 事務傳播機制?

2022-09-27 21:01:29

Spring 事務傳播機制是指,包含多個事務的方法在相互呼叫時,事務是如何在這些方法間傳播的。

既然是「事務傳播」,所以事務的數量應該在兩個或兩個以上,Spring 事務傳播機制的誕生是為了規定多個事務在傳播過程中的行為的。比如方法 A 開啟了事務,而在執行過程中又呼叫了開啟事務的 B 方法,那麼 B 方法的事務是應該加入到 A 事務當中呢?還是兩個事務相互執行互不影響,又或者是將 B 事務巢狀到 A 事務中執行呢?所以這個時候就需要一個機制來規定和約束這兩個事務的行為,這就是 Spring 事務傳播機制所解決的問題。

Spring 事務傳播機制有哪些?

Spring 事務傳播機制可使用 @Transactional(propagation=Propagation.REQUIRED) 來定義,Spring 事務傳播機制的級別包含以下 7 種:

  1. Propagation.REQUIRED:預設的事務傳播級別,它表示如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務。
  2. Propagation.SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行。
  3. Propagation.MANDATORY:(mandatory:強制性)如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常。
  4. Propagation.REQUIRES_NEW:表示建立一個新的事務,如果當前存在事務,則把當前事務掛起。也就是說不管外部方法是否開啟事務,Propagation.REQUIRES_NEW 修飾的內部方法會新開啟自己的事務,且開啟的事務相互獨立,互不干擾。
  5. Propagation.NOT_SUPPORTED:以非事務方式執行,如果當前存在事務,則把當前事務掛起。
  6. Propagation.NEVER:以非事務方式執行,如果當前存在事務,則丟擲異常。
  7. Propagation.NESTED:如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於 PROPAGATION_REQUIRED。

以上 7 種傳播機制,可根據「是否支援當前事務」的維度分為以下 3 類:

看到這裡,有人可能會說:說了這麼多,我也看不懂啊,即使看懂了,我也記不住啊?這要咋整?

沒關係,接下來我們用一個例子,來說明這 3 類事務傳播機制的區別。

以情侶之間是否要買房為例,我們將以上 3 類事務傳播機制可以看作是戀愛中的 3 類女生型別:

  • 普通型
  • 強勢型
  • 懂事型

這三類女生如下圖所示:

支援當前事務的「女生」,這裡的事務指的是「房子」,它分為 3 種(普通型女生):

  • Propagation.REQUIRED(需要有房子):有房子了咱們一起住,沒房子了咱們一起賺錢買房子。
  • Propagation.SUPPORTS(可以有房子):有房子了就一起住,沒房子了咱們就一起租房子。
  • Propagation.MANDATORY(強制有房子):有房子了就一起住,沒房子了就分手。

不支援當前事務的「女生」也分為 3 種(強勢型或者叫事業型):

  • Propagation.REQUIRES_NEW:不要你的房子,必須一起賺錢買房子。
  • Propagation.NOT_SUPPORTED:不要你的房子,必須一起租房子。
  • Propagation.NEVER:必須一起租房子,你有房子就分手。

最後一種是巢狀性事務 Propagation.NESTED,它屬於懂事型女友,如果有房子了就以房子為基礎做點小生意,賣個花生、水果啥的,如果買賣成了,那就繼續發展;如果失敗了,至少還有房子;如果沒房子也沒關係,一起賺錢買房子。

事務傳播機制使用與演示

接下來我們演示一下事務傳播機制的使用,以下面 3 個最典型的事務傳播級別為例:

  • 支援當前事務的 REQUIRED;
  • 不支援當前事務的 REQUIRES_NEW;
  • 巢狀事務 NESTED。

下來我們分別來看。

事務傳播機制的範例,需要用到以下兩張表:

-- 使用者表
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `password` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `createtime` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;

-- 紀錄檔表
CREATE TABLE `log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` text NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

建立一個 Spring Boot 專案,核心業務程式碼有 3 個:UserController、UserServcie 以及 LogService。在 UserController 裡面呼叫 UserService 新增使用者,並呼叫 LogService 新增紀錄檔。

REQUIRED 使用演示

REQUIRED 支援當前事務。
UserController 實現程式碼如下,其中 save 方法開啟了事務:

@RestController
public class UserController {
    @Resource
    private UserService userService;
    @Resource
    private LogService logService;

    @RequestMapping("/save")
    @Transactional
    public Object save(User user) {
        // 插入使用者操作
        userService.save(user);
        // 插入紀錄檔
        logService.saveLog("使用者插入:" + user.getName());
        return true;
    }
}

UserService 實現程式碼如下:

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public int save(User user) {
        return userMapper.save(user);
    }
}

LogService 實現程式碼如下:

@Service
public class LogService {
    @Resource
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public int saveLog(String content) {
        // 出現異常
        int i = 10 / 0;
        return logMapper.saveLog(content);
    }
}

執行結果:程式報錯,兩張表中都沒有插入任何資料。

執行流程描述:

  1. 首先 UserService 中的新增使用者方法正常執行完成。
  2. LogService 儲存紀錄檔程式報錯,因為使用的是 UserController 中的全域性事務,所以整個事務回滾,步驟 1 中的操作也跟著回滾。
  3. 所以資料庫中沒有新增任何資料。

REQUIRED_NEW 使用演示

REQUIRED_NEW 不支援當前事務。
UserController 實現程式碼:

@RequestMapping("/save")
@Transactional
public Object save(User user) {
    // 插入使用者操作
    userService.save(user);
    // 插入紀錄檔
    logService.saveLog("使用者插入:" + user.getName());
    return true;
}

UserService 實現程式碼:

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int save(User user) {
        System.out.println("執行 save 方法.");
        return userMapper.save(user);
    }
}

LogService 實現程式碼:

@Service
public class LogService {
    @Resource
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int saveLog(String content) {
        // 出現異常
        int i = 10 / 0;
        return logMapper.saveLog(content);
    }
}

程式執行結果:

User 表中成功新增了一條使用者資料,Log 表執行失敗,沒有加入任何資料,但它並沒有影響到 UserController 中的事務執行。

通過以上結果可以看出:LogService 中使用的是單獨的事務,雖然 LogService 中的事務執行失敗了,但並沒有影響 UserController 和 UserService 中的事務。

NESTED 使用演示

NESTED 是巢狀事務。
UserController 實現程式碼如下:

@RequestMapping("/save")
@Transactional
public Object save(User user) {
    // 插入使用者操作
    userService.save(user);
    return true;
}

UserService 實現程式碼如下:

@Transactional(propagation = Propagation.NESTED)
public int save(User user) {
    int result = userMapper.save(user);
    System.out.println("執行 save 方法.");
    // 插入紀錄檔
    logService.saveLog("使用者插入:" + user.getName());
    return result;
}

LogService 實現程式碼如下:

@Transactional(propagation = Propagation.NESTED)
public int saveLog(String content) {
    // 出現異常
    int i = 10 / 0;
    return logMapper.saveLog(content);
}

最終執行結果,使用者表和紀錄檔表都沒有新增任何資料。

執行流程描述:

  1. UserController 中呼叫了 UserService 的新增使用者方法,UserService 使用 NESTED 迴圈巢狀事務,併成功執行了新增使用者的方法。
  2. UserService 中呼叫了 LogService 的新增方法,LogService 使用了 NESTED 迴圈巢狀事務,但在方法執行中出現的異常,因此回滾了當前事務。
  3. 因為 UserService 使用的是巢狀事務,所以發生回滾的事務是全域性的,也就是說 UserService 中的新增使用者方法也被回滾了,最終執行結果是使用者表和紀錄檔表都沒有新增任何資料。

總結

Spring 事務傳播機制是包含多個事務的方法在相互呼叫時,事務是如何在這些方法間傳播的。事務的傳播級別有 7 個,支援當前事務的:REQUIRED、SUPPORTS、MANDATORY;不支援當前事務的:REQUIRES_NEW、NOT_SUPPORTED、NEVER,以及巢狀事務 NESTED,其中 REQUIRED 是預設的事務傳播級別。

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

公眾號:Java面試真題解析

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