作者:小傅哥
部落格:https://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收穫!?
一、前言:一個Bug
沒想到一個Bug,竟然搞我兩次!
我大抵是捲上癮了,橫豎都睡不著,坐起來身來開啟Mac和外接顯示器,這Bug沒有由來,默然看著列印異常的螢幕,一個是我的,另外一個也是我的。
最近可能是卷原始碼,捲上癮了。先是《手寫Spring》,再是《手寫Mybatis》,但沒想到一個小問題竟然搞了我2次!
今天這個問題主要體現在大家平常用的Mybatis,在插入資料的時候,我們可以把庫表索引的返回值通過入參物件返回回來。但是通過我自己手寫的Mybatis,每次返回來的都是0,而不是最後插入庫表的索引值。因為是手寫的,不是直接使用Mybatis,所以我會從檔案的解析、物件的對映、SQL的查詢、結果的封裝等一直排查下去,但竟然問題都不在這?!
- 就是這個 selectKey 的配置,在執行插入SQL後,開始執行獲取最後的索引值。
- 通常只要配置的沒問題,返回物件中也有對應的 id 欄位,那麼就可以正確的拿到返回值了。PS:問題就出現在這裡,小傅哥手寫的 Mybatis 竟然只難道返回一個0!
二、分析:診斷異常
可能大部分研發夥伴沒有閱讀過 Mybatis 原始碼,所以可能不太清楚這裡發生了什麼,小傅哥這裡給大家畫張圖,告訴你發生了什麼才讓返回的結果為0的。
- Mybatis 的處理過程可以分為兩個大部分來看,一部分是解析,另外一部分是使用。解析的時候把 Mapper XML 中的 insert 標籤語句解析出來,同時解析 selectKey 標籤。最終解析完成後,把解析的語句資訊使用 MappedStatement 對映語句類存放起來。便於後續在 DefaultSqlSession 執行操作的時候,可以從 Configuration 配置項中獲取出來使用。
- 那麼這裡有一個非常重要的點,就是執行 insert 插入的時候,裡面還包含了一句查詢的操作。那也就是說,我們會在一次 Insert 中,包含兩條執行語句。重點:bug就發生在這裡,為什麼呢?因為最開始這兩條語句執行的時候,在獲取連結的時候,每一條都是獲取一個新的連結,那麼也就是說,insert xxx、select LAST_INSERT_ID() 在兩個 connection 連線執行時,其實是不對的,沒法獲取到插入後的索引 ID,只有在一個連結或者一個事務下(一次 commit)才能有事務的特性,獲取插入資料後的自增ID。
而因為這部分最開始手寫 JdbcTransaction 實現 Transaction 介面獲取連線的時候,每一次都是新的連結,程式碼塊如下;
- 這裡的連結獲取,最開始沒有 if null 的判斷,每次都是直接獲取連結,所以這種非一個連結下的兩條 SQL 操作,所以必然不會獲得到正確的結果,相當於只是單獨執行
SELECT LAST_INSERT_ID()
所以最終的查詢結果為 0 了就!你可以測試把這條語句複製到 SQL查詢工具中執行
- 這裡的連結獲取,最開始沒有 if null 的判斷,每次都是直接獲取連結,所以這種非一個連結下的兩條 SQL 操作,所以必然不會獲得到正確的結果,相當於只是單獨執行
三、震驚:同一個坑
? 但其實就這麼一個連結的問題,在小傅哥手寫Spring中也同樣遇到過。
在 Spring 中有一部分是關於事務的處理,其實這些事務的操作也是對 JDBC 的包裝操作,依賴於資料來源獲得的連結來管理事務。而我們通常使用 Spring 也是結合著 Mybatis 配置上資料來源的方式進行使用,那麼在一個事務下操作多個 SQL 語句的時候,是怎麼獲得同一個連結的呢。因為從上面??的案例中,我們得知保證事務的特性,需要在同一個連結下,即使是操作多條SQL
由於多個SQL的操作,已經是相當於每次都獲取一個新的 Session 有一個新的連結從連線池中獲得,但為了能達到事務的特性,所以在需要有事務操作下的多個 SQL 前需要開啟事務操作,無論是手動還是註解。
而這個事務的開啟動作處理做一些事務傳播行為和隔離級別的限制,其實更重要的是讓多個 SQL 的執行獲取的連結,需要是同一個。所以這裡就引入了 ThreadLocal 基於它在同一個執行緒操作下儲存資訊的同步特性,其實這裡的從事務下獲取的連結,其實就是儲存到 TransactionSynchronizationManager#resources 屬性中的。
雖然就這麼一小塊內容,但在小傅哥最開始手寫Spring的時候,也是給漏下了。直到到測試的時候,才發現連結發現事務總是不成功,最初還以為是整個切面邏輯沒有切進去或者是我的操作方式有誤。直到逐步排查除錯程式碼,發現原來多個SQL的執行竟然不是獲得的同一個連結,所以也就沒法讓事務生效。
四、常見:事務失效
可能就是這麼一個小小的連結問題,有時候就會引起一堆的異常,如果說我們沒有學習過原始碼,那麼可能也不知道這樣的問題到底是如何發生的。所以往往深入的研究和探索,才能讓你解釋一個問題的時候,更加簡單直接。
那麼你說,事務失效的原因還有哪些?- 分享一些常見,如果你還有遇到其他的,可以發到評論區一起看看。
- 資料庫引擎不支援事務:這裡以 MySQL 為例,其 MyISAM 引擎是不支援事務操作的,InnoDB 才是支援事務的引擎,一般要支援事務都會使用 InnoDB。https://dev.mysql.com/doc/refman/8.0/en/storage-en... 從 MySQL 5.5.5 開始的預設儲存引擎是:InnoDB,之前預設的都是:MyISAM,所以這點要值得注意,底層引擎不支援事務再怎麼搞都是白搭。
- 方法不是 public 的:來自 Spring 官方文件【When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.】@Transactional 只能用於 public 的方法上,否則事務不會失效,如果要用在非 public 方法上,可以開啟 AspectJ 代理模式。
- 沒有被 Spring 管理:
// @Service - 這裡被註釋掉了 public class OrderServiceImpl implements OrderService { @Transactional public void placeOrder(Order order) { // ... } }
- 資料來源沒有配置事務管理器:一般來自於自研的資料庫路由元件
@Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
- 異常被吞了。catch 後直接吃了,事務異常無法回滾。同時要配置上對應的異常
@Transactional(rollbackFor = Exception.class)
五、總結:學習經驗
很多類似這樣的技術問題,都是來自於小傅哥對原始碼的學習,最開始是遇到問題的時候去翻看原始碼,雖然很多時候也很難把整個邏輯捋順,但一點點的積累確實會讓研發人員對技術有更加夯實的認知。
那麼在現在我之所以去手寫Spring、手寫Mybatis,也是希望通過把這樣的知識全部整理處理,從中學習複雜邏輯的設計方案、設計原則和如何運用設計模式解決複雜場景的問題。PS:通常我們的業務程式碼複雜度很難到這個程度,所以在見過”天“後,以後所承接的業務就很容易做設計了。
另外就是對各類技術細節的把控,以及積累於這樣的經驗把相關技術設計運用到一些類似 SpringBoot Starter 等的開發,只有類似這樣的廣度、高度、深度,才能真的把個人的研發能力提升起來。PS:也是為了在技術的路上走的更遠,無論是高階開發、架構師、CTO!