當 Transactional 碰到鎖,有個大坑!一定要小心

前幾天在某平臺看到一個技術問題,很有意思啊。

涉及到的兩個技術點,大家平時開發使用的也比較多,但是屬於一個小細節,深挖下去,還是有點意思的。

來,先帶你看一下問題是什麼,同時給你解讀一下這個問題:

https://segmentfault。com/q/1010000040361592

首先,這位同學給出了一個程式碼片段:

當 Transactional 碰到鎖,有個大坑!一定要小心

他說他有一個 func 方法,這個方法裡面幹了兩件事:

1。先查詢資料庫裡面的商品庫存。

2。如果還有庫存,那麼對庫存進行減一操作,模擬商品賣出。

對於第二件事,提問的同學其實寫了兩個操作在裡面,所以我再細分一下:

2。1 對庫存進行減一操作。

2。2 在訂單表插入訂單資料。

很顯然,這兩個操作都會對資料庫進行操作,且應該是應該原子性的操作。

所以,在方法上加了一個

@Transactional

註解。

接著,為了解決併發訪問的問題,他用 lock 把整個程式碼包裹了起來,保證在單體結構下,同一時刻只有一個請求能去執行減少庫存,生成訂單的操作。

非常的完美。

首先,先把大前提申明一下:MySQL 資料庫的隔離機制使用的是可重複讀級別。

當 Transactional 碰到鎖,有個大坑!一定要小心

這個時候,問題就來了。

如果是高併發的情況下,假設真的就有多個執行緒同時呼叫 func 方法。

要保證一定不能出現超賣的情況,那麼就需要事務的開啟與提交能完整的包裹在 lock 與 unlock之間。

顯然事務的開啟一定是在 lock 之後的。

故關鍵在於事務的提交是否一定在 unlock 之前?

如果事務的提交在 unlock 之前,沒有問題。

因為事務已經提交了,代表庫存一定減下來了,而這個時候鎖還沒釋放,所以,其他執行緒也進不來。

畫個簡單的示意圖如下:

當 Transactional 碰到鎖,有個大坑!一定要小心

等 unlock 之後,再進來一個執行緒,執行查詢資料庫的操作,那麼查詢到的值一定是減去庫存之後的值。

但是,如果事務的提交是在 unlock 之後,那麼有意思的事情就出現了,你很有可能發生超賣的情況。

上面的圖就變成了這樣的了,注意最後兩個步驟調換了:

當 Transactional 碰到鎖,有個大坑!一定要小心

舉個例子。

假設現在庫存就只有一個了。

這個時候 A,B 兩個執行緒來請求下單。

A 請求先拿到鎖,然後查詢出庫存為一,可以下單,走了下單流程,把庫存減為 0 了。

但是由於 A 先執行了 unlock 操作,釋放了鎖。

B 執行緒看到後馬上就衝過來拿到了鎖,並執行了查詢庫存的操作。

注意了,這個時候 A 執行緒還沒來得及提交事務,所以 B 讀取到的庫存還是 1,如果程式沒有做好控制,也走了下單流程。

哦豁,超賣了。

所以,再次重申問題:

在上面的示例程式碼的情況下,如果事務的提交在 unlock 之前,是沒有問題的。但是如果在 unlock 之後是會有問題的。

那麼事務的提交到底是在 unlock 之前還是之後呢?

這個事情,先把問題聽懂了,接著我們先按下不表。你可以簡單的思考一下。

當 Transactional 碰到鎖,有個大坑!一定要小心

我想先聊聊這句被我輕描淡寫,一筆帶過,你大機率沒有注意到的話:

顯然事務的開啟一定是在 lock 之後的。

這句話,不是我說的,是提問的同學說的:

當 Transactional 碰到鎖,有個大坑!一定要小心

你有沒有一絲絲疑問?

怎麼就顯然了?哪裡就顯然了?為什麼不是一進入方法就開啟事務了?

請給我證據。

來吧,瞅一眼證據。

當 Transactional 碰到鎖,有個大坑!一定要小心

事務開啟時機

證據,我們需要去原始碼裡面找。

另外,我不得不多說一句 Spring 在事務這塊的原始碼寫的非常的清晰易懂,看起來基本上沒有什麼障礙。

所以如果你不知道怎麼去啃原始碼,那麼事務這塊原始碼,也許是你撕開原始碼的一個口子。

好了,不多說了,去找答案。

答案就藏在這個方法裡面的:

org。springframework。jdbc。datasource。DataSourceTransactionManager#doBegin

當 Transactional 碰到鎖,有個大坑!一定要小心

先看我下面框起來的那一行日誌:

Switching JDBC Connection [HikariProxyConnection@946359486 wrapping com。mysql。jdbc。JDBC4Connection@7a24806] to manual commit

你知道的,我是個技術博主,偶爾教點單詞。

Switching,轉換。

Connection,連結。

manual commit,手動提交。

Switching 。。。 to 。。。,把什麼轉換為什麼。

沒想到吧,這次學技術的同時不僅學了幾個單詞,還會了一個語法。

當 Transactional 碰到鎖,有個大坑!一定要小心

所以,上面那句話翻譯過來就非常簡單了:

把資料庫連線切換為手動提交。

然後,我們看一下列印這行日誌的程式碼邏輯,也就是被框起來的程式碼部分。

我單獨拿出來:

當 Transactional 碰到鎖,有個大坑!一定要小心

邏輯非常清晰,就是把連線的 AutoCommit 引數從 ture 修改為 false。

那麼現在問題就來了,這個時候,事務啟動了嗎?

我覺得沒啟動,只是就緒了而已。

啟動和就緒還是有一點點差異的,就緒是啟動之前的步驟。

那麼事務的啟動有哪些方式呢?

第一種:使用啟動事務的語句,這種是顯式的啟動事務。比如 begin 或 start transaction 語句。與之配套的提交語句是 commit,回滾語句是 rollback。

第二種:autocommit 的值預設是 1,含義是事務的自動提交是開啟的。如果我們執行 set autocommit=0,這個命令會將這個執行緒的自動提交關掉。意味著如果你只執行一個 select 語句,這個事務就啟動了,而且並不會自動提交。這個事務持續存在直到你主動執行 commit 或 rollback 語句,或者斷開連線。

很顯然,在 Spring 裡面採用的是第二種方式。

而上面的程式碼

con。setAutoCommit(false)

只是把這個連結的自動提交關掉。

事務真正啟動的時機是什麼時候呢?

前面說的 begin/start transaction 命令並不是一個事務的起點,在執行到它們之後的第一個操作 InnoDB 表的語句,事務才算是真正啟動。

如果你想要馬上啟動一個事務,可以使用 start transaction with consistent snapshot 這個命令。需要注意的是這個命令在讀已提交的隔離級別(RC)下是沒意義的,和直接使用 start transaction 一個效果。

回到在前面的問題:什麼時候才會執行第一個 SQL 語句?

就是在 lock 程式碼之後。

所以,顯然事務的開啟一定是在 lock 之後的。

這一個簡單的“顯然”,先給大家鋪墊一下。

接下來,給大家上個動圖看一眼,更加直觀。

首先說一下這個 SQL:

select * from information_schema。innodb_trx;

不多解釋,你只要知道這是查詢當前資料庫有哪些事務正在執行的語句就行。

你就注意看下面的動圖,是不是第 27 行查詢語句執行完成之後,查詢事務的語句才能查出資料,說明事務這才真正的開啟:

當 Transactional 碰到鎖,有個大坑!一定要小心

最後,我們把目光轉移到這個方法的註釋上:

當 Transactional 碰到鎖,有個大坑!一定要小心

寫這麼長一段註釋,意思就是給你說,這個引數我們預設是 ture,原因就是在某些 JDBC 的驅動中,切換為自動提交是一個很重的操作。

那麼在哪設定的為 true 呢?

沒看到程式碼,我一般是不死心的。

所以,一起去看一眼。

setAutoCommit 這個方法有好幾個實現類,我也不知道具體會走哪一個:

當 Transactional 碰到鎖,有個大坑!一定要小心

所以,我們可以在下面這個介面打上一個斷點:

java。sql。Connection#setAutoCommit

當 Transactional 碰到鎖,有個大坑!一定要小心

然後重啟程式,IDE 會自動幫你判斷走那個實現類的:

當 Transactional 碰到鎖,有個大坑!一定要小心

可以看到,預設確實是 true。

等等,你不會真的以為我是想讓你看這個 true 吧?

我是想讓你知道這個除錯技巧啊。

不知道有多少個小夥伴曾經問過我:這個介面實現類好多啊,我怎麼知道在哪打斷點啊?

我說:很簡單啊,就在每個實現類的第一行程式碼打上斷點就好了。

然後他說:別鬧,我經常給你的文章一鍵三聯。

我當時就被感動了,既然是這樣的好讀者,我當然把可以直接在介面上打斷點的這個小技巧教給他啦。

當 Transactional 碰到鎖,有個大坑!一定要小心

好了,不扯遠了。

再說一個小細節,這一小節就收尾。

你再去看這小節的開頭,我直接說答案藏在這個方法裡面:

org。springframework。jdbc。datasource。DataSourceTransactionManager#doBegin

直接把答案告訴你了,隱去了探索的過程。

但是這個東西,就像是數學公式推導一樣,省略了一步,就會讓人看起來一臉懵逼。

就像下面這個小耗子一樣:

當 Transactional 碰到鎖,有個大坑!一定要小心

所以,我是怎麼知道在這個地方打斷點的呢?

答案就是呼叫棧。

先給大家看一下我的程式碼:

當 Transactional 碰到鎖,有個大坑!一定要小心

啥也先不管,上來就先在 26 行,方法入口處打上斷點,跑起來:

當 Transactional 碰到鎖,有個大坑!一定要小心

誒,你看這個呼叫棧,我框起來的這個地方:

當 Transactional 碰到鎖,有個大坑!一定要小心

看這個名字,你就不好奇嗎?

它簡直就是在跳著腳,在喊你:點我,快,愣著幹啥,你TM快點我啊。我這裡有秘密!

然後,我就這樣輕輕的一點,就到了這裡:

org。springframework。transaction。interceptor。TransactionAspectSupport#invokeWithinTransaction

這裡有個切面,可以理解為 try 裡面就是在執行我們的業務程式碼邏輯:

當 Transactional 碰到鎖,有個大坑!一定要小心

而在 try 程式碼塊,執行我們的業務程式碼之前,有這樣的一行程式碼:

當 Transactional 碰到鎖,有個大坑!一定要小心

找到這裡了,你就在這一行程式碼之前,再輕輕的打個斷點,然後除錯進去,就能找到這一小節開始的時候,說的這個方法:

org。springframework。jdbc。datasource。DataSourceTransactionManager#doBegin

不信?你看嘛,我不騙你。

它們之間只隔了三個呼叫:

當 Transactional 碰到鎖,有個大坑!一定要小心

這樣就找到答案了。

呼叫棧,另一個除錯原始碼小技巧,屢試不爽,送給你。

之前還是之後

好了,前面是開胃菜,可能有的同學吃開胃菜就已經弄飽了。

沒事,現在上正餐,再按一按還是能吃進去的。

當 Transactional 碰到鎖,有個大坑!一定要小心

還是拿前面的這份程式碼來說事,流程就是這樣的:

當 Transactional 碰到鎖,有個大坑!一定要小心

1。先拿鎖。

2。查詢庫存。

3。判斷是否還有庫存。

4。有庫存則執行減庫存,建立訂單的邏輯。

5。沒有庫存則返回。

6。釋放鎖。

所以程式碼是這樣的:

當 Transactional 碰到鎖,有個大坑!一定要小心

完全符合我們之前的那份程式碼片段,有事務,也有鎖:

當 Transactional 碰到鎖,有個大坑!一定要小心

回到我們最開始丟擲來的問題:

在上面的示例程式碼的情況下那麼事務的提交到底是在 unlock 之前還是之後呢?

我們可以帶入一個具體的場景。

比如我資料庫裡面有 10 個頂配版的 iPad,原價 1。6w 元一臺,現在單價 1w 一個,這個價格夠秒殺吧?

當 Transactional 碰到鎖,有個大坑!一定要小心

反正一共就 10 臺,所以,我的資料庫裡面是這樣的,

當 Transactional 碰到鎖,有個大坑!一定要小心

然後我搞 100 個人來搶東西,不過分吧?

我這裡用 CountDownLatch 來模擬一下併發:

當 Transactional 碰到鎖,有個大坑!一定要小心

執行一下,先看結果,立馬就見分曉:

當 Transactional 碰到鎖,有個大坑!一定要小心

動圖右邊的部分:

上面是瀏覽器請求,觸發 Controller 的程式碼。

然後中間是產品表,有 10 個庫存。

最下面是訂單表,沒有一條資料。

觸發了程式碼之後,庫存為 0 了,沒有問題。

但是,訂單居然有 20 筆!

也就是說超賣了 10 個ipad pro 頂配版!

超賣的,可不在活動預算範圍內啊!

那可就是一個 1。6w 啊,10 個就是 16w 啊。

就這麼其貌不揚,人畜無害,甚至看起來猥猥瑣瑣的程式碼,居然讓我虧了整整 16w 。

當 Transactional 碰到鎖,有個大坑!一定要小心

其實,結果出現了,答案也就隨之而來了。

在上面的示例程式碼的情況下,事務的提交在 unlock 之後。

當 Transactional 碰到鎖,有個大坑!一定要小心

其實你仔細分析後,猜也能猜出來,肯定是在 unlock 之後的。

而且上面的描述“unlock之後”其實是有一定的迷惑性的,因為釋放鎖是一個比較特別的操作。

換一個描述,就比較好理解了:

在上面的示例程式碼的情況下,事務的提交在方法執行結束之後。

你細品,這個描述是不是迷惑性就沒有那麼強了,甚至你還會恍然大悟:這不是常識嗎?

當 Transactional 碰到鎖,有個大坑!一定要小心

為什麼是方法結束之後,分析具體原因之前,我想先簡單分析一下這樣的程式碼寫出來的原因。

我猜可能是這樣的。

最開始的程式碼結構是這樣:

當 Transactional 碰到鎖,有個大坑!一定要小心

然後,寫著寫著發現不對,併發的場景下,庫存是一個共享的資源,這玩意得加鎖啊。

於是搞了這出:

當 Transactional 碰到鎖,有個大坑!一定要小心

後面再次審查程式碼的時候,發現:喲,這個第三步得是一個事務操作才行呀。

於是程式碼就成了這樣:

當 Transactional 碰到鎖,有個大坑!一定要小心

演進路線非常合理,最終的程式碼看起來也簡直毫無破綻。

但是問題到底出在哪裡了呢?

當 Transactional 碰到鎖,有個大坑!一定要小心

找答案

答案還是在這個類裡面:

org。springframework。transaction。interceptor。TransactionAspectSupport#invokeWithinTransaction

當 Transactional 碰到鎖,有個大坑!一定要小心

前面我們聊事務開啟的時候,說的是第 382 行程式碼。

然後 try 程式碼塊裡面執行的是我們的業務程式碼。

現在,我們要研究事務的提交了,所以主要看我框起來的地方。

首先 catch 程式碼塊裡面,392 行,看方法名稱已經非常的見名知意了:

completeTransactionAfterThrowing

在丟擲異常之後完成事務的提交。

你看我的程式碼,只是用到了

@Transactional

註解,並沒有指定異常。

那麼問題就來了:

Spring 管理的事務,預設回滾的異常是什麼呢?

如果你不知道答案,就可以帶著問題去看原始碼。

如果你知道答案,但是沒有親眼看到對應的程式碼,那麼也可以去尋找原始碼。

如果你知道答案,也看過這部分原始碼,溫故而知新。

先說答案:

預設回滾的異常是 RuntimeException 或者 Error

我只需要在業務程式碼裡面丟擲一個 RuntimeException 的子類,比如這樣的:

當 Transactional 碰到鎖,有個大坑!一定要小心

然後在 392 行打上斷點,開始除錯就完事了:

當 Transactional 碰到鎖,有個大坑!一定要小心

只需要往下除錯幾步,你就能走到這個方法來:

org。springframework。transaction。interceptor。RuleBasedTransactionAttribute#rollbackOn

當 Transactional 碰到鎖,有個大坑!一定要小心

發現這個 winner 物件為空,接著走了這個邏輯:

return super。rollbackOn(ex);

答案就藏著這行程式碼的背後:

當 Transactional 碰到鎖,有個大坑!一定要小心

如果異常型別是 RuntimeException 或者 Error 的子類,那麼就返回 true,即需要回滾,呼叫 rollback 方法:

當 Transactional 碰到鎖,有個大坑!一定要小心

如果返回為 false,則表示不需要回滾,呼叫 commit 方法:

當 Transactional 碰到鎖,有個大坑!一定要小心

那麼怎麼讓它返回 false 呢?

很簡單嘛,這樣一搞就好了:

當 Transactional 碰到鎖,有個大坑!一定要小心

框架給你留了口子,你就把它用起來。

當我把程式碼改成上面那樣,然後重新啟動專案,再次訪問程式碼。

我們去尋找出現指定異常不回滾的具體的實現邏輯在哪。

其實也在我們剛剛看到的方法裡面:

當 Transactional 碰到鎖,有個大坑!一定要小心

你看,這個時候 winner 不為 null 了。它是一個 NoRollbackRuleAttribute 物件了。

所以就走入這行程式碼,返回 false 了:

return !(winner instanceof NoRollbackRuleAttribute);

於是,就成功走到了 else 分支裡面,出了異常也 commit 了,你說神奇不神奇:

當 Transactional 碰到鎖,有個大坑!一定要小心

寫到這裡的時候,我突然想到了一個騷操作,甚至有可能變成一道沙雕面試題:

當 Transactional 碰到鎖,有個大坑!一定要小心

這個操作騷不騷,到底會回滾呢還是不回滾呢?

當 Transactional 碰到鎖,有個大坑!一定要小心

如果你在專案裡看到這樣的程式碼肯定是要罵一句傻 逼 的。

但是面試官就喜歡搞這些陰間的題目。

我想到這個問題的時候,我也不知道答案是什麼,但是我知道答案還是在原始碼裡面:

當 Transactional 碰到鎖,有個大坑!一定要小心

首先,從結果上可以直觀的看到,經過 for 迴圈之後, winner 是 RollbackRuleAttribute 物件,所以下面的程式碼返回 true,需要回滾:

return !(winner instanceof NoRollbackRuleAttribute);

問題就變成了 winner 為什麼經過 for 迴圈之後是 RollbackRuleAttribute?

答案需要你自己去除錯一下,很容易就明白了,我描述起來比較費勁。

簡單一句話:導致 winner 是 RollbackRuleAttribute 的原因,就是因為被迴圈的這個 list 是先把 RollbackRuleAttribute 物件 add 了進去。

那麼為什麼 RollbackRuleAttribute 物件先加入到集合呢?

org。springframework。transaction。annotation。SpringTransactionAnnotationParser#parseTransactionAnnotation(org。springframework。core。annotation。AnnotationAttributes)

當 Transactional 碰到鎖,有個大坑!一定要小心

別問,問就是因為程式碼是這樣寫的。

為什麼程式碼要這樣寫呢?

我想可能設計這塊程式碼的開發人員覺得 rollbackFor 的優先順序比 noRollbackFor 高吧。

再來一個問題:

Spring 原始碼怎麼匹配當前這個異常是需要回滾的?

別想那麼複雜,大道至簡,直接遞迴,然後一層層的找父類,對比名稱就完事了。

當 Transactional 碰到鎖,有個大坑!一定要小心

你注意截圖裡面的註釋:

一個是 Found it!

表示找到了,匹配上了,用了感嘆號表示很開心。

一個是 If we‘ve gone as far as we can go and haven’t found it。。。

啥意思呢,這個 as far as 在英語裡面是一個連詞,表示“直到。。為止。。”的意思。引導的是狀語從句,強調的是程度或範圍。

所以,上面這句話的意思就是:

如果我們已經走到我們能走的最遠的地方,還沒匹配上,程式碼就只能這樣寫了:

當 Transactional 碰到鎖,有個大坑!一定要小心

異常類,最遠的地方就是 Throwable。class。沒匹配上,就返回 -1。

好了,透過兩個沒啥卵用的知識點,順帶學了點實戰英語,關於業務程式碼出了異常回滾還是提交這一塊的程式碼就差不多了。

但是我還是建議大家親自去 Debug 一下,可太有意思了。

然後我們接著聊正常場景下的提交。

當 Transactional 碰到鎖,有個大坑!一定要小心

這個程式碼塊裡面,try 我們也聊了,catch 我們也聊了。

就差個 finally 了。

我看網上有的文章說 finally 裡面就是 commit 的地方。

錯了啊,老弟。

這裡只是把資料庫連線給重置一下。

方法上已經給你說的很清楚了:

當 Transactional 碰到鎖,有個大坑!一定要小心

Spring 的事務是基於 ThreadLocal 來做的。在當前的這個事務裡面,可能有一些隔離級別、回滾型別、超時時間等等的個性化配置。

不管是這個事務正常返回還是出現異常,只要它完事了,就得給把這些個性化的配置全部恢復到預設配置。

所以,放到了 finally 程式碼塊裡面去執行了。

真正的 commit 的地方是這行程式碼:

當 Transactional 碰到鎖,有個大坑!一定要小心

那麼問題又來了:

走到這裡來了,事務一定會提交嗎?

話可別說的那麼絕對,兄弟,看程式碼:

org。springframework。transaction。support。AbstractPlatformTransactionManager#commit

當 Transactional 碰到鎖,有個大坑!一定要小心

在 commit 之前還有兩個判斷,如果事務被標記為 rollback-only 了,還是得回滾。

而且,你看日誌。

我這事務還沒提交呢,鎖就被釋放了?

當 Transactional 碰到鎖,有個大坑!一定要小心

接著往下看 commit 相關的邏輯,我們就會遇到老朋友:

當 Transactional 碰到鎖,有個大坑!一定要小心

HikariCP,SpringBoot 2。0 之後的預設連線池,強得一比,在之前的文章裡面介紹過。

關於事務的提交,就不大篇幅的介紹了。

給大家指個路:

com。mysql。cj。protocol。a。NativeProtocol#sendQueryString

在這個方法的入口處打上斷點:

當 Transactional 碰到鎖,有個大坑!一定要小心

然後你會發現很多的 SQL 都會經過這個地方。

所以,為了你順利除錯,你需要在斷點上設定一下:

當 Transactional 碰到鎖,有個大坑!一定要小心

這樣只有 SQL 語句是 commit 的時候才會停下來。

又一個除錯小細節,送給你,不客氣。

現在,我們知道原因了,那我現在把程式碼稍微變一下:

當 Transactional 碰到鎖,有個大坑!一定要小心

把 ReentrantLock 換成了 synchronized。

那你說這個程式碼還會不會有問題?

當 Transactional 碰到鎖,有個大坑!一定要小心

說沒有問題的同學請好好反思一下。

這個地方的原理和前面講的東西是一模一樣的呀,肯定也是有問題的。

這個加鎖方式就是錯誤的。

所以你記住了,以後面試官問你

@Transactional

的時候,你把標準答案先背一遍之後,如果你對鎖這塊的知識點非常的熟悉,就可以在不經意間說一下結合鎖用的時候的異常場景。

別說你寫的,就說你 review 程式碼的時候發現的,深藏功與名。

另外記得擴充套件一下,現在都是叢集服務了,加鎖得上分散式鎖。

但是原理還這個原理。

既然都聊到分散式鎖了,這和麵試官又得大戰幾個回合。

是你主動提起的,把面試官引到了你的主戰場,拿幾分,不過分吧。

一個面試小技巧,送給你,不客氣。

當 Transactional 碰到鎖,有個大坑!一定要小心

解決方案

現在我們知道問題的原因了。

解決方案其實都呼之欲出了嘛。

正確的使用鎖,把整個事務放在鎖的工作範圍之內:

當 Transactional 碰到鎖,有個大坑!一定要小心

這樣,就可以保證事務的提交一定是在 unlock 之前了。

對不對?

當 Transactional 碰到鎖,有個大坑!一定要小心

說對的同學,今天就先到這裡,請回去等通知啊。

別被帶到溝裡去了呀,朋友。

你仔細想想這個事務會生效嗎?

提示到這裡還沒想明白的同學,趕緊去搜一下事務失效的幾種場景。

我這裡說一個能正常使用的場景:

當 Transactional 碰到鎖,有個大坑!一定要小心

只是這種自己注入自己的方式,我覺得很噁心。

如果專案裡面出現了這樣的程式碼,一定是程式碼分層沒有做好,專案結構極其混亂。

不推薦。

還可以使用程式設計式事務的方式去寫,自己去控制事務的開啟、提交、回滾。

比直接使用

@Transactional

靠譜。

除此之外,還有一個騷一點的解決方案。

其他地方都不動,就只改一下 @Transactional 這個地方:

當 Transactional 碰到鎖,有個大坑!一定要小心

把隔離級別序列化,再次跑測試用例,絕對不會出現超賣的情況。

甚至都不需要加鎖的邏輯。

你覺得好嗎?

當 Transactional 碰到鎖,有個大坑!一定要小心

好啥啊?

序列化效能跟不上啊!

這玩意太悲觀了,對於同一行的資料,讀和寫的時候都會進行加鎖操作。當讀寫鎖出現衝突的時候,後面來的事務就排隊等著。

這個騷操作,知道就行了,別用。

你就當是一個沒啥卵用的知識點就行了。

但是,如果你們是一個不追求效能的場景,這個沒有卵用的知識點就變成騷操作了。

rollback-only

前面提到了這個 rollback-only,為了更好的行文,所以我一句話就帶過了,其實它也是很有故事的,單獨拿一節出來簡單說一下,給大家模擬一下這個場景。

以後你見到這個異常就會感覺很親切。

Spring 的事務傳播級別預設是 REQUIRED,含義是如果當前沒有事務,就新建一個事務,如果上下文中已經有一個事務,則共享這個事務。

直接上程式碼:

當 Transactional 碰到鎖,有個大坑!一定要小心

這裡有 sellProduct、sellProductBiz 兩個事務,sellProductBiz 是內層事務,它會丟擲了異常。

當執行整個邏輯的時候,會丟擲這個異常:

Transaction rolled back because it has been marked as rollback-only

當 Transactional 碰到鎖,有個大坑!一定要小心

根據這個異常的堆疊,可以找到這個地方,在前面出現過:

當 Transactional 碰到鎖,有個大坑!一定要小心

所以,我們只需要分析這個 if 條件為什麼滿足了,就大概摸清楚脈絡了。

if (!shouldCommitOnGlobalRollbackOnly() && defStatus。isGlobalRollbackOnly())

前面的 shouldCommitOnGlobalRollbackOnly 預設為 false:

當 Transactional 碰到鎖,有個大坑!一定要小心

問題就精簡為了:defStatus。isGlobalRollbackOnly() 為什麼是true?

為什麼?

因為 sellProductBiz 丟擲異常後,會呼叫 completeTransactionAfterThrowing 方法執行回滾邏輯。

肯定是這個方法裡面搞事情了啊。

org。springframework。transaction。support。AbstractPlatformTransactionManager#processRollback

當 Transactional 碰到鎖,有個大坑!一定要小心

在這裡,把連結的 rollbackOnly 置為了 true。

所以,後面的事務想要 commit 的時候,一檢查這個引數,哦豁,回滾吧。

大概就是這樣的:

當 Transactional 碰到鎖,有個大坑!一定要小心

如果這不是你期望的異常,怎麼解決呢?

理解了事務的傳播機制就簡單的一比:

當 Transactional 碰到鎖,有個大坑!一定要小心

就這樣,跑起來沒毛病,互不干擾。

當 Transactional 碰到鎖,有個大坑!一定要小心

好了,本文的技術部分就到這裡啦。