為什么 insert 配置 "SELECT LAST_INSERT_ID()" 返回個(gè)0呢?
一、前言:一個(gè)Bug
沒(méi)想到一個(gè)Bug,竟然搞我兩次!
我大抵是卷上癮了,橫豎都睡不著,坐起來(lái)身來(lái)打開(kāi)Mac和外接顯示器,這Bug沒(méi)有由來(lái),默然看著打印異常的屏幕,一個(gè)是我的,另外一個(gè)也是我的。
今天這個(gè)問(wèn)題主要體現(xiàn)在大家平常用的Mybatis,在插入數(shù)據(jù)的時(shí)候,我們可以把庫(kù)表索引的返回值通過(guò)入?yún)?duì)象返回回來(lái)。但是通過(guò)我自己手寫的Mybatis,每次返回來(lái)的都是0,而不是最后插入庫(kù)表的索引值。因?yàn)槭鞘謱懙模皇侵苯邮褂肕ybatis,所以我會(huì)從文件的解析、對(duì)象的映射、SQL的查詢、結(jié)果的封裝等一直排查下去,但竟然問(wèn)題都不在這?!
- 就是這個(gè) selectKey 的配置,在執(zhí)行插入SQL后,開(kāi)始執(zhí)行獲取最后的索引值。
- 通常只要配置的沒(méi)問(wèn)題,返回對(duì)象中也有對(duì)應(yīng)的 id 字段,那么就可以正確的拿到返回值了。PS:?jiǎn)栴}就出現(xiàn)在這里,小傅哥手寫的 Mybatis 竟然只難道返回一個(gè)0!
二、分析:診斷異常
可能大部分研發(fā)伙伴沒(méi)有閱讀過(guò) Mybatis 源碼,所以可能不太清楚這里發(fā)生了什么,小傅哥這里給大家畫張圖,告訴你發(fā)生了什么才讓返回的結(jié)果為0的。
- Mybatis 的處理過(guò)程可以分為兩個(gè)大部分來(lái)看,一部分是解析,另外一部分是使用。解析的時(shí)候把 Mapper XML 中的 insert 標(biāo)簽語(yǔ)句解析出來(lái),同時(shí)解析 selectKey 標(biāo)簽。最終解析完成后,把解析的語(yǔ)句信息使用 MappedStatement 映射語(yǔ)句類存放起來(lái)。便于后續(xù)在 DefaultSqlSession 執(zhí)行操作的時(shí)候,可以從 Configuration 配置項(xiàng)中獲取出來(lái)使用。
- 那么這里有一個(gè)非常重要的點(diǎn),就是執(zhí)行 insert 插入的時(shí)候,里面還包含了一句查詢的操作。那也就是說(shuō),我們會(huì)在一次 Insert 中,包含兩條執(zhí)行語(yǔ)句。重點(diǎn):bug就發(fā)生在這里,為什么呢?因?yàn)樽铋_(kāi)始這兩條語(yǔ)句執(zhí)行的時(shí)候,在獲取鏈接的時(shí)候,每一條都是獲取一個(gè)新的鏈接,那么也就是說(shuō),insert xxx、select LAST_INSERT_ID() 在兩個(gè) connection 連接執(zhí)行時(shí),其實(shí)是不對(duì)的,沒(méi)法獲取到插入后的索引 ID,只有在一個(gè)鏈接或者一個(gè)事務(wù)下(一次 commit)才能有事務(wù)的特性,獲取插入數(shù)據(jù)后的自增ID。
- 而因?yàn)檫@部分最開(kāi)始手寫 JdbcTransaction 實(shí)現(xiàn) Transaction 接口獲取連接的時(shí)候,每一次都是新的鏈接,代碼塊如下;
- 這里的鏈接獲取,最開(kāi)始沒(méi)有 if null 的判斷,每次都是直接獲取鏈接,所以這種非一個(gè)鏈接下的兩條 SQL 操作,所以必然不會(huì)獲得到正確的結(jié)果,相當(dāng)于只是單獨(dú)執(zhí)行SELECT LAST_INSERT_ID() 所以最終的查詢結(jié)果為 0 了就!你可以測(cè)試把這條語(yǔ)句復(fù)制到 SQL查詢工具中執(zhí)行
三、震驚:同一個(gè)坑
但其實(shí)就這么一個(gè)鏈接的問(wèn)題,在小傅哥手寫Spring中也同樣遇到過(guò)。
在 Spring 中有一部分是關(guān)于事務(wù)的處理,其實(shí)這些事務(wù)的操作也是對(duì) JDBC 的包裝操作,依賴于數(shù)據(jù)源獲得的鏈接來(lái)管理事務(wù)。而我們通常使用 Spring 也是結(jié)合著 Mybatis 配置上數(shù)據(jù)源的方式進(jìn)行使用,那么在一個(gè)事務(wù)下操作多個(gè) SQL 語(yǔ)句的時(shí)候,是怎么獲得同一個(gè)鏈接的呢。因?yàn)閺纳厦????的案例中,我們得知保證事務(wù)的特性,需要在同一個(gè)鏈接下,即使是操作多條SQL
由于多個(gè)SQL的操作,已經(jīng)是相當(dāng)于每次都獲取一個(gè)新的 Session 有一個(gè)新的鏈接從連接池中獲得,但為了能達(dá)到事務(wù)的特性,所以在需要有事務(wù)操作下的多個(gè) SQL 前需要開(kāi)啟事務(wù)操作,無(wú)論是手動(dòng)還是注解。
而這個(gè)事務(wù)的開(kāi)啟動(dòng)作處理做一些事務(wù)傳播行為和隔離級(jí)別的限制,其實(shí)更重要的是讓多個(gè) SQL 的執(zhí)行獲取的鏈接,需要是同一個(gè)。所以這里就引入了 ThreadLocal 基于它在同一個(gè)線程操作下保存信息的同步特性,其實(shí)這里的從事務(wù)下獲取的鏈接,其實(shí)就是保存到 TransactionSynchronizationManager#resources 屬性中的。
雖然就這么一小塊內(nèi)容,但在小傅哥最開(kāi)始手寫Spring的時(shí)候,也是給漏下了。直到到測(cè)試的時(shí)候,才發(fā)現(xiàn)鏈接發(fā)現(xiàn)事務(wù)總是不成功,最初還以為是整個(gè)切面邏輯沒(méi)有切進(jìn)去或者是我的操作方式有誤。直到逐步排查調(diào)試代碼,發(fā)現(xiàn)原來(lái)多個(gè)SQL的執(zhí)行竟然不是獲得的同一個(gè)鏈接,所以也就沒(méi)法讓事務(wù)生效。
四、常見(jiàn):事務(wù)失效
可能就是這么一個(gè)小小的鏈接問(wèn)題,有時(shí)候就會(huì)引起一堆的異常,如果說(shuō)我們沒(méi)有學(xué)習(xí)過(guò)源碼,那么可能也不知道這樣的問(wèn)題到底是如何發(fā)生的。所以往往深入的研究和探索,才能讓你解釋一個(gè)問(wèn)題的時(shí)候,更加簡(jiǎn)單直接。
那么你說(shuō),事務(wù)失效的原因還有哪些?- 分享一些常見(jiàn),如果你還有遇到其他的,可以發(fā)到評(píng)論區(qū)一起看看。
- 數(shù)據(jù)庫(kù)引擎不支持事務(wù):這里以 MySQL 為例,其 MyISAM 引擎是不支持事務(wù)操作的,InnoDB 才是支持事務(wù)的引擎,一般要支持事務(wù)都會(huì)使用 InnoDB。https://dev.mysql.com/doc/refman/8.0/en/storage-en... 從 MySQL 5.5.5 開(kāi)始的默認(rèn)存儲(chǔ)引擎是:InnoDB,之前默認(rèn)的都是:MyISAM,所以這點(diǎn)要值得注意,底層引擎不支持事務(wù)再怎么搞都是白搭。
- 方法不是 public 的:來(lái)自 Spring 官方文檔【W(wǎng)hen 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 的方法上,否則事務(wù)不會(huì)失效,如果要用在非 public 方法上,可以開(kāi)啟 AspectJ 代理模式。
- 沒(méi)有被 Spring 管理:// @Service - 這里被注釋掉了 public class OrderServiceImpl implements OrderService { @Transactional public void placeOrder(Order order) { // ... } }。
- 數(shù)據(jù)源沒(méi)有配置事務(wù)管理器:一般來(lái)自于自研的數(shù)據(jù)庫(kù)路由組件@Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
- 異常被吞了。catch 后直接吃了,事務(wù)異常無(wú)法回滾。同時(shí)要配置上對(duì)應(yīng)的異常@Transactional(rollbackFor = Exception.class)
五、總結(jié):學(xué)習(xí)經(jīng)驗(yàn)
很多類似這樣的技術(shù)問(wèn)題,都是來(lái)自于小傅哥對(duì)源碼的學(xué)習(xí),最開(kāi)始是遇到問(wèn)題的時(shí)候去翻看源碼,雖然很多時(shí)候也很難把整個(gè)邏輯捋順,但一點(diǎn)點(diǎn)的積累確實(shí)會(huì)讓研發(fā)人員對(duì)技術(shù)有更加夯實(shí)的認(rèn)知。
那么在現(xiàn)在我之所以去手寫Spring、手寫Mybatis,也是希望通過(guò)把這樣的知識(shí)全部整理處理,從中學(xué)習(xí)復(fù)雜邏輯的設(shè)計(jì)方案、設(shè)計(jì)原則和如何運(yùn)用設(shè)計(jì)模式解決復(fù)雜場(chǎng)景的問(wèn)題。PS:通常我們的業(yè)務(wù)代碼復(fù)雜度很難到這個(gè)程度,所以在見(jiàn)過(guò)”天“后,以后所承接的業(yè)務(wù)就很容易做設(shè)計(jì)了。
另外就是對(duì)各類技術(shù)細(xì)節(jié)的把控,以及積累于這樣的經(jīng)驗(yàn)把相關(guān)技術(shù)設(shè)計(jì)運(yùn)用到一些類似 SpringBoot Starter 等的開(kāi)發(fā),只有類似這樣的廣度、高度、深度,才能真的把個(gè)人的研發(fā)能力提升起來(lái)。