Spring事務的1道面試題

程序员半支烟發表於2024-10-10

每次聊起Spring事務,好像很熟悉,又好像很陌生。本篇透過一道面試題和一些實踐,來拆解幾個Spring事務的常見坑點。

原理

Spring事務的原理是:透過AOP切面的方式實現的,也就是透過代理模式去實現事務增強。

具體過程是:對包含@Transactional註解的方法進行攔截,然後重寫,重新在方法里加入異常回滾的邏輯。而且,每個執行緒都是獨立管理自己的事務,相互隔離。

原理簡單,使用起來也簡單,也就是在方法上打上@Transactional註解,然後事務就正常生效了。也很少有人去驗證異常情況下是否能真正的回滾。

Spring事務讓我熟悉的地方是哪哪看起來都簡單,讓我陌生的地方使用時的變種較多,有時候莫名其妙的不生效。

原始碼

以上原理的相關原始碼如下:

實踐出真知

但是 [半支菸] 偶爾會在編碼過程中發現有些場景下的事務是失效的,總有些情況讓你想不到,總有一些坑點等你去跳。

[半支菸] 覺得驗證事務的最好方式就是:記住基本原則 + 動手實踐。記住基本原則可以快速處理常規問題,動手實踐可以驗證偏門問題或者不確定的問題。

幾種事務不生效的用法

如下是常見的幾種Spring事務不生效的用法,有空的讀者一定要牢記,對日常編碼很有幫助,同時面試時也能說幾句。

  • private方法

Spring是透過AOP代理的方式實現事務增強的,但是private方法無法被代理,所以在private方法上打@Transactional註解是不生效的。

  • final、static修飾的方法

和private方法類似,final和static修飾的方法也無法被代理,所以@Transactional註解也不生效。

因為,static是屬於類方法,final修飾的方法無法被重寫,自然也就無法植入事務增強程式碼。

  • Bean物件沒有被Spring託管

某個類一定要被Spring託管,那才能透過@Transactional註解去增強事務。如果只有@Transactional註解,而沒有把類交給Spring託管,事務也是不生效的。類似如下情況:

// 此處沒有@Service註解,此類不被spring託管,及時有@Transactional也不生效
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public final void createAndUpdateUser() {
        createUser();
        updateUserById();
    }

    public void createUser() {
        User user = new User();
        user.setId(2L);
        user.setName("test2");
        user.setEmail("test2" + "@test.com");
        userMapper.insert(user);
        System.out.println("create user");
    }

    public void updateUserById() {
        User user = userMapper.findById(1L);
        user.setName("admin1");
        userMapper.update(user);
        int i = 1 / 0; // 此處會丟擲異常
        System.out.println("update user");
    }
}
  • 異常被吞掉

如果在業務程式碼裡,透過try......catch捕獲了異常,同時又沒有繼續丟擲異常時,Spring事務也是不生效的。

因為代理增強的邏輯就是要發現了異常,才能回滾事務。如果異常被方法本身吞掉了,則代理會認為沒有異常,從而無法回滾。

  • 非RuntimeException異常

Spring事務預設會回滾RuntimeException 及其子類,以及 Error 型別的異常。如果是其餘異常,則不會回滾。原始碼處可見:

這種非RuntimeException異常場景下,需要做2個動作從而保證事務回滾。

  1. 捕獲異常,然後丟擲自定義異常。
  2. 自行在@Transactional註解中增加@Transactional(rollbackFor = XxxxxxxException.class)屬性。或者直接使用rollbackFor = Exception.class,也就免去了第一步。
  • 非同步執行緒的場景

多個執行緒的場景下,只需要牢記每個執行緒只管理自己的事務即可。每個執行緒都有一個獨立的事務上下文,存在ThreadLocal中,所以事務資訊在不同執行緒之間是隔離的。

  • 重災區:在同一個類中呼叫本類的方法

這個失效場景,是最容易出錯的,而且變種還多。在同一個類中呼叫本類的方法時,牢記以下2點,即可破局:

  1. 是否會開啟事務依賴此類的第一個被外部呼叫的方法。如果此類的第一個被外部呼叫的方法有@Transactional註解,那事務生效。
  2. 呼叫自己內部方法時,採用的是this.xxxMethod()的方式,這種方式是不會走AOP代理的,所以被呼叫的內部方法的@Transactional註解不生效。

如果確實需要呼叫內部方法,並且要事務生效的話,那隻能將被呼叫的內部方法獨立到新的類中,同時交給Spring管理。

一道面試題

以上關於事務不生效的用法都比較好記,只有在同一個類中呼叫本類的方法場景下存在多種變種。具體請看這道面試題。請問以下createAndUpdateUser方法的事務生效嗎?

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public final void createAndUpdateUser() { //注意這裡有final修飾
        createUser();
        updateUserById();
    }

    @Transactional
    public void createUser() {
        User user = new User();
        user.setId(2L);
        user.setName("test2");
        user.setEmail("test2" + "@test.com");
        userMapper.insert(user);
        System.out.println("create user");
    }


    @Transactional(rollbackFor = Exception.class)
    public void updateUserById() {
        User user = userMapper.findById(1L);
        user.setName("admin1");
        userMapper.update(user);
        int i = 1 / 0; // 此處會丟擲異常
        System.out.println("update user");
    }
}

如果按照重災區:在同一個類中呼叫本類的方法裡提到的2個原則,則事務全部生效。

如果按照final、static修飾的方法裡提到的原則,則事務全部不生效。

那結果如何呢?結果是以上方法的事務全部生效。

為什麼呢?這裡在補充一個原則:final修飾的方法如果帶上@Transactional註解,事務情況按照被呼叫的方法自身的事務託管情況而定。

因為以上程式碼中的createUser方法和updateUserById方法,都有@Transactional註解,所以都生效。

這種特殊情況也實在是讓人瞠目,不過只需要牢記以上幾種不生效的用法即可,誰沒事兒寫這種@Transactional + final的程式碼呢?除了面試會問......

總結

本篇主要聊了幾種事務不生效的使用者,有興趣的讀者可以記一下。同時,還出了一道特殊場景的面試題,供讀者自行實踐。希望對你有幫助!

本篇完結!歡迎 關注、加V(yclxiao)交流、全網可搜(程式設計師半支菸)

原文連結:https://mp.weixin.qq.com/s/V5KpVk0kDhc9vWctOy7X9A

相關文章