作者今天在開發一個後臺發送消息的功能時,由於需要給多個用戶發送消息,於是使用了 mybatis plus 提供的 saveBatch() 方法,在測試環境測試通過上預發佈後,測試反應發送消息介面很慢得等 5、6 秒,於是我就登錄預發佈環境查看執行日誌,發現是 mybatis plus 提供的 sav ...
作者今天在開發一個後臺發送消息的功能時,由於需要給多個用戶發送消息,於是使用了 mybatis plus 提供的 saveBatch() 方法,在測試環境測試通過上預發佈後,測試反應發送消息介面很慢得等 5、6 秒,於是我就登錄預發佈環境查看執行日誌,發現是 mybatis plus 提供的 saveBatch() 方法執行很慢導致,於是也就有了本篇文章。
mybatis plus 是一個流行的 ORM 框架,它基於 mybatis,提供了很多便利的功能,比如代碼生成器、通用 CRUD、分頁插件、樂觀鎖插件等。它可以讓我們更方便地操作資料庫,減少重覆的代碼,提高開發效率。
註意:本文所使用的 mybatis plus 版本是 3.5.2 版本。
案發現場還原
/**
* 先保存通知消息,在批量保存用戶通知記錄
*/
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveNotice(Notify notify, String receiveUserIds) {
long begin = System.currentTimeMillis();
notify.setCreateTime(new Date());
notify.setCreateBy(ShiroUtil.getSessionUid());
if (notify.getPublishTime() == null) {
notify.setPublishTime(new Date());
}
boolean insert = save(notify);
List<NotifyRecord> collect = new ArrayList<>();
List<String> receiveUserList = fillNotifyRecordList(notify, receiveUserIds, collect);
notifyRecordService.saveBatch(collect);
long end = System.currentTimeMillis();
System.out.println(end - begin);
...
return insert;
}
/**
* 根據用戶id,組裝用戶通知記錄集合,返回200條記錄
*/
public List<String> fillNotifyRecordList(Notify notify, String receiveUserIds, List<NotifyRecord> collect) {
List<String> noticeRecordList = new ArrayList<>(200);
...
// 組將兩百條用戶通知記錄
return noticeRecordList;
}
如上代碼,我有一個 saveNotice() 方法用於保存通知消息以及用戶通知記錄。執行邏輯如下,
- 保存通知消息
- 根據用戶 id,組裝用戶通知記錄集合,返回 200 條用戶通知記錄
- 批量保存用戶通知記錄集合
前兩步驟耗時都很少,我們直接看第三步操作耗時,結合 sql 執行日誌,如下,
-- slow sql 5542 millis. INSERT INTO oa_notify_record ( notifyId, receiveUserId, receiveUserName, isRead, createTime ) VALUES ( ?, ?, ?, ?, ? )[225,"fcd90fe3990e505d07c90a238f75e9c1","niuwawa",false,"2023-10-30 23:54:04"]
5681
再結合 mybatis free log 插件列印完整 sql 如下圖,
可以看出,我們批量保存用戶通知記錄是一條一條保存得,已經可以猜測就是批量插入方法導致耗時較高。
這裡使用 mybatis log free 插件,它可以自動幫我們在控制台列印完整得 mybatis sql 語句。有需要可以在 idea 插件中心搜索 mybatis log free 下載安裝。
結合 saveBatch() 底層源碼也能夠看出,mybatis plus 對於批量操作是在 executeBatch() 方法內使用 for 迴圈執行插入操作得,源碼如下圖,
到這裡我們應該也能猜出了在測試環境執行較快得原因,因為在測試環境需要批量保存得用戶通知記錄比較少,只有幾條記錄,所以很快。但是上預發佈後,由於預發佈中需要批量保存得用戶通知記錄比較多達到了數百條,所以執行較慢,耗時達到了 5、6 秒之久。
由上述源碼可以看出,mybatis plus 的批量操作底層使用的還是 mybatis 提供的 batch 模式實現批量插入以及更新的。而 mybatis 提供的 batch 模式操作底層使用的還是 jdbc 驅動提供的批量操作模式,jdbc 批量操作示例代碼如下,
public static void main(String[] args) {
Connection conn = null;
PreparedStatement statement = null;
try {
// 資料庫連接
String url = "jdbc:mysql://*************?autoReconnect=true&nullCatalogMeansCurrent=true&failOverReadOnly=false&useUnicode=true&characterEncoding=UTF-8";
String user = "******";
String password = "************";
// 添加批處理參數
// url = url + "&rewriteBatchedStatements=true";
// 載入驅動類
Class.forName("com.mysql.cj.jdbc.Driver");
// 創建連接
conn = DriverManager.getConnection(url, user, password);
// 創建預編譯 sql 對象
statement = conn.prepareStatement("UPDATE table_test_demo set code = ? where id = ?");
long a = System.currentTimeMillis(); // 計時
// 這裡添加 100 個批處理參數
for (int i = 1; i <= 100; i++) {
statement.setString(1, "測試1");
statement.setInt(2, i);
statement.addBatch(); // 批量添加
}
long b = System.currentTimeMillis(); // 計時
System.out.println("添加參數耗時:" + (b-a)); // 計時
int[] r = statement.executeBatch(); // 批量提交
statement.clearBatch(); // 清空批量添加的 sql 命令列表緩存
long c = System.currentTimeMillis(); // 計時
System.out.println("執行sql耗時:" + (c-b)); // 計時
} catch (Exception e) {
e.printStackTrace();
} finally {
// 主動釋放資源
try {
if (statement != null) {
statement.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
- statement.addBatch() 將 sql 語句打包到一個容器中
- statement.executeBatch() 將容器中的 sql 語句提交
- statement.clearBatch() 清空容器,為下一次打包做準備
推薦博主開源的 H5 商城項目waynboot-mall,這是一套全部開源的微商城項目,包含三個項目:運營後臺、H5 商城前臺和服務端介面。實現了商城所需的首頁展示、商品分類、商品詳情、商品 sku、分詞搜索、購物車、結算下單、支付寶/微信支付、收單評論以及完善的後臺管理等一系列功能。 技術上基於最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中間件。分模塊設計、簡潔易維護,歡迎大家點個 star、關註博主。
github 地址:https://github.com/wayn111/waynboot-mall
那麼問題出現在哪裡了?明明已經使用了批量操作,但耗時還是很慢,別急,跟著我往下看。
解決方法
到這裡,也就是本文得重點所在了,那怎麼解決這個問題嘞?如何既利用 mybatis plus 提供得便攜性,也能夠解決批量操作耗時較高得問題。
雖然我們使用了 mybatis plus -> mybatis -> jdbc 這一條批量操作鏈路,但是其實我們還需要在 jdbcurl 上添加一個 rewriteBatchedStatements=true 參數即可解決這個問題。
MySQL 的 JDBC 連接的 url 中要加 rewriteBatchedStatements 參數,並保證 5.1.13 以上版本的驅動,才能實現高性能的批量插入。
MySQL JDBC 驅動在預設情況下會無視 executeBatch()語句,把我們期望批量執行的一組 sql 語句拆散,一條一條地發給 MySQL 資料庫,批量插入實際上是單條插入,直接造成較低的性能。只有把 rewriteBatchedStatements 參數置為 true, 驅動才會幫你批量執行 SQL。另外這個選項對 INSERT/UPDATE/DELETE 都有效。
rewriteBatchedStatements=true 的意思是,當你在 Java 程式中使用批量插入/修改/刪除(batching)時,MySQL JDBC 驅動程式將嘗試重新編寫(rewrite)你的 SQL 語句,以便更有效地執行這些批量插入操作。
OK,在我們給 jdbcurl 上添加了參數後,看看效果,如下圖,
可以看到 jdbcurl 添加了 rewriteBatchedStatements=true 參數後,批量操作的執行耗時已經只有 200 毫秒,自此也就解決了 mybatis plus 提供的 saveBatch() 方法執行耗時較高得問題。
總結
mybatis plus 給開發人員帶來了很多便利,但是其中也有一些坑點,比如上文所提到得批量操作耗時問題,如果不註意的話,就有可能調入坑裡,各位開發同學可以檢查自己或者公司項目中 jdbcurl 是否缺失 rewriteBatchedStatements=true 參數,加以改正,避免重覆掉入這個坑裡。