Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Batch Update
Batch Update — это техника оптимизации производительности баз данных, при которой несколько операций модификации (INSERT, UPDATE, DELETE) группируются и отправляются на сервер БД одной партией, вместо отправки каждой операции отдельно. Это значительно снижает сетевые задержки и повышает пропускную способность.
Проблема без batch обновлений
Неоптимизированный подход - множество круговых поездок (round-trips):
// ПЛОХО: 1000 операций = 1000 сетевых запросов!
public void updateEmployeesSalary(List<Employee> employees) throws SQLException {
String updateQuery = "UPDATE employees SET salary = ? WHERE id = ?";
for (Employee emp : employees) {
try (PreparedStatement stmt = connection.prepareStatement(updateQuery)) {
stmt.setDouble(1, emp.getSalary());
stmt.setLong(2, emp.getId());
stmt.executeUpdate(); // Отправляем на сервер
}
}
// Итого: 1000 TCP соединений = очень медленно!
}
Проблемы такого подхода:
- Задержка за счёт сетевого взаимодействия (RTT - Round Trip Time)
- Нагрузка на сервер БД из-за множества отдельных транзакций
- Низкая пропускная способность
- Истощение ресурсов соединения
Batch Update с PreparedStatement
Оптимизированный подход - одна операция для всех данных:
// ХОРОШО: 1000 операций в одном batch!
public void updateEmployeesSalaryBatch(List<Employee> employees) throws SQLException {
String updateQuery = "UPDATE employees SET salary = ? WHERE id = ?";
try (PreparedStatement stmt = connection.prepareStatement(updateQuery)) {
int batchSize = 1000;
int count = 0;
for (Employee emp : employees) {
stmt.setDouble(1, emp.getSalary());
stmt.setLong(2, emp.getId());
stmt.addBatch(); // Добавляем в батч
if (++count % batchSize == 0) {
int[] results = stmt.executeBatch(); // Отправляем партию
stmt.clearBatch();
System.out.println("Обработано " + results.length + " записей");
}
}
// Обработка оставшихся записей
if (count % batchSize != 0) {
stmt.executeBatch();
}
}
}
Batch INSERT
Массовая вставка данных:
public void insertProductsBatch(List<Product> products) throws SQLException {
String insertQuery = "INSERT INTO products (name, price, category) VALUES (?, ?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(insertQuery)) {
int batchSize = 500;
int count = 0;
for (Product product : products) {
stmt.setString(1, product.getName());
stmt.setDouble(2, product.getPrice());
stmt.setString(3, product.getCategory());
stmt.addBatch();
if (++count % batchSize == 0) {
int[] results = stmt.executeBatch();
stmt.clearBatch();
}
}
if (count % batchSize != 0) {
stmt.executeBatch();
}
}
}
Batch DELETE
Массовое удаление:
public void deleteInactiveUsersBatch(List<Long> userIds) throws SQLException {
String deleteQuery = "DELETE FROM users WHERE id = ?";
try (PreparedStatement stmt = connection.prepareStatement(deleteQuery)) {
for (Long userId : userIds) {
stmt.setLong(1, userId);
stmt.addBatch();
if (userIds.indexOf(userId) % 100 == 0) {
stmt.executeBatch();
stmt.clearBatch();
}
}
stmt.executeBatch();
}
}
Batch обновления с Hibernate/JPA
Использование батчей в ORM:
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository repository;
@Autowired
private EntityManager entityManager;
// Способ 1: Использование flush и clear для управления памятью
public void updateEmployeesSalaryBatch(List<Employee> employees) {
int batchSize = 500;
for (int i = 0; i < employees.size(); i++) {
Employee emp = employees.get(i);
emp.setSalary(emp.getSalary() * 1.1);
entityManager.merge(emp);
// Периодически сбрасываем батч
if (i % batchSize == 0 && i > 0) {
entityManager.flush();
entityManager.clear(); // Освобождаем память
}
}
entityManager.flush();
}
}
Конфигурация Hibernate для батчей:
# application.properties
spring.jpa.properties.hibernate.jdbc.batch_size=20
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true
В коде:
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
public void insertProductsBatch(List<Product> products) {
// Hibernate автоматически батчит благодаря конфигурации
repository.saveAll(products);
}
}
Native batch updates с JPA
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
@Modifying
@Transactional
@Query("UPDATE Employee e SET e.salary = e.salary * 1.1 WHERE e.departmentId = :deptId")
int increaseSalaryByDepartment(@Param("deptId") Long deptId);
}
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository repository;
@Transactional
public void updateEmployeesByDepartment(List<Long> departmentIds) {
for (Long deptId : departmentIds) {
int updated = repository.increaseSalaryByDepartment(deptId);
System.out.println("Обновлено " + updated + " сотрудников");
}
}
}
Управление размером батча
Оптимальный размер батча зависит от:
public class BatchConfiguration {
// Общие рекомендации:
// - Маленькие записи (< 100 bytes): 500-1000
// - Средние записи (100-1000 bytes): 100-500
// - Большие записи (> 1000 bytes): 10-100
public static final int SMALL_BATCH_SIZE = 1000; // VARCHAR, INTEGER
public static final int MEDIUM_BATCH_SIZE = 500; // JSON, CLOB
public static final int LARGE_BATCH_SIZE = 50; // BLOB, большие данные
public void processBatch(List<Record> records, int batchSize) {
for (int i = 0; i < records.size(); i += batchSize) {
int end = Math.min(i + batchSize, records.size());
List<Record> batch = records.subList(i, end);
processSingleBatch(batch);
}
}
}
Сравнение производительности
Тест: Вставка 10,000 записей
@SpringBootTest
public class BatchPerformanceTest {
@Test
public void testNonBatchInsertion() {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
Product p = new Product("Product " + i, 100.0);
productRepository.save(p); // Каждая вставка = отдельная транзакция
}
long elapsed = System.currentTimeMillis() - startTime;
System.out.println("Без батча: " + elapsed + "ms"); // ~2500-3000ms
}
@Test
public void testBatchInsertion() {
long startTime = System.currentTimeMillis();
List<Product> products = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
products.add(new Product("Product " + i, 100.0));
}
productRepository.saveAll(products); // Один батч
long elapsed = System.currentTimeMillis() - startTime;
System.out.println("С батчем: " + elapsed + "ms"); // ~200-300ms
}
}
Обработка ошибок в батчах
public void updateBatchWithErrorHandling(List<Employee> employees) throws SQLException {
String updateQuery = "UPDATE employees SET salary = ? WHERE id = ?";
try (PreparedStatement stmt = connection.prepareStatement(updateQuery)) {
List<Employee> failedUpdates = new ArrayList<>();
int batchSize = 100;
int count = 0;
for (Employee emp : employees) {
try {
stmt.setDouble(1, emp.getSalary());
stmt.setLong(2, emp.getId());
stmt.addBatch();
if (++count % batchSize == 0) {
int[] results = stmt.executeBatch();
stmt.clearBatch();
// Проверяем результаты
for (int result : results) {
if (result == Statement.EXECUTE_FAILED) {
failedUpdates.add(emp);
}
}
}
} catch (SQLException e) {
failedUpdates.add(emp);
logger.error("Ошибка обновления сотрудника " + emp.getId(), e);
}
}
if (!failedUpdates.isEmpty()) {
logger.warn("Не удалось обновить " + failedUpdates.size() + " записей");
}
}
}
Best Practices
1. Всегда используй PreparedStatement для батчей:
// ✅ Правильно
try (PreparedStatement stmt = connection.prepareStatement(query)) {
stmt.addBatch();
stmt.executeBatch();
}
// ❌ Неправильно
Statement stmt = connection.createStatement();
stmt.addBatch(query); // Небезопасно
2. Управляй транзакциями:
public void batchUpdateWithTransaction(List<Employee> employees) throws SQLException {
connection.setAutoCommit(false);
try {
// batch update логика
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(true);
}
}
3. Учитывай ограничения памяти:
// Для больших наборов данных используй потоковую обработку
public void streamProcessBatch(Stream<Employee> employees) {
employees
.collect(Collectors.toList())
.stream()
.collect(Collectors.groupingBy(
e -> employees.indexOf(e) / 100 // батчи по 100
))
.forEach(this::updateBatch);
}
Заключение
Batch обновления — это критическая техника оптимизации для работы с большими объёмами данных. При правильном использовании они могут улучшить производительность в 5-10 раз, снизить нагрузку на БД и повысить пропускную способность приложения.