Что делает Propagation.NESTED
Если активная транзакция уже существует:
🟢 Junior Level
Propagation.NESTED — это тип распространения транзакций в Spring Framework, который создает вложенную логическую транзакцию через механизм точек сохранения (Savepoints) внутри уже существующей физической транзакции.
Суть в 30 секундах
Если активная транзакция уже существует:
- Spring не открывает второе соединение с базой данных (в отличие от
REQUIRES_NEW), а создает JDBC Savepoint в текущем соединении. - Если во вложенном методе происходит ошибка, транзакция откатывается только до этой точки сохранения (
ROLLBACK TO SAVEPOINT). - Внешняя транзакция может перехватить исключение в блоке
try-catchи продолжить работу, успешно закоммитив остальные операции! - Однако если в итоге упадет внешняя (родительская) транзакция, то откатятся абсолютно все изменения, включая те, которые выполнил
NESTED-метод.
Если активной транзакции не было — NESTED ведет себя точно так же, как REQUIRED (открывает новую физическую транзакцию).
Внешняя транзакция (REQUIRED)
|-- START TRANSACTION (Connection #1)
| INSERT INTO orders ... (Успех)
|
| Вложенный вызов (NESTED):
| |-- SAVEPOINT nested_sp_1
| | INSERT INTO order_items ... (ОШИБКА!)
| |-- ROLLBACK TO SAVEPOINT nested_sp_1 <-- Откат только позиций!
|
| Внешний метод ловит ошибку в catch:
| INSERT INTO audit_log ... (Успех)
|-- COMMIT (Сохраняются orders и audit_log, но не order_items!)
Наглядный пример: обработка пакета заказов с частичным сохранением
package com.example.service;
import com.example.dto.OrderDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class BatchOrderProcessor {
private final SingleOrderService singleOrderService;
@Transactional // Внешняя транзакция: единый физический коннект
public void processBatch(List<OrderDto> orders) {
for (OrderDto order : orders) {
try {
// Вызов вложенного метода с точкой сохранения (Savepoint)
singleOrderService.processSingleOrder(order);
} catch (Exception ex) {
// Откатился только конкретный заказ! Вся пачка не ломается!
log.error("Заказ #{} не обработан: {}", order.getId(), ex.getMessage());
}
}
// Финальный COMMIT: фиксирует все успешно обработанные заказы
}
}
package com.example.service;
import com.example.dto.OrderDto;
import com.example.repository.OrderJdbcRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class SingleOrderService {
private final OrderJdbcRepository orderJdbcRepository;
@Transactional(propagation = Propagation.NESTED)
public void processSingleOrder(OrderDto order) {
orderJdbcRepository.save(order);
if (order.getAmount() <= 0) {
// Выброс исключения откатит транзакцию только до SAVEPOINT этого заказа
throw new IllegalArgumentException("Сумма заказа должна быть положительной");
}
}
}
🟡 Middle Level
Архитектура: как Spring реализует NESTED через JDBC Savepoints
В основе работы Propagation.NESTED лежит интерфейс SavepointManager (реализованный в JdbcTransactionObjectSupport):
Вызов: batchService.process() -> singleService.processSingleOrder()
|
v
[ Spring AOP Proxy ]
|
v
[ TransactionInterceptor ]
|
1. Проверяет TransactionSynchronizationManager: транзакция активна?
2. Вызывает transactionStatus.createAndHoldSavepoint():
-> Connection connection = getDataSource().getConnection();
-> Savepoint sp = connection.setSavepoint("NESTED_SAVEPOINT_1");
|
v
[ Целевой метод сервиса ]
|
-----------------------------------------
| При УСПЕХЕ: | При ИСКЛЮЧЕНИИ:
| connection.releaseSavepoint(sp); | connection.rollback(sp);
| (Savepoint освобождается, | (Откат только изменений до точки,
| но COMMIT пока НЕ вызывается!) | флаг rollback-only НЕ выставляется!)
-----------------------------------------
|
v
Внешний метод завершается -> connection.commit()
(Физический коммит ВСЕХ успешных операций в БД)
Сравнение трех ключевых типов: REQUIRED vs REQUIRES_NEW vs NESTED
| Критерий | REQUIRED |
REQUIRES_NEW |
NESTED |
|---|---|---|---|
| Количество коннектов к БД | 1 соединение | 2 независимых соединения | 1 соединение |
| Физический коммит вложенного метода | Вместе с родителем | Немедленно при выходе из метода | Вместе с родителем |
| Реакция на ошибку во внутреннем методе | Вся транзакция метится rollback-only |
Откатывается только внутренний метод | Откатывается до Savepoint |
| Если внешний метод упал ПОСЛЕ внутреннего | Откатывается всё | Внутренний коммит СОХРАНЯЕТСЯ | Откатывается ВСЁ |
| Поддержка в Hibernate JPA | Из коробки (100%) | Из коробки (100%) | ⚠️ Ограничена (требует спец. настроек) |
| Поддержка в JTA (XA distributed tx) | Да | Да | ❌ Нет (JTA не поддерживает Savepoint) |
| Опасность Pool Deadlock | Нет | ⚠️ Высокая при малом пуле HikariCP | Нет (используется тот же коннект) |
🔴 Senior Level
Проблема JPA / Hibernate L1 Cache при использовании Savepoint
Главная архитектурная причина, почему Propagation.NESTED редко используется в проектах со Spring Data JPA / Hibernate:
JDBC Savepoint откатывает состояние страниц и строк в реляционной базе данных, но
EntityManager(кэш 1-го уровня /PersistenceContext) ничего не знает об этом откате!
Пошаговая катастрофа несинхронизированного кэша Hibernate:
1. singleService.processSingleOrder() [NESTED]
2. entityManager.persist(orderItem);
-> Hibernate регистрирует сущность orderItem в PersistenceContext со статусом MANAGED.
3. Внутренний метод выполняет entityManager.flush() -> в БД летит SQL INSERT.
4. Происходит валидационная ошибка -> Spring выполняет connection.rollback(savepoint).
-> В базе данных строка INSERT физически удалена!
5. Внешний метод ловит ошибку в catch и продолжает работу:
-> Но в памяти JVM в Hibernate PersistenceContext сущность orderItem ВСЁ ЕЩЕ ЧИСЛИТСЯ MANAGED!
6. Внешний метод завершается, срабатывает финальный flush() / commit():
-> Hibernate видит сущность со статусом MANAGED и пытается отправить SQL UPDATE или повторный INSERT,
что приводит к EntityNotFoundException, OptimisticLockException или нарушению целостности ключей!
Как безопасно готовить NESTED в Spring Data JPA
Если в проекте с JPA действительно необходим частичный откат, Senior Engineer предпринимает два шага:
Шаг 1: Явное включение Savepoint в JpaTransactionManager
По умолчанию JpaTransactionManager бросает NestedTransactionNotSupportedException. В конфигурации приложения необходимо разрешить вложенные транзакции:
package com.example.config;
import jakarta.persistence.EntityManagerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager txManager = new JpaTransactionManager(emf);
// Принудительно разрешаем использование Savepoint в JPA:
txManager.setNestedTransactionAllowed(true);
return txManager;
}
}
Шаг 2: Ручная зачистка L1 Cache (EntityManager.clear()) при ошибке
Чтобы удалить «фантомные» сущности из памяти после отката Savepoint, вызывающий сервис обязан очистить контекст персистентности:
package com.example.service;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class SafeNestedProcessor {
@PersistenceContext
private final EntityManager entityManager;
private final NestedItemService nestedItemService;
@Transactional
public void processSafeBatch(List<ItemDto> items) {
for (ItemDto item : items) {
try {
nestedItemService.processItem(item); // NESTED
} catch (Exception ex) {
// КРИТИЧЕСКИ ВАЖНО: очистить L1 Cache, чтобы откатанные
// сущности не попытались синхронизироваться при общем коммите!
entityManager.clear();
}
}
}
}
4 Tricky Questions
1. Что произойдет, если родительская транзакция откатится ПОСЛЕ того, как вложенный метод с Propagation.NESTED успешно завершил свою работу?
Ответ:
Все изменения, выполненные успешным вложенным методом, будут полностью откатаны вместе с родительской транзакцией.
В отличие от Propagation.REQUIRES_NEW, где внутренний метод коммитится физически в отдельном соединении и его фиксация необратима, Propagation.NESTED удерживает изменения в рамках того же самого открытого физического соединения. При откате внешней транзакции вызывается глобальный connection.rollback(), который стирает все изменения в транзакции независимо от того, насколько успешно были освобождены промежуточные точки сохранения (Savepoints).
2. Почему попытка вызвать метод с Propagation.NESTED в стандартном приложении со Spring Boot + Hibernate приводит к NestedTransactionNotSupportedException?
Ответ:
Потому что в JpaTransactionManager флаг nestedTransactionAllowed по умолчанию жестко выставлен в false.
Разработчики Spring Framework намеренно заблокировали NESTED по умолчанию для JPA, чтобы защитить разработчиков от скрытой рассинхронизации памяти EntityManager (кэш первого уровня, dirty checking, карты snapshot) с состоянием строк в СУБД после отката к точке сохранения. Для безопасного применения NESTED рекомендуется использовать чистый JDBC (DataSourceTransactionManager), JdbcTemplate, JOOQ или MyBatis.
3. Освобождаются ли строчные блокировки (Row Locks) в базе данных при откате к точке сохранения (ROLLBACK TO SAVEPOINT)?
Ответ:
Да, блокировки, захваченные внутри блока точки сохранения, освобождаются.
Когда СУБД (PostgreSQL, MySQL InnoDB, Oracle) выполняет ROLLBACK TO SAVEPOINT sp:
- Все изменения данных, выполненные после создания точки
sp, откатываются по журналу отката (Undo Log / WAL). - Все эксклюзивные блокировки строк (
X-locks), наложенные запросамиUPDATE,DELETEилиSELECT FOR UPDATEпосле момента создания точкиsp, немедленно снимаются. - При этом блокировки строк, захваченные родительской транзакцией до создания
sp, остаются на месте и удерживаются до финального коммита/отката транзакции.
4. В чем фундаментальная разница в обработке ошибок между REQUIRED и NESTED, если вызывающий метод оборачивает вызов в try-catch?
Ответ:
- При
REQUIRED: внутренний метод выполняется в той же транзакции. При возникновении исключения внутреннийTransactionInterceptorперехватывает его и безусловно вызываетtransactionStatus.setRollbackOnly(). Когда родительский метод перехватывает ошибку вcatchи завершается штатно, Spring при коммите видит флагrollback-onlyи выбрасываетUnexpectedRollbackException, откатывая всю транзакцию целиком. - При
NESTED: внутреннийTransactionInterceptorперехватывает исключение и вызываетconnection.rollback(savepoint). Он НЕ выставляет флагrollback-onlyна глобальной транзакции. В результате родительский метод спокойно ловит исключение вcatch, а родительская транзакция успешно коммитится.
🎯 Шпаргалка для интервью
30-секундный ответ
«
Propagation.NESTEDсоздает вложенную транзакцию с использованием точки сохранения (JDBC Savepoint) в рамках одного и того же физического соединения. В отличие отREQUIRES_NEW, здесь не тратится дополнительный коннект из пула и нет риска Connection Pool Deadlock. При ошибке во вложенном методе откат происходит только до точки сохранения, позволяя внешней транзакции перехватить ошибку и закоммитить остальную работу. Однако если в конце упадет внешняя транзакция, откатятся абсолютно все данные, включая вложенные. В Hibernate JPANESTEDпо умолчанию отключен из-за рассинхронизации L1-кэша».
Чек-лист ключевых понятий
- Механизм: JDBC Savepoint (
connection.setSavepoint(),connection.rollback(savepoint)). - Коннекты к БД: Ровно 1 соединение (нет оверхеда пула).
- Откат родителя: Откатывает все вложенные Savepoint.
- Откат вложенного: Не ставит флаг
rollback-onlyна родителе (позволяет обработать вtry-catch). - JPA конфликт:
JpaTransactionManagerбросаетNestedTransactionNotSupportedException, так как L1 Cache не умеет откатываться вместе с БД.
Красные флаги (чего ни в коем случае нельзя говорить)
- ❌ «NESTED открывает вторую физическую транзакцию и берет второй коннект» (Это делает
REQUIRES_NEW.NESTEDиспользует Savepoint в текущем коннекте). - ❌ «Если родительская транзакция откатится, вложенный метод NESTED останется в базе» (При глобальном rollback откатится всё).
- ❌ «NESTED работает в Spring Data JPA из коробки без всяких настроек» (Упадет с
NestedTransactionNotSupportedException).