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

use*_*807 21 java mysql spring jdbctemplate spring-batch

我正在尝试找到更快的批量插入方法.

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

INSERT INTO TABLE(x, y, i) VALUES(1,2,3), (1,2,3), ... , (1,2,3)
Run Code Online (Sandbox Code Playgroud)

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

min[38ms], avg[50ms], max[190ms] per batch
Run Code Online (Sandbox Code Playgroud)

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

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

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

sql的样子

INSERT INTO TABLE(x, y, i) VALUES(1,2,3);
Run Code Online (Sandbox Code Playgroud)

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

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

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

小智 16

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

?useServerPrepStmts =假rewriteBatchedStatements =真

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


Rak*_*oni 12

我也遇到过与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();
Run Code Online (Sandbox Code Playgroud)

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


Car*_*sta 9

我发现在调用中设置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!!!
Run Code Online (Sandbox Code Playgroud)

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;
.....
Run Code Online (Sandbox Code Playgroud)

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

希望这可以帮助 !


小智 8

只需使用交易.在方法上添加@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();
            }
        });
    }
Run Code Online (Sandbox Code Playgroud)

  • 我也在连接URL中使用BatchPreparedStatementSetter和rewriteBatchedStatements = true。但是批量更新甚至比单个更新语句还要慢。作为最后的手段,我使用@Transactional注释进行了射击。批处理语句的工作速度提高了5倍。有人可以阐明为什么会这样吗?我真的很想知道为什么。 (2认同)

Evg*_*rov 6

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

public void insertBatch(final List<Customer> 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();
    }
  });
}
Run Code Online (Sandbox Code Playgroud)

如果你有这样的东西.Spring会做类似的事情:

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

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


reb*_*ace 5

我不知道这是否适合您,但这是我最终使用的一种无 Spring 方式。它比我尝试过的各种 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){}
Run Code Online (Sandbox Code Playgroud)

然后创建一个准备好的语句并使用批量插入值加载它,然后作为单个批量插入执行...

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){}
    }
}
Run Code Online (Sandbox Code Playgroud)

显然我已经删除了错误处理,并且查询和记录对象是名义上的等等。

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

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

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

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

如果您使用我概述的方法,您可以做同样的事情(使用带有多个 VALUES 列表的准备好的语句),然后当您最终遇到那个边缘情况时,处理起来会容易一些,因为您可以构建和执行具有完全正确数量的 VALUES 列表的最后一个语句。这有点hacky,但大多数优化的东西都是。


Jef*_*ado 5

我在使用 Spring JDBC 批处理模板时也遇到了一些不愉快。就我而言,使用纯 JDBC 会很疯狂,所以我使用NamedParameterJdbcTemplate. 这在我的项目中是必须的。但是在数据库中插入成百上千行的速度很慢。

为了了解发生了什么,我在批量更新期间使用 VisualVM 对其进行了采样,瞧:

visualvm 显示速度慢的地方

减慢进程的原因是,在设置参数时,Spring JDBC 正在查询数据库以了解每个参数的元数据。而在我看来,这是查询每个参数的每一行数据库每次。所以我只是教 Spring 忽略参数类型(正如Spring 文档中关于批量操作对象列表的警告):

    @Bean(name = "named-jdbc-tenant")
    public synchronized NamedParameterJdbcTemplate getNamedJdbcTemplate(@Autowired TenantRoutingDataSource tenantDataSource) {
        System.setProperty("spring.jdbc.getParameterType.ignore", "true");
        return new NamedParameterJdbcTemplate(tenantDataSource);
    }
Run Code Online (Sandbox Code Playgroud)

注意:必须创建 JDBC 模板对象之前设置系统属性。可以只设置application.properties,但这解决了,我再也没有碰过这个