Mybatis Plus saveBatch批量插入如何高效

10,444 阅读2分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第 1 篇文章,点击查看活动详情

前言

最近写代码由于业务逻辑的原因,会经常出现对数据的批量插入的操作。 所以在写代码插入数据的时候, Java 开发人员在必须执行 INSERT、UPDATE 和 DELETE 语句时使用接口的 executeUpdate 方法,从 Java 1.2 开始,该Statement接口一直提供addBatch我们可以用来批处理多个语句的接口 ,原本以为这种批量更新的方法会很高效,避免了一条一条的去插入数据,但是实际发现这样在数据量相对大的情况下也是比较慢的。

关于saveBatch方法

BFF7B225-0BA7-4303-BAF9-2ABF547A0F7E.png

看了源码发现,保存有两个方法,一个是save()方法一个是saveBatch()方法,save方法是去调BaseMapper封装的单条保存数据的insert()方法,而 saveBatch方法有两个入参,一个是对于批量更新传入的一个列表集合,第二参数是默认的批量插入数量1000,也就是说默认一个批次会插入1000条数据。

5585C7FA-5415-4E2D-9B2E-3AF2D579A4DA.png

B79FB460-5105-4102-98DB-49A8D52CB64C.png

接着看saveBatch方法里面最终也是调了executeBatch方法,看到executeBatch方法你又会大吃一惊,**这不也是循环的去拼接单条insert语句嘛,没错底层就是循环拼接insert语句,当一个批次数量到达1000条的时候会交给mysql执行。

MySQL rewriteBatchedStatements 配置属性原理

下面我们通过 MySQL JDBC 驱动程序代码进行查看批次插入

String INSERT = "insert into post (id, title) values (%1$d, 'Post no. %1$d')";
 
try(Statement statement = connection.createStatement()) {
    for (long id = 1; id <= 10; id++) {
        statement.addBatch(
            String.format(INSERT, id)
        );
    }
    statement.executeBatch();
}

将单个insert语句提交到 statement

if (this.rewriteBatchedStatements.getValue() && nbrCommands > 4) {
    return executeBatchUsingMultiQueries(
        multiQueriesEnabled,
        nbrCommands,
        individualStatementTimeout
    );
}
 
updateCounts = new long[nbrCommands];
 
for (int i = 0; i < nbrCommands; i++) {
    updateCounts[i] = -3;
}
 
int commandIndex = 0;
 
for (commandIndex = 0; commandIndex < nbrCommands; commandIndex++) {
    try {
        String sql = (String) batchedArgs.get(commandIndex);
        updateCounts[commandIndex] = executeUpdateInternal(sql, true, true);
         
        ...
    } catch (SQLException ex) {
        updateCounts[commandIndex] = EXECUTE_FAILED;
 
        ...
    }

因为rewriteBatchedStatements ,每个 INSERT 语句将使用方法调用false单独执行。executeUpdateInternal 因此,即使我们使用addBatchand ,默认情况下,MySQL 在使用普通 JDBC对象executeBatch时仍会单独执行 INSERT 语句。Statement

但是,如果我们启用rewriteBatchedStatementsJDBC 配置属性:

MysqlDataSource dataSource = new MysqlDataSource();
 
String url = “jdbc:mysql://localhost/high_performance_java_persistence?useSSL=false;
dataSource.setURL(url);
dataSource.setUser(username());
dataSource.setPassword(password());
dataSource.setRewriteBatchedStatements(true);

现在执行executeBatch方法,我们会看见executeBatchUsingMultiQueries被调用了

92F6427B-69FF-4D0A-B380-D12AB53B7CE0.png

并且该executeBatchUsingMultiQueries方法会将各个 INSERT 语句连接到 aStringBuilder并运行单个execute调用:

StringBuilder queryBuf = new StringBuilder();
 
batchStmt = locallyScopedConn.createStatement();
JdbcStatement jdbcBatchedStmt = (JdbcStatement) batchStmt;
 
…
int argumentSetsInBatchSoFar = 0;
 
for (commandIndex = 0; commandIndex < nbrCommands; commandIndex++) {
    String nextQuery = (String) this.query.getBatchedArgs().get(commandIndex);
    …
    queryBuf.append(nextQuery);
    queryBuf.append(“;”);
    argumentSetsInBatchSoFar++;
}
 
if (queryBuf.length() > 0) {
    try {
        batchStmt.execute(queryBuf.toString(), java.sql.Statement.RETURN_GENERATED_KEYS);
    } catch (SQLException ex) {
        sqlEx = handleExceptionForBatch(
            commandIndex - 1, argumentSetsInBatchSoFar, updateCounts, ex
        );
    }
 
    …
}

因此,对于普通的 JDBCStatement批处理,MySQLrewriteBatchedStatements配置属性将附加当前批处理的语句并在单个数据库往返中执行它们。 上面对rewriteBatchedStatements原理介绍也是我查询资料才知道的,引用vladmihalcea.com/mysql-rewri…

这里我的理解是使用 rewriteBatchedStatements=true 时,JDBC 会将尽可能多的查询打包到单个网络数据包中,从而降低网络开销。

saveBatch批量实现高效插入

64B8D06B-920C-44EF-9E5E-B27EC974D111.png 在数据源配置的url后面追加rewriteBatchedStatements=true

56C9E61A-6108-4605-A9D5-929173697CF7.png 然后我们通过循环向List列表里添加3000条需要保存的对象,调用saveBatch方法,同时我们打印出执行方法前后的时间,算出需要执行多少时间。

5415681F-E57E-4505-AA94-0B8B7DA3F964.png

BFA98150-6177-4CF6-B672-C7DB931EE5F8.png

上面是没有加rewriteBatchedStatements=true的执行结果,下面添加后的执行结果,由此可见执行的效率差距还是比较大的。

总结

当我们使用批量保存的时候,将 rewriteBatchedStatements 与 JDBC PreparedStatement 批处理一起使用,能更加提高我们处理批次保存的效率。