spring-5-事務

羊37發表於2024-06-16

參考:

spring 事務失效的 11 種場景

一、事務基礎

1.什麼是事務

事務是指作為單個邏輯工作單元執行的一系列操作,要麼全部成功執行,要麼全部失敗回滾到初始狀態,保證資料的一致性和完整性。事務具有ACID特性,即原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和永續性(Durability)。

Spring的事務是指在Spring框架中對資料庫操作進行管理的機制。透過Spring的事務管理,可以確保一組資料庫操作要麼全部成功提交,要麼全部失敗回滾,保持資料的一致性和完整性。Spring的事務管理可以透過宣告式事務和程式設計式事務兩種方式來實現。

簡而言之,Spring事務要學習的就是,在接入資料庫後咱們如何結合Spring框架管理好事務。

2.ACID特性

3.Spring中進行事務管理的2種方式

  • 宣告式事務
  • 程式設計式事務
特性 宣告式事務 程式設計式事務
定義 使用註解或XML配置宣告事務邊界 手動編寫程式碼管理事務
使用簡便性 高。透過註解或XML配置即可完成事務管理 低。需要顯式編碼來管理事務
程式碼可讀性 高。事務邊界清晰,程式碼簡潔 低。混合了業務邏輯和事務管理程式碼
靈活性 低。基於配置的方式,靈活性較低 高。可以在程式碼中靈活控制事務
侵入性 低。對業務邏輯程式碼侵入性小 高。對業務邏輯程式碼侵入性大
配置複雜度 低。透過註解或XML配置,簡單明瞭 高。需要顯式編寫事務管理程式碼
維護性 高。配置與業務邏輯分離,便於維護 低。事務管理程式碼與業務邏輯耦合,不易維護
效能控制 中。大多數情況下效能表現良好 高。可以更精細地控制事務的行為和效能
學習成本 低。Spring 提供了便捷的註解和配置方式 高。需要熟悉 Spring 的事務管理 API
適用場景 適用於大多數常見的事務管理場景 適用於需要細粒度控制事務的特殊場景

宣告式事務:適用於大多數常見的事務管理場景,透過簡單的註解或XML配置即可完成事務管理,適合對事務管理要求不是很複雜的情況下使用。

程式設計式事務:適用於需要細粒度控制事務的特殊場景,透過手動編寫程式碼管理事務,可以靈活地控制事務的行為和效能,但相對複雜且侵入性較大。

二、事務隔離級別

參考下我這篇文章:Mysql-事務的基本特性和隔離級別

隔離級別 髒讀 不可重複讀 幻讀
READ UNCOMMITTED(未提交讀)
READ COMMITTED(已提交讀) ×
REPEATABLE READ(可重複讀) × ×
SERIALIZABLE(序列化) × × ×
  • MySQL中,預設的隔離級別是 REPEATABLE READ,即RR可重複讀。
  • Oracle中,預設的隔離級別是 READ COMMITTED,即RC讀已提交。

三、宣告式事務和程式設計式事務

下方例子中,為了演示,我們做以下例子。

mysql庫:za7za8

表名:u_user

image-20240615210932877

CREATE TABLE `u_user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `name` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名',
  `age` int NOT NULL COMMENT '年齡',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

現在是個空表,後方我們的目的是插入條user資料,並將年齡更新。

  • 插入一條資料

    INSERT INTO `za7za8`.`u_user`(`name`, `age`) VALUES ('yang', 10);
    
  • 更新資料

    UPDATE `za7za8`.`u_user` SET `age` = 12 WHERE `name` = 'yang';
    

先手動演示下,等會演示時我們清空庫。

image-20240615211604409

SpringBoot的專案呢,我們也不需要web啥的,關鍵是這兩個依賴。

<spring-boot.version>2.6.13</spring-boot.version>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.7</version>
        </dependency>

image-20240615213810526

驗證下能不能查出來資料,剛才不是演示插入了條。

image-20240615213728801

然後呢,我們準備下方法,也驗證下先。

public interface IUserService extends IService<User> {
    User insertAndUpdate(User user,int age);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Override
    public User insertAndUpdate(User user, int age) {
        save(user);
        user.setAge(age);
        updateById(user);
        return user;
    }

}

使用者su,年齡更新成199,驗證下。

image-20240615215238177

image-20240615215521129

嗯,資料庫中也正常,序號別關心,我剛才驗證刪除了下,這是第三條資料了。

image-20240615215256402

現在,我們怎麼讓這個事務出問題呢,那就是插入後,我們把它id更新掉,更新查詢id=xx的時候查不到更新不了。

理想的情況是更新失敗後,開始插入的資料會消失,資料庫中不會有髒資料,這才叫事務。

那就在更新語句加個時間等等唄,讓我們有時間手動操作,稍微改造下程式碼,更新失敗時丟擲異常。

    @Override
    public User insertAndUpdate(User user, int age) {
        // 儲存
        save(user);

        // 嘗試休眠
        try {
            Thread.sleep(30 * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 更新age
        user.setAge(age);
        boolean updateFlag = updateById(user);
        if (!updateFlag) {
            throw new RuntimeException("更新UserAge異常!");
        }

        // 返回db記錄
        return getById(user.getId());
    }

我先清空表,然後用新使用者li來試下,發現有資料後,我們就把id更新掉,讓它更新不了。

表已經清空

image-20240616152712756

資料庫插入資料li

image-20240616152806469

發現寫入了id為1的使用者li資料,我們把id更新成2。

image-20240616152739825

image-20240616152754477

接著程式裡嘗試用userId為1的來更新,失敗了,丟擲異常。

image-20240616152907107

最後再來查詢一下

image-20240616152924822

經過這個過程,如果不考慮事務,我們會發現,哎,資料庫裡有髒資料了,不符合我們的預期。

1.宣告式事務

1.1 使用註解@Transactional

一個基本的方式,就是在方法/類上加上@Transactional註解。

image-20240616153437212

欄位 型別 預設值 描述
value String "" transactionManager 的別名。定義要使用的事務管理器的名稱。
transactionManager String "" value 的別名。定義要使用的事務管理器的名稱。
label String[] {} 事務限定符的標籤陣列。
propagation Propagation Propagation.REQUIRED 定義事務傳播型別,確定事務之間的關係。
isolation Isolation Isolation.DEFAULT 定義事務隔離級別,控制事務之間的隔離程度。
timeout int -1 定義事務的超時時間(以秒為單位),負值表示沒有超時。
timeoutString String "" 以字串格式定義事務的超時時間,允許更靈活地指定持續時間。
readOnly boolean false 指定事務是否為只讀,只讀事務在讀取資料時進行了最佳化。
rollbackFor Class<? extends Throwable>[] {} 指定應觸發回滾的異常類陣列。
rollbackForClassName String[] {} 指定應觸發回滾的異常類名稱陣列(以字串形式)。
noRollbackFor Class<? extends Throwable>[] {} 指定不應觸發回滾的異常類陣列。
noRollbackForClassName String[] {} 指定不應觸發回滾的異常類名稱陣列(以字串形式)。

回顧之前更新userId的場景,我們會發現問題的原因在我們本意它是一個事務,但是。

  • A操作:idea中跑的程式
  • B操作:手動運算元據庫

兩者之間的隔離性出現問題了,我手動操作的時候,看到了本意是事務的idea程式中跑的資料,類似於髒讀

我們直接加上@Transactional註解試下?

image-20240616154315037

清空表,重複下操作。

image-20240616154453124

哎?我們會發現,這個時候手動操作查不到資料了。

image-20240616154521537

最後等待方法完成,整個過程都是順利的。

image-20240616154553421

image-20240616154602442

這是為啥?咋還改不了了?這就是事務的用處。

使用了事務後,預設開啟了我們RR級別。

image-20240616154730447

在RR級別下,根據MVCC機制,我們手動操作B是看不到剛剛插入的資料的。

1.2 使用xml檔案

2.程式設計式事務

程式設計式事務,就是不利用註解等操作,我們自己手動寫程式碼來完成。

程式設計式事務主要透過 TransactionTemplate 或者直接使用 PlatformTransactionManager 來實現。

使用程式設計式事務的場景:

  • 動態控制事務邊界:有些複雜的業務邏輯需要在執行時決定事務的邊界,程式設計式事務可以提供這種靈活性。
  • 在非 Spring 管理的物件中使用事務:在一些非 Spring 管理的物件中使用事務管理,此時可以透過程式設計式事務來實現。
  • 對效能有特殊要求:程式設計式事務比宣告式事務具有更低的開銷,因為它不需要進行 AOP 代理的處理。

2.1 使用 TransactionTemplate

@Service
public class MyService {

    @Resource
    private TransactionTemplate transactionTemplate;

    public void doSomething() {
        transactionTemplate.execute(status -> {
            // 在此處執行你的業務邏輯
            // 如果丟擲 RuntimeException 或 Error,事務將回滾
            // 否則事務將提交
            return null;
        });
    }
}

2.2 使用PlatformTransactionManager

@Service
public class MyService {

    @Resource
    private TransactionTemplate transactionTemplate;

    public void doSomething() {
        DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
        // 設定事務的傳播行為、隔離級別等屬性
        defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

        TransactionStatus status = transactionManager.getTransaction(defaultTransactionDefinition);

        try {
            // 在此處執行你的業務邏輯
            
            // 提交事務
            transactionManager.commit(status);
        } catch (Exception e) {
            // 回滾事務
            transactionManager.rollback(status);
        }
    }
}

四、事務管理器

事務管理器 簡介 適用場景 特點
DataSourceTransactionManager 用於 JDBC 資料來源的事務管理器 直接使用 JDBC 進行資料庫操作的應用程式 - 輕量級,效能好
- 簡單易用,適用於純 JDBC 場景
JpaTransactionManager 用於 JPA 的事務管理器 使用 JPA 進行持久化操作的應用程式,例如 Spring Data JPA - 支援 JPA 標準
- 可與 Spring Data JPA 無縫整合
HibernateTransactionManager 用於 Hibernate 的事務管理器 直接使用 Hibernate API 進行持久化操作的應用程式 - 深度整合 Hibernate 特性
- 支援 Hibernate 特有功能

五、事務超時與只讀屬性

1.事務超時(timeout)

事務超時屬性定義了一個事務應該在多長時間內完成,如果事務在指定的時間內沒有完成,它將被自動回滾。

設定事務超時的主要目的是避免長時間執行的事務佔用資源,導致系統效能下降。

@Service
public class MyService {
    
 	// 設定超時時間為5秒
    @Transactional(timeout = 5)
    public void doSomething() {
        // 執行業務邏輯
        // 如果在5秒內沒有完成事務,將自動回滾
    }
}

2.只讀事務(readOnly)

只讀事務屬性用於宣告事務中的操作不會修改資料庫內容。

設定只讀事務的主要目的是讓資料庫能夠最佳化事務處理,因為資料庫知道它不需要為只讀操作持有鎖或維持更復雜的事務機制。

Spring 的 @Transactional 註解中的 readOnly 屬性主要是一個提示,告訴 Spring 和底層資料庫驅動這個事務應該是隻讀的。

Spring 會嘗試將這個資訊傳遞給底層的資料庫驅動或 JPA 實現,以便資料庫可以進行相應的最佳化。

@Service
public class MyService {

    // 設定為只讀事務
    @Transactional(readOnly = true) 
    public void readOnlyOperation() {
        // 執行只讀操作,例如查詢
    }
}

那咱們的Mysql是支援只讀事務的,用SET TRANSACTION即可。

-- 設定只讀事務
SET TRANSACTION READ ONLY;

-- 開始事務
START TRANSACTION;

-- 在事務中執行查詢操作
SELECT * FROM my_table;

-- 提交事務
COMMIT;

image-20240616170826784

六、事務回滾與異常處理

image-20240616171601757

1.預設回滾

在 Spring事務中,預設情況下。

  • 未檢查異常(RuntimeException 或其子類):自動回滾
  • 已檢查異常(Exception 或其子類):不會回滾
@Service
public class MyService {

    @Transactional
    public void performOperation() {
        try {
            // 執行業務邏輯

            // 模擬未檢查異常
            if (true) {
                throw new RuntimeException("模擬未檢查異常");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

在事務方法中丟擲 RuntimeException 或其子類,事務自動回滾。

那我catch了留空不處理?會怎麼樣?

那你這個方法最後它沒拋異常出來唄,事務將不會回滾。

2.使用 @Transactional 註解進行回滾控制

@Transactional 註解提供了一些屬性來控制事務的回滾行為:

  • rollbackFor:指定哪些異常會觸發事務回滾。
  • noRollbackFor:指定哪些異常不會觸發事務回滾。

哎,那我rollbackFor裡面寫個Exception呢?

    @Transactional(rollbackFor = Exception.class) 

在這個示例中,即使丟擲的是 Exception,事務也會回滾,因為 rollbackFor 屬性指定了 Exception.class

3.手動回滾

如果想要手動回滾也是可以的,不過,你用好上面的註解就夠了,不用這麼麻煩。

// 手動回滾事務
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 

4.注意區分e.printStackTrace()和throw new RuntimeException

  • e.printStackTrace()

    是一個用於列印異常堆疊跟蹤資訊的方法。這種方法只是輸出異常資訊,並不會重新丟擲異常,因此事務管理器不會感知到異常的存在。

    如果在事務中使用 e.printStackTrace() 而不重新丟擲異常,事務將會被視為成功,並且會被提交。

  • throw new RuntimeException("模擬未檢查異常")

    丟擲一個新的 RuntimeException,這種方法會將異常傳遞給呼叫者,Spring才會檢測到這個異常並回滾事務。

七、巢狀事務與儲存點

在 Spring 事務管理中,巢狀事務和儲存點(Savepoints)是兩個用於處理複雜事務場景的高階特性。

它們幫助在多個子事務中維護事務的一致性,並允許在事務的中間點進行部分回滾。

1.巢狀事務

巢狀事務是指在一個外部事務中包含一個或多個內部事務。

Spring 本身不直接支援巢狀事務,但是透過合適的傳播行為(propagation behavior),可以實現類似巢狀事務的效果。

傳播行為參考:八、事務傳播行為

2.儲存點

這個主要是針對部分回滾的場景。

儲存點允許你在一個事務的中間點設定一個回滾點,以便在出現問題時回滾到該儲存點,而不是完全回滾整個事務。

儲存點(Savepoints)必須透過程式設計式事務管理來實現,宣告式事務管理(基於註解的方式)不直接支援儲存點的建立和回滾。

            // 設定儲存點
            Object savepoint = status.createSavepoint();
 			// 發生異常時回滾到儲存點,而不是回滾整個事務
            status.rollbackToSavepoint(savepoint);
@Service
public class MyService {
    
    @Resource
    private TransactionTemplate transactionTemplate;

    public void performOperation() {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = transactionManager.getTransaction(def);

        try {
            // 執行業務邏輯

            // 設定儲存點
            Object savepoint = status.createSavepoint();

            try {
                // 執行可能會丟擲異常的業務邏輯
                
            } catch (Exception e) {
                // 發生異常時回滾到儲存點,而不是回滾整個事務
                status.rollbackToSavepoint(savepoint);
            }

            // 提交事務
            transactionManager.commit(status);
        } catch (Exception e) {
            // 完全回滾事務
            transactionManager.rollback(status);
        }
    }

八、事務傳播行為

Spring 的事務傳播機制定義了事務方法是如何相互影響的。

透過傳播行為,我們可以指定一個事務方法是否應該執行在現有事務中,或者應該啟動一個新的事務等。

簡而言之,就是別人呼叫我這個方法的時候,我該怎麼辦?

是加入之前的事務呢、還是單獨新建一個事務呢,又或者?

示例:

    @Transactional(propagation = Propagation.NEVER)

spring事務的7種傳播行為。

傳播行為 描述 典型場景
REQUIRED 如果當前存在事務,則加入該事務。
如果當前沒有事務,則建立一個新的事務。
預設傳播行為,確保所有操作在同一事務中執行。
SUPPORTS 如果當前存在事務,則加入該事務。
如果當前沒有事務,則以非事務方式執行。
不強制要求事務的讀操作。
MANDATORY 如果當前存在事務,則加入該事務。
如果當前沒有事務,則丟擲異常。
必須在事務中執行的操作,由外部呼叫確保事務存在。
REQUIRES_NEW 無論是否存在當前事務,都建立一個新的事務。
如果當前存在事務,則掛起當前事務。
需要獨立事務的操作,例如獨立的日誌記錄。
NOT_SUPPORTED 如果當前存在事務,則掛起當前事務,並以非事務方式執行。 不希望在事務中執行的操作。
NEVER 如果當前存在事務,則丟擲異常。
如果當前沒有事務,則以非事務方式執行。
確保操作不在事務中執行。
NESTED 如果當前存在事務,則在當前事務中建立一個巢狀事務。
如果當前沒有事務,則建立一個新的事務。
需要部分回滾的複雜事務。

例如:

@Service
public class ExampleService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void method1() {
        // 主事務邏輯開始
        System.out.println("method1: 主事務開始");

        // 執行method2,建立巢狀事務
        try {
            method2();
        } catch (Exception e) {
            System.out.println("method1: 捕獲到異常 " + e.getMessage());
        }

        // 主事務邏輯繼續
        System.out.println("method1: 主事務繼續");

        // 主事務邏輯結束
        System.out.println("method1: 主事務結束");
    }

    @Transactional(propagation = Propagation.NESTED)
    public void method2() {
        // 巢狀事務邏輯開始
        System.out.println("method2: 巢狀事務開始");

        // 模擬操作和異常
        if (true) { // 可以根據實際條件進行調整
            throw new RuntimeException("method2: 巢狀事務發生異常");
        }

        // 巢狀事務邏輯結束
        System.out.println("method2: 巢狀事務結束");
    }
}

在上面的程式碼中:

  • method1:Propagation.REQUIRED,如果當前存在事務,則加入該事務。如果當前沒有事務,則建立一個新的事務。
  • method2:Propagation.NESTED,如果當前存在事務,則在當前事務中建立一個巢狀事務。如果當前沒有事務,則建立一個新的事務。

執行流程

  1. method1 被呼叫,並開啟一個新的事務。
  2. method1 呼叫 method2。由於 method2 使用 Propagation.NESTED,所以在 method1 的事務中建立一個巢狀事務。
  3. method2 中,丟擲一個執行時異常,導致 method2 的巢狀事務回滾到儲存點。
  4. method1 捕獲到 method2 丟擲的異常,繼續執行剩下的事務邏輯。

預期輸出

method1: 主事務開始
method2: 巢狀事務開始
method1: 捕獲到異常 method2: 巢狀事務發生異常
method1: 主事務繼續
method1: 主事務結束

九、事務實現原理

十、事務失效場景

1.方法許可權為private

由於事務是基於AOP的,咱們的CGLIB又是靠繼承來動態代理的。

所以呢,spring 要求被代理方法必須是public的。

private、default 或 protected 的話,spring 不會提供事務功能,原始碼也會檢查是不是public的。

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    // Don't allow no-public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
    }
    // ...

例如下面這個,事務就不會生效。

@Service
public class MyService {

    @Transactional
    private void performOperation() {
        // 事務將不會生效,因為方法不是公共的
    }
}

2.方法為final或者static

原理同上。

3.同類內部方法呼叫

Spring 的事務管理是透過 AOP 實現的。

Spring 使用代理物件來攔截對目標方法的呼叫,並在方法執行前後插入事務管理邏輯。

當你從外部呼叫一個標註了 @Transactional 的方法時,實際上是呼叫了該方法的代理物件,代理物件會在呼叫實際方法之前開啟事務,並在方法執行完成後提交或回滾事務。

當一個類的方法呼叫同一個類的另一個方法時,這種呼叫是直接的,不會經過代理物件。這意味著事務管理邏輯不會被觸發,因為代理物件的攔截器根本沒有機會插入事務管理邏輯。

@Service
public class MyService {

    @Transactional
    public void outerMethod() {
        // 直接呼叫事務不會生效
        innerMethod();
    }

    @Transactional
    public void innerMethod() {
        // 事務將不會生效
    }
}

我從外部物件呼叫innerMethod(),事務會生效,因為實際上呼叫的是代理物件。

我從內部呼叫innerMethod(),事務會失效,直接拿著this.xx就執行了。

4.未被spring管理

使用 spring 事務的前提是,物件要被 spring 管理,像下方這個就漏了@Service。

public class UserService {
 
    @Transactional
    public void method() {
        // 事務將不會生效
    }    
}

5.多執行緒

多執行緒環境下,不同執行緒拿到的資料庫連線都不一樣,跨執行緒則失效了。

6.非事務支援的DB

例如,咱們Mysql的myisam 儲存引擎不支援事務。

相關文章