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

Posted

技术标签:

【中文标题】为啥 Spring 的 jdbcTemplate.batchUpdate() 这么慢?【英文标题】:Why Spring's jdbcTemplate.batchUpdate() so slow?为什么 Spring 的 jdbcTemplate.batchUpdate() 这么慢? 【发布时间】:2013-12-20 01:35:35 【问题描述】:

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

我尝试使用 jdbcTemplate.update(String sql) 插入多个批次,其中 sql 是由 StringBuilder 构建的,看起来像:

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

批次大小正好是 1000。我插入了近 100 个批次。 我用秒表检查了时间,发现了插入时间:

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 并发现有一千个插入。 我用秒表检查了时间,发现了插入时间:

min[900ms], avg[1100ms], max[2000ms] 每批次

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

【问题讨论】:

我在尝试使用 Spring Batch 时遇到了类似的问题。我发现使用 JDBC 连接(PreparedStatement.addBatch() 并调用 executeBatch() 并提交每千条记录)手动执行插入操作比使用 Spring 快一个数量级。从来没有真正弄清楚为什么,尽管我怀疑这与提交的应用方式有关。我在 Spring 中使用了从 100 到 10000 的各种批量大小。 可能和底层连接的flush值有关? 你在 JDBC 连接字符串中使用rewriteBatchedStatements=true 吗? Spring 文档指出 Will fall back to separate updates on a single PreparedStatement if the JDBC driver does not support batch updates. "&rewriteBatchedStatements=true";为我工作并获得认可。 这可能是因为自动提交在连接上为真。如果利用了 spring 事务或数据源已关闭自动提交,则不应发生此行为。 【参考方案1】:

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

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

...具有讽刺意味的是,Spring 在“注释”部分 link

下记录了这种行为

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

或者,您可以考虑指定相应的 JDBC 显式类型,通过“BatchPreparedStatementSetter”(如 如前所示),通过一个显式类型数组给 'List' 基于调用,通过 'registerSqlType' 调用 自定义 'MapSqlParameterSource' 实例,或通过 'BeanPropertySqlParameterSource' 从 Java 声明的属性类型,即使是空值。

【讨论】:

【参考方案2】:

我在使用 Spring JDBC 批处理模板时也遇到了一些糟糕的事情。就我而言,使用纯 JDBC 会很疯狂,所以我使用了NamedParameterJdbcTemplate。这在我的项目中是必须的。但是在数据库中插入数百或数千行是很慢的。

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

减缓这个过程的原因是,在设置参数时,Spring JDBC 正在查询数据库以了解元数据 each 参数。在我看来,它每次都在为每一行的每个参数查询数据库。所以我只是教 Spring 忽略参数类型(正如 Spring documentation about batch operating a list of objects 中警告的那样):

    @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,但这解决了,我再也没有碰过这个

【讨论】:

哇,这将我的插入时间从 3 秒减少到 10 毫秒。这种类型检查一定是 Spring-JDBC 中的错误!? 实际上,这个可能的性能问题记录在本​​小节底部的信息框中:docs.spring.io/spring/docs/current/spring-framework-reference/… 我丢失了获得此提示的来源。谢谢,@marstran! @JeffersonQuesado - 你能在这里指导我吗 - ***.com/questions/66142330/… ?【参考方案3】:

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

【讨论】:

这是用于哪个数据库的?【参考方案4】:

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 batch insert performance

【讨论】:

就我而言,将时间减少了一半。 对我来说也有很​​大的性能提升(10 倍)。对于 Oracle 用户,这似乎是唯一的选择。 @Transactional 没有任何区别。 @Saurabh 你在哪个版本的 oracle db/driver 遇到了减速问题? @yolob21 - Oracle 11g 对于大量未提交的批次使用单个提交的另一个主题也需要探讨,即如果您碰巧有 100k 个项目,即使您继续执行 - ps.executeBatch() 定期执行(比如说对于 1000 个项目),但最后一次提交所有 hold up 语句,这可能仍然会使应用程序崩溃。【参考方案5】:

我发现在调用中设置 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,发现大部分时间都花在了试图了解每个字段的性质上,这是为每条记录做的。

希望这会有所帮助!

【讨论】:

能否请您在这里指导我:***.com/questions/66142330/…?【参考方案6】:

只需使用事务。在方法上添加@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();
            
        );
    

【讨论】:

令人印象深刻,在我的情况下加速了 15-20 倍。 我也在连接 URL 中使用 BatchPreparedStatementSetter 和 rewriteBatchedStatements=true。但是批量更新甚至比单个更新语句还要慢。作为最后的手段,我尝试了@Transactional 注释。它的批处理语句的工作速度快了 5 倍。有人可以解释为什么会这样吗?我真的很想知道为什么。【参考方案7】:

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

?useServerPrepStmts=false&rewriteBatchedStatements=true

见:JDBC batch insert performance

【讨论】:

这应该是被接受的答案。对我来说,它提高了 10 倍的性能。 @Community 我试图将它用于 DB2,但得到连接重置异常。如果我从 URL 中删除它,一切正常。你能告诉我为什么我会得到这个以及如何解决它吗? 那么 PostgreSQL 呢? 对于 Postgres,我发现相当于设置 prepareThreshold=0。但我不确定它是否会在某些情况下影响性能...... 对于甲骨文?【参考方案8】:

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

这种方法只是使用 java.sql 包和 PreparedStatement 的批处理接口的直接 JDBC。这是我可以将 2400 万条记录导入 MySQL 数据库的最快方法。

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

我试图将 2400 万条记录插入 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)
    

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

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

看起来您的原始方法可能是在不使用“LOAD DATA INFILE”方法的情况下将批量数据加载到 MySQL 中的最快方法。来自 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 Batch 语句在每个插入或块上执行并提交,这会减慢速度。在这里,您最后只有一个提交。 +1 不幸的是,preparedStatement.executeBatch() 得到了相同的结果,每个插入都是单独调用的。【参考方案9】:

将您的 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() 方法中指定的次数一样多。所以编写插入语句的正确方法是只使用一个值子句。 你可以看看http://docs.spring.io/spring/docs/3.0.x/reference/jdbc.html

【讨论】:

如果你查看 mysql_log 你会看到记录的顺序:set auto_commit=0, insert into table(x,y,i) values(1,2,3), insert, more insert以及更多的插入、提交、设置 autocommit=1。但它不是一个“批次”,它看起来像一个事务。这是进行插入的最慢的方法。有没有什么工具,女巫可以提出“插入t(x,y,i)值(),(),();”之类的创建请求? 虽然这很好地描述了如何使用 jdbcTemplate 批量更新,但我看不出这与原始示例有何不同。 @netta OP 执行 INSERT INTO TABLE(x, y, i) VALUES(1,2,3), (1,2,3), ... , (1,2,3) a 1000 次,而他/她必须执行 INSERT INTO TABLE(x, y, i) VALUES(1,2,3) 1000 次 您的回答似乎在各方面都是错误的。 1. 在一个 SQL 查询中指定多个占位符组比简单地将多个 SQL 查询发送到 DB 服务器是一种更好的优化。 2. 如果 jdbc 驱动程序支持批处理执行,那么 jdbcTemplate 将永远不会按照您描述的方式工作,而是会创建一批 SQL 查询并将整个批处理发送到数据库。请参阅 github 上的源代码,自 2008 年以来批量更新没有更改。要更好地理解,请阅读此***.com/questions/47664889/…

以上是关于为啥 Spring 的 jdbcTemplate.batchUpdate() 这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

Spring 从入门到精通系列 11—— Spring 中的 JdbcTemplate

Spring--JdbcTemplate

spring boot 与 JdbcTemplate 一起工作

Spring之004: jdbcTemplate基本使用Spring实物控制

Spring JdbcTemplate+JdbcDaoSupport实例

Spring5——JdbcTemplate笔记