工作當中經常會遇到既需要開啟事務管理,同時也需要同步保證執行緒安全的場景。
比如一個方法
@Transactional
public synchronized void test(){
//
}
不知道大家有沒有這樣寫過?
這樣寫會有問題嗎?
眾所周知,spring使用動態代理加AOP實現事務管理。那麼上面的方法實際上需要簡化成3個步驟:
void begin();
@Transactional
public synchronized void test(){
//
}
void commit();
// void rollback();
先看事務本身,這裡簡化為test()
一個方法,從而忽略事務的傳播來看,它是不受synchronized
影響的。因為它能正常commit
或rollback
不受其它執行緒影響。
再看同步這一塊,這裡很明顯就有點問題了。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
達到預期效果呢?加上隔離級別
看看效果
能滿足要求,但是誰會在生產環境使用讀未提交級別?
依次往上,
讀已提交,經測試不滿足,
可重複讀,mysql預設級別,一開始就是,不滿足。
這倒是走向了另一個極端,通過測試可以看到,由於在SERIALIZABLE
隔離級別下,會給表加個鎖,因此在第2個執行緒執行到Select
的時候就會一直等待到鎖超時。
在這一個固定的測試場景曲線滿足了業務要求,但是它還是進入了test
方法,因此不滿足synchronized
的要求。
而且這種隔離級別和讀未提交一樣屬於兩個極端,它會極大的抑制並行數,在生產環境中也極少使用,在這裡屬於既不實用也不好用。
給select查詢語句手動上鎖。
測試結果就不截圖了,這種是比SERIALIZABLE
要實用一些,它只加行鎖,其它的話類似,並不能完全達到synchronized
的要求。
是否使用看場景。
把transcational
和synchronized
分開,作用在兩個方法。比如synchronized
在上層方法。
千萬別寫成這樣,這樣事務不生效了。
最好也別寫到最頂層如controller
層,這樣感覺把通道門口就給堵死了的感覺。只是加個中間層。
這樣,表哥有了第1次的經驗過後,表妹在第2次來的時候就被小區安保直接給攔住啦,在表哥完全跑路之前沒有撬鎖的機會了。
另外,一定是synchronized
在呼叫層,transcational
在被呼叫層。不能弄反了,弄反了就和之前沒區別了。
當transcational遇上synchronized,不要搞在一起,會出事。
如果要用最好是分開。