日常开发中,当遇到数据的批量插入和更新等问题时经常会使用到 JDBC 的 executeBatch()方法,来实现批量执行语句的功能。近期项目组在开发过程中,遇到了一个奇怪的现象,使用了 executeBatch()方法进行批量数据处理,在测试过程中发现数据库(MySQL8.0.18)端日志收到的请求 SQL 仍是逐条收到的,并不是预期的批量收到请求批量执行。

为验证此问题,进行如下测试。通过循环的方式分别批量执行 3 条 update 语句,调试方式查看执行情况。发现数据库端日志,三条 update 是分别收到的,如图 1 所示,数据库端收到的 SQL 均有时间间隔。在详细调试应用端请求,发现通过 executeBatch()方法实际与数据库进行了 3 次交互,每次仅发送了一条 SQL 请求(如图 2 所示)。

String sql = "update test set name='test' where id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
for (int i = 1; i < n ;i++) {
  ps.setInt(1, i);
  ps.addBatch();
}
ps.executeBatch();

|500

图 1 批量执行 3 条 SQL 数据库端日志

|500
图 2 批量执行 3 条 SQL 的详细报文中交互情况

这是什么原因?executeBatch()没生效?批量方法使用有问题?带着这些问题查阅 MySQL 驱动源码(版本 8.0.18)一探究竟。分别查阅 Statement 和 PrepareStatement 的 executeBatch()实现方式,发现要达到批量方式执行效果,二者均对关键属性 rewriteBatchedStatements 进行了判断。还可以发现当 PrepareStatement 中含有空语句,或实际批量执行的 SQL 数量未大于 3 条(使用 Statement 时未大于 4 条),MySQL 驱动仍将继续按照单条 SQL 的方式进行执行,而非批量执行。因此,在 JDBC 连接串中增加该参数配置,如:

jdbc:mysql://ip:端口/mytest?rewriteBatchedStatements=true

设置后增多批量 SQL 执行数量时(如循环添加 4 条 SQL),数据库端收到的 SQL 请求变为批量收到。详细调试应用端请求时,发现应用端实际与数据库交互变为 1 次(如图 5 所示),发送的报文请求是整体待执行的 SQL(如图 6 所示)。SQL 请求终于成功实现批量执行。

|500

图 3  Statement 实现的 SQL 批量执行逻辑

|500

图 4  PrepareStatement 现的 SQL 批量执行逻辑

|500

图 5 批量执行 4 条 SQL 数据库端日志

|500

图 6 批量执行 4 条 SQL 的详细报文中交互情况

除 rewriteBatchedStatements 参数外,驱动源码中对 allowMultiQueries 也进行了判断。该参数是否影响批量处理和执行呢?经查阅官网 allowMultiQueries 不影响 addBatch()和 executeBatch()的执行,该配置控允许使用“;”在一条语句中分隔多个查询

|500

图 7 allowMultiQueries 参数

若未设置该参数,MySQL 驱动层将尝试向数据库发送开启多语句支持的请求,并在最终执行完成后,再次向数据库发送关闭多语句支持的请求。额外的开启和关闭多语句支持的请求动作,可能对一些进行了分布式事务进行多语句提交优化的分布式数据库造成影响。

因此,作为最佳实践,建议在 jdbc 参数中 rewriteBatchedStatements 和 allowMultiQueries=true 同时设置。

|500

图 8 开启 MultiQueries

|475

图 9 发送开启 MultiQueries 请求

|500

图 10 关闭 MultiQueries

|500

图 11 发送关闭 MultiQueries 请求

结论:

使用 JDBC 的 executeBatch()批量执行方法,需要 JDBC 参数需设置 rewriteBatchedStatements=true,并且批量执行的数据大于 3 条,并不包含空语句时。驱动将以批量的形式进行交互。并且建议设置 allowMultiQueries=true 参数,可减少对数据库的额为请求。