我們一起聊聊 Spring 是如何管理事務的,你學會了嗎?
本篇我們事務中的基本概念開始,討論下在 Spring 框架中是如何實現事務管理的,并重點講解基于 @Transactional 注解的聲明式事務的實際應用。本文僅涉及概念性的知識,原理解析將會另起一篇單獨進行介紹。
事務的基礎知識
事務(Transaction)是數據庫管理系統中一組邏輯操作的集合,這些操作作為一個整體被對待,不可拆分,以保證數據的一致性和完整性。事務具有四個關鍵屬性,通常被稱為 ACID 特性。
ACID 特性
ACID 指的是事務的四個基本特性:
- 原子性(Atomicity):一個事務中的所有操作要么全部完成,要么完全不執行。如果事務的一部分失敗,則整個事務都會被回滾。例如,在銀行轉賬的場景中,從賬戶 A 轉賬到賬戶 B 需要兩個步驟:減少 A 的余額和增加 B 的余額。這兩個步驟必須作為一個整體成功或失敗,否則可能會導致資金丟失或重復。
- 一致性(Consistency):事務必須保證數據庫從一個一致狀態轉換到另一個一致狀態,即事務執行前后數據完整性約束沒有被破壞。還是以銀行轉賬為例,一致性確保在轉賬前后,兩個賬戶的總金額保持不變,即使發生故障也不會影響這一原則。
- 隔離性(Isolation):并發執行的多個事務之間不會互相干擾。每個事務都應該獨立地運行,就好像它是系統中唯一存在的事務一樣。假設同時有兩個轉賬請求,一個是 A 向 B 轉賬,另一個是 C 向 D 轉賬。這兩個事務應該互不影響,它們的結果應該是各自獨立且正確的。
- 持久性(Durability):一旦事務提交,它對數據庫所做的更改就是永久性的,即使系統發生故障也不會丟失這些更改。比如,當銀行轉賬完成后,即使服務器突然斷電,轉賬記錄也應當保存下來,確保用戶的資金變動信息不會丟失。
其他特性
除了 ACID 特性之外,還有以下幾個重要的事務屬性:
- 事務回滾(Rollback):當事務遇到錯誤或異常時,可以撤銷所有已經執行的操作,使數據庫恢復到事務開始前的狀態。這是保證事務原子性和一致性的關鍵手段。例如,在一個復雜的業務流程中,如果其中一步驟失敗,整個事務將被回滾,確保之前的所有變更都取消,從而維持系統的穩定狀態。
- 事務超時(Timeout):為事務設定一個最大允許執行時間,超過這個時間則自動終止事務,以防止長時間占用資源。這對于避免死鎖和提高系統響應速度非常重要。例如,在高并發環境中,某些長時間運行的事務可能導致資源鎖定,影響其他事務的正常進行。通過設置合理的超時值,可以及時釋放資源,保證系統的流暢運行。
- 只讀事務(Read-only Transactions):某些場景下,事務只需要讀取數據而不需要修改,這時可以聲明事務為只讀模式以優化性能。只讀事務告訴數據庫引擎當前事務不會修改任何數據,因此它可以采用更高效的查詢策略,如跳過某些類型的鎖檢查。這不僅提高了查詢的速度,還減少了對共享資源的競爭壓力。
并發事務中存在的問題
當多個事務同時訪問同一份數據時,可能會出現以下幾種問題:
- 臟讀(Dirty Read):一個事務能夠讀取另一個未提交事務的數據。例如,T1 修改了一行數據但尚未提交,此時 T2 讀取到了這行未提交的數據;如果 T1 回滾,那么 T2 讀取到的數據就是無效的。
- 不可重復讀(Non-repeatable Read):在同一個事務中,兩次讀取同一行數據返回不同的結果,因為在這兩次讀之間,另一個事務對該行進行了修改并提交。比如,在 T1 中第一次讀取某行后,T2 修改了該行并提交,然后 T1 再次讀取同一行時發現數據已改變。
- 幻讀(Phantom Read):在一個事務中,兩次相同查詢的結果集不同,這通常是因為在兩次查詢之間有其他事務插入或刪除了滿足條件的行。例如,T1 查詢所有滿足條件 A 的記錄,之后 T2 插入了一條新的符合條件 A 的記錄并提交,再之后 T1 再次查詢相同條件 A 的記錄時會多出一條新記錄。
事務隔離級別
為了解決并發事務所引發的問題,在數據庫中引入了事務隔離級別。主要有以下幾種:
- 讀未提交(Read Uncommitted):最低的隔離級別,它允許一個事務讀取另一個事務尚未提交的數據,存在臟讀、不可重復讀、幻讀的風險。這個級別提供了最高的并發性和性能,但由于缺乏安全性,在實際應用中很少使用。
- 讀已提交(Read Committed):在這種隔離級別下,一個事務只能讀取到已經提交的數據,從而避免了臟讀現象。然而,仍然可能發生不可重復讀和幻讀,因為在兩次讀之間可能有其他事務提交了更新或插入了新行。
- 可重復讀(Repeatable Read):此級別的隔離確保在同一事務中多次讀取相同的數據將得到一致的結果,避免了不可重復讀。不過,仍然有可能出現幻讀問題,因為新的行可以在兩次讀之間被插入或刪除。
- 串行化(Serializable):最高級別的隔離,通過強制執行嚴格的鎖機制來避免任何并發問題。在這個級別上,事務按照順序執行,如同它們是在單線程環境中一樣,這樣就徹底避免了臟讀、不可重復讀和幻讀的可能性。然而,這也意味著更高的鎖定開銷,性能較低,一般很少使用。
下表可以更直觀的展示不同事務隔離級別所解決的問題:
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
讀未提交(Read Uncommitted) | √ | √ | √ |
讀已提交(Read Committed) | × | √ | √ |
可重復讀(Repeatable Read) | × | × | √ |
串行化(Serializable) | × | × | × |
Spring 管理事務的方式
Spring 管理事務有兩種方式:編程式事務管理和聲明式事務管理。
編程式事務管理
編程式事務管理允許我們在開發時,可以直接手動的控制事務的生命周期,包括開始、提交和回滾等操作。Spring 提供了兩種主要的方式來進行編程式事務管理:
- TransactionTemplate:是一個模板方法的實現類,簡化了編程式事務管理的復雜度。TransactionTemplate 中提供了一個 execute() 方法,用于包裝需要在事務上下文中執行的操作。使用這種方式的優點在于代碼更加簡潔,并且可以通過回調接口輕松處理異常情況。
// 使用 TransactionTemplate 進行編程式事務管理
@Autowired
private TransactionTemplate transactionTemplate;
public void transferFunds(Account fromAccount, Account toAccount, BigDecimal amount) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
// 執行轉賬邏輯
fromAccount.withdraw(amount);
toAccount.deposit(amount);
} catch (Exception e) {
// 如果拋出異常,事務將自動回滾
throw new RuntimeException("Transfer failed", e);
}
}
});
}
- PlatformTransactionManager:對于更復雜的業務場景,可以直接使用更底層的 PlatformTransactionManager 接口提供的 API 來手動管理事務。這種方式的靈活性更高,粒度更精細,但也入侵了業務代碼,增加了代碼的復雜性。PlatformTransactionManager 包含了 getTransaction()、commit() 和 rollback() 等方法,分別用來獲取事務狀態、提交事務和回滾事務。
// 使用 PlatformTransactionManager 進行編程式事務管理
@Autowired
private PlatformTransactionManager transactionManager;
public void transferFunds() {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 執行復雜的業務邏輯
// ...
transactionManager.commit(status);
} catch (RuntimeException e) {
transactionManager.rollback(status);
throw e;
}
}
對于上述兩種編程式事務管理方案,Spring 官方更推薦使用 TransactionTemplate,因為它封裝了大部分底層細節,使得代碼更加清晰易懂。但是,對于那些需要細粒度控制事務行為的業務場景,就需要使用 PlatformTransactionManager 了。
聲明式事務管理
聲明式事務管理利用面向切面編程(AOP)來自動管理事務邊界。這種方式的主要優點在于減少了樣板代碼的數量,提高了開發效率。Spring 的聲明式事務可以通過 XML 配置文件或 @Transactional 注解來進行配置。
- XML 配置:早期版本的 Spring 主要依賴于 XML 文件來定義事務規則。這種方式需要維護大量額外的配置文件,增加了項目的復雜性。
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceMethods" expression="execution(* com.example.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
</aop:config>
- @Transactional 注解:使用 @Transactional 注解來定義事務邊界,不僅簡化了配置,而且不會污染業務代碼,使用起來更加方便,便于理解和維護。
// 還是以上述轉賬邏輯為例
@Service
public class AccountService {
@Transactional
public void transferFunds(Account fromAccount, Account toAccount, BigDecimal amount) {
// 執行轉賬邏輯
fromAccount.withdraw(amount);
toAccount.deposit(amount);
}
}
通過上述對 Spring 中編程式事務與聲明式事務的對比可以看出,基于 @Transactional 注解的聲明式事務明顯更具優勢,而且 Spring 官方也是倡導這種非侵入式的開發方式。以下是 Spring 官方對如何選擇這兩種事務的建議:
圖片
翻譯過來大體意思是:如果應用中的事務操作很少,編程式事務管理如使用 TransactionTemplate 可以提供更直接的控制和靈活性;如果具有多個事務操作,聲明式事務管理更為合適,因為它配置簡單,可以將事務管理邏輯從業務代碼中分離出來,保持代碼清晰。聲明式事務管理因其簡潔性和低侵入性而在多數情況下是更佳的選擇。
話說回來,事務多的場景下都可以使用聲明式事務管理,少的時候也用沒什么問題吧~
@Transactional 注解介紹
@Transactional 注解是 Spring 框架提供的一個用于管理事務的注解,這個注解允許我們以聲明的方式定義事務邊界,簡化事務管理的過程,它是利用 AOP 實現的。@Transactional 注解包含很多屬性,我們通過合理配置這些屬性,就可以在開發時精確控制事務的行為,確保應用程序的一致性和可靠性。
首先,我們通過源碼來看下這個注解的屬性。
@Transactional 注解源碼
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
/**
* value 和 transactionManager 是等價的,用于指定要使用的事務管理器的名稱。
* 在多數據源或多事務管理器的應用場景中,可以通過這兩個屬性明確指出具體使用哪個
* 事務管理器來管理當前事務。
*/
@AliasFor("transactionManager")
String value() default "";
// 與 value 一個意思
@AliasFor("value")
String transactionManager() default "";
// 暫未使用
String[] label() default {};
// 該屬性定義了事務的傳播機制,默認值是 Propagation.REQUIRED
Propagation propagation() default Propagation.REQUIRED;
// 指定事務的隔離級別,DEFAULT 是默認使用底層數據庫的默認隔離級別
Isolation isolation() default Isolation.DEFAULT;
/**
* 指定事務超時時間,事務必須完成的最大秒數,如果事務在規定時間內未能完成,將會自動回滾
* 默認值 -1,沒有超市限制
*/
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
// 以字符串的形式指定超時時間
String timeoutString() default "";
// 標記當前事務是否為只讀事務
boolean readOnly() default false;
/**
* 列出哪些異常類型可以導致事務回滾(列出的類型及其子類都會導致回滾)
* 運行時異常(RuntimeException 及其子類)和 Error 會導致回滾,而檢查異常不會,
* 如需要回滾,可通過該屬性指定。比如:rollbackFor = Exception.class
* 檢查異常與非檢查異常還分不太清的同學,可以去看下下邊這篇文章:
* https://mp.weixin.qq.com/s/JMVmrhaFA0EXetmsohUt1Q?token=1081902717&lang=zh_CN
*/
Class<? extends Throwable>[] rollbackFor() default {};
// 與 rollbackFor 類似,是通過指定類名字符串的方式指定回滾異常類
String[] rollbackForClassName() default {};
// 指定哪些異常類型不應該觸發事務回滾
Class<? extends Throwable>[] noRollbackFor() default {};
// 以指定類名的方式指定哪些異常類型不應該觸發事務回滾
String[] noRollbackForClassName() default {};
}
事務傳播行為
事務的傳播行為決定了當方法被調用時,如何處理現有的事務上下文或創建新的事務。Spring 中定義了事務的七種傳播行為:
public enum Propagation {
/**
* 默認的傳播行為,如果當前存在事務,則加入該事務;如果不存在,則創建一個新的事務。
* 能夠確保所有相關操作都在同一個事務上下文中進行。
*/
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
/**
* 如果當前存在事務,則加入該事務;如果沒有,則以非事務方式執行。
* 適用于那些在有無事務環境中都可以的方法,比如說在讀取數據時,通常不需要事務支持。
*/
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
/**
* 如果當前存在事務,則加入該事務;如果不存在,則拋出異常。很少使用
* 它要求必須在一個已經存在的事務上下文中執行,否則將拋出異常。
*/
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
/**
* 總是創建一個新的事務,即使當前已經存在事務也會將其掛起。
* 通常用于需要獨立于外部事務執行的操作,例如發送電子消息或記錄日志等非關鍵業務,
* 防止它們的失敗影響主事務的狀態。
*/
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
/**
* 以非事務方式執行,并且如果當前存在事務,則暫停當前事務。
* 用于那些明確不需要事務支持的任務,比如文件上傳下載等操作,即使是在事務上下文中被調用,
* 它也會暫時停止現有的事務,直到完成自己的任務后再恢復原來的事務狀態。
*/
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
/**
* 以非事務方式執行,如果當前存在事務,則拋出異常。
* 與 MANDATORY 相反,NEVER 確保方法永遠不會在一個事務上下文中執行
*/
NEVER(TransactionDefinition.PROPAGATION_NEVER),
/**
* 如果當前存在事務,則在嵌套事務內執行;如果沒有,則創建一個新的事務。
* 嵌套事務允許內部事務獨立于外部事務進行提交或回滾,但仍然共享相同的資源鎖定。
*/
NESTED(TransactionDefinition.PROPAGATION_NESTED);
}
事務隔離級別
SQL 標準定義了四種不同的隔離級別來控制一個事務對另一個事務可見的數據范圍(見第一章節事務基礎知識中)。Spring 中也定義了與 SQL 標準相對應的隔離級別如下:
public enum Isolation {
// 使用數據庫默認的隔離級別
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
// 以下 4 種與 SQL 標準相對應
// 允許臟讀、不可重復讀和幻讀
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
// 防止臟讀,但允許不可重復讀和幻讀
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
// 防止臟讀和不可重復讀,但允許幻讀
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
// 完全防止臟讀、不可重復讀和幻讀,提供最高級別的隔離,但可能導致較低的并發性能
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
}
Spring 為什么要自己提供一套隔離級別?
首先,不同的數據庫管理系統(DBMS)可能支持不同的隔離級別名稱和行為。例如,MySQL 的 InnoDB 存儲引擎默認使用的是 REPEATABLE_READ,而 Oracle 和 SQL Server 默認采用的是 READ_COMMITTED。為了給使用者提供一個統一的接口,Spring 定義了一套標準的隔離級別枚舉值:DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ 和 SERIALIZABLE。這樣做可以讓使用者不必關心底層數據庫的具體實現,只需按照通用的標準來設定隔離級別即可。
@Transactional 注解的用法
還是先看下 Spring 官方文檔的介紹:
You can apply the @Transactional annotation to an interface definition, a method on an interface, a class definition, or a public method on a class.
The Spring team recommends that you annotate only concrete classes (and methods of concrete classes) with the @Transactional annotation, as opposed to annotating interfaces. You certainly can place the @Transactional annotation on an interface (or an interface method), but this works only as you would expect it to if you use interface-based proxies. The fact that Java annotations are not inherited from interfaces means that, if you use class-based proxies (proxy-target-class="true") or the weaving-based aspect (mode="aspectj"), the transaction settings are not recognized by the proxying and weaving infrastructure, and the object is not wrapped in a transactional proxy.
文檔中介紹:@Transactional 注解可以應用于接口、接口方法、具體類或其公共方法上。且 Spring 團隊不推薦將 @Transactional 作用于接口上,因為 Java 注解不會從接口繼承,在使用基于類的代理(cglib 代理)或 AspectJ 時,接口上的事務設置將不被識別,也就會失效。所以開發中常用的方式是將其標記在類或者類方法上:
- 作用于類時:
- 默認應用到所有公共方法:當 @Transactional 應用于一個具體類時,它將應用于該類中的所有公共方法。該類每個被調用的公共方法都將根據注解中指定的事務屬性(如傳播行為、隔離級別等)運行在一個事務上下文中。
- 可以被方法級別的注解覆蓋:如果類中某個方法也有自己的 @Transactional 注解,則該方法級別的配置會覆蓋類級別的配置。
- 作用于方法時:
- 精確控制單個方法的事務行為:將 @Transactional 直接應用于方法上可以更精細地控制每個方法的事務特性。這樣可以在同一個類中為不同方法設定不同的事務規則,例如不同的傳播行為、隔離級別或超時時間等。
- 避免不必要的事務開銷:只有那些真正需要事務管理的方法才應該標記上 @Transactional。如果整個類都被標記了,但并非該類的所有方法都需要事務支持,那么可能會引入不必要的性能開銷。
注意,在 Spring Boot 項目中,由于自動裝配的支持,直接使用 @Transactional 注解即可啟用事務管理。相比之下,傳統 Spring 項目需要顯式配置:在 applicationContext.xml 中使用 <tx:annotation-driven/> 或在 Java 配置類上添加 @EnableTransactionManagement 注解來開啟事務支持。
事務失效場景
某些情況下,雖然加上了 @Transactional 注解,但是事務仍然可能不會按照預期工作,導致數據不一致等問題,這里列舉一下幾種開發中常見的場景,如果遇到事務失效問題,按以下幾種情況排查基本可解決問題:
- 訪問權限問題:Spring 的代理機制只能攔截 public 方法的調用。對于非 public 方法,代理無法對其進行增強,因此事務管理器不能介入這些方法的執行過程。
@Service
public class MyService {
@Transactional
private void updateData() {
// 事務不會生效
}
}
- 方法自調用問題:在一個類內部一個非事務方法調用了事務方法,此時事務不會按預期生效。因為事務是通過 AOP 實現的,由于 Spring AOP 的代理機制,默認情況下只有外部通過代理對象調用的方法才會被攔截并應用事務管理,而內部方法的調用是通過this 來調用的,this 指向的是代理的目標對象,也就是原始對象,不會經過代理,因此事務不會生效。
@Service
public class MyService {
@Transactional
public void methodA() {
// 正常的事務管理
}
public void methodB() {
methodA(); // 類內部自調用,事務不會生效
}
}
- 吞異常:Spring 默認只在遇到運行時異常(RuntimeException)或錯誤(Error)時回滾事務。如果異常被捕獲而不拋出,或者拋出了非運行時異常而沒有在注解中指定,事務將正常提交。所以確保異常能夠傳播到方法外,或者顯式配置 rollbackFor 屬性以響應特定類型的檢查型異常。
@Service
public class MyService {
@Transactional
public void processData() {
try {
// 數據庫操作,發生異常
} catch (Exception e) {
// 異常被捕獲但未處理或重新拋出
log.error("An error occurred", e);
}
}
@Transactional(rollbackFor = Exception.class)
public void processData() {
}
}
- Bean 未被 Spring 管理:如果一個類沒有被 Spring 容器管理,那么即使該類上的方法使用了 @Transactional 注解,事務也不會生效。@Transactional 的事務管理依賴于 Spring 的 AOP 代理機制,只有由 Spring 容器創建和管理的對象才能正確應用這些代理。
public class MyService {
@Transactional
public void processData() {
// 數據庫操作
}
}
// 沒有通過 Spring 容器獲取 bean 對象
MyService service = new MyService();
service.processData(); // 事務不會生效
- 異步方法:Spring 的事務管理基于當前線程的事務上下文進行的,而事務上下文是存儲在 TransactionSynchronizationManager 類中的線程局部變量(ThreadLocal)中的,因此當一個方法上同時標記了 @Async 和 @Transactional 注解時,事務管理可能不會按預期工作,因為實際的業務邏輯在新線程中執行,而事務上下文不能夠正確地傳播到新線程中。所以,應盡量避免這兩個注解同時標記在同一個方法上,可以將事務操作單獨抽取。
@Service
public class MyService {
@Async
@Transactional
public void asyncMethod() {
// 數據庫操作
// 事務可能不會按預期生效
}
}
本文主要討論了 Spring 編程式和聲明式兩種管理事務的方式以及 @Transactional 注解的使用和常見問題分析,下篇我將會從源碼的角度分析 @Transactional 注解的解析(代理生成)以及事務的執行過程。