我大抵是捲上癮了,橫豎睡不着!竟讓一個Bug,搞我兩次!

語言: CN / TW / HK

作者:小傅哥
博客: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查詢工具中執行

三、震驚:同一個坑

😂 但其實就這麼一個鏈接的問題,在小傅哥手寫Spring中也同樣遇到過。

在 Spring 中有一部分是關於事務的處理,其實這些事務的操作也是對 JDBC 的包裝操作,依賴於數據源獲得的鏈接來管理事務。而我們通常使用 Spring 也是結合着 Mybatis 配置上數據源的方式進行使用,那麼在一個事務下操作多個 SQL 語句的時候,是怎麼獲得同一個鏈接的呢。因為從上面👆🏻的案例中,我們得知保證事務的特性,需要在同一個鏈接下,即使是操作多條SQL

由於多個SQL的操作,已經是相當於每次都獲取一個新的 Session 有一個新的鏈接從連接池中獲得,但為了能達到事務的特性,所以在需要有事務操作下的多個 SQL 前需要開啟事務操作,無論是手動還是註解。

而這個事務的開啟動作處理做一些事務傳播行為和隔離級別的限制,其實更重要的是讓多個 SQL 的執行獲取的鏈接,需要是同一個。所以這裏就引入了 ThreadLocal 基於它在同一個線程操作下保存信息的同步特性,其實這裏的從事務下獲取的鏈接,其實就是保存到 TransactionSynchronizationManager#resources 屬性中的。

雖然就這麼一小塊內容,但在小傅哥最開始手寫Spring的時候,也是給漏下了。直到到測試的時候,才發現鏈接發現事務總是不成功,最初還以為是整個切面邏輯沒有切進去或者是我的操作方式有誤。直到逐步排查調試代碼,發現原來多個SQL的執行竟然不是獲得的同一個鏈接,所以也就沒法讓事務生效。

四、常見:事務失效

可能就是這麼一個小小的鏈接問題,有時候就會引起一堆的異常,如果説我們沒有學習過源碼,那麼可能也不知道這樣的問題到底是如何發生的。所以往往深入的研究和探索,才能讓你解釋一個問題的時候,更加簡單直接。

那麼你説,事務失效的原因還有哪些?- 分享一些常見,如果你還有遇到其他的,可以發到評論區一起看看。

  1. 數據庫引擎不支持事務:這裏以 MySQL 為例,其 MyISAM 引擎是不支持事務操作的,InnoDB 才是支持事務的引擎,一般要支持事務都會使用 InnoDB。https://dev.mysql.com/doc/refman/8.0/en/storage-en... 從 MySQL 5.5.5 開始的默認存儲引擎是:InnoDB,之前默認的都是:MyISAM,所以這點要值得注意,底層引擎不支持事務再怎麼搞都是白搭。
  2. 方法不是 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 代理模式。
  3. 沒有被 Spring 管理:// @Service - 這裏被註釋掉了 public class OrderServiceImpl implements OrderService { @Transactional public void placeOrder(Order order) { // ... } }
  4. 數據源沒有配置事務管理器:一般來自於自研的數據庫路由組件 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
  5. 異常被吞了。catch 後直接吃了,事務異常無法回滾。同時要配置上對應的異常 @Transactional(rollbackFor = Exception.class)

五、總結:學習經驗

很多類似這樣的技術問題,都是來自於小傅哥對源碼的學習,最開始是遇到問題的時候去翻看源碼,雖然很多時候也很難把整個邏輯捋順,但一點點的積累確實會讓研發人員對技術有更加夯實的認知。

那麼在現在我之所以去手寫Spring、手寫Mybatis,也是希望通過把這樣的知識全部整理處理,從中學習複雜邏輯的設計方案、設計原則和如何運用設計模式解決複雜場景的問題。PS:通常我們的業務代碼複雜度很難到這個程度,所以在見過”天“後,以後所承接的業務就很容易做設計了。

另外就是對各類技術細節的把控,以及積累於這樣的經驗把相關技術設計運用到一些類似 SpringBoot Starter 等的開發,只有類似這樣的廣度、高度、深度,才能真的把個人的研發能力提升起來。PS:也是為了在技術的路上走的更遠,無論是高級開發、架構師、CTO!