對比 MyBatis 和 MyBatis-Plus 批次插入、批次更新的效能和區別

2023-09-06 12:00:30

1 環境準備

demo 地址:learn-mybatis · Sean/spring-cloud-alibaba - 碼雲(gitee.com)

1.1 搭建 MyBatis-Plus 環境

  1. 建立 maven springboot 工程
  2. 匯入依賴:web 啟動器、jdbc、、java 連線 mysql、Lombok、druid 連線池啟動器、mybatis-plus 啟動器
  3. 編寫 yaml 組態檔,設定 druid 資料來源、mybatis-plus

注意要點:

  1. mapper 介面繼承 BaseMapper<實體類>
  2. service 介面繼承 IService<實體類>
  3. service 介面實現類繼承 ServiceImpl<mapper 介面, 實體類> 實現 service 介面

1.2 批次插入測試介面

  1. MyBatis 批次插入介面
     @GetMapping("/mybatis-batch-insert")
     public String mybatisBatchInsert(){
       // 開始時間
       long stime = System.currentTimeMillis();    
       // 批次插入
       orderService.mySaveBatch(list);
       // 結束時間
       long etime = System.currentTimeMillis();
       return "mybatis 批次插入 1w 條資料的時間: " + (etime - stime) / 1000.0 + "秒";
     }
    
    Mybatis 的批次插入是呼叫 mapper 的批次插入介面,使用 標籤拼接 sql 進行插入
  2. Mybatis-Plus 批次插入介面
    @GetMapping("/mybatis-batch-insert")
    public String mybatisBatchInsert(){
        // 開始時間
        long stime = System.currentTimeMillis();
        // 批次插入
        orderService.mySaveBatch(list);
        // 結束時間
        long etime = System.currentTimeMillis();
        return "mybatis 批次插入 1w 條資料的時間: " + (etime - stime) / 1000.0 + "秒";
    }
    
    MyBatis-Plus 的批次插入是呼叫 mybatis-plus 的 IService 介面的 saveBatch 進行批次插入

1.3 批次更新測試介面

  1. MyBatis 批次更新介面

    @GetMapping("/mybatis-batch-update")
    public String mybatisBatchUpdate(){
        long stime = System.currentTimeMillis();
        orderService.myUpdateBatchById(updateList);
        long etime = System.currentTimeMillis();
        return "mybatis 批次更新 1w 條資料的時間: " + (etime - stime) / 1000.0 + "秒";
    }
    

    MyBatis 的批次插入是呼叫 mapper 的批次更新介面,使用 標籤拼接 sql 進行更新,是將多個更新語句拼接在同一個 mapper 介面中,需要在資料庫連線 url 新增 allowMultiQueries=true 開啟多查詢

  2. MyBatis-Plus 批次更新介面

    @GetMapping("/mybatis-plus-batch-update")
    public String mybatisPlusBatchUpdate(){
        long stime = System.currentTimeMillis();
        orderService.updateBatchById(updateList);
        long etime = System.currentTimeMillis();
        return "mybatis plus 批次更新 1w 條資料的時間: " + (etime - stime) / 1000.0 + "秒";
    }
    

    MyBatis -Plus 的批次更新是呼叫 mybatis-plus 的 IService 介面的 updateBatchById 進行批次更新

2. 效能對比

經過預熱,儘量避免了快取的影響。

2.1 批次插入效能對比

資料量:1w 條資料,每條資料 4 個欄位

測試結果:

  • MyBatis:5 次執行平均耗時:0.3212 秒
  • MyBatis-Plus:5次執行平均耗時:1.35 秒
  • MyBatis-Plus(開啟批次處理):5次執行平均耗時:0.2424 秒

2.2 批次更新效能對比

資料量:1w 條資料,每條資料更新 4 個欄位

測試結果:

  • MyBatis:5 次執行平均耗時:1.0378 秒
  • MyBatis-Plus:5次執行平均耗時:1.6448 秒
  • MyBatis-Plus(開啟批次處理):5次執行平均耗時:0.743 秒

3. 原理探究

3.1 批次插入

3.1.1 MyBatis

MyBatis 的批次插入 xml

<insert id="mySaveBatch">
    insert into order_table(id, product_id, total_amount, status) values
    <foreach collection="list" separator="," item="item">
        (#{item.id}, #{item.productId}, #{item.totalAmount}, #{item.status})
    </foreach>
</insert>

通過 標籤,將插入的資料拼接成一條 SQL 語句,一次性進行插入操作,拼接完的 SQL 語句如下例子:

insert into order_table(id, product_id, total_amount, status) values(1, 2. 2.0, 1),(2, 2, 2.0, 1);

3.1.2 MyBatis-Plus

MyBatis-Plus 的批次插入本質採用 for 遍歷每條資料依次插入,但使用了批次處理優化,預設是每 1000 條資料,重新整理一次 statement 提交到資料庫,執行插入操作。

注意:批次處理需要在資料庫連線中新增 rewriteBatchedStatements=true 否則 jdbc 驅動在預設情況下會無視executeBatch() 語句

原始碼如下:

@Transactional(rollbackFor = Exception.class)   // 開啟事務
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
    String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);    // 獲得插入 statement
    // 執行批次處理操作
    // 引數是:待插入集合,批次大小(預設1000),函數式介面 accept
    return executeBatch(entityList, batchSize, (sqlSession, entity) ->  sqlSession.insert(sqlStatement, entity)); 
}
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
    Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
    return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {
        int size = list.size();
        int idxLimit = Math.min(batchSize, size);
        int i = 1;
        // 遍歷插入
        for (E element : list) {
            // 呼叫 sqlSession.insert(sqlStatement, entity)); 
            // 對元素執行插入操作,但此時資料庫還沒真正執行插入語句
            consumer.accept(sqlSession, element);   
            // 計數達到 1000 或者 集合結束
            if (i == idxLimit) {        
                 // 重新整理 statement 提交批次處理語句,資料庫真正執行 SQL
                sqlSession.flushStatements();
                idxLimit = Math.min(idxLimit + batchSize, size);
            }
            i++;
        }
    });
}

3.2 批次更新

3.2.1 MyBatis

MyBatis 的批次更新 xml

<update id="myUpdateBatchById">
    <foreach collection="list" item="item" index="index" open="" close="" separator=";">
        update order_table
        <set>
            product_id = #{item.productId},
            total_amount = #{item.totalAmount},
            status = #{item.status}
        </set>
        where id = #{item.id}
    </foreach>
</update>

通過 標籤,拼接成多條更新的 SQL 語句,一次性提交資料庫執行。語句例子如下:

update order_table set product_id = 1, total_amount = 2, status = 1 where id = 1;
update order_table set product_id = 1, total_amount = 2, status = 1 where id = 2;

3.1.2 MyBatis-Plus

MyBatis-Plus 批次更新的原理基本和其批次插入的原理一致,都是呼叫 executeBatch 執行批次處理操作。

4. 優缺點分析

4.1 批次插入

對於批次插入,MyBatis 拼接 SQL 的寫法比 MyBatis-Plus 的批次插入方法有明顯更高的效能。
但在開啟批次處理優化之後,MyBatis-Plus 的方法效能更高了。

MyBatis:

  • 優點:效能較高
  • 缺點:在處理巨量資料量(SQL 語句大於 64MB 時)會報錯,MySQL 8.0.33 預設最大 SQL 大小為 64MB
    也要考慮記憶體異常等問題。

MyBatisPlus:

  • 優點:分組提交,適用於巨量資料量的處理
  • 缺點:單條插入語句執行,效能較低

總結:

  • 沒開啟批次處理時:10w 資料量以下建議使用 MyBatis、10w 資料量以上建議使用 MyBatis-Plus
  • 開啟批次處理時:建議使用 MyBatis-Plus

4.2 批次更新

對於批次更新,MyBatis 拼接 SQL 的寫法與 MyBatis-Plus 的批次更新方法無明顯的效能差別.
大於巨量資料量,拼接寫法甚至容易出現記憶體耗盡等問題,相比之下 MyBatis-Plus 的方法更優。

總結:建議使用 MyBatis-Plus