你好呀,我是歪歪。
這期給大家分享一個讀者給我分享的一個關於 MyBatis 的「程式設計小技巧」,說真的,這騷操作,直接把我看得一愣一愣的。
我更情願叫它:坑你沒商量之埋雷大法。
為了讓你絲滑入戲,我還是先給你搞個 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」等等這些變數名而血壓上升,氣大傷身。