聊聊 JDBC 的 executeBatch || 对比下不同数据库对 JDBC batch 的实现细节
聊聊 JDBC 的 executeBatch || 对比下不同数据库对 JDBC batch 的实现细节 || 剖析下 Mysql 的 参数 rewriteBatchedStatements || 剖析下 pg 的参数 reWriteBatchedInserts
大家好,我是明哥!
上篇博文,“对比下 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
JDBC 引入上述 batch 功能的主要目的,是加快对客户端SQL的执行和响应速度,并进而提高数据库整体并发度,而 jdbc batch 能够提高对客户端SQL的执行和响应速度,其主要原理有:
减少了JDBC客户端和数据库服务器之间网络传输的开销:使用 batch 功能前,每提交一个SQL,都需要一次网络IO开销,且提交后需要等待服务端返回结果后,才能提交下一个SQL;而使用 batch 功能后,客户端的多个SQL是一起提交给服务器的,只涉及到一次网络IO开销(single database round trip),其示意图如下:
当batch底层使用的是静态SQL并参数化执行时(JAVA中一般是使用类java.sql.PreparedStatement 来参数化执行静态SQL),数据库服务器可以只做一次解析:利用对参数化机制的支持,数据库服务器仅需要对 PreparedStatement 做一次解析(sql parse),即可传入不同参数执行该 batch 中所有的 SQL;
网上有个帖子,详细对比了不同场景下,不同数据库的插入和更新性能的差异,可以看出,ORACLE/PG/MYSQL 使用 batch 功能后,性能都有了3-5被的提高:
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 的批量插入实际上是单条插入:
测试代码如下:
/** * 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 使用了批量插入功能;
测试代码如下:
/** * 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和绑定变量:
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;
当参数 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'
参考链接:
https://leanpub.com/high-performance-java-persistence/read
http://java-persistence-performance.blogspot.com/2013/05/batch-writing-and-dynamic-vs.html
https://www.alibabacloud.com/blog/how-to-write-into-a-database-using-rewritebatchedinserts-parameter_597796
微信公众号主要用来信息的传播和分享,同名知识星球主要用来知识的沉淀和积累! 欢迎大家扫码加入免费知识星球 “明哥的IT随笔”,这是一个围绕泛大数据生态的技术交流社区,可以探讨任何IT技术话题和工作上的问题,一起学习共同进步!
聊聊 JDBC 的 executeBatch || 对比下不同数据库对 JDBC batch 的实现细节相关推荐
- idea如何给oracle添加数据_intelij idea下使用java和JDBC连接oracle数据库及简单的SQL操作...
intelij idea下使用java和JDBC连接oracle数据库及简单的SQL操作 发布时间:2018-07-04 10:09, 浏览次数:2532 , 标签: intelij idea jav ...
- jdbc和mysql做游戏排行榜_MySQL数据库与JDBC编程
欢迎关注公众号:xfxuezhang MySQL数据库与JDBC编程 JDBC (Java Database Connectivity) DDL(Data Definition Language,数据 ...
- mysql jdbc execute_MySQL JDBC Statement.executeBatch实践问题
MySQL JDBC Statement.executeBatch实践(执行效率低) 现在很少使用原生jdbc去实现代码, 最近在测试MySQL批处理数据遇到一个问题: 执行Statement.exe ...
- Java数据库编程(JDBC)-入门笔记
数据库(DB) 简介: • DB: Database = Data + Base • 数据库:数据+库,存放数据的库(场所) • 数据:规范.半规范.不规范数据 • 库 – 一个空间,一个场所 – 停 ...
- web前端,数据库,jdbc
刘国斌 77331283 bjliugb@tedu.cn ----------------web-day01.html ###网站的架构 - CS:Client S ...
- Java小白修炼手册--第四阶段--JDBC(Java Database Connectivity : Java访问数据库的解决方案 )
目录 JDBC原理 JDBC标准 JDBC是什么 使用JDBC优点 JDBC接 口及数据库厂商实现 JDBC工作原理 Driver ( 驱动程序)接口及驱动类加载 Connection( 连接,关 ...
- JAVA数据库编程(JDBC技术)-入门笔记
本菜鸟才介入Java,我现在不急着去看那些基本的语法或者一些Java里面的版本的特征或者是一些晋级的知识,因为有一点.Net的OOP编程思想,所以对于Java的这些语法以及什么的在用到的时候在去发现学 ...
- Linux 下Oracle Client JAVA JDBC 集成点滴
首先Java下根据JDBC规范连接数据库,有几种形式,参考 http://djdnmq.iteye.com/blog/356468 oracle 驱动oci thin 区别 Oracle客户端准备 1 ...
- oracle orm 实例 java_Oracle数据库的JDBC查询实例
作为Java与数据库交互最古老的.最基础的规范,JDBC规范提供了访问底层数据库的接口,其他ORM框架都是在JDBC这块基石上构建的.下面我们看一个基本的JDBC查询例子: 首先在pom.xml中加入 ...
最新文章
- VMware Virtual SAN 互操作性:OpenStack
- Luogu P3975 [TJOI2015]弦论
- strlen() Bug
- C# unicode 编码 和 解码
- 【JAVA】接口中的default和static方法
- 为什么大公司都不用mfc和qt_百度竞价推广效果下降,为什么有的老板还是只愿意做百度推广?...
- 开源编译工具和编译软件
- CentOS 7 根目录分区扩容
- 优班图linux 命令,Linux 常用命令
- 两种前端在线json编辑器方案(无法解决number精度丢失问题)
- python读excel表格数据绘制图表_Python读取Excel数据并生成图表过程解析
- 数据库----如何将oracle语句转换成mysql语句
- DoG算子和LoG算子
- MySQL的登陆【数据库系统】
- 荐书 | 心理学如何编程,看看这9本书
- win服务器系统设置休眠时间,win7系统电脑设置休眠时间的操作方法
- 常微分方程各种类型方程表格汇总
- 阿里云MVP北京闭门会圆满落幕 多把“利剑”助力开发者破阵蜕变...
- Unveiling the java.lang.Out OfMemoryError
- GO connectex: A connection attempt failed because the connected party did not properly respond 已解决