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