你好,我是yes。
在群里看到小夥伴在討論一份面試題,我一看好傢夥!密密麻麻的一堆!

不過還好不要求候選人寫出來,只需要面試口述回答即可。
這類面試題其實相比尋常的八股文質量高多了,畢竟更貼合實際應用,比如第一題就貼了堆棧的信息讓你說出其原由,這就比較考察平常經驗的累積了。
不過有小夥伴看到第一題就不會了,今天我就先來盤盤第一題,後面如果你們想要其他題的解析,可以留言給我,我再寫寫。
報錯重現先來個高糊截圖,不知道你們看得清不?

主要報錯信息就是:Transaction rolled back because it has been marked as rollback-only。
這句話其實很好理解,事務被回滾了,因為它已經被標記只能回滾。
毫無疑問這是一個事務問題,如果要復現這個問題,首選我們需要兩個 service(邏輯簡單,僅為舉例)。
AddressService可以看到邏輯非常簡單,就是一個插入,方法用事務註解標記了,不過這裡的插入是會拋錯的。
具體我是通過把 address 實體和數據庫字段格式弄成不一致,來造成拋錯調用,模擬事務的失敗。
然後我們再來一個 UserService。裡面的邏輯也不難,先插入地址,再插入用戶,並且用 try catch 包裹了地址的插入,目的是為了防止地址插入拋錯影響用戶的插入。
並且這個插入方法也用事務註解標註了。
j接下來我們調用 UserService#insert ,會發生什麼呢?
我們理下:首先 addressService#errorInvoker 會拋錯,但是我們用 try catch,所以按理來說影響不到後面的邏輯,緊接着執行用戶的插入,最後數據庫成功插入了一條數據?
錯啦!
執行的結果如下:

可以看到,我們復現了第一題的問題啦!看到這裡,你是不是已經有點感覺了?但是又有點奇怪?
原理分析我們都用 try catch包裹了會出錯邏輯,為什麼會影響到後面的事務提交?
可以看到,我們的案例涉及了兩個 service 中的兩個方法,且這兩個方法都標註了@Transactional 註解。
這個註解裡面有事務傳播機制的設置,我們沒填,所以默認為 Propagation.REQUIRED。

我們再看看下這個字段的注釋:

REQUIRED:如果已經有事務就用當前的事務,沒得話就新起一個事務。
基於這些前提,我們分析一下邏輯:
首選我們調用 UserService#insert ,由於標記了事務註解,因此已經被代理了, 我們調用了代理邏輯,默認是 事務傳播級別是 Propagation.REQUIRED ,所以已經新起了一個事務。
緊接着執行 AddressService#errorInvoker ,這個方法也被 @Transactional 標記,所以也被代理了,默認事務傳播級別也是 Propagation.REQUIRED,而當前已經有一個由 UserService#insert 發起的事務了,所以就用這個事務。
緊接着執行地址的插入邏輯,由於字段類型不對,插入報錯,於是觸發事務回滾邏輯。
但是由於是否提交事務得由外層事務決定,於是乎它只能做個標記,來設置當前事務只能回滾。
緊接着插入錯誤被拋出,不過被 try catch 攔截,不影響後面的邏輯,於是接着處理 userMapper.insert(user),由於沒有拋錯,所以順利執行,打算提交事務。
而此時這個事務因為剛才的拋錯,已經被打上了回滾標記,所以提交失敗,報錯的原因就是
Transaction rolled back because it has been marked as rollback-only
好了原理已經分析完畢(更具體可以看後面源碼分析),那如何解決這個問題呢?
解決方案第一個方案把 addressService#errorInvoker 方法上的事務註解刪了,這樣拋錯壓根就不會影響當前事務,也符合本身要求的業務邏輯:地址插入不影響用戶插入的事務提交。
第二個方案修改 addressService#errorInvoker 的事務傳播機制為:REQUIRES_NEW 或 NESTED。
REQUIRES_NEW:無論如何都新起一個事務,因此執行 AddressService#errorInvoker 時候會新起一個事務,報錯的話影響的是新起的事務,跟 UserService#insert 起的事務沒關係。
NESTED:如果已經有事務,則會起一個嵌套事務,嵌套事務回滾並不會影響外部事務。
簡單源碼分析因為用了事務註解,所以原來的 service 會被代理執行,而代理邏輯會執行到TransactionAspectSupport#invokeWithinTransaction

地址的插入拋錯,所以會被 catch 到走 completeTransactionAfterThrowing 的邏輯,而其內部實際會執行下面這段方法:

注釋說:我們並不會回滾,別怕,如果被標記了回滾標識,我們會回滾的。
commit 後面實際的邏輯會執行到下面這個判斷,而這個參數默認的配置就是 true。
也就是說內部事務失敗是否標記主事務為 rollback-only 默認為 true。
因此內部事務拋錯會執行了下面這個邏輯,即:

然後這一 part 就結束了,把錯誤拋出來,被 try catch 捕獲,緊接着執行用戶的插入,後面的執行很順利,沒拋錯,於是正常提交事務,但在提交的過程中查到了當前事務已經被標記成 rollback-only。

於是要執行 processRollback 方法,這裡注意下參數 unexpected 是 true。
看下內部邏輯會執行回滾工作,然後就會看到拋出的那個錯誤了:

簡單分析完畢,有興趣的可以自己打下斷點,這部分邏輯還是比較簡單清晰的。
最後好了,第一題分析就到這了,大家應該都理解了吧,所以 try catch 也不一定是萬能的,平時使用的時候還是得看仔細了。
這其實也算是一個事務傳播機制的一個實戰,用起來比較隱蔽,不過了解之後還是比較簡單的。
關於上面的一些別的面試題,有興趣的可以留言,我看哪個多拿出來先寫寫。
我是yes,從一點點到億點點,我們下篇見!