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