@Transactional中使用線程鎖導(dǎo)致了鎖失效,震驚我一整年!
今天給大家分享一個線上系統(tǒng)里發(fā)現(xiàn)的生產(chǎn)實踐案例,就是平時大家應(yīng)該都會用@Transactional注解去實現(xiàn)事務(wù)是不是?因為這個注解底層說白了很簡單,就是會去代理你這個方法的執(zhí)行,一旦代理了你的方法執(zhí)行,其實就可以在方法執(zhí)行前開一個事務(wù),方法執(zhí)行完以后如果成功就提交事務(wù),有異常就回滾事務(wù)。
這樣就可以讓你這個方法里所有的數(shù)據(jù)庫操作匯集到一個事務(wù)里去了,這個相信大家其實都是懂的,平時 開發(fā)也都是這么做的。
那大家有沒有想過,要是我們在這個事務(wù)注解里用了多線程并發(fā)加鎖的代碼,可能會導(dǎo)致這個鎖失效,也就是沒法實現(xiàn)多線程在加鎖代碼里串行加鎖執(zhí)行?這個簡直是一個巨坑,妥妥的線上生產(chǎn)事故案例,下面我們就開始分下這個案例。
一、@Transactional與線程鎖的基本使用
首先,我們簡要回顧一下@Transactional和線程鎖的基本用法。
1. @Transactional注解
@Transactional注解可以應(yīng)用于接口定義、接口中的方法、類定義或類中的public方法上。其主要作用是聲明一個方法需要在事務(wù)環(huán)境中執(zhí)行。Spring框架會在運行時通過AOP(面向切面編程)代理機制,自動管理事務(wù)的開啟、提交和回滾。
@Service
public class SomeService {
@Transactional
public void someTransactionalMethod() {
// 業(yè)務(wù)邏輯
}
}2. 線程鎖(如ReentrantLock)
線程鎖用于控制多個線程對共享資源的并發(fā)訪問,防止數(shù)據(jù)不一致的問題。ReentrantLock是Java并發(fā)包java.util.concurrent.locks中的一個類,它提供了比synchronized關(guān)鍵字更靈活的鎖定操作。
public class SomeClass {
private final Lock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {
// 業(yè)務(wù)邏輯
} finally {
lock.unlock();
}
}
}二、@Transactional中使用線程鎖導(dǎo)致的問題
在@Transactional注解的方法內(nèi)部使用線程鎖時,可能會遇到鎖失效的問題。這是因為@Transactional通過AOP在目標方法執(zhí)行前后進行事務(wù)的開啟和提交,而線程鎖則直接作用于方法內(nèi)部的代碼塊。這種機制上的差異導(dǎo)致了事務(wù)和鎖的管理在時間上不一致,進而引發(fā)鎖失效。
示例場景
假設(shè)我們有一個服務(wù)類,其中有一個方法需要在事務(wù)環(huán)境中更新數(shù)據(jù)庫記錄,并在這個過程中使用線程鎖控制并發(fā)訪問。
@Service
public class UpdateService {
private final Lock lock = new ReentrantLock();
@Transactional
public void updateData() {
lock.lock();
try {
// 模擬數(shù)據(jù)庫更新操作
System.out.println("Updating data...");
// 假設(shè)這里有一些耗時的數(shù)據(jù)庫操作
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}在這個例子中,雖然我們在方法內(nèi)部使用了ReentrantLock來加鎖,但鎖的釋放是在事務(wù)提交之前完成的。如果在鎖釋放后、事務(wù)提交前,有其他線程進入并嘗試更新相同的數(shù)據(jù),就可能讀取到未提交的數(shù)據(jù),從而導(dǎo)致數(shù)據(jù)不一致。
因為一旦把事務(wù)和鎖放一起用,就會顯得有點詭異,你上面的代碼想的是說用事務(wù)注解控制數(shù)據(jù)庫事務(wù),異常就回滾,成功就提交,對吧?然后你想加鎖以后就是每個線程串行執(zhí)行,一個線程加鎖,更新數(shù)據(jù)庫,提交事務(wù),釋放鎖,下一個線程過來加鎖,讀取更新數(shù)據(jù)庫,注意,這里應(yīng)該是接著上一個現(xiàn)成的更新結(jié)果來做的,完了再提交事務(wù),釋放鎖,對吧?
問題是,如果忽略了事務(wù)注解的工作機制,忘了那個事務(wù)控制其實是在鎖代碼外面的,因為spring會用AOP代理機制接管方法執(zhí)行,事務(wù)管控是在方法執(zhí)行外面的,所以很可能你開啟一共事務(wù),然后加鎖,執(zhí)行數(shù)據(jù)庫更新,接著就直接釋放鎖了,然后此時事務(wù)可能還沒提交!!!!
接著別的線程就可以進入一個方法了,此時他會開啟一個自己的事務(wù),在mysql層面多個事務(wù)并發(fā)的時候是有自己的隔離機制的,跟你的代碼里的加鎖是沒直接關(guān)系的,此時新的線程是可以進入代碼塊拿到鎖的,畢竟你之前一個線程都釋放代碼里的鎖了!
然后新的線程執(zhí)行數(shù)據(jù)庫的讀取和更新操作,其實是基于上一個線程的事務(wù)沒提交的那個臟數(shù)據(jù)在執(zhí)行,所以此時就會出現(xiàn)數(shù)據(jù)不一致的情況,看起來就跟多個線程亂序更新數(shù)據(jù)庫一樣,跟你想的就不一樣了,對吧?
所以這就是所謂的事務(wù)注解里線程加鎖可能導(dǎo)致鎖沒生效,多個線程還是亂序在執(zhí)行。
三、問題分析
問題的根源在于@Transactional和線程鎖的管理機制不同步。@Transactional通過AOP代理在方法執(zhí)行前后進行事務(wù)操作,而線程鎖則是直接在方法內(nèi)部控制并發(fā)。當(dāng)方法執(zhí)行完畢后,即使事務(wù)還未提交,鎖已經(jīng)被釋放,這就為其他線程提供了進入并操作共享資源的機會。
四、解決方案
為了解決@Transactional中使用線程鎖導(dǎo)致的鎖失效問題,我們可以采用以下幾種方案:
1. 將事務(wù)管理和鎖操作分離
將需要加鎖的業(yè)務(wù)邏輯封裝到一個單獨的方法中,并在調(diào)用該方法前手動管理事務(wù)。這種方式可以避免@Transactional和線程鎖在時間上的不一致。也就是通過手動管控事務(wù)提交和回滾,跟代碼里的加鎖同步一致,避免這個問題。
按照我們的想法,說白了就是應(yīng)該是在加鎖代碼里面讓事務(wù)先提交,然后再釋放鎖,這樣就可以保證多個線程對數(shù)據(jù)庫的更新是串行的。
@Service
public class UpdateService {
private final Lock lock = new ReentrantLock();
@Autowired
private PlatformTransactionManager transactionManager;
public void updateData() {
lock.lock();
try {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 模擬數(shù)據(jù)庫更新操作
System.out.println("Updating data...");
// 假設(shè)這里有一些耗時的數(shù)據(jù)庫操作
Thread.sleep(1000);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}注意:這種方式雖然解決了鎖失效的問題,但手動管理事務(wù)會使代碼變得復(fù)雜,且容易出錯。
2. 使用@Transactional單獨一個方法
將需要事務(wù)支持的方法單獨提出來,并確保該方法不包含任何鎖操作。在調(diào)用該方法前,通過其他方式(如使用代理類或直接在調(diào)用者處)管理鎖。這個本質(zhì)其實也是在鎖范圍內(nèi)讓事務(wù)先執(zhí)行和提交,只不過通過方法的提取避免了手動加提交事務(wù),其實是更加的優(yōu)雅的!
@Service
public class UpdateServiceImpl implements UpdateService {
@Autowired
@Lazy
private UpdateServiceImpl self;
private final Lock lock = new ReentrantLock();
@Transactional
public void updateDataTransactional() {
// 模擬數(shù)據(jù)庫更新操作
System.out.println("Updating data in transaction...");
// 假設(shè)這里有一些耗時的數(shù)據(jù)庫操作
Thread.sleep(1000);
}
public void updateData() {
lock.lock();
try {
self.updateDataTransactional();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}這種方式將事務(wù)管理和鎖操作分離到不同的方法中,既保證了事務(wù)的正確性,又避免了鎖失效的問題。
3. 使用數(shù)據(jù)庫鎖代替線程鎖
在某些情況下,我們可以考慮使用數(shù)據(jù)庫本身的鎖機制來替代線程鎖。數(shù)據(jù)庫鎖可以更加精確地控制對共享資源的訪問,且與事務(wù)管理緊密結(jié)合,不易出現(xiàn)鎖失效的問題。
五、總結(jié)
在@Transactional注解的方法內(nèi)部使用線程鎖時,由于事務(wù)管理和鎖操作在時間上的不一致,可能會導(dǎo)致鎖失效的問題。為了解決這個問題,我們可以將事務(wù)管理和鎖操作分離,使用編程式事務(wù)管理,或者將需要事務(wù)支持的方法單獨提出來,并通過其他方式管理鎖。同時,我們也可以考慮使用數(shù)據(jù)庫鎖來替代線程鎖,以更好地保證數(shù)據(jù)的一致性和完整性。
希望這篇文章能幫助你更好地理解@Transactional中使用線程鎖導(dǎo)致的問題,并提供實用的解決方案。在實際開發(fā)中,根據(jù)具體場景選擇合適的方法,可以有效避免類似問題的發(fā)生。


























