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















 
 
 








 
 
 
 