Использование метода flush() на каждые 100 строк транзакции 10 000 замедлений

У меня есть образец проекта с использованием spring-boot с spring-data-jpa и postgres db с одной таблицей.

Я пытаюсь INSERT 10 000 записей в цикле в таблицу и измерять время выполнения - включение или отключение метода flush() из класса EntityManager для каждых 100 записей.

Ожидаемый результат заключается в том, что время выполнения с включенным методом flush() намного меньше, чем с отключенным, но на самом деле у меня есть противоположный результат.

UserService.java

package sample.data;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    UserRepository userRepository;

    public User save(User user) {
        return userRepository.save(user);
    }
}

UserRepository.java

package sample.data;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> { }

Application.java

package sample;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;

import sample.data.User;
import sample.data.UserService;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@SpringBootApplication
@EnableJpaRepositories(considerNestedRepositories = true)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Autowired
    private UserService userService;

    @PersistenceContext
    EntityManager entityManager;

    @Bean
    public CommandLineRunner addUsers() {
        return new CommandLineRunner() {
            @Transactional
            public void run(String... args) throws Exception {
                long incoming = System.currentTimeMillis();
                for (int i = 1; i <= 10000; i++) {
                    userService.save(new User("name_" + i));

                    if (i % 100 == 0) {
                        entityManager.flush();
                        entityManager.clear();
                    }
                }
                entityManager.close();
                System.out.println("Time: " + (System.currentTimeMillis() - incoming));
            }
        };
    }
}

Ответ 1

Убедитесь, что вы включили пакет JDBC в настройке поставщика сохранения. Если вы используете Hibernate, добавьте это в свои свойства Spring:

spring.jpa.properties.hibernate.jdbc.batch_size=20   // or some other reasonable value

Без включения пакетной обработки я предполагаю, что регрессия производительности обусловлена накладными расходами на очистку контекста персистентности каждые 100 объектов, но я не уверен в этом (вам придется измерять).

ОБНОВИТЬ:

Фактически, включение JDBC или его отключение не повлияет на то, что при выполнении flush() каждый раз в то время не будет быстрее, чем без него. То, что вы контролируете с помощью ручного flush() - это не то, как выполняется промывка (с помощью пакетных операторов или унитарных вставок), но вместо этого вы контролируете, когда будет выполняться промывка базы данных.

Итак, вы сравниваете следующее:

  1. С помощью flush() каждые 100 объектов: вы вставляете 100 экземпляров в базу данных на флеш, и вы делаете это 10000/100 = 100 раз.
  2. Без flush(): вы просто собираете все 10000 объектов в контексте в памяти и делаете 10000 вставок при совершении транзакции.

Дозировка JDBC на другом влияет на то, как происходит промывка, но все равно такое же количество выражений, выданных с помощью flush() vs без flush().

Преимущество очистки и очистки каждый раз в цикле заключается в том, чтобы избежать возможного OutOfMemoryError из-за того, что кеш содержит слишком много объектов.

Ответ 2

Написание микро-теста сложно, что в значительной степени иллюстрирует Алексей Шипилев в своем сообщении "JMH vs Caliper: reference thread". Ваш случай не является точным критерием, но:

  1. Ниже 10000 повторений не позволит JVM разогреваться и JIT код по умолчанию. Перед тем, как измерить производительность кода, разогрейте JVM.

  2. System.nanoTime() не System.currentTimeMillis() для измерения прошедшего времени. Если вы измеряете ms ваши результаты будут искажены дрейфом часов в System.currentTimeMillis().

  3. Скорее всего, вы захотите измерить это на конце базы данных, чтобы точно определить узкое место. Без узкого места трудно понять, какова основная причина, например, ваша база данных может находиться на другой стороне Атлантического океана, а стоимость сетевого подключения затмила стоимость заявлений INSERT.

  4. Является ли ваш бенчмарк достаточно изолированным? Если база данных разделяется несколькими пользователями и подключениями, отличными от вашего теста, производительность будет отличаться.

Найдите узкое место в текущей настройке, сделайте предположение о том, как ее проверить, измените контрольный показатель, чтобы он соответствовал предположению, а затем снова измеряйте для подтверждения. Это единственный способ понять это.

Ответ 3

Самая дорогая часть сохраняющегося объекта - это запись в базу данных. Время, проведенное с сохранением сущности в JPA, тривиально в сравнении, поскольку это чистая операция в памяти. Это IO по сравнению с памятью.

Запись в базу данных также может иметь довольно значительные статические накладные расходы, а это означает, что количество раз, которое вы пишете в базе данных, может повлиять на время выполнения. Когда вы вызываете EntityManager#flush, вы указываете Hibernate записывать все ожидающие изменения в базу данных.

Итак, что вы делаете, это сравнение исполнения с 100 записями базы данных с одной записью базы данных. Из-за накладных расходов IO первая будет значительно медленнее.

Ответ 4

Два аспекта, которые не упомянуты другими ответами. Помимо промывки вам необходимо очистить сеанс Hibernate. Без его очистки он будет расти и будет влиять на потребление памяти, что может привести к снижению производительности.

Еще одна вещь, когда сохраняющиеся объекты удостоверяются, что ваш генератор ID использует hilosequence. Если ваши идентификаторы 1,2,3,4,5..... каждая вставка будет иметь дополнительный обратный ход, чтобы увеличить ID.

Ответ 5

Не могли бы вы объяснить, почему вы верите:

Ожидаемый результат заключается в том, что время выполнения с включенным методом flush() намного меньше, чем с отключенным

Мне кажется, что это принципиально ошибочное предположение. Нет никаких оснований полагать, что выполнение этой тривиальной операции в 10k раз будет FASTER с флешем, чем без.

Пока все записи вписываются в память, я ожидаю, что версия без промежуточного флеша будет быстрее. Что указывает на то, что выполнение сетевого ввода-вывода для доступа к базе данных 100 раз должно быть быстрее, чем выполнение 1 раз в конце?