Почему 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();