繁体   English   中英

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

[英]Why Spring's jdbcTemplate.batchUpdate() so slow?

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

我尝试使用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 检查了时间并发现了插入时间:

每批最小[900ms]、平均[1100ms]、最大[2000ms]

那么,有人可以向我解释一下,为什么 jdbcTemplate 在此方法中进行分离插入吗? 为什么方法的名称是batchUpdate 或者我可能以错误的方式使用这种方法?

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

?useServerPrepStmts=false&rewriteBatchedStatements=true

请参阅: 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 批量插入性能

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

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

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 ,发现大部分时间都花在了试图了解每个字段的性质上,这是为每个记录制作的。

希望这有帮助!

只需使用事务。 在方法上添加@Transactional。

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

@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();
            }
        });
    }

将您的 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();
    }
  });
}

如果你有这样的事情。 Spring 将执行以下操作:

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

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

我不知道这是否适合您,但这是我最终使用的一种无 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){}

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

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){}
    }
}

显然,我已经删除了错误处理,并且查询和 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 模板 batchUpdate 方法以使用每个 'setValues' 调用指定的多个 VALUES 进行插入,但在迭代插入的一组内容时,您必须手动跟踪索引值。 当插入的事物总数不是您在准备好的语句中拥有的 VALUES 列表数量的倍数时,您会在最后遇到一个令人讨厌的边缘情况。

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

我在使用 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);
    }

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

@Rakesh 给出的解决方案对我有用。 性能显着提升。 较早的时间是 8 分钟,此解决方案耗时不到 2 分钟。

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();

Spring Batch 中的JdbcBatchItemWriter.write()链接)遇到了一些严重的性能问题,最终找出了JdbcTemplate.batchUpdate()的写入逻辑委托。

添加 Java 系统属性spring.jdbc.getParameterType.ignore=true完全解决了性能问题(从每秒 200 条记录到 ~ 5000)。 该补丁已在 Postgresql 和 MsSql 上进行测试(可能不是特定于方言的)

...具有讽刺意味的是,Spring 在“注释”部分链接下记录了此行为

在这种情况下,通过在基础 PreparedStatement 上自动设置值,每个值对应的 JDBC 类型需要从给定的 Java 类型派生。 虽然这通常很有效,但也有可能出现问题(例如,Map-contained null 值)。 Spring,默认情况下,在这种情况下调用 ParameterMetaData.getParameterType,这对于您的 JDBC 驱动程序来说可能很昂贵。 如果遇到性能问题,您应该使用最新的驱动程序版本并考虑将 spring.jdbc.getParameterType.ignore 属性设置为 true(作为 JVM 系统属性或类路径根目录中的 spring.properties 文件)——例如,在 Oracle 12c (SPR-16139) 上报告。

或者,您可以考虑明确指定相应的 JDBC 类型,通过“BatchPreparedStatementSetter”(如前所示),通过给定基于“List<Object[]>”的调用的显式类型数组,通过“registerSqlType”调用自定义“MapSqlParameterSource”实例,或通过“BeanPropertySqlParameterSource”从 Java 声明的属性类型派生 SQL 类型,即使是 null 值。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM