mybatis plus很好,但是我被它坑了!

waynaqua發表於2023-10-31

作者今天在開發一個後臺傳送訊息的功能時,由於需要給多個使用者傳送訊息,於是使用了 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() 方法用於儲存通知訊息以及使用者通知記錄。執行邏輯如下,

  1. 儲存通知訊息
  2. 根據使用者 id,組裝使用者通知記錄集合,返回 200 條使用者通知記錄
  3. 批次儲存使用者通知記錄集合

前兩步驟耗時都很少,我們直接看第三步操作耗時,結合 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 plus很好,但是我被它坑了!

 

可以看出,我們批次儲存使用者通知記錄是一條一條儲存得,已經可以猜測就是批次插入方法導致耗時較高。

這裡使用 mybatis log free 外掛,它可以自動幫我們在控制檯列印完整得 mybatis sql 語句。有需要可以在 idea 外掛中心搜尋 mybatis log free 下載安裝。

結合 saveBatch() 底層原始碼也能夠看出,mybatis plus 對於批次操作是在 executeBatch() 方法內使用 for 迴圈執行插入操作得,原始碼如下圖,

mybatis plus很好,但是我被它坑了!

 

mybatis plus很好,但是我被它坑了!

 

到這裡我們應該也能猜出了在測試環境執行較快得原因,因為在測試環境需要批次儲存得使用者通知記錄比較少,只有幾條記錄,所以很快。但是上預釋出後,由於預釋出中需要批次儲存得使用者通知記錄比較多達到了數百條,所以執行較慢,耗時達到了 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 上新增了引數後,看看效果,如下圖,

mybatis plus很好,但是我被它坑了!

 

可以看到 jdbcurl 新增了 rewriteBatchedStatements=true 引數後,批次操作的執行耗時已經只有 200 毫秒,自此也就解決了 mybatis plus 提供的 saveBatch() 方法執行耗時較高得問題。

總結

mybatis plus 給開發人員帶來了很多便利,但是其中也有一些坑點,比如上文所提到得批次操作耗時問題,如果不注意的話,就有可能調入坑裡,各位開發同學可以檢查自己或者公司專案中 jdbcurl 是否缺失 rewriteBatchedStatements=true 引數,加以改正,避免重複掉入這個坑裡。

相關文章