为什么Spring的jdbcTemplate.batchUpdate()这么慢?

我正试图找到更快的批量插入方法

我尝试使用jdbcTemplate.update(String sql)插入几个批处理,其中sql由StringBuilder 构建 ,如下所示:

INSERT INTO TABLE(x, y, i) VALUES(1,2,3), (1,2,3), ... , (1,2,3) 

批量大小正好是1000.我插入了近100批。 我使用StopWatch检查了时间并找出了插入时间:

 min[38ms], avg[50ms], max[190ms] per batch 

我很高兴,但我想让我的代码变得更好。

之后,我尝试使用jdbcTemplate.batchUpdate,如:

  jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { // ... } @Override public int getBatchSize() { return 1000; } }); 

sql的样子

 INSERT INTO TABLE(x, y, i) VALUES(1,2,3); 

我很失望! jdbcTemplate以分开的方式执行1000行批处理的每个插入。 我在mysql_log上找到了,发现有一千个插入。 我使用StopWatch检查了时间并找出了插入时间:

min [900ms],avg [1100ms],每批最大[2000ms]

那么,任何人都可以向我解释一下,为什么jdbcTemplate在这个方法中做了单独的插入? 为什么方法的名称是batchUpdate ? 或者我可能是以错误的方式使用这种方法?

JDBC连接URL中的这些参数可以对批处理语句的速度产生很大影响 – 根据我的经验,它们可以加快速度:

?useServerPrepStmts =假rewriteBatchedStatements =真

请参阅: JDBC批处理插入性能

我也遇到了与Spring JDBC模板相同的问题。 可能在Spring Batch中,语句在每个插件或块上执行并提交,这会减慢速度。

我用原始JDBC批处理插入代码替换了jdbcTemplate.batchUpdate()代码,并找到了主要的性能改进

 DataSource ds = jdbcTemplate.getDataSource(); Connection connection = ds.getConnection(); connection.setAutoCommit(false); String sql = "insert into employee (name, city, phone) values (?, ?, ?)"; PreparedStatement ps = connection.prepareStatement(sql); final int batchSize = 1000; int count = 0; for (Employee employee: employees) { ps.setString(1, employee.getName()); ps.setString(2, employee.getCity()); ps.setString(3, employee.getPhone()); ps.addBatch(); ++count; if(count % batchSize == 0 || count == employees.size()) { ps.executeBatch(); ps.clearBatch(); } } connection.commit(); ps.close(); 

检查此链接以及JDBC批量插入性能

将您的sql插入更改为INSERT INTO TABLE(x, y, i) VALUES(1,2,3) 。 框架为您创建一个循环。 例如:

 public void insertBatch(final List customers){ String sql = "INSERT INTO CUSTOMER " + "(CUST_ID, NAME, AGE) VALUES (?, ?, ?)"; getJdbcTemplate().batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { Customer customer = customers.get(i); ps.setLong(1, customer.getCustId()); ps.setString(2, customer.getName()); ps.setInt(3, customer.getAge() ); } @Override public int getBatchSize() { return customers.size(); } }); } 

如果你有这样的事情。 Spring会做类似的事情:

 for(int i = 0; i < getBatchSize(); i++){ execute the prepared statement with the parameters for the current iteration } 

框架首先从查询( sql变量)创建PreparedStatement,然后调用setValues方法并执行语句。 重复的次数与在getBatchSize()方法中指定的次数相同。 因此,编写insert语句的正确方法是只有一个values子句。 你可以看看http://docs.spring.io/spring/docs/3.0.x/reference/jdbc.html

只需使用交易。 在方法上添加@Transactional。

如果使用多个数据源@Transactional(“dsTxManager”),请务必声明正确的TX管理器。 我有一个插入60000记录的情况。 大约需要15秒。 没有其他调整:

 @Transactional("myDataSourceTxManager") public void save(...) { ... jdbcTemplate.batchUpdate(query, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ... } @Override public int getBatchSize() { if(data == null){ return 0; } return data.size(); } }); } 

我不知道这是否适合你,但这是我最终使用的无弹簧方式。 它比我尝试的各种Spring方法快得多。 我甚至尝试使用其他答案描述的JDBC模板批量更新方法,但即使这样比我想要的慢。 我不确定这笔交易是什么,互联网也没有多少答案。 我怀疑它与如何处理提交有关。

这种方法只是使用java.sql包和PreparedStatement的批处理接口的直接JDBC。 这是我能够将24M记录导入MySQL数据库的最快方法。

我或多或少只是构建了“记录”对象的集合,然后在批量插入所有记录的方法中调用下面的代码。 构建集合的循环负责管理批量大小。

我试图将24M记录插入到MySQL数据库中,并且使用Spring批处理每秒约200条记录。 当我切换到这种方法时,它每秒达到约2500条记录。 所以我的24M记录负载从理论上的1.5天增加到大约2.5小时。

首先创建一个连接……

 Connection conn = null; try{ Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(connectionUrl, username, password); }catch(SQLException e){}catch(ClassNotFoundException e){} 

然后创建一个预准备语句并使用批量值插入它,然后作为单个批处理插入执行…

 PreparedStatement ps = null; try{ conn.setAutoCommit(false); ps = conn.prepareStatement(sql); // INSERT INTO TABLE(x, y, i) VALUES(1,2,3) for(MyRecord record : records){ try{ ps.setString(1, record.getX()); ps.setString(2, record.getY()); ps.setString(3, record.getI()); ps.addBatch(); } catch (Exception e){ ps.clearParameters(); logger.warn("Skipping record...", e); } } ps.executeBatch(); conn.commit(); } catch (SQLException e){ } finally { if(null != ps){ try {ps.close();} catch (SQLException e){} } } 

显然我已经删除了error handling,查询和Record对象是名义上的等等。

编辑:由于您的原始问题是将插入值与foobar值(?,?,?),(?,?,?)…(?,?,?)方法与Spring批处理进行比较,这里有一个更直接的响应:

看起来您的原始方法可能是将批量数据加载到MySQL而不使用“LOAD DATA INFILE”方法的最快方法。 来自MysQL文档的引用( http://dev.mysql.com/doc/refman/5.0/en/insert-speed.html ):

如果要同时从同一客户端插入多行,请使用包含多个VALUES列表的INSERT语句一次插入多行。 与使用单独的单行INSERT语句相比,这要快得多(在某些情况下要快很多倍)。

您可以修改Spring JDBC Template batchUpdate方法,以便根据每个’setValues’调用指定多个VALUES进行插入,但是在迭代插入的一组内容时,您必须手动跟踪索引值。 当插入的内容总数不是您准备好的语句中的VALUES列表数的倍数时,最后会遇到一个令人讨厌的边缘情况。

如果你使用我概述的方法,你可以做同样的事情(使用带有多个VALUES列表的预准备语句),然后当你到达最后的边缘情况时,它更容易处理,因为你可以构建和执行最后一个声明具有恰当数量的VALUES列表。 它有点hacky,但大多数优化的东西都是。

我发现在调用中设置argTypes数组有一个重大改进

在我的例子中,使用Spring 4.1.4和Oracle 12c,插入包含35个字段的5000行:

 jdbcTemplate.batchUpdate(insert, parameters); // Take 7 seconds jdbcTemplate.batchUpdate(insert, parameters, argTypes); // Take 0.08 seconds!!! 

argTypes参数是一个int数组,您可以通过这种方式设置每个字段:

 int[] argTypes = new int[35]; argTypes[0] = Types.VARCHAR; argTypes[1] = Types.VARCHAR; argTypes[2] = Types.VARCHAR; argTypes[3] = Types.DECIMAL; argTypes[4] = Types.TIMESTAMP; ..... 

我调试了org \ springframework \ jdbc \ core \ JdbcTemplate.java,发现大部分时间都是在尝试了解每个字段的性质时消耗的,这是针对每条记录进行的。

希望这可以帮助 !