聊聊 JDBC 的 executeBatch || 对比下不同数据库对 JDBC batch 的实现细节

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 聊聊 JDBC 的 executeBatch || 对比下不同数据库对 JDBC batch 的实现细节

大家好,我是明哥!

上篇博文,“对比下 datax 的 OceanBase/MYSQL 不同数据同步方案的效率差异 || 聊聊参数 rewriteBatchedStatements” 发表后,有小伙伴问到不同数据库对 JDBC 批量更新的实现细节,所以通过本片博文,我们系统看下 jdbc batch 相关知识。

1. JDBC batch 为什么能提高性能?

通过查看源码可知,JDBC1.2 引入了 Batch 功能,涉及的API主要有以下四个:

  • java.sql.Statement#addBatch
  • java.sql.PreparedStatement#addBatch
  • java.sql.Statement#executeBatch
  • java.sql.Statement#clearBatch

640.png640.png640.png640.png


JDBC 引入上述 batch 功能的主要目的,是加快对客户端SQL的执行和响应速度,并进而提高数据库整体并发度,而 jdbc batch 能够提高对客户端SQL的执行和响应速度,其主要原理有:

  • 减少了JDBC客户端和数据库服务器之间网络传输的开销:使用 batch 功能前,每提交一个SQL,都需要一次网络IO开销,且提交后需要等待服务端返回结果后,才能提交下一个SQL;而使用 batch 功能后,客户端的多个SQL是一起提交给服务器的,只涉及到一次网络IO开销(single database round trip),其示意图如下:


640.png640.png


  • 当batch底层使用的是静态SQL并参数化执行时(JAVA中一般是使用类java.sql.PreparedStatement 来参数化执行静态SQL),数据库服务器可以只做一次解析:利用对参数化机制的支持,数据库服务器仅需要对 PreparedStatement 做一次解析(sql parse),即可传入不同参数执行该 batch 中所有的 SQL;
  • 网上有个帖子,详细对比了不同场景下,不同数据库的插入和更新性能的差异,可以看出,ORACLE/PG/MYSQL 使用 batch 功能后,性能都有了3-5被的提高:



640.png640.png640.png


2. JDBC batch 的使用场景和限制有哪些?

  • batch 功能对 statement 和 PreparedStatement 都有效,但为了避免 SQL 注入的风险,不推荐使用动态SQL,而是推荐使用静态 SQL 和绑定变量(也就是使用 PreparedStatement 和 stored procedures);
  • 从上述JDBC的API源码可以看出,batch 功能对所有SQL 都有效, 包括 SELECT/INSERT/UPDATE/DELETE,但由于使用 batch 功能后,返回值是 int[] 数组,不太方便获取 batch 底层每个sql的执行结果,所以大家一般不会对 SELECT 语句使用 batch 功能 (毕竟select查询的目的是获得每个select语句的结果resultSet),而只会在大量 INSERT/UPDATE/DELETE 的场景下,尤其是大批量插入的场景下,使用 batch 功能,所以大家提到 batch时,常说“批量更新”;(数据仓库数据湖等涉及到数据集成和ETL的场景,经常会使用到批量插入);
  • 另外需要注意的是,使用 batch 功能并不代表所有的 SQL 都在一个事务里:在 autocommit 模式下,after each statement you have created, the database will ensure that the result persists correctly before moving on to the next statement,if the nth sentence of a batch raises a constraint exception, all previously inserted rows will not be rollbacked;

3. 不同数据库对 JDBC batch 的实现有何异同?

  • 由于 batch 功能是通过 JDBC API 定义的,是 JDBC 规范的一部分,所以所有实现了 JDBC 接口的数据库驱动,都需要实现这些接口,所以 mysql/oracle/pg/db2/sqlserver 等所有提供了 JDBC 接口的数据库,原则上都支持 jdbc batch 功能;
  • 但不同数据库对这些接口的具体实现是不同的,所以其最终效果和使用细节会有些差异,甚至相同数据库驱动的不同版本,也可能会有所差异,所以用户在使用前,需要查看下对应数据库的说明,不能想当然地认为,某个数据驱动的参数,也同样使用于其它数据库驱动;
  • 这里重点指出下,jdbc batch 相关参数,mysql 有参数 rewriteBatchedStatements,postgresql 有参数 reWriteBatchedInserts;

4. 详解 mysql 的参数 rewriteBatchedStatements

  • MySQL JDBC 驱动虽然实现了各个 JDBC batch api,但默认情况下,其对 batch 功能的支持仅仅是语法层面的支持,并没有真正通过 batch 功能提升性能:即使用户代码中编写了 executeBatch(), mysql 仍会把用户期望批量执⾏的⼀组sql语句拆散,逐条发给MySQL数据库,所以 mysql 的批量插入实际上是单条插入,性能较低;(By default, the MySQL Jdbc driver ignores the executeBatch () statement, disassembles a set of SQL statements that we expect to execute in batches, and sends them to the MySQL database one by one, which directly causes lower performance.)
  • 比如以下批量插入测试代码,虽然代码中使用了batch api ps.executeBatch(),但通过 wireshark 在代码执行时抓包查看,可以发现底是将批量执⾏的⼀组sql语句拆散并逐条发给数据库服务器的,也就是说 mysql 的批量插入实际上是单条插入:

640.png

640.png


  • 测试代码如下:
/**
     * mysql batch insert, without rewriteBatchedStatements=true in the url
     */
    public static void testMysqlBatch1(){
        try{
            Class<?> mysqlDriverClass = Class.forName("com.mysql.cj.jdbc.Driver");
            Connection con=DriverManager.getConnection("jdbc:mysql://10.23.2.215:33061/hs_cic","root","hundsun");
            String sql = "insert into test_liming (AIR_CODE, AIR_NAME) values (?,?)";
            PreparedStatement ps = con.prepareStatement(sql);
            for (int i = 0; i < 10; i++) {
                ps.setString(1,Integer.toString(i));
                ps.setString(2,Integer.toString(i));
                ps.addBatch();
            }
            int[] results = ps.executeBatch();
            for (int i = 0; i < results.length; i++) {
                System.out.println("results:" + results[i]);
            }
            con.close();
        }catch(Exception e){ System.out.println(e);}
    }
  • Mysql 提供了其特有的 JDBC 连接参数 rewriteBatchedStatements,当把该参数置为 true 时, mysql jdbc 驱动会在客户端重写用户提交的原始 SQL,并将重写后的 SQL “send the batched statements in a single request”;
  • 比如以下批量插入测试代码,代码中使用了batch api ps.executeBatch(),且 url中指定了rewriteBatchedStatements=true, 通过 wireshark 在代码执行时抓包查看,可以发现底是将批量执⾏的⼀组 sql Insert 语句,改写为一条 batched 语句 insert into tableA (colA,colB) values (colA-value1,colB-value1),(colA-value2,colB-value2),(colA-value3,colB-value3), 并通过一次请求发送给数据库服务器的,也就是说此时 mysql 使用了批量插入功能;

640.png


  • 测试代码如下:
/**
     * mysql batch insert, with rewriteBatchedStatements=true in the url
     */
    public static void testMysqlBatch2(){
        try{
            Class<?> mysqlDriverClass = Class.forName("com.mysql.cj.jdbc.Driver");
            Connection con=DriverManager.getConnection("jdbc:mysql://10.23.2.215:33061/hs_cic?rewriteBatchedStatements=true","root","hundsun");
            String sql = "insert into test_liming (AIR_CODE, AIR_NAME) values (?,?)";
            PreparedStatement ps = con.prepareStatement(sql);
            for (int i = 0; i < 10; i++) {
                ps.setString(1,Integer.toString(i));
                ps.setString(2,Integer.toString(i));
                ps.addBatch();
            }
            int[] results = ps.executeBatch();
            for (int i = 0; i < results.length; i++) {
                System.out.println("results:" + results[i]);
            }
            con.close();
        }catch(Exception e){ System.out.println(e);}
    }
  • 经笔者测试总结,对批量插入的ps.executeBatch(),mysql jdbc 驱动,会改写批量中的一组sql为一条 “multi-values” 语句,并一次性提交给数据库服务器:
  • batchInsert(10 records) 会被改写为 "insert into t (…) values (…), (…), (…)” 并一次性提交;
  • 如果不能被改写为 "multi-values", 则会改写为多个;分割的sql语句并一次性提交:语句 “INSERT INTO TABLE(col1) VALUES (?) ON DUPLICATE KEY UPDATE col2=?” 与变量 [1,2] 和 [2,3],会被改写为 “INSERT INTO TABLE(col1) VALUES (1) ON DUPLICATE KEY UPDATE col2=2;INSERT INTO TABLE(col1) VALUES (3) ON DUPLICATE KEY UPDATE col2=4” 并一次性提交;
  • 经笔者测试总结,对批量删除和批量更新的ps.executeBatch(),mysql jdbc 驱动,会改写SQL语句,改写为多条;分割的 SQL 语句,并一次性提交给数据库服务器:
  • batchDelete(10 records) 会被改写为 "delete from t where id = 1; delete from t where id = 2; delete from t where id = 3;…."并一次性提交;
  • batchUpdate(10 records) 会被改写为 “update t set… where id = 1; update t set… where id = 2; update t set… where id = 3…” 并一次性提交;
  • 有的小伙伴,可能会有疑问,为什么 MYSQL 不像别的数据库,比如 oracle 和 postgresql 那样,默认支持 batch 功能呢?为什么必须手动指定参数 rewriteBatchedStatements=true,才会支持通过改写SQL 支持batch 功能呢?笔者也不是很清楚,不过大概猜测如下:
  • 改写后的SQL语句,很多时候并不只是简单地通过符号”;”来分割和追加原始的多个SQL,这有时候并不是我们期望的;
  • 并不是所有的 SQL 语句都能被很好地改写;
  • 当部分语句的执行可能会出错时,错误处理不太方便,查看 update counts 也不太方便;
  • 可能还有其它笔者不知道的原因。

最后总结 rewriteBatchedStatements 如下:

  • MySQL JDBC driver defines the rewriteBatchedStatements connection property, so that statements get rewritten into a single String buffer;
  • Without setting this property, the MySQL driver simply executes each DML statement separately, therefore defeating the purpose of batching;
  • property rewriteBatchedStatements may be specified in either the connection URL or an additional Properties object parameter to DriverManager.getConnection;
  • In order to fetch the auto-generated row keys, the batch must contain insert statements only;
  • For PreparedStatement, this property rewrites the batched insert statements into a multi-value insert;
  • the driver is not able to use server-side prepared statements when enabling rewriting;

5. 抓个包看看,批量插入时 oracle 底层的 SQL 语句

作为跟 Mysql 的对比,我们测试下 oracle 中批量插入时,抓包获取的SQL语句。

  • oracle批量插入时,抓包内容如下,可以看到,底层使用了静态SQL和绑定变量:

640.png


  • oracle批量插入,底层使用了ps.executeBatch,测试代码如下:
public static void testOracleBatch(){
        try{
            Class<?> driverManagerClass = Class.forName("java.sql.DriverManager");
            Class<?> driverClass = Class.forName("java.sql.Driver");
            Class<?> oracleDriverClass = Class.forName("oracle.jdbc.driver.OracleDriver");
          Connection con=DriverManager.getConnection("jdbc:oracle:thin:@//10.20.155.75:1521/pdb19","hs_cic_cda","hundsun");
            String sql = "insert into test_liming (AIR_CODE, AIR_NAME) values (?,?)";
            PreparedStatement ps = con.prepareStatement(sql);
            for (int i = 0; i < 10; i++) {
                ps.setString(1,Integer.toString(i));
                ps.setString(2,Integer.toString(i));
                ps.addBatch();
            }
            int[] results = ps.executeBatch();
            for (int i = 0; i < results.length; i++) {
                System.out.println("results:" + results[i]);
            }
            con.close();
        }catch(Exception e){ System.out.println(e);}
    }

6. 介绍下 postgreSql 的参数 reWriteBatchedInserts

  • postgreSql 跟 oracle 一样,默认都是支持 jdbc batch 功能的(这点跟MYSQL不同!);
  • 但为了进一步优化性能,pg 在9.4.1208 版本后,又提供了参数 reWriteBatchedInserts,该参数默认值为 FALSE;

640.png


  • 当参数 reWriteBatchedInserts 为true时,pgjdbc 会将批量的 “insert into ... values(?, ?)” 改写为 “insert into ... values(?, ?), (?, ?)” ;
  • 这样的改写的好处是:减少了每个 statement 的开销;
  • 这样的改写的坏处是:如果某个语句执行失败的话, 整个 batch 都会失败; The default value is false.
  • 比如某次线上案列,查看pg日志,开启reWriteBatchedInserts前后日志如下:
未开启参数reWriteBatchedInserts的日志:
LOG:  execute S_2: insert into post (title, id) values ($1, $2)
DETAIL:  parameters: $1 = 'Post no. 1', $2 = '1'
LOG:  execute S_2: insert into post (title, id) values ($1, $2)
DETAIL:  parameters: $1 = 'Post no. 2', $2 = '2'
开启参数reWriteBatchedInserts的日志:
LOG:  execute <unnamed>: insert into post (title, id) values ($1, $2),($3, $4),($5, $6),($7, $8),($9, $10),($11, $12),($13, $14),($15, $16)
DETAIL:  parameters: $1 = 'Post no. 1', $2 = '1', $3 = 'Post no. 2', $4 = '2', $5 = 'Post no. 3', $6 = '3', $7 = 'Post no. 4', $8 = '4', $9 = 'Post no. 5', $10 = '5', $11 = 'Post no. 6', $12 = '6', $13 = 'Post no. 7', $14 = '7', $15 = 'Post no. 8', $16 = '8'

参考链接:

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
SQL Java 关系型数据库
JDBC数据库的连接
JDBC数据库的连接
34 0
|
5天前
|
SQL druid Java
JDBC&数据库连接池
JDBC&数据库连接池
|
6天前
|
SQL Java 数据库连接
Java从入门到精通:2.3.1数据库编程——学习JDBC技术,掌握Java与数据库的交互
ava从入门到精通:2.3.1数据库编程——学习JDBC技术,掌握Java与数据库的交互
|
27天前
|
SQL Java 数据库连接
使用JDBC进行数据库操作:Java Web开发的数据库连接
【4月更文挑战第3天】Java Web开发中,JDBC是与数据库交互的关键,提供统一访问关系型数据库的规范。核心组件包括DriverManager、Connection、Statement和ResultSet。使用流程涉及加载驱动、建立连接、创建Statement、执行SQL及处理结果,最后关闭资源。最佳实践包括使用try-with-resources、PreparedStatement、事务管理等。在Web开发中,JDBC用于用户认证、数据持久化、检索和事务管理。虽然有ORM工具,但掌握JDBC基础仍然重要。
|
2月前
|
安全 Java 数据库连接
jdbc实现批量给多个表中更新数据(解析Excel表数据插入到数据库中)
jdbc实现批量给多个表中更新数据(解析Excel表数据插入到数据库中)
155 0
|
3月前
|
SQL druid Java
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)
30 0
|
3月前
|
SQL Java 关系型数据库
数据库-----JDBC技术
数据库-----JDBC技术
211 0
|
4月前
|
SQL Java 数据库连接
JDBC增删改查案例讲解
JDBC增删改查案例讲解
19 0
|
4月前
|
SQL Java 数据库连接
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)(下)
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)
370 1
|
4月前
|
SQL Java 数据库连接
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)(中)
JDBC技术【分页查询、数据库连接池、应用程序分层、封装通用的BaseDao】(四)-全面详解(学习总结---从入门到深化)
28 0