Опирается на правила:
R-SHUT-DB-1…R-SHUT-DB-3иR-SHUT-DB-X1из Graceful Shutdown Style Guide → раздел 4. БД и persistence.
Важно знать
- HikariCP закрывается после Spring shutdown phases — дефолт Spring Boot правильный.
- Не переопределять
@PreDestroyна DataSource — может закрыть pool до завершения scheduled-tasks.- Активные транзакции дожимаются graceful-механизмами своих контекстов: HTTP, @Scheduled, @KafkaListener, @Async.
- Liquibase/Flyway работают на startup, не на shutdown — миф «что-то почистить» неверен.
pool.shutdown()в@Order(LOWEST_PRECEDENCE)— закроет pool до scheduled tasks → SQLException.- Главный принцип: trust Spring lifecycle ordering, не переопределяй.
DB-pool — критический ресурс. Закрыть его слишком рано = @Scheduled-job не сможет committed его последнюю транзакцию, side-effect-вызов внешней системы прошёл, а DB-запись откатилась → inconsistency. UCP формулирует одно правило: не вмешиваться в lifecycle.
HikariCP закрывается в последнюю очередь
R-SHUT-DB-1: Spring Boot делает правильно по дефолту.
Spring lifecycle на shutdown (упрощённо):
Phase 1: SmartLifecycle stop в reverse order
- Web container (Tomcat) graceful drain
- KafkaListenerContainer.stop()
- TaskScheduler.shutdown()
- @Async ThreadPoolTaskExecutor.shutdown()
Phase 2: ApplicationContext.close()
- Beans destroyed in reverse order
- HikariDataSource.close() в самом конце
Это означает: к моменту закрытия pool все active transactions уже завершены (либо committed, либо rolled back в своих контейнерах). Pool закрывает unused connections.
Не переопределяй порядок. Не пиши @PreDestroy на DataSource. Не вмешивайся в BeanDestructionAware.
Активные транзакции дожимаются
R-SHUT-DB-2: каждый контекст имеет свой механизм.
HTTP-handler в transaction
@PostMapping("/orders")
@Transactional
public OrderResponse create(@RequestBody @Valid CreateOrderRequest req) {
var order = orderRepository.save(...);
paymentService.charge(order);
return OrderResponse.from(order);
}
При SIGTERM:
- Graceful HTTP (см. HTTP drain) даёт handler завершиться.
@Transactionalcommit при exit метода (если успел вtimeout-per-shutdown-phase).- Если timeout — transaction rollback → клиент получает 5xx, но DB consistent.
@Scheduled в transaction
@Scheduled(fixedDelay = 30_000)
@Transactional
public void processOutbox() {
var batch = outboxRepository.findUnpublished(50);
batch.forEach(this::publish);
}
При SIGTERM:
- Spring TaskScheduler.shutdown() ждёт текущую итерацию (см. Scheduled / Async / outbox).
- Текущая
@Transactionalлибо commit, либо rollback.
@KafkaListener в transaction
@KafkaListener(topics = "orders.events")
@Transactional
public void onEvent(OrderEvent event) {
...
}
При SIGTERM:
- Kafka listener container ждёт текущий batch (см. Kafka shutdown).
@Transactionalзавершается с commit/rollback.
@Async в transaction
@Async
@Transactional
public void processInBackground(Long orderId) {
...
}
При SIGTERM:
- ThreadPoolTaskExecutor.shutdown() с
setWaitForTasksToCompleteOnShutdown(true). awaitTermination(20)— даём 20 секунд.- Если не успел —
interrupt(),InterruptedException, transaction rollback.
@Async — самый сложный случай, см. Scheduled / Async / outbox.
Liquibase/Flyway — только startup
R-SHUT-DB-3: миф «миграции при выходе» не существует.
Liquibase / Flyway:
- Запускаются при старте приложения.
- Применяют новые migration scripts.
- После — закрывают свои internal connections.
На shutdown — ничего не делают. Не пиши @PreDestroy для «cleanup миграций» — нечего чистить.
Если используется shutdown-script (раздать SQL на shutdown) — это анти-паттерн. Все миграции — на startup, без исключений.
Что запрещено
pool.shutdown() в @PreDestroy
R-SHUT-DB-X1:
// КАТАСТРОФА
@Component
@RequiredArgsConstructor
@Order(Ordered.LOWEST_PRECEDENCE)
public class DataSourceCleaner {
private final DataSource dataSource;
@PreDestroy
public void cleanup() {
((HikariDataSource) dataSource).close();
}
}
Проблемы:
- Spring уже знает, как закрыть DataSource. Дублирование = двойной close (
IllegalStateException). @Order(LOWEST_PRECEDENCE)не гарантирует, что выполнится последним — другие компоненты с тем же@Orderмогут выполниться после.- Закрытие pool до завершения scheduled tasks — все pending DB operations падают с
SQLException: HikariDataSource closed.
Корректно — ничего не делать. Spring Boot закрывает DataSource в правильную фазу.
Аналогично:
- Не пиши
dataSource.unwrap(HikariDataSource.class).close()в shutdown hook. - Не используй custom
BeanDestructionAwareдля DataSource. - Не настраивай
lazyInitializationчерез@PreDestroy.
Что запрещено — таблица
| Антипаттерн | Правило | Что взамен |
|---|---|---|
DataSource.close() в @PreDestroy | R-SHUT-DB-X1 | trust Spring lifecycle |
pool.shutdown() в @PreDestroy с @Order | R-SHUT-DB-X1 | ничего, Spring сам |
Кастомный BeanDestructionAware для DataSource | R-SHUT-DB-1 | дефолт Spring |
| Long-running queries без timeout | R-SHUT-DB-2 | statement_timeout на DB session |
JdbcTemplate.execute("...") для shutdown cleanup | R-SHUT-DB-3 | startup migrations |
@PreDestroy с DB-операцией | R-SHUT-DB-X1 | shutdown hook = closing, не writing |
Connection.close() руками без try-with-resources | R-SHUT-DB-1 | Spring/jOOQ управляет |
| Pre-stop SQL script для cleanup | R-SHUT-DB-3 | startup migrations only |
Куда дальше
- Graceful Shutdown → раздел 4. БД и persistence — нормативные формулировки.
- JVM/Spring конфигурация — общий graceful Spring.
- Scheduled / Async / outbox — TaskScheduler shutdown.
- HTTP drain — handler transactions.
- Kafka shutdown — listener transactions.
- Идемпотентность in-flight — partial commits.
- PG runtime → optimistic lock — interrupted transactions.