多線程下的事務(wù)失效及解決方案
使用 DataSourceTransactionManager 是在 Spring 框架中進行數(shù)據(jù)庫事務(wù)管理的一種常見方式。
它提供了一種簡便的方式來處理事務(wù),并能夠有效地解決多線程環(huán)境下的事務(wù)隔離和一致性問題。
單核cpu的多線程模型圖如下所示:

從上述模型可以知道,線程1、線程2會因為cpu的來回切換,導(dǎo)致當(dāng)前事務(wù)的上下文不同,因此處于失效狀態(tài)。
1、定義
多線程和并發(fā)是計算機科學(xué)中與執(zhí)行多個任務(wù)相關(guān)的兩個重要概念。
盡管它們在某些方面密切相關(guān),但它們代表了不同的概念,下面是它們之間的主要區(qū)別和聯(lián)系。
1.1、多線程
多線程是指在一個進程中同時運行多個線程。每個線程有自己的執(zhí)行路徑,可以在共享的內(nèi)存空間中執(zhí)行,也可以共享進程的資源。
如下圖所示:

多線程的一個主要目標(biāo)是利用多核 CPU 的能力,以提高程序的并行性和響應(yīng)速度。
1.2、并發(fā)
并發(fā)是指兩個或多個任務(wù)或進程在同一時間段內(nèi)進行處理,通常是通過多個線程、進程或任務(wù)的交替執(zhí)行來實現(xiàn)。
如下圖所示:

并發(fā)不一定指真正的同時運行,它可能是通過快速切換來模擬同時運行的效果。并發(fā)可以在單線程或多線程環(huán)境中實現(xiàn),只要在時間上有重疊即可。
2、多線程事務(wù)
多線程事務(wù)和單線程事務(wù)是兩種對事務(wù)處理的不同實現(xiàn)方式,特別是在涉及數(shù)據(jù)庫和并發(fā)操作的場景中。
2.1、定義
單線程事務(wù):
- 在單線程環(huán)境中,事務(wù)在一個線程中完成,沒有其他線程的干擾。
- 所有的操作順序執(zhí)行,事務(wù)的開始、提交和回滾都在同一個線程中處理。
多線程事務(wù):
- 在多線程環(huán)境中,多個線程可以訪問和操作同一事務(wù)。可能會有多個線程并發(fā)執(zhí)行的事務(wù),或同一事務(wù)在不同線程中交替進行。
- 這種方式更復(fù)雜,因為需要考慮多個線程對共享數(shù)據(jù)的競爭、隔離和一致性。
2.2、主要區(qū)別
并發(fā)性:
- 單線程事務(wù):操作是線性的,只有一個線程在執(zhí)行事務(wù),與其他事務(wù)完全隔離。通常更容易實現(xiàn)和維護。
- 多線程事務(wù):事務(wù)的并發(fā)處理可能導(dǎo)致數(shù)據(jù)競爭,多個線程可能同時讀取和寫入相同的數(shù)據(jù),導(dǎo)致數(shù)據(jù)的不一致性。
數(shù)據(jù)一致性:
- 單線程事務(wù):由于沒有其他線程參與,數(shù)據(jù)一致性容易維護。只要在事務(wù)中完成的操作是原子性的,就能保證一致性。
- 多線程事務(wù):需依賴事務(wù)隔離級別來保證一致性,例如讀已提交、可重復(fù)讀,以及串行化等。必須采用鎖或其他機制來處理并發(fā)沖突。
鎖的使用:
- 單線程事務(wù):通常不會涉及復(fù)雜的鎖管理。
- 多線程事務(wù):需要使用鎖或其他并發(fā)控制機制(如樂觀鎖、悲觀鎖)來處理數(shù)據(jù)競爭,提高系統(tǒng)的并發(fā)性,同時減少沖突。
性能:
- 單線程事務(wù):性能簡單易測,因為沒有其他操作對于事務(wù)的直接影響。
- 多線程事務(wù):性能會受到并發(fā)訪問的影響,需要考慮鎖的爭用和等待時間,以及如何平衡事務(wù)的執(zhí)行效率。
2.3、聯(lián)系
事務(wù)的性質(zhì):
- 無論是單線程還是多線程,事務(wù)均有原子性、一致性、隔離性和持久性(ACID)屬性。
- 這些屬性是保證數(shù)據(jù)正確性和完整性的重要原則。
使用的技術(shù):
- 兩者都可以應(yīng)用于數(shù)據(jù)庫系統(tǒng)中,無論是在單用戶環(huán)境還是多用戶環(huán)境,確保數(shù)據(jù)的完整性和一致性。
設(shè)計考慮:
- 設(shè)計事務(wù)時,單線程和多線程的事務(wù)處理都需要考慮數(shù)據(jù)的一致性和隔離性。
- 雖然實現(xiàn)細(xì)節(jié)不同,但在設(shè)計原則上是相通的。
2.4、DataSourceTransactionManager
實現(xiàn)了 PlatformTransactionManager 接口,主要用于基于 JDBC 的數(shù)據(jù)源事務(wù)管理。
通過它可以控制事務(wù)的邊界,例如開始事務(wù)、提交事務(wù)和回滾事務(wù)。這種方式非常適合于多線程環(huán)境下的數(shù)據(jù)庫操作。
2.5、TransactionSynchronizationManager
是 Spring 框架的一部分,專門用于管理與事務(wù)相關(guān)的同步任務(wù)。
它通常用于將事務(wù)資源與當(dāng)前線程綁定,使得在當(dāng)前線程中的所有操作都可以共享相同的事務(wù)上下文。
在使用時,TransactionSynchronizationManager 經(jīng)常與數(shù)據(jù)源管理器(如 DataSourceTransactionManager)結(jié)合使用。使用 TransactionSynchronizationManager 綁定當(dāng)前線程
TransactionSynchronizationManager 的主要用途是確保在當(dāng)前線程中執(zhí)行的所有數(shù)據(jù)庫操作都可以利用同一個事務(wù)。利用它可以將事務(wù)上下文與當(dāng)前線程捆綁在一起。
代碼如下圖所示:
public void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map<Object, Object> map = this.transactionContext.getResources();
Object oldValue = map.put(actualKey, value);
if (oldValue != null) {
throw new IllegalStateException("Already value [" + oldValue + "] for key [" + actualKey + "] bound to context [" + this.transactionContext.getName() + "]");
} else {
if (logger.isTraceEnabled()) {
logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to context [" + this.transactionContext.getName() + "]");
}
}
}3、事務(wù)處理
事務(wù)處理分為編程式和聲明式兩種。
3.1、底層原理
當(dāng)目標(biāo)類被spring管理時(即@Component,@Service,@controller,@Repository注解時),spring會為目標(biāo)對象創(chuàng)建一個代理對象。代理對象負(fù)責(zé)攔截目標(biāo)方法的調(diào)用,并在必要時應(yīng)用事務(wù)管理(AOP思想)。
代理對象內(nèi)部包含一個事務(wù)攔截器TransactionInterceptor,負(fù)責(zé)處理事務(wù)相關(guān)的邏輯。
事務(wù)攔截器會檢查方法上是否添加了@Transactional注解,來決定是否應(yīng)用事務(wù)(JDBC的connection調(diào)用setAutoCommit(false),然后會將connection存到ThreadLocal(本地線程)中,
(為了嵌套方法可以拿到外層事務(wù)的connection對象,只有使用同一個connection對象才能保證使用同一個事物)
然后去執(zhí)行方法,從而執(zhí)行數(shù)據(jù)庫操作,然后再執(zhí)行嵌套方法,然后再嵌套方法的代理對象,就可以拿到外層事務(wù)的Thread Local中connection對象,從而執(zhí)行方法,嵌套方法發(fā)現(xiàn)ThreadLocal中有值,他不會提交事務(wù),而是統(tǒng)一的交給外層事務(wù)進行提交)
事務(wù)攔截器在目標(biāo)方法執(zhí)行前后應(yīng)用事務(wù)通知,(前置通知和后置通知)在方法執(zhí)行前,事務(wù)攔截器啟動事務(wù);
在方法執(zhí)行后,根據(jù)方法的執(zhí)行結(jié)果決定事務(wù)的提交或回滾。
關(guān)于更多詳細(xì)了解,可參考:如何合理使用Spring的事務(wù)
如下圖所示:

3.2、失效原因
如果在方法中開啟新線程去調(diào)用嵌套方法時,這時嵌套線程就拿不到外層事務(wù)的ThreadLocal中的connection對象。
關(guān)于ThreadLocal是綁定在主線程上的。
所以在新的線程中去調(diào)用嵌套方法時,就拿不到外層事務(wù)的connection對象,然后就會自己新建一個事務(wù),自己存一個connection到自己的ThreadLocal中,
這樣的話,方法和嵌套方法就各開啟了一個事務(wù),且是同級事務(wù),相互不影響。
但是這樣在外層事務(wù)失敗回滾的時候,內(nèi)層事務(wù)不受外層事務(wù)的影響,不進行回滾的話,就會造成事務(wù)回滾但還是寫入數(shù)據(jù)庫中的現(xiàn)象,我們要保證數(shù)據(jù)庫數(shù)據(jù)的一致性!
3.3、解決方案
1、使用TransactionSynchronizationManager
在創(chuàng)建一個異步線程之后,可以手動的往嵌套方法的Tread Local中將外層事務(wù)connection對象存入,那么嵌套方法就可以從自己的Thread Local中拿到外層事務(wù)的connection了,這樣方法和嵌套方法用的就是同一個事務(wù)了。
這樣就不會出現(xiàn)多線程事務(wù)失效的情況了。
擴展:
編程式事務(wù)中DataSourceTransactionManager類org.springframework.jdbc.datasource.DataSourceTransactionManager中的doBegin方法就是spring底層在開啟事務(wù)的時候進行調(diào)用的,在方法中獲取數(shù)據(jù)庫連接,并開啟事務(wù)。


因此如果想要獲取對應(yīng)的connection,可以調(diào)用事務(wù)同步管理器的getResource()方法,
將spring容器當(dāng)中的datasource傳進來,就可以拿到,
然后再調(diào)用BindResource方法就可以將異步線程中的ThreadLocal中存入外層事務(wù)的connection。
解決方案代碼展示:
private DataSource datasource;
ConnectionHolder connectionHolder = (ConnectionHolder)TransactionSynchronizedManager.getResource(dataSource);
new Thread(()->{
//綁定主線程的connection到子線程中
TransactionSynchronizedManager.bindResource(dataSource,connectionHolder);
//調(diào)用方法
})2、join() 方法
核心代碼示例:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class TransferExample {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = context.getBean(TransferService.class);
// 使用線程數(shù)組來存儲線程
Thread[] threads = new Thread[3];
// 創(chuàng)建多個線程來執(zhí)行轉(zhuǎn)賬
for (int i = 0; i < threads.length; i++) {
String fromAccount = "accountA"; // 設(shè)定轉(zhuǎn)賬來源賬戶
String toAccount = "accountB"; // 設(shè)定轉(zhuǎn)賬目標(biāo)賬戶
double amount = 100.0 * (i + 1); // 每個線程按順序轉(zhuǎn)賬不同金額
threads[i] = new Thread(() -> {
try {
transferService.transfer(fromAccount, toAccount, amount);
System.out.println("Transfer of " + amount + " from " + fromAccount + " to " + toAccount + " completed.");
} catch (Exception e) {
System.out.println("Transfer failed: " + e.getMessage());
}
});
threads[i].start(); // 啟動線程
}
// 等待所有線程完成
for (Thread thread : threads) {
try {
thread.join(); // 等待每個子線程完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}
context.close();
}
}通過適當(dāng)?shù)氖聞?wù)管理和使用 join() 方法等待子線程,然后將所有操作放在各自的事務(wù)中,這樣能夠確保數(shù)據(jù)的一致性和完整性。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
MyBatis核心概念?+?環(huán)境搭建?+?CRUD實戰(zhàn)詳解
文章主要介紹了MyBatis的核心知識,從JDBC痛點開始,闡述了MyBatis的優(yōu)勢,包括連接池管理、SQL與Java代碼解耦、自動結(jié)果集映射、參數(shù)自動映射等,感興趣的朋友跟隨小編一起看看吧2026-05-05
Java面試題沖刺第十三天--數(shù)據(jù)庫(3)
這篇文章主要為大家分享了最有價值的三道數(shù)據(jù)庫面試題,涵蓋內(nèi)容全面,包括數(shù)據(jù)結(jié)構(gòu)和算法相關(guān)的題目、經(jīng)典面試編程題等,感興趣的小伙伴們可以參考一下2021-07-07
Spring源碼解析之BeanPostProcessor知識總結(jié)
今天給大家?guī)淼奈恼率荢pring的相關(guān)知識,文章圍繞著BeanPostProcessor的使用展開,文中有非常詳細(xì)的介紹,需要的朋友可以參考下2021-06-06
Spring Security的持久化用戶和授權(quán)實現(xiàn)方式
文章介紹了如何使用JdbcUserDetailsManager實現(xiàn)數(shù)據(jù)庫讀取用戶,并展示了如何配置SpringSecurity進行授權(quán)管理,通過創(chuàng)建數(shù)據(jù)庫表、配置數(shù)據(jù)庫連接和修改SecurityConfig,實現(xiàn)了用戶權(quán)限的控制2025-02-02

