我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第 1 篇文章,点击查看活动详情
前言
最近写代码由于业务逻辑的原因,会经常出现对数据的批量插入的操作。 所以在写代码插入数据的时候, Java 开发人员在必须执行 INSERT、UPDATE 和 DELETE 语句时使用接口的 executeUpdate 方法,从 Java 1.2 开始,该Statement接口一直提供addBatch我们可以用来批处理多个语句的接口 ,原本以为这种批量更新的方法会很高效,避免了一条一条的去插入数据,但是实际发现这样在数据量相对大的情况下也是比较慢的。
关于saveBatch方法
看了源码发现,保存有两个方法,一个是save()方法一个是saveBatch()方法,save方法是去调BaseMapper封装的单条保存数据的insert()方法,而 saveBatch方法有两个入参,一个是对于批量更新传入的一个列表集合,第二参数是默认的批量插入数量1000,也就是说默认一个批次会插入1000条数据。
接着看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被调用了
并且该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批量实现高效插入
在数据源配置的url后面追加rewriteBatchedStatements=true
然后我们通过循环向List列表里添加3000条需要保存的对象,调用saveBatch方法,同时我们打印出执行方法前后的时间,算出需要执行多少时间。
上面是没有加rewriteBatchedStatements=true的执行结果,下面添加后的执行结果,由此可见执行的效率差距还是比较大的。
总结
当我们使用批量保存的时候,将 rewriteBatchedStatements 与 JDBC PreparedStatement 批处理一起使用,能更加提高我们处理批次保存的效率。