當transcational遇上synchronized

2023-03-24 12:01:32

工作當中經常會遇到既需要開啟事務管理,同時也需要同步保證執行緒安全的場景。
比如一個方法

@Transactional
public synchronized void test(){
    // 
}

不知道大家有沒有這樣寫過?
這樣寫會有問題嗎?

眾所周知,spring使用動態代理加AOP實現事務管理。那麼上面的方法實際上需要簡化成3個步驟:

void begin();

@Transactional
public synchronized void test(){
    // 
}

void commit();
// void rollback();

先看事務本身,這裡簡化為test()一個方法,從而忽略事務的傳播來看,它是不受synchronized 影響的。因為它能正常commitrollback不受其它執行緒影響。

再看同步這一塊,這裡很明顯就有點問題了。synchronized 至少無法作用於事務開啟提交這一步驟。假設一個執行緒先在此方法做了update,在return之後commit之前,另一個執行緒進入了此方法,進行了select,
除非事務隔離級別為讀未提交(READ_UNCOMMITTED)(話說回來誰會在生產環境用這種隔離級別呢),不然第二個執行緒讀到的是未修改的值。
而這肯定並未與使用synchronized 的初衷相符。


口說無憑,show you me code。

@Transactional
public synchronized void test(){
    log.info("表哥,我進來了哦!");
    // select
    Person person = personDao.selectByPrimaryKey("1");
    log.info("person.name = " + person.getName());
    // update
    person.setName("你是誰?你根本不是我表哥!");
    int result = personDao.updateByPrimaryKeySelective(person);
    log.info("result = " + result);
}

然後在TransactionAspectSupport.commitTransactionAfterReturning()加個斷點。

再模擬兩個請求執行,在斷點處觀察第2次請求與第1次請求select到的值是不是一致。一致說明synchronized 在這裡沒有達到預期目的。

注意斷點時選thread不要選all,不然第2個執行緒進不來。

通過執行結果,可以看到第2個執行緒執行的時候存取到了還是原值張三,並在update時等待鎖超時。


通過什麼方法可以讓synchronized 達到預期效果呢?

手動開啟事務?pass。

事務傳播級別?無關。

事務隔離級別?

讀未提交 不實用。還是試試效果。

加上隔離級別

看看效果

能滿足要求,但是誰會在生產環境使用讀未提交級別?
依次往上,
讀已提交,經測試不滿足,
可重複讀,mysql預設級別,一開始就是,不滿足。

SERIALIZABLE

這倒是走向了另一個極端,通過測試可以看到,由於在SERIALIZABLE隔離級別下,會給表加個鎖,因此在第2個執行緒執行到Select的時候就會一直等待到鎖超時。
在這一個固定的測試場景曲線滿足了業務要求,但是它還是進入了test方法,因此不滿足synchronized的要求。

而且這種隔離級別和讀未提交一樣屬於兩個極端,它會極大的抑制並行數,在生產環境中也極少使用,在這裡屬於既不實用也不好用。

for update

給select查詢語句手動上鎖。

測試結果就不截圖了,這種是比SERIALIZABLE要實用一些,它只加行鎖,其它的話類似,並不能完全達到synchronized的要求。
是否使用看場景。

給test方法加一層呼叫方法

transcationalsynchronized分開,作用在兩個方法。比如synchronized在上層方法。

千萬別寫成這樣,這樣事務不生效了。
最好也別寫到最頂層如controller層,這樣感覺把通道門口就給堵死了的感覺。只是加個中間層。

這樣,表哥有了第1次的經驗過後,表妹在第2次來的時候就被小區安保直接給攔住啦,在表哥完全跑路之前沒有撬鎖的機會了。

另外,一定是synchronized在呼叫層,transcational在被呼叫層。不能弄反了,弄反了就和之前沒區別了。

總結

當transcational遇上synchronized,不要搞在一起,會出事。
如果要用最好是分開。