Почему 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], max [2000ms] за пакет

Итак, может ли кто-нибудь объяснить мне, почему jdbcTemplate делает разделенные вставки в этом методе? Почему имя метода batchUpdate? Или, может быть, я использую этот метод неправильно?

Ответ 1

Эти параметры в URL-адресе JDBC-соединения могут иметь большое значение в скорости пакетных утверждений. По моему опыту они ускоряют работу:

useServerPrepStmts = ложно &? RewriteBatchedStatements = истина

Смотрите: Производительность пакетной вставки JDBC

Ответ 2

Я также столкнулся с той же проблемой с шаблоном Spring JDBC. Вероятно, в Spring Batch оператор выполнялся и фиксировался при каждой вставке или фрагментах, что замедляло работу.

Я заменил код jdbcTemplate.batchUpdate() на оригинальный код пакетной вставки JDBC и обнаружил значительное улучшение производительности.

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

Ответ 3

Просто используйте транзакцию. Добавьте метод @Transactional по методу.

Обязательно объявите правильного диспетчера TX, если используете несколько источников данных @Transactional ( "dsTxManager" ). У меня есть случай, когда вставляем 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();
            }
        });
    }

Ответ 4

Измените свою вставку 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
}

Структура сначала создает PreparedStatement из запроса (переменная sql), тогда вызывается метод setValues ​​и выполняется оператор. который повторяется столько раз, сколько вы указываете в методе getBatchSize(). Таким образом, правильный способ записи инструкции insert состоит только из одного предложения value. Вы можете взглянуть на http://docs.spring.io/spring/docs/3.0.x/reference/jdbc.html

Ответ 5

Я не знаю, будет ли это работать для вас, но здесь Spring -бесполезный способ, которым я в конечном итоге использовал. Это было значительно быстрее, чем различные методы Spring, которые я пробовал. Я даже попытался использовать метод пакетного обновления шаблона JDBC, который описывает другой ответ, но даже это было медленнее, чем я хотел. Я не уверен, в чем заключалась сделка, и у Интернетов также не было много ответов. Я подозревал, что это связано с тем, как обрабатываются коммиты.

Этот подход является просто прямым JDBC с использованием пакетов java.sql и пакетного интерфейса PreparedStatement. Это был самый быстрый способ получить записи 24M в базе данных MySQL.

Я более или менее просто создал коллекции "записей" объектов, а затем назвал приведенный ниже код в методе, который вставлял все записи. Цикл, который построил коллекции, отвечал за управление размером партии.

Я пытался вставить записи 24M в базу данных MySQL, и она собиралась ~ 200 записей в секунду с помощью пакета Spring. Когда я переключился на этот метод, он увеличился до ~ 2500 записей в секунду. поэтому мой 24-мегапиксельный рекордный объем загрузки составлял от 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){}
    }
}

Очевидно, что я удалил обработку ошибок, а объект запроса и записи - условный и еще что-то.

Edit: Поскольку ваш оригинальный вопрос заключался в сравнении вставки с значениями foobar (?,?,?), (?,?,?)... (?,?,?) До Spring, здесь более прямой ответ на этот

Похоже, что ваш оригинальный метод, скорее всего, самый быстрый способ загрузки массовых данных в MySQL без использования чего-то вроде подхода LOAD DATA INFILE. Цитата из документов MysQL (http://dev.mysql.com/doc/refman/5.0/en/insert-speed.html):

Если вы одновременно вставляете много строк из одного и того же клиента, используйте инструкции INSERT с несколькими списками VALUES, чтобы вставить несколько строк за раз. Это значительно быстрее (во много раз быстрее в некоторых случаев), чем использование отдельных однострочных инструкций INSERT.

Вы можете изменить метод batchUpdate Spring JDBC Template batchUpdate, чтобы сделать вставку с несколькими значениями VALUES, указанными для вызова 'setValues', но вам придется вручную отслеживать значения индекса, когда вы перебираете множество вещей, вставлено. И вы столкнулись с неприятным случаем в конце, когда общее количество вставленных вещей не кратно количеству списков VALUES, которые у вас есть в вашем подготовленном заявлении.

Если вы используете подход, который я опишу, вы можете сделать то же самое (использовать подготовленный оператор с несколькими списками VALUES), а затем, когда вы доберетесь до этого крайнего случая в конце, вам будет немного легче справиться, потому что вы можете построить и выполнить одно последнее выражение с точно правильным количеством списков VALUES. Это немного хаки, но самые оптимизированные вещи.

Ответ 6

Я нашел значительное улучшение, задав массив 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 и обнаружил, что большую часть времени было потрачено, пытаясь узнать природу каждого поля, и это было сделано для каждой записи.

Надеюсь, это поможет!

Ответ 7

Решение, данное @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();