角度新奇!第一次看到這樣使用MyBatis的,看得我一愣一愣的。

你好呀,我是歪歪。

這期給大家分享一個讀者給我分享的一個關於 MyBatis 的“編程小技巧”,說真的,這騷操作,直接把我看得一愣一愣的。

我更情願叫它:坑你沒商量之埋雷大法。

Demo

爲了讓你絲滑入戲,我還是先給你搞個 Demo。

因爲要使用到 MyBatis 嘛,所以我們先搞兩個表。

一個表叫做 product 表,表結構非常簡單:

另一個表叫做 order_info 表,表結構也非常簡單:

看到這兩個表出現的時候,你就知道我的場景是啥了,肯定是賣貨嘛。

庫存減一,訂單加一。

大家再熟悉不過的場景了。

分分鐘能寫出這樣的僞代碼:

public void saleProduct(){
    //更新庫存,庫存減一
    productMapper.updateProductCount();
    //保存訂單信息
    orderInfoMapper.saveOrderInfo();
}

當然了,這個僞代碼你一眼就能看出問題:減庫存和保存訂單應該是一個事務操作,所以應該把這兩個動作包裹在事務裏面。

於是我們的僞代碼變成了這樣:

public void saleProduct() {
    //開啓事務
    begin;
    //更新庫存,庫存減一
    Boolean updateSuccess = productMapper.updateProductCount();
    //保存訂單信息
    orderInfoMapper.saveOrderInfo();
    if (updateSuccess) {
        //提交事務
        commit;
    } else {
        //回滾事務
        rollback
    }
}

當時讀者給我舉例的時候,完全是另外一個場景,和賣貨完全沒有任何關係。

讀者舉的例子大概是幾個表之間有關聯關係,如果一個表的某條數據被刪除了,另外幾個表裏面對應的數據也要刪除,還有一個表需要更新狀態。

爲了更好的展示這個“編程小技巧”,我才把場景簡化到了前面提到的賣貨的樣子。

前面說的是僞代碼。

現在我給你展示一下用“編程小技巧”寫出來的真實的代碼。

首先是 controller 接口:

@GetMapping("/sale")
public void sale() {
    productMapper.selaProduct();
}

然後是這個 productMapper 的 selaProduct 接口:

是的,你沒有看錯,這就是一個 MyBatis 的 mapper 接口,接下來就直接到了 mapper.xml 文件裏面:

這寫法,這小技巧,我都不打算問你騷不騷,我就問你見沒見過?

能用嗎?

歪師傅還是太年輕,見識不夠,在這之前從來沒見過在 mapper.xml 裏面能這樣去寫 sql 的。

不說見過,在我的小腦袋裏面,我是壓根就沒想過這樣去寫。所以看到這個寫法的第一反應是:這能行嗎?這不行吧?

於是,秉承着大膽假設小心求證的態度,寫了上面的 Demo。

項目啓動之後發起調用,控制檯直接報了錯:

看到這個報錯的時候,我下意識的覺得就是 MyBatis 不支持這樣的寫法,直接報錯了,這也符合我之前的認知。

但是,在讀者的指導下,他提醒我在數據庫連接的配置上加上這樣的配置:

allowMultiQueries=true

我的 Demo 啓動的時候,確實沒有加這個配置。但是看到這個配置的一瞬間,我開始覺得有點意思了。

因爲我知道這個配置是幹嘛的。

見名知意嘛:allow Multi Queries,允許進行多個查詢。

最常用的場景就是用 foreach 標籤來進行批量插入或者更新的時候會用到這個配置。

在這個參數的加持下,前面 mapper.xml 裏面的寫的那個 sql,很有可能就能正常執行了。

因爲加入這個配置之後,可以在一個數據庫連接中執行多個 sql 語句,而對於 MyBatis 或者 MySQL 的驅動來說,它並不區這“多個 sql”都是 insert 語句還是 update 語句,或者是混合着都有的語句。

我也去 MySQL 官網上查詢了這個配置的含義:

https://dev.mysql.com/doc/connector-j/8.1/en/connector-j-connp-props-security.html#cj-conn-prop_allowMultiQueries

對於這個參數,官網上就一句話:

Allow the use of ";" to delimit multiple queries during one statement. This option does not affect the 'addBatch()' and 'executeBatch()' methods, which rely on 'rewriteBatchStatements' instead.
允許在一條語句中使用"; "分隔多個查詢。該選項不會影響 "addBatch() "和 "executeBatch() "方法,因爲它們依賴於 "rewriteBatchStatements"。

在介紹 allowMultiQueries 的時候,還提到了一個 rewriteBatchStatements 參數。

關於這個參數是幹啥的,我這裏就不展開描述了,我只能說這兩個玩意是一套組合拳,裏面也大有文章,如果你不知道,建議你去了解一下。

就當是課後習題了。

我們還是先跟着主幹走。

當我在數據庫連接上追加配置 allowMultiQueries=true 之後,重啓了服務。

再次發起調用。

爲了表示我的震驚,我給你搞個動圖:

庫存減一,訂單加一,方法執行成功了。

還真 TM 能用,你說這事搞的,實屬是開了眼了。

這波漲知識了,屬於未曾設想過的道路。

埋雷

千萬別這樣寫!

聽歪師傅一句勸,千萬別這樣寫!

首先這樣的寫法就不符合絕大部分程序員的認知。

試問誰能想到最後的 mapper.xml 裏面,並不只是簡簡單單的 sql,裏面居然還埋在一坨業務邏輯呢?

關鍵是這樣寫也埋雷啊。

舉個簡單的例子,這樣的寫法,完全沒有考慮庫存是否足夠的情況:

比如,當前庫存沒有了,按照這樣的寫法,還是會在 order_info 表裏面插入一條數據。

超賣了,朋友。

只有 commit,沒有考慮回滾的情況。

而且這樣寫根本就完全不可能考慮超賣的情況,因爲你拿不到扣減庫存的操作是否執行成功,從而無法判斷是需要 commit 還是 rollback。

什麼,你問我能不能寫存儲過程來判斷?

能,MyBatis 確實可以調用存儲過程。

首先,存儲過程還是得在 MySQL 裏面寫好,MyBatis 只是發起調用。

其次,趕緊打消你這個越走越遠的騷想法,老老實實的寫 Java 代碼來解決這個問題,它不香嗎?

什麼,你又問我如果是不需要判斷前一條 sql 是否執行成功的場景呢?

比如我前面提到的讀者舉的例子,幾個表之間有關聯關係,如果一個表的某條數據被刪除了,另外幾個表裏面對應的數據也要刪除,還有一個表需要更新狀態。

大概是這樣的:

begin;
delete from table1 where user_id=xxx;
delete from table2 where user_id=xxx;
delete from table3 where user_id=xxx;
update table4 set user_status=1 where user_id=xxx;
commit;

和賣貨的場景不一樣的是,在這個場景下如果每個 sql 執行成功,則代表業務執行成功。

看起來,似乎沒什麼問題。

但是我問你一個問題:這一組 SQL 一定會走都 commit 嗎?

你好好想想?

肯定不一定嘛,保不齊執行的過程中出什麼幺蛾子。

舉個最簡單的例子,表寫錯了:

在這個場景下,再次發起調用:

程序報錯說找不到這個表。

那麼請問:此時,訂單表是否應該有數據被插入?

出異常了,肯定不應該有數據插入。我看了數據庫,確實也沒有新數據插入。

看起來確實沒問題。

那麼再請問:在這種寫法的情況下,當前這個事務是被回滾了還是被提交了?

。。。

。。。

。。。

正確答案是被掛起了。

通過執行下面這個 SQL,我們可以獲取到當前事務列表:

SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

通過查詢結果可以發現,在我們程序拋出異常之後,當前事務還在 RUNNING 狀態:

而且,這個事務在服務重啓之前,將一直在 RUNNING 狀態,即被掛起了。

但僅從程序的角度看,拋出異常,沒有數據,符合預期,沒有任何毛病。

埋雷了。

所以,聽歪師傅一句勸,千萬別這樣寫!

老老實實的寫大家都看得懂的 Java 代碼,不要在 mapper.xml 裏面搞事情。

擴展

其實我覺得吧,前面都屬於卵用不大的知識點,因爲大家一般都不會這樣去寫。

但是既然都寫到這裏了,場景也有了,我也給大家擴展一個稍微有點用的知識。

還是在賣貨的場景下。

訂單加一,庫存減一是這樣的。

begin;
INSERT INTO order_info(`buy_name`, `buy_goods`) VALUES ('歪師傅''ipad pro頂配版');
update product set product_count=product_count-1 where id=1 and product_count>0;
commit;

而庫存減一,訂單加一是這樣的:

begin;
update product set product_count=product_count-1 where id=1 and product_count>0;
INSERT INTO order_info(`buy_name`, `buy_goods`) VALUES ('歪師傅''ipad pro頂配版');
commit;

都是包裹在事務裏面,爲了簡化代碼,我們假設庫存非常夠用,先不考慮 rollback 的場景。

請問是“訂單加一,庫存減一”的性能好,還是“庫存減一,訂單加一”的性能好,還是說這二者沒有什麼區別?

首先,從執行結果上看,這二者確實是沒有什麼區別的,都能保證業務場景的正確性。

但是當你考慮性能的時候,肯定是“訂單加一,庫存減一”的性能更好。

如果你沒想明白的話,我給你一個簡單的提示:在業務正確的前提下,加鎖的代碼越靠近解鎖的代碼,是不是性能越好?

如果你還沒想明白的話,我再給你一個提示:庫存減一,它會加鎖嗎?你不管它是加表鎖、間隙鎖還是記錄鎖,我就問你它加不加鎖?

如果你還沒反應過來的話,說明你對於 MySQL 的加鎖機制掌握的有點薄弱,可以去加固一下。

我直接公佈答案了:

update product set product_count=product_count-1 where id=1 and product_count>0;

因爲 where 條件中是 id=1,所以鎖是加在唯一索引上的,而且表中存在該記錄,所以只會對 id=1 這行記錄加鎖。

針對 id=1 這一個產品來說,如果它是一個熱點商品,我們採取“訂單加一,庫存減一”的寫法,性能會更高一點。

因爲在加鎖頻率相同的情況下,解鎖越快的,性能越高。

上個圖你就明白了:

調換一個 SQL 的事兒,性能就上去了,我就問你舒不舒服?

最後,再說個不相關的:

我在文章最開始的地方給了這樣的一個圖片:

你不覺得彆扭嗎?

sela 是什麼鬼?

很明顯,這個地方是一個單純的拼寫錯誤,想要打出的單詞是 sale:

請問,當你在程序裏面看到這樣的拼寫的時候,你會怎麼辦?

如果是我,我會主動把 selaProduct 修改爲 saleProduct,其他什麼都不會動。

這就是我在之前的文章中提到的一個編碼規則,童子軍軍規:

修改一個拼寫錯誤的方法名、變量名,在代碼裏面也是一件很重要的小事。

這不是代碼潔癖,這是基本的職業道德。

因爲你也不想下一個接手你代碼的人,因爲看到一堆叫做“succeess、createTiem、lastUpdataBy、bussinessDate、proudectName”等等這些變量名而血壓上升,氣大傷身。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章