【MyBatis】saveBatch 性能調優

来源:https://www.cnblogs.com/natee/archive/2023/05/24/17428877.html
-Advertisement-
Play Games

最近在壓測一批介面,發現介面處理速度慢的有點超出預期,感覺很奇怪,後面定位發現是資料庫批量保存這塊很慢。 這個項目用的是 mybatis-plus,批量保存直接用的是 mybatis-plus 提供的 saveBatch。 我點進去看了下源碼,感覺有點不太對勁: 繼續追蹤了下,從這個代碼來看,確實是 ...


最近在壓測一批介面,發現介面處理速度慢的有點超出預期,感覺很奇怪,後面定位發現是資料庫批量保存這塊很慢。 

這個項目用的是 mybatis-plus,批量保存直接用的是 mybatis-plus 提供的 saveBatch。 我點進去看了下源碼,感覺有點不太對勁:

 繼續追蹤了下,從這個代碼來看,確實是 for 迴圈一條一條執行了 sqlSession.insert,下麵的 consumer 執行的就是上面的 sqlSession.insert:

 然後累計一定數量後,一批 flush。從這點來看,這個 saveBach 的性能肯定比直接一條一條 insert 快。

我直接進行一個粗略的實驗,簡單創建了一張表來對比一波!

 

1、1000條數據,一條一條插入

@Test
void MybatisPlusSaveOne() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("mybatis plus save one");
        for (int i = 0; i < 1000; i++) {
            OpenTest openTest = new OpenTest();
            openTest.setA("a" + i);
            openTest.setB("b" + i);
            openTest.setC("c" + i);
            openTest.setD("d" + i);
            openTest.setE("e" + i);
            openTest.setF("f" + i);
            openTest.setG("g" + i);
            openTest.setH("h" + i);
            openTest.setI("i" + i);
            openTest.setJ("j" + i);
            openTest.setK("k" + i);
            //一條一條插入
            openTestService.save(openTest);
        }
        sqlSession.commit();
        stopWatch.stop();
        log.info("mybatis plus save one:" + stopWatch.getTotalTimeMillis());
    } finally {
        sqlSession.close();
    }
}

 可以看到,執行一批 1000 條數的批量保存,耗費的時間是 121011 毫秒。

 

2、1000條數據用 mybatis-plus 自帶的 saveBatch 插入

@Test
void MybatisPlusSaveBatch() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        List<OpenTest> openTestList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            OpenTest openTest = new OpenTest();
            openTest.setA("a" + i);
            openTest.setB("b" + i);
            openTest.setC("c" + i);
            openTest.setD("d" + i);
            openTest.setE("e" + i);
            openTest.setF("f" + i);
            openTest.setG("g" + i);
            openTest.setH("h" + i);
            openTest.setI("i" + i);
            openTest.setJ("j" + i);
            openTest.setK("k" + i);
            openTestList.add(openTest);
        }
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("mybatis plus save batch");
        //批量插入
        openTestService.saveBatch(openTestList);
        sqlSession.commit();
        stopWatch.stop();
        log.info("mybatis plus save batch:" + stopWatch.getTotalTimeMillis());
    } finally {
        sqlSession.close();
    }
}

 耗費的時間是 59927 毫秒,比一條一條插入快了一倍,從這點來看,效率還是可以的。

然後常見的還有一種利用拼接 SQL 方式來實現批量插入,我們也來對比試試看性能如何。

3、1000 條數據用手動拼接 SQL 方式插入, 搞個手動拼接:

 來跑跑下性能如何:

@Test
void MapperSaveBatch() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        List<OpenTest> openTestList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            OpenTest openTest = new OpenTest();
            openTest.setA("a" + i);
            openTest.setB("b" + i);
            openTest.setC("c" + i);
            openTest.setD("d" + i);
            openTest.setE("e" + i);
            openTest.setF("f" + i);
            openTest.setG("g" + i);
            openTest.setH("h" + i);
            openTest.setI("i" + i);
            openTest.setJ("j" + i);
            openTest.setK("k" + i);
            openTestList.add(openTest);
        }
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("mapper save batch");
        //手動拼接批量插入
        openTestMapper.saveBatch(openTestList);
        sqlSession.commit();
        stopWatch.stop();
        log.info("mapper save batch:" + stopWatch.getTotalTimeMillis());
    } finally {
        sqlSession.close();
    }
}

 耗時只有 2275 毫秒,性能比 mybatis-plus 自帶的 saveBatch 好了 26 倍!

這時,我又突然回想起以前直接用 JDBC 批量保存的介面,那都到這份上了,順帶也跑跑看!

4、1000 條數據用 JDBC executeBatch 插入

@Test
void JDBCSaveBatch() throws SQLException {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    Connection connection = sqlSession.getConnection();
    connection.setAutoCommit(false);

    String sql = "insert into open_test(a,b,c,d,e,f,g,h,i,j,k) values(?,?,?,?,?,?,?,?,?,?,?)";
    PreparedStatement statement = connection.prepareStatement(sql);
    try {
        for (int i = 0; i < 1000; i++) {
            statement.setString(1,"a" + i);
            statement.setString(2,"b" + i);
            statement.setString(3, "c" + i);
            statement.setString(4,"d" + i);
            statement.setString(5,"e" + i);
            statement.setString(6,"f" + i);
            statement.setString(7,"g" + i);
            statement.setString(8,"h" + i);
            statement.setString(9,"i" + i);
            statement.setString(10,"j" + i);
            statement.setString(11,"k" + i);
            statement.addBatch();
        }
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("JDBC save batch");
        statement.executeBatch();
        connection.commit();
        stopWatch.stop();
        log.info("JDBC save batch:" + stopWatch.getTotalTimeMillis());
    } finally {
        statement.close();
        sqlSession.close();
    }
}

 

耗時是 55663 毫秒,所以 JDBC executeBatch 的性能跟 mybatis-plus 的 saveBatch 一樣(底層一樣)。

綜上所述,拼接 SQL 的方式實現批量保存效率最佳

但是我又不太甘心,總感覺應該有什麼別的法子,然後我就繼續跟著 mybatis-plus 的源碼 debug 了一下,跟到了 MySQL 的驅動,突然發現有個 if 裡面的條件有點顯眼:

 就是這個叫 rewriteBatchedStatements 的玩意,從名字來看是要重寫批操作的 Statement,前面batchHasPlainStatements 已經是 false,取反肯定是 true,所以只要這參數是 true 就會進行一波操作。

我看了下預設是 false。

 同時我也上網查了下 rewriteBatchedStatements 參數,好家伙,好像有用!

 直接將 jdbcurl 加上了這個參數:

 然後繼續跑了下 mybatis-plus 自帶的 saveBatch,果然性能大大提高,跟拼接 SQL 差不多!

 順帶我也跑了下 JDBC 的 executeBatch ,果然也提高了。

 然後我繼續 debug ,來探探 rewriteBatchedStatements 究竟是怎麼 rewrite 的! 如果這個參數是 true,則會執行下麵的方法且直接返回:

 看下 executeBatchedInserts 究竟幹了什麼:

 看到上面我圈出來的代碼沒,好像已經有點感覺了,繼續往下 debug。

果然!SQL 語句被 rewrite了:

 對插入而言,所謂的 rewrite 其實就是將一批插入拼接成 insert into xxx values (a),(b),(c)...這樣一條語句的形式然後執行,這樣一來跟拼接 SQL 的效果是一樣的。

那為什麼預設不給這個參數設置為 true 呢?主要有以下兩點:

如果批量語句中的某些語句失敗,則預設重寫會導致所有語句都失敗。

批量語句的某些語句參數不一樣,則預設重寫會使得查詢緩存未命中。

看起來影響不大,所以我給我的項目設置上了這個參數!

最後

稍微總結下我粗略的對比(雖然粗略,但實驗結果符合原理層面的理解),如果你想更準確地做實驗,可以使用 JMH,並且測試更多組數(如 5000,10000等)的情況。

 

所以如果有使用 JDBC 的 Batch 性能方面的需求,要將 rewriteBatchedStatements 設置為 true,這樣能提高很多性能。

然後如果喜歡手動拼接 SQL 要註意一次拼接的數量,分批處理。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在前端開發過程中,我們在設置預設值的時候,會經常用到 || 和 ?? , 但是這兩者有什麼區別呢?分別在什麼場景下麵使用呢,會有哪些坑呢,今天我們來梳理下呢。 ## || 的語法作用 console.log(null || 1) //輸出 1 console.log(undefined || 1) ...
  • 在 HTML 中,屬性(Attribute)和屬性(Property)是用於描述 HTML 元素的相關特性的術語。 屬性(Attribute)是指在 HTML 標簽中聲明的附加信息。它們以鍵值對的形式出現,用於提供元素的初始狀態或配置選項。屬性的名稱是不區分大小寫的,並且值可以是字元串或布爾值。 例 ...
  • ### GC 優化 #### 1.防止大對象Buffer到記憶體中 **現象**:當大包請求時,YGC 耗時嚴重 **原因**:預設情況下 Zuul2 並不會緩存請求體(DirectByteBuffer),也就意味著它會先發送接收到的請求 Headers 到後端服務,之後接收到請求體再繼續發送到後端服 ...
  • 一、什麼是微服務 微服務是一種技術架構,通常我們可以把它理解為一組可以相互之間協同工作的應用程式或服務,這些應用程式或服務能夠被單獨部署到不同的伺服器中,並且能夠自主運行和維護。 微服務技術只是一個名稱而已,或許我們在日常工作中已經或多或少在使用其中的一種或幾種技術和架構,但我們並沒有將其稱之為微服 ...
  • 汽車之家電商系統誕生在2014年,成長於2016~2019年,並經歷多年雙11、818晚會的洪峰考驗,沉澱了穩定可靠、性能卓越的線上交易能力。隨著業務中台的建設浪潮興起,2019年進入中台化建設階段,輸出其在汽車電商領域五年沉澱的能力,助力汽車電商行業發展,加速企業數字化轉型! ...
  • ## 初步瞭解 ### 總體架構設計 Mybatis 整體框架如下: ![img](https://zhangjiahao-blog.oss-cn-beijing.aliyuncs.com/picgo/202305161021323.png) ##### 介面層 MyBatis 和資料庫的交互有兩種 ...
  • # Java語法基礎 ## 註釋 註釋是對代碼的解釋和說明文字,可以提高程式的可讀性,因此在程式中添加必要的註釋文字十分重要。Java中的註釋分為三種: 單行註釋。單行註釋的格式是使用//,從//開始至本行結尾的文字將作為註釋文字。 ~~~java // 這是單行註釋文字 ~~~ 多行註釋。多行註釋 ...
  • ## 教程簡介 Apache Kafka由Scala寫成。Kafka最初是由LinkedIn開發,並於2011年初開源。2012年10月從Apache Incubator畢業。該項目的目標是為處理實時數據提供一個統一、高通量、低等待的平臺。 Kafka是一個分散式的、分區的、多複本的日誌提交服務。它 ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...