SpringBoot | 1.4 資料庫事務處理
前言
前面講解了Sring的AOP,可以知道它是用來抽取公共程式碼,增強方法的。而在JDBC操作資料庫進行資料處理時,有很多重複的公共程式碼;事務的提交與回滾跟AOP的約定流程很相似。因此,Spring資料庫事務程式設計的思想基於AOP的設計思想,資料庫事務處理是AOP的一種典型應用。
1. 事務的一些概念
首先我們要對事務常用概念有一個瞭解。
什麼事務:
- 事務是資料庫操作最基本單元,邏輯上一組操作,要麼都成功,如果有一個失敗所有操作都失敗
- 典型場景:銀行轉賬
資料庫事務四個特性(ACID):
- 原子性(業務單元的操作要麼全部成功,要麼全部失敗)
- 一致性(事務完成時,所有資料保持一致)
- 隔離性(核心,為了壓制丟失更新的產生,處理高併發的關鍵)
- 永續性(事務結束後,所有資料固化到一個地方,如:磁碟)
事務的操作方法:
- 宣告式事務管理(註解方式)
- 程式設計式事務管理(xml配置)
這裡僅討論宣告式事務管理
2. 註解宣告式事務管理
Spring AOP的約定,會將我們的程式碼織入到約定的流程中。基於AOP思想的事務處理,也有這樣一個約定,其中最重要的註解是@Transactional
@Transactional
- 事務性的
- 可以標註在類和方法上,推薦類上;
- 該註解可以配置一些屬性,如:事務隔離級別、傳播行為與異常型別等。Spring IoC容器在載入時將配置資訊解析,存到事務定義器
TransactionDefinition
Spring資料庫事務約定:
具體流程:當事務啟動時,Spring會根據事務定義器內的配置設定事務。首先根據傳播行為確定事務策略;然後是隔離級別、超越時間、只讀等內容設定。直到呼叫開發者的業務程式碼,此時若沒有異常,Spring資料庫攔截器會替我們提交事務;如果發生異常,需要判斷事務定義器內配置,若事務定義器約定了該型別異常不回滾,則提交事務;若沒有配置或配置回滾,則進行事務回滾並丟擲異常。
@Transactional原始碼
從原始碼中知可以配置哪些資訊:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { //通過bean name制定事務管理器 @AliasFor("transactionManager") String value() default ""; //同value屬性 @AliasFor("value") String transactionManager() default ""; String[] label() default {}; //制定傳播行為(重點) Propagation propagation() default Propagation.REQUIRED; //制定隔離級別(重點) Isolation isolation() default Isolation.DEFAULT; //制定超時時間(單位秒) int timeout() default -1; String timeoutString() default ""; //是否只讀事件 boolean readOnly() default false; //方法在發生指定異常時回滾,預設所有異常回滾 Class<? extends Throwable>[] rollbackFor() default {}; //方法在發生指定異常名稱時回滾,預設所有異常回滾 String[] rollbackForClassName() default {}; //方法在發生指定異常時“不”回滾,預設所有異常回滾 Class<? extends Throwable>[] noRollbackFor() default {}; //方法在發生指定異常名稱時“不”回滾,預設所有異常回滾 String[] noRollbackForClassName() default {}; }
3. 隔離級別
從上面分析可知,隔離級別isolation與傳播行為propagation是@Transactional註解的兩個十分重要的配置項,因此這裡單獨拿出來講。
丟失更新:
- 第一類丟失更新:一個事務回滾,另一個事務提交引發資料不一致。(如今資料庫系統已解決)
- 第二類丟失更新:事務1無法知道事務2存在,按事務1提交結果。(需要設定隔離級別)
三類讀的問題:
- 髒讀:一個事務讀取另一個事務沒有提交的資料;
- 不可重複讀:庫存對於事務2而言是個可變化值;不可重複讀的是資料庫單一記錄值。
- 幻讀:幻讀的資料不是資料庫儲存值,是統計值。
四類隔離級別:
用來解決上述三類讀問題,隔離級別由高到低分為:未提交讀、讀寫提交、可重複讀、序列化。
未提交讀:
- 允許一個事務讀取另一個事務沒有提交的資料;
- 優點:併發能力高;
- 缺點:可能發生髒讀;
- 是最低的隔離級別,一種危險的隔離級別。
讀寫提交:
- 一個事務只能讀取另一個事務已經提交的資料;
- 優點:解決髒讀問題;
- 缺點:可能造成不可重複讀問題。
可重複度:
- 克服不可重複讀問題;
- 優點:克服不可重複讀問題;
- 缺點:
序列化:
- 要求所有SQL按順序執行;
- 優點:保證資料一致性;
- 缺點:效能低。
使用合理的隔離級別解決三類讀的問題:
考慮效能,在實際中會以讀寫提交為主,其能防止髒讀,不能避免不可重複讀與幻讀。為了克服資料不一致性與效能問題,可以使用樂觀鎖或使用Redis作為資料載體。
對於隔離級別,不同資料庫支援不同:Oracle支援讀寫提交和序列化,預設讀寫提交;MySQL支援4種,預設可重複讀。
修改隔離級別的方法:
- 在@Transactional註解上配置屬性;
@Service @Transactional(isolation = Isolation.REPEATABLE_READ) public class UserService{
- 通過application.properties配置檔案配置;
#隔離級別數字配置含義:
#-1 資料庫預設隔離級別
#1 未提交讀
#2 讀寫提交
#4 可重複讀
#8 序列化
#tomcat資料來源預設隔離級別
spring.datasource.tomcat.default-transaction-isolation=2
#dbcp2資料庫連線池預設隔離級別
#spring.datasource.dbcp2.default-transaction-isolation=2
4. 傳播行為
傳播行為是方法間呼叫事務採取的策略問題。如在處理批量檔案時,大部分成功,小部分失敗,我們只希望那小部分失敗的回滾。
Spring在Propagation原始碼中定義了7種傳播行為:
public enum Propagation {
/**
* 需要事務,預設傳播行為,如果當前存在事務,就沿用當前事務,
* 否則新建一個事務執行子方法
*/
REQUIRED(0),
/**
* 支援事務,如果當前存在事務,就沿用當前事務,
* 否則繼續採用無事務方式執行子方法
*/
SUPPORTS(1),
/**
* 必須使用事務,如果當前存在事務,就沿用當前事務,
* 如果當前沒有事務,則會丟擲異常
*/
MANDATORY(2),
/**
* 無論當前事務是否存在,都會建立新事務執行方法,
* 這樣新事務就可以擁有新的鎖和隔離級別等特性,與當前事務相互獨立
*/
REQUIRES_NEW(3),
/**
* 不支援事務,當前存在事務時,將掛起事務,執行方法
*/
NOT_SUPPORTED(4),
/**
* 不支援事務,如果當前存在事務時,則丟擲異常,否則繼續使用無事務機制執行
*/
NEVER(5),
/**
* 在當前方法呼叫子方法時,如果子方法發生異常
* 只回滾子方法執行過的sql,而不回滾當前方法的事務
*/
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
其中,REQUIRED
、REQUIRES_NEW
、NESTED
三種傳播行為最常用。
新增傳播行為方法:
@Service
@Transactional(propagation = Propagation.REQUIRED)
public class UserService{
對於NESTED
而言,並不是所有資料庫支援儲存點技術,因此Spring的內部規則是:如果資料庫支援儲存點技術,就啟用儲存點技術;反之則新建一個任務去執行子方法,相當於REQUIRES_NEW
。
NESTED
與REQUIRES_NEW
的區別是:前者會沿用當前事務的隔離級別和鎖等特性,後者擁有自己的隔離級別和鎖等特性。
5. @Transactional自呼叫失效問題
一個類自身方法之間的呼叫,每次呼叫不能產生新的事務。
失效原因:
AOP原理是動態代理,而自呼叫是類自身的呼叫,不是代理物件去呼叫,就不會產生AOP,開發者程式碼無法織入到約定流程中去。
自呼叫失效問題解決:
- 用一個service呼叫另一個service;
- 從Spring IoC容器中使用applicationContext.getBean方法獲取代理物件。