簡體   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