為什么 insert 配置 "SELECT LAST_INSERT_ID()" 返回個0呢?
一、前言:一個Bug
沒想到一個Bug,竟然搞我兩次!
我大抵是卷上癮了,橫豎都睡不著,坐起來身來打開Mac和外接顯示器,這Bug沒有由來,默然看著打印異常的屏幕,一個是我的,另外一個也是我的。
今天這個問題主要體現在大家平常用的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查詢工具中執行
三、震驚:同一個坑
但其實就這么一個鏈接的問題,在小傅哥手寫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 等的開發,只有類似這樣的廣度、高度、深度,才能真的把個人的研發能力提升起來。