前幾天在某平台看到一個技術問題,很有意思啊。
涉及到的兩個技術點,大家平時開發使用的也比較多,但是屬于一個小細節,深挖下去,還是有點意思的。
來,先帶你看一下問題是什麽,同時給你解讀一下這個問題:
https://segmentfault.com/q/1010000040361592
首先,這位同學給出了一個代碼片段:
![](http://image.uc.cn/s/wemedia/s/upload/2024/ed46cf2420010b9c98f1ab7c8e8e875f.png)
他說他有一個 func 方法,這個方法裏面幹了兩件事:
1.先查詢數據庫裏面的商品庫存。2.如果還有庫存,那麽對庫存進行減一操作,模擬商品賣出。對于第二件事,提問的同學其實寫了兩個操作在裏面,所以我再細分一下:
2.1 對庫存進行減一操作。2.2 在訂單表插入訂單數據。很顯然,這兩個操作都會對數據庫進行操作,且應該是應該原子性的操作。
所以,在方法上加了一個 @Transactional 注解。
接著,爲了解決並發訪問的問題,他用 lock 把整個代碼包裹了起來,保證在單體結構下,同一時刻只有一個請求能去執行減少庫存,生成訂單的操作。
非常的完美。
首先,先把大前提申明一下:MySQL 數據庫的隔離機制使用的是可重複讀級別。
![](http://image.uc.cn/s/wemedia/s/upload/2024/6429393a6433df862ef9985e8b35979d.png)
這個時候,問題就來了。
如果是高並發的情況下,假設真的就有多個線程同時調用 func 方法。
要保證一定不能出現超賣的情況,那麽就需要事務的開啓與提交能完整的包裹在 lock 與 unlock之間。
顯然事務的開啓一定是在 lock 之後的。
故關鍵在于事務的提交是否一定在 unlock 之前?
如果事務的提交在 unlock 之前,沒有問題。
因爲事務已經提交了,代表庫存一定減下來了,而這個時候鎖還沒釋放,所以,其他線程也進不來。
畫個簡單的示意圖如下:
![](http://image.uc.cn/s/wemedia/s/upload/2024/6fa83a350eff45666db1c98376a19422.png)
等 unlock 之後,再進來一個線程,執行查詢數據庫的操作,那麽查詢到的值一定是減去庫存之後的值。
但是,如果事務的提交是在 unlock 之後,那麽有意思的事情就出現了,你很有可能發生超賣的情況。
上面的圖就變成了這樣的了,注意最後兩個步驟調換了:
![](http://image.uc.cn/s/wemedia/s/upload/2024/a8a590a9b84d6424152b44ec9ce71307.png)
舉個例子。
假設現在庫存就只有一個了。
這個時候 A,B 兩個線程來請求下單。
A 請求先拿到鎖,然後查詢出庫存爲一,可以下單,走了下單流程,把庫存減爲 0 了。
但是由于 A 先執行了 unlock 操作,釋放了鎖。
B 線程看到後馬上就沖過來拿到了鎖,並執行了查詢庫存的操作。
注意了,這個時候 A 線程還沒來得及提交事務,所以 B 讀取到的庫存還是 1,如果程序沒有做好控制,也走了下單流程。
哦豁,超賣了。
所以,再次重申問題:
在上面的示例代碼的情況下,如果事務的提交在 unlock 之前,是沒有問題的。但是如果在 unlock 之後是會有問題的。
那麽事務的提交到底是在 unlock 之前還是之後呢?
這個事情,先把問題聽懂了,接著我們先按下不表。你可以簡單的思考一下。
![](http://image.uc.cn/s/wemedia/s/upload/2024/e5c98cbeded80541f9ace7162ba65048.png)
我想先聊聊這句被我輕描淡寫,一筆帶過,你大概率沒有注意到的話:
顯然事務的開啓一定是在 lock 之後的。
這句話,不是我說的,是提問的同學說的:
![](http://image.uc.cn/s/wemedia/s/upload/2024/70d3d61354c85abbf193921dd75bf70d.png)
你有沒有一絲絲疑問?
怎麽就顯然了?哪裏就顯然了?爲什麽不是一進入方法就開啓事務了?
請給我證據。
來吧,瞅一眼證據。
![](http://image.uc.cn/s/wemedia/s/upload/2024/e2c800677e021d2e2913530651f66884.png)
證據,我們需要去源碼裏面找。
另外,我不得不多說一句 Spring 在事務這塊的源碼寫的非常的清晰易懂,看起來基本上沒有什麽障礙。
所以如果你不知道怎麽去啃源碼,那麽事務這塊源碼,也許是你撕開源碼的一個口子。
好了,不多說了,去找答案。
答案就藏在這個方法裏面的:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
![](http://image.uc.cn/s/wemedia/s/upload/2024/7d08b8656a91e1bd487e675af8ab1d1b.png)
先看我下面框起來的那一行日志:
Switching JDBC Connection [HikariProxyConnection@946359486 wrapping com.mysql.jdbc.JDBC4Connection@7a24806] to manual commit
你知道的,我是個技術博主,偶爾教點單詞。
Switching,轉換。
Connection,鏈接。
manual commit,手動提交。
Switching ... to ...,把什麽轉換爲什麽。
沒想到吧,這次學技術的同時不僅學了幾個單詞,還會了一個語法。
![](http://image.uc.cn/s/wemedia/s/upload/2024/38b92fa85510a4bb210b8c66fe7fb26a.png)
所以,上面那句話翻譯過來就非常簡單了:
把數據庫連接切換爲手動提交。
然後,我們看一下打印這行日志的代碼邏輯,也就是被框起來的代碼部分。
我單獨拿出來:
![](http://image.uc.cn/s/wemedia/s/upload/2024/4f6bba4688e9be4e0459d0d08fdb59b9.png)
邏輯非常清晰,就是把連接的 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 行查詢語句執行完成之後,查詢事務的語句才能查出數據,說明事務這才真正的開啓:
![](http://image.uc.cn/s/wemedia/s/upload/2024/aa45152c8a5ffd39d5035a80602b5eee.gif)
最後,我們把目光轉移到這個方法的注釋上:
![](http://image.uc.cn/s/wemedia/s/upload/2024/a1bef84526204ad609d49f22891839a0.png)
寫這麽長一段注釋,意思就是給你說,這個參數我們默認是 ture,原因就是在某些 JDBC 的驅動中,切換爲自動提交是一個很重的操作。
那麽在哪設置的爲 true 呢?
沒看到代碼,我一般是不死心的。
所以,一起去看一眼。
setAutoCommit 這個方法有好幾個實現類,我也不知道具體會走哪一個:
![](http://image.uc.cn/s/wemedia/s/upload/2024/84475ed1d00bfcef8948339deffd9cbb.png)
所以,我們可以在下面這個接口打上一個斷點:
java.sql.Connection#setAutoCommit
![](http://image.uc.cn/s/wemedia/s/upload/2024/64bf3f3d687fe18fde3f23c7b98f8de7.png)
然後重啓程序,IDE 會自動幫你判斷走那個實現類的:
![](http://image.uc.cn/s/wemedia/s/upload/2024/a82592fe8982576961a93ccbc7261200.png)
可以看到,默認確實是 true。
等等,你不會真的以爲我是想讓你看這個 true 吧?
我是想讓你知道這個調試技巧啊。
不知道有多少個小夥伴曾經問過我:這個接口實現類好多啊,我怎麽知道在哪打斷點啊?
我說:很簡單啊,就在每個實現類的第一行代碼打上斷點就好了。
然後他說:別鬧,我經常給你的文章一鍵三聯。
我當時就被感動了,既然是這樣的好讀者,我當然把可以直接在接口上打斷點的這個小技巧教給他啦。
![](http://image.uc.cn/s/wemedia/s/upload/2024/884d2618084ec6ee807e2cd5786c1c93.png)
好了,不扯遠了。
再說一個小細節,這一小節就收尾。
你再去看這小節的開頭,我直接說答案藏在這個方法裏面:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
直接把答案告訴你了,隱去了探索的過程。
但是這個東西,就像是數學公式推導一樣,省略了一步,就會讓人看起來一臉懵逼。
就像下面這個小耗子一樣:
![](http://image.uc.cn/s/wemedia/s/upload/2024/a551dec93ade974ce7ef76f9274b306a.png)
所以,我是怎麽知道在這個地方打斷點的呢?
答案就是調用棧。
先給大家看一下我的代碼:
![](http://image.uc.cn/s/wemedia/s/upload/2024/e8f4cd680723bd63a68bc60bc0df3016.png)
啥也先不管,上來就先在 26 行,方法入口處打上斷點,跑起來:
![](http://image.uc.cn/s/wemedia/s/upload/2024/e7015a6d4e8f0cea34d0433b71537085.png)
诶,你看這個調用棧,我框起來的這個地方:
![](http://image.uc.cn/s/wemedia/s/upload/2024/fd50c036f0e009f6e6250b3f32a5e240.png)
看這個名字,你就不好奇嗎?
它簡直就是在跳著腳,在喊你:點我,快,愣著幹啥,你TM快點我啊。我這裏有秘密!
然後,我就這樣輕輕的一點,就到了這裏:
org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
這裏有個切面,可以理解爲 try 裏面就是在執行我們的業務代碼邏輯:
![](http://image.uc.cn/s/wemedia/s/upload/2024/de3c179500b0c3c67cf4a136d853a81f.png)
而在 try 代碼塊,執行我們的業務代碼之前,有這樣的一行代碼:
![](http://image.uc.cn/s/wemedia/s/upload/2024/5947cfe83a0cbf36d7ddef1f4910f376.png)
找到這裏了,你就在這一行代碼之前,再輕輕的打個斷點,然後調試進去,就能找到這一小節開始的時候,說的這個方法:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
不信?你看嘛,我不騙你。
它們之間只隔了三個調用:
![](http://image.uc.cn/s/wemedia/s/upload/2024/2aabd38ea193c010b99f08e3f5b13e91.png)
這樣就找到答案了。
調用棧,另一個調試源碼小技巧,屢試不爽,送給你。
之前還是之後好了,前面是開胃菜,可能有的同學吃開胃菜就已經弄飽了。
沒事,現在上正餐,再按一按還是能吃進去的。
![](http://image.uc.cn/s/wemedia/s/upload/2024/0fc45f695cdde6654f73e9bd5652455d.gif)
還是拿前面的這份代碼來說事,流程就是這樣的:
![](http://image.uc.cn/s/wemedia/s/upload/2024/240b48e45d128de9f253e3781dc3274b.png)
所以代碼是這樣的:
![](http://image.uc.cn/s/wemedia/s/upload/2024/35b506b866aec40ea1446b89cac10caa.png)
完全符合我們之前的那份代碼片段,有事務,也有鎖:
![](http://image.uc.cn/s/wemedia/s/upload/2024/832329d8df32cc670914b254e674989b.png)
回到我們最開始抛出來的問題:
在上面的示例代碼的情況下那麽事務的提交到底是在 unlock 之前還是之後呢?
我們可以帶入一個具體的場景。
比如我數據庫裏面有 10 個頂配版的 iPad,原價 1.6w 元一台,現在單價 1w 一個,這個價格夠秒殺吧?
![](http://image.uc.cn/s/wemedia/s/upload/2024/f3db232ed5c27eec72b1156e90cbdc3b.jpg)
反正一共就 10 台,所以,我的數據庫裏面是這樣的,
![](http://image.uc.cn/s/wemedia/s/upload/2024/0bedc34facb99bf0050f051e8afaeb90.png)
然後我搞 100 個人來搶東西,不過分吧?
我這裏用 CountDownLatch 來模擬一下並發:
![](http://image.uc.cn/s/wemedia/s/upload/2024/905425e522d8d98bafc9238ac9deab5a.png)
執行一下,先看結果,立馬就見分曉:
![](http://image.uc.cn/s/wemedia/s/upload/2024/7aa48c37c03799453cacdfb3ad8bb690.gif)
動圖右邊的部分:
上面是浏覽器請求,觸發 Controller 的代碼。
然後中間是産品表,有 10 個庫存。
最下面是訂單表,沒有一條數據。
觸發了代碼之後,庫存爲 0 了,沒有問題。
但是,訂單居然有 20 筆!
也就是說超賣了 10 個ipad pro 頂配版!
超賣的,可不在活動預算範圍內啊!
那可就是一個 1.6w 啊,10 個就是 16w 啊。
就這麽其貌不揚,人畜無害,甚至看起來猥猥瑣瑣的代碼,居然讓我虧了整整 16w 。
![](http://image.uc.cn/s/wemedia/s/upload/2024/8480165771334e9e56c9bde84057d82a.png)
其實,結果出現了,答案也就隨之而來了。
在上面的示例代碼的情況下,事務的提交在 unlock 之後。
![](http://image.uc.cn/s/wemedia/s/upload/2024/a8a590a9b84d6424152b44ec9ce71307.png)
其實你仔細分析後,猜也能猜出來,肯定是在 unlock 之後的。
而且上面的描述“unlock之後”其實是有一定的迷惑性的,因爲釋放鎖是一個比較特別的操作。
換一個描述,就比較好理解了:
在上面的示例代碼的情況下,事務的提交在方法運行結束之後。
你細品,這個描述是不是迷惑性就沒有那麽強了,甚至你還會恍然大悟:這不是常識嗎?
![](http://image.uc.cn/s/wemedia/s/upload/2024/79393088c4321670422774552549a685.png)
爲什麽是方法結束之後,分析具體原因之前,我想先簡單分析一下這樣的代碼寫出來的原因。
我猜可能是這樣的。
最開始的代碼結構是這樣:
![](http://image.uc.cn/s/wemedia/s/upload/2024/30820b581b1094861ae6af45967f2de8.png)
然後,寫著寫著發現不對,並發的場景下,庫存是一個共享的資源,這玩意得加鎖啊。
于是搞了這出:
![](http://image.uc.cn/s/wemedia/s/upload/2024/4ca5fae1f8d9af175c2a0387385c571f.png)
後面再次審查代碼的時候,發現:喲,這個第三步得是一個事務操作才行呀。
于是代碼就成了這樣:
![](http://image.uc.cn/s/wemedia/s/upload/2024/6ec5c120616a6cf4a6345c08a91b21c8.png)
演進路線非常合理,最終的代碼看起來也簡直毫無破綻。
但是問題到底出在哪裏了呢?
![](http://image.uc.cn/s/wemedia/s/upload/2024/d1d2ca25b93ecb277badb1be3bf787c5.png)
答案還是在這個類裏面:
org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
![](http://image.uc.cn/s/wemedia/s/upload/2024/bcd81542f48bab1f94f6e8fa3ee4ed94.png)
前面我們聊事務開啓的時候,說的是第 382 行代碼。
然後 try 代碼塊裏面執行的是我們的業務代碼。
現在,我們要研究事務的提交了,所以主要看我框起來的地方。
首先 catch 代碼塊裏面,392 行,看方法名稱已經非常的見名知意了:
completeTransactionAfterThrowing 在抛出異常之後完成事務的提交。
你看我的代碼,只是用到了 @Transactional 注解,並沒有指定異常。
那麽問題就來了:
Spring 管理的事務,默認回滾的異常是什麽呢?
如果你不知道答案,就可以帶著問題去看源碼。
如果你知道答案,但是沒有親眼看到對應的代碼,那麽也可以去尋找源碼。
如果你知道答案,也看過這部分源碼,溫故而知新。
先說答案:默認回滾的異常是 RuntimeException 或者 Error。
我只需要在業務代碼裏面抛出一個 RuntimeException 的子類,比如這樣的:
![](http://image.uc.cn/s/wemedia/s/upload/2024/5ec406c6b84461d62ef4c2f313183c14.png)
然後在 392 行打上斷點,開始調試就完事了:
![](http://image.uc.cn/s/wemedia/s/upload/2024/14ed1a3635c32b6b3decf88d90a879b9.png)
只需要往下調試幾步,你就能走到這個方法來:
org.springframework.transaction.interceptor.RuleBasedTransactionAttribute#rollbackOn
![](http://image.uc.cn/s/wemedia/s/upload/2024/f088fa3fae67d8246e8a559d3f12d310.png)
發現這個 winner 對象爲空,接著走了這個邏輯:
return super.rollbackOn(ex);
答案就藏著這行代碼的背後:
![](http://image.uc.cn/s/wemedia/s/upload/2024/ae6410dac93169e168b5d2c44c6761f4.png)
如果異常類型是 RuntimeException 或者 Error 的子類,那麽就返回 true,即需要回滾,調用 rollback 方法:
![](http://image.uc.cn/s/wemedia/s/upload/2024/26cd1a75e3a804ac10a0b62de4b817b8.png)
如果返回爲 false,則表示不需要回滾,調用 commit 方法:
![](http://image.uc.cn/s/wemedia/s/upload/2024/ccf1ad37b55964bf714e62b507025726.png)
那麽怎麽讓它返回 false 呢?
很簡單嘛,這樣一搞就好了:
![](http://image.uc.cn/s/wemedia/s/upload/2024/ac9a81d7e5a3ebf90ab6c5c853f88e1f.png)
框架給你留了口子,你就把它用起來。
當我把代碼改成上面那樣,然後重新啓動項目,再次訪問代碼。
我們去尋找出現指定異常不回滾的具體的實現邏輯在哪。
其實也在我們剛剛看到的方法裏面:
![](http://image.uc.cn/s/wemedia/s/upload/2024/479249dc4f95206dc5fddc4bbac32dea.png)
你看,這個時候 winner 不爲 null 了。它是一個 NoRollbackRuleAttribute 對象了。
所以就走入這行代碼,返回 false 了:
return !(winner instanceof NoRollbackRuleAttribute);
于是,就成功走到了 else 分支裏面,出了異常也 commit 了,你說神奇不神奇:
![](http://image.uc.cn/s/wemedia/s/upload/2024/3ad23d7a98cb07e0671cea7ce2c387ac.png)
寫到這裏的時候,我突然想到了一個騷操作,甚至有可能變成一道沙雕面試題:
![](http://image.uc.cn/s/wemedia/s/upload/2024/656da0a03339047791dcaeb6db94fa60.png)
這個操作騷不騷,到底會回滾呢還是不回滾呢?
![](http://image.uc.cn/s/wemedia/s/upload/2024/28d5af71ded57c51eaf753d40675d5cd.png)
如果你在項目裏看到這樣的代碼肯定是要罵一句傻b的。
但是面試官就喜歡搞這些陰間的題目。
我想到這個問題的時候,我也不知道答案是什麽,但是我知道答案還是在源碼裏面:
![](http://image.uc.cn/s/wemedia/s/upload/2024/74d7afde7ec44775330caf36b06b1111.png)
首先,從結果上可以直觀的看到,經過 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)
![](http://image.uc.cn/s/wemedia/s/upload/2024/dac088299996174dde34b80bad3d1c85.png)
別問,問就是因爲代碼是這樣寫的。
爲什麽代碼要這樣寫呢?
我想可能設計這塊代碼的開發人員覺得 rollbackFor 的優先級比 noRollbackFor 高吧。
再來一個問題:
Spring 源碼怎麽匹配當前這個異常是需要回滾的?
別想那麽複雜,大道至簡,直接遞歸,然後一層層的找父類,對比名稱就完事了。
![](http://image.uc.cn/s/wemedia/s/upload/2024/e11161f237afa0c47d2dcfb93d5c0dc8.png)
你注意截圖裏面的注釋:
一個是 Found it!
表示找到了,匹配上了,用了感歎號表示很開心。
一個是 If we've gone as far as we can go and haven't found it...
啥意思呢,這個 as far as 在英語裏面是一個連詞,表示“直到..爲止..”的意思。引導的是狀語從句,強調的是程度或範圍。
所以,上面這句話的意思就是:
如果我們已經走到我們能走的最遠的地方,還沒匹配上,代碼就只能這樣寫了:
![](http://image.uc.cn/s/wemedia/s/upload/2024/a27dd14dfe34e8407ee199becc7fd95b.png)
異常類,最遠的地方就是 Throwable.class。沒匹配上,就返回 -1。
好了,通過兩個沒啥卵用的知識點,順帶學了點實戰英語,關于業務代碼出了異常回滾還是提交這一塊的代碼就差不多了。
但是我還是建議大家親自去 Debug 一下,可太有意思了。
然後我們接著聊正常場景下的提交。
![](http://image.uc.cn/s/wemedia/s/upload/2024/511199d4ea55a9974046daf98512dacb.png)
這個代碼塊裏面,try 我們也聊了,catch 我們也聊了。
就差個 finally 了。
我看網上有的文章說 finally 裏面就是 commit 的地方。
錯了啊,老弟。
這裏只是把數據庫連接給重置一下。
方法上已經給你說的很清楚了:
![](http://image.uc.cn/s/wemedia/s/upload/2024/6a44f50b2a8f69b28bd361794b5feb91.png)
Spring 的事務是基于 ThreadLocal 來做的。在當前的這個事務裏面,可能有一些隔離級別、回滾類型、超時時間等等的個性化配置。
不管是這個事務正常返回還是出現異常,只要它完事了,就得給把這些個性化的配置全部恢複到默認配置。
所以,放到了 finally 代碼塊裏面去執行了。
真正的 commit 的地方是這行代碼:
![](http://image.uc.cn/s/wemedia/s/upload/2024/f81ca343bf0e046746edee1a63ae88c2.png)
那麽問題又來了:
走到這裏來了,事務一定會提交嗎?
話可別說的那麽絕對,兄弟,看代碼:
org.springframework.transaction.support.AbstractPlatformTransactionManager#commit
![](http://image.uc.cn/s/wemedia/s/upload/2024/620ee49b129e348b3fea599045aec174.png)
在 commit 之前還有兩個判斷,如果事務被標記爲 rollback-only 了,還是得回滾。
而且,你看日志。
我這事務還沒提交呢,鎖就被釋放了?
![](http://image.uc.cn/s/wemedia/s/upload/2024/9353d2f30bb246648723ac47f03efc14.png)
接著往下看 commit 相關的邏輯,我們就會遇到老朋友:
![](http://image.uc.cn/s/wemedia/s/upload/2024/3b8b8811bdeb75ff64d5418608945a3c.png)
HikariCP,SpringBoot 2.0 之後的默認連接池,強得一比,在之前的文章裏面介紹過。
關于事務的提交,就不大篇幅的介紹了。
給大家指個路:
com.mysql.cj.protocol.a.NativeProtocol#sendQueryString
在這個方法的入口處打上斷點:
![](http://image.uc.cn/s/wemedia/s/upload/2024/a7eb133c9c4478ba7a5388a692ececa1.png)
然後你會發現很多的 SQL 都會經過這個地方。
所以,爲了你順利調試,你需要在斷點上設置一下:
![](http://image.uc.cn/s/wemedia/s/upload/2024/a048a6cf5100f947947617b4b6e1f8e6.png)
這樣只有 SQL 語句是 commit 的時候才會停下來。
又一個調試小細節,送給你,不客氣。
現在,我們知道原因了,那我現在把代碼稍微變一下:
![](http://image.uc.cn/s/wemedia/s/upload/2024/99a4dd9637dc9a6c88c292ac34483fb6.png)
把 ReentrantLock 換成了 synchronized。
那你說這個代碼還會不會有問題?
![](http://image.uc.cn/s/wemedia/s/upload/2024/c514c7aece8cd30896a76626d7b6e9ff.png)
說沒有問題的同學請好好反思一下。
這個地方的原理和前面講的東西是一模一樣的呀,肯定也是有問題的。
這個加鎖方式就是錯誤的。
所以你記住了,以後面試官問你 @Transactional 的時候,你把標准答案先背一遍之後,如果你對鎖這塊的知識點非常的熟悉,就可以在不經意間說一下結合鎖用的時候的異常場景。
別說你寫的,就說你 review 代碼的時候發現的,深藏功與名。
另外記得擴展一下,現在都是集群服務了,加鎖得上分布式鎖。
但是原理還這個原理。
既然都聊到分布式鎖了,這和面試官又得大戰幾個回合。
是你主動提起的,把面試官引到了你的主戰場,拿幾分,不過分吧。
一個面試小技巧,送給你,不客氣。
解決方案現在我們知道問題的原因了。
解決方案其實都呼之欲出了嘛。
正確的使用鎖,把整個事務放在鎖的工作範圍之內:
![](http://image.uc.cn/s/wemedia/s/upload/2024/73b621f6e013bdba0e46153905397388.png)
這樣,就可以保證事務的提交一定是在 unlock 之前了。
對不對?
![](http://image.uc.cn/s/wemedia/s/upload/2024/92d3102804b4118ba619973350fa289e.gif)
說對的同學,今天就先到這裏,請回去等通知啊。
別被帶到溝裏去了呀,朋友。
你仔細想想這個事務會生效嗎?
提示到這裏還沒想明白的同學,趕緊去搜一下事務失效的幾種場景。
我這裏說一個能正常使用的場景:
![](http://image.uc.cn/s/wemedia/s/upload/2024/0bf7aca74637e5cac11d754015d1cce2.png)
只是這種自己注入自己的方式,我覺得很惡心。
如果項目裏面出現了這樣的代碼,一定是代碼分層沒有做好,項目結構極其混亂。
不推薦。
還可以使用編程式事務的方式去寫,自己去控制事務的開啓、提交、回滾。
比直接使用 @Transactional 靠譜。
除此之外,還有一個騷一點的解決方案。
其他地方都不動,就只改一下 @Transactional 這個地方:
![](http://image.uc.cn/s/wemedia/s/upload/2024/d01159079464ee88f0fd4a60289a77bd.png)
把隔離級別串行化,再次跑測試用例,絕對不會出現超賣的情況。
甚至都不需要加鎖的邏輯。
你覺得好嗎?
![](http://image.uc.cn/s/wemedia/s/upload/2024/1507bdbe5e0f36c9ffd50819ba0c1c3c.png)
好啥啊?
串行化性能跟不上啊!
這玩意太悲觀了,對于同一行的數據,讀和寫的時候都會進行加鎖操作。當讀寫鎖出現沖突的時候,後面來的事務就排隊等著。
這個騷操作,知道就行了,別用。
你就當是一個沒啥卵用的知識點就行了。
但是,如果你們是一個不追求性能的場景,這個沒有卵用的知識點就變成騷操作了。
rollback-only前面提到了這個 rollback-only,爲了更好的行文,所以我一句話就帶過了,其實它也是很有故事的,單獨拿一節出來簡單說一下,給大家模擬一下這個場景。
以後你見到這個異常就會感覺很親切。
Spring 的事務傳播級別默認是 REQUIRED,含義是如果當前沒有事務,就新建一個事務,如果上下文中已經有一個事務,則共享這個事務。
直接上代碼:
![](http://image.uc.cn/s/wemedia/s/upload/2024/406ee0ea0bdb7c71e04518d1245a43ee.png)
這裏有 sellProduct、sellProductBiz 兩個事務,sellProductBiz 是內層事務,它會抛出了異常。
當執行整個邏輯的時候,會抛出這個異常:
Transaction rolled back because it has been marked as rollback-only
![](http://image.uc.cn/s/wemedia/s/upload/2024/f00d401aee463d52c7593907448c4641.png)
根據這個異常的堆棧,可以找到這個地方,在前面出現過:
![](http://image.uc.cn/s/wemedia/s/upload/2024/de4b26ec77e0022cba173adfb90d1f6c.png)
所以,我們只需要分析這個 if 條件爲什麽滿足了,就大概摸清楚脈絡了。
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly())
前面的 shouldCommitOnGlobalRollbackOnly 默認爲 false:
![](http://image.uc.cn/s/wemedia/s/upload/2024/5114e135723f0a9ef785c20bc02f012a.png)
問題就精簡爲了:defStatus.isGlobalRollbackOnly() 爲什麽是true?
爲什麽?
因爲 sellProductBiz 抛出異常後,會調用 completeTransactionAfterThrowing 方法執行回滾邏輯。
肯定是這個方法裏面搞事情了啊。
org.springframework.transaction.support.AbstractPlatformTransactionManager#processRollback
![](http://image.uc.cn/s/wemedia/s/upload/2024/acd94638685b2ef2fcf8a1873406bd9c.png)
在這裏,把鏈接的 rollbackOnly 置爲了 true。
所以,後面的事務想要 commit 的時候,一檢查這個參數,哦豁,回滾吧。
大概就是這樣的:
![](http://image.uc.cn/s/wemedia/s/upload/2024/d725c431b77166487af985c822f54d56.png)
如果這不是你期望的異常,怎麽解決呢?
理解了事務的傳播機制就簡單的一比:
![](http://image.uc.cn/s/wemedia/s/upload/2024/4ebaa685919b93f030b20eabaa248748.png)
就這樣,跑起來沒毛病,互不幹擾。
![](http://image.uc.cn/s/wemedia/s/upload/2024/42ec7385f93f4375508bfc9ecc75b9a9.png)
來源:公衆號
作者:why技術