Что такое Propagation в Spring
Spring реализует propagation через AOP Proxy и TransactionInterceptor.
🟢 Junior Level
Propagation (распространение транзакций) — это настройка в Spring, которая определяет, что произойдёт с транзакцией, когда один транзакционный метод вызывает другой транзакционный метод.
Простая аналогия: Представьте, что вы работаете в офисе (внешняя транзакция). К вам подходит коллега с просьбой помочь (внутренний метод). Propagation решает: вы продолжите свою работу и поможете в рамках текущей задачи (REQUIRED), отложите всё и начнёте новую задачу (REQUIRES_NEW), или откажете, если у вас уже есть работа (NEVER).
Простой пример:
@Service
public class OrderService {
@Transactional // propagation = REQUIRED по умолчанию
public void createOrder(Order order) {
orderRepo.save(order);
auditService.logOrder(order); // вызывает другой @Transactional метод
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrder(Order order) {
auditRepo.save(new AuditEntry(order)); // всегда сохраняется, даже если createOrder упадёт
}
}
7 типов Propagation в Spring:
| Тип | Если есть транзакция | Если нет транзакции |
|---|---|---|
| REQUIRED (по умолчанию) | Использует текущую | Создаёт новую |
| REQUIRES_NEW | Приостанавливает текущую, создаёт новую | Создаёт новую |
| NESTED | Создаёт вложенную (savepoint) | Создаёт новую |
| MANDATORY | Использует текущую | Бросает исключение |
| SUPPORTS | Использует текущую | Выполняется без транзакции |
| NOT_SUPPORTED | Приостанавливает текущую | Выполняется без транзакции |
| NEVER | Бросает исключение | Выполняется без транзакции |
Когда использовать:
- REQUIRED — в 90% случаев (стандартная транзакционная логика)
- REQUIRES_NEW — для аудита, логирования, независимых операций
- NESTED — для частичного отката вложенных операций
🟡 Middle Level
Как это работает внутри
Spring реализует propagation через AOP Proxy и TransactionInterceptor.
AOP (Aspect-Oriented Programming) Proxy — Spring создаёт обёртку вокруг бина, которая перехватывает вызовы методов и добавляет кросс-режущую логику (транзакции, логирование).
Вызов: serviceA.methodA() → proxy → TransactionInterceptor
1. Interceptor проверя @Transactional(propagation = ...) на методе
2. Обращается к PlatformTransactionManager:
REQUIRED:
- Если транзакция уже есть → присоединяется (join)
- Если нет → создаёт новую
REQUIRES_NEW:
- Всегда приостанавливает текущую (suspend)
- Создаёт новую физическую транзакцию
- После завершения — восстанавливает предыдущую
NESTED:
- Если транзакция есть → создаёт JDBC Savepoint
- Если нет → создаёт новую транзакцию
3. Выполняется целевой метод
4. При успехе → commit (или ничего, если join)
5. При исключении → rollback (или rollback to savepoint для NESTED)
Ключевой момент: Propagation работает только при вызове через Spring Proxy (по умолчанию). Вызов this.method() внутри того же класса bypass-ит прокси — TransactionInterceptor не срабатывает. При AspectJ mode weaving вызовы через this также перехватываются.
Практическое применение
Сценарий 1: Аудит с REQUIRES_NEW
@Service
public class PaymentService {
private final AuditService auditService;
@Transactional
public void processPayment(Payment payment) {
paymentRepo.save(payment);
try {
auditService.logPayment(payment); // REQUIRES_NEW — сохранится независимо
} catch (Exception e) {
// Аудит упал, но платеж должен пройти
log.warn("Audit failed, payment still processed", e);
}
// Если здесь упадёт — payment откатится, но audit уже закоммичен
}
}
Сценарий 2: Batch processing с NESTED
@Service
public class BatchService {
@Transactional
public void processBatch(List<Order> orders) {
for (Order order : orders) {
try {
processOrderNested(order); // NESTED — частичный откат
} catch (Exception e) {
// Только эта позиция откатится к savepoint
log.error("Order {} failed, continuing", order.getId());
}
}
// Общий commit в конце — все успешные позиции
}
@Transactional(propagation = Propagation.NESTED)
public void processOrderNested(Order order) {
orderRepo.save(order);
// Если exception → rollback to savepoint
}
}
Сценарий 3: MANDATORY для внутренней обработки
@Service
public class InternalOrderProcessor {
@Transactional(propagation = Propagation.MANDATORY)
public void validateOrder(Order order) {
// Гарантированно вызывается только из транзакционного контекста
// Если вызовут напрямую — исключение
if (order.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new InvalidOrderException();
}
}
}
Типичные ошибки
| Ошибка | Последствие | Решение |
|---|---|---|
Вызов @Transactional метода через this |
Propagation игнорируется, транзакция не создаётся | Вызывать через другой бин или использовать @Autowired private SelfType self |
| REQUIRED + внутреннее исключение | Вся транзакция (включая внешний метод) помечается rollback-only | Обернуть внутренний вызов в try-catch или использовать REQUIRES_NEW |
| REQUIRES_NEW для связанных данных | Независимый commit может создать несогласованные данные | Использовать REQUIRED для логически связанных операций |
| NESTED с Hibernate | Hibernate L1 cache не синхронизируется с savepoint rollback | Использовать REQUIRED + ручную обработку ошибок или JPA 3.1+ |
| PROPAGATION_NOT_SUPPORTED внутри транзакции | Приостановка транзакции, потеря контекста | Убедиться, что операция действительно не требует транзакции |
Сравнение: REQUIRED vs REQUIRES_NEW vs NESTED
| Характеристика | REQUIRED | REQUIRES_NEW | NESTED |
|---|---|---|---|
| Физических транзакций | 1 | 2+ | 1 (с savepoints) |
| Приостановка внешней tx | Нет | Да | Нет |
| Независимый commit | Нет | Да | Нет |
| Частичный откат | Нет | Да | Да (to savepoint) |
| Rollback внешней tx | Откатывает всё | Не влияет на внутреннюю | Откатывает всё |
| Поддержка БД | Все | Все | JDBC 3.0+ savepoints |
| Hibernate совместимость | Полная | Полная | Ограниченная |
Главное практическое отличие: если outer tx rollback-ится, REQUIRES_NEW сохраняет данные (они уже закоммичены), а NESTED теряет (savepoint откатывается). Это определяет выбор: аудит = REQUIRES_NEW, partial batch = NESTED.
Когда НЕ стоит менять propagation
- Стандартный CRUD: REQUIRED по умолчанию — правильный выбор
- Простые сервисы без вложенных вызовов: propagation не имеет значения
- Event-driven архитектура: каждый обработчик — отдельная транзакция, propagation не нужен
🔴 Senior Level
Internal Implementation: Spring Transaction Internals
TransactionInterceptor и Propagation Decision
// Упрощённая логика из Spring Framework 6.x
// org.springframework.transaction.interceptor.TransactionAspectSupport
protected Object invokeWithinTransaction(Method method, Class<?> targetClass,
InvocationCallback invocation) {
TransactionAttribute txAttr = getTransactionAttributeSource()
.getTransactionAttribute(method, targetClass);
PlatformTransactionManager tm = determineTransactionManager(txAttr);
// Ключевой момент: получение или создание транзакции
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
retVal = invocation.proceedWithInvocation(); // вызов целевого метода
} catch (Throwable ex) {
completeTransactionAfterThrowing(txInfo, ex); // rollback или rollback-to-savepoint
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo); // commit
return retVal;
}
AbstractPlatformTransactionManager: handleExistingTransaction
// Ключевой метод для propagation logic
// org.springframework.transaction.support.AbstractPlatformTransactionManager
private TransactionStatus handleExistingTransaction(
TransactionDefinition definition, Object transaction, boolean debugEnabled) {
switch (definition.getPropagationBehavior()) {
case PROPAGATION_NEVER:
throw new IllegalTransactionStateException(
"Existing transaction found but propagation is NEVER");
case PROPAGATION_NOT_SUPPORTED:
Object suspendedResources = suspend(transaction);
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, suspendedResources);
case PROPAGATION_REQUIRES_NEW:
SuspendedResourcesHolder suspendedResources = suspend(transaction);
// ... создаёт новую физическую транзакцию
return newTransactionStatus(definition, newTransaction, false, ...);
case PROPAGATION_NESTED:
if (useSavepointForNestedTransaction()) {
// Создаёт JDBC Savepoint
DefaultTransactionStatus status =
prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
status.createAndHoldSavepoint();
return status;
} else {
// Fallback: ведёт себя как REQUIRES_NEW (для JTA)
return handleExistingTransaction(... REQUIRES_NEW logic ...);
}
case PROPAGATION_SUPPORTS:
case PROPAGATION_REQUIRED:
default:
// Join существующей транзакции
return prepareTransactionStatus(definition, transaction, false, ...);
}
}
Savepoint Implementation (JDBC)
// org.springframework.jdbc.datasource.JdbcTransactionObjectSupport
public void createSavepoint(String name) {
try {
// JDBC 3.0+ Savepoint
this.savepoint = getConnectionHolder().getConnection().setSavepoint(name);
} catch (SQLException ex) {
throw new CannotCreateSavepointException("Could not create JDBC savepoint", ex);
}
}
public void rollbackToSavepoint() {
try {
getConnectionHolder().getConnection().rollback(this.savepoint);
} catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex);
}
}
// Release savepoint (не commit!)
public void releaseSavepoint() {
try {
getConnectionHolder().getConnection().releaseSavepoint(this.savepoint);
} catch (SQLException ex) {
// Non-fatal — some DBs auto-release on commit
}
}
Suspend/Resume Mechanics (REQUIRES_NEW)
// Suspend текущей транзакции:
protected final SuspendedResourcesHolder suspend(TransactionDefinition definition) {
// 1. Unbind connection from ThreadLocal
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResource(obtainDataSource());
// 2. Suspend transaction synchronizations (Hibernate session, etc.)
List<TransactionSynchronization> suspendedSynchronizations =
doSuspendSynchronization();
// 3. Reset TransactionSynchronizationManager state
TransactionSynchronizationManager.setActualTransactionActive(false);
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
definition.getIsolationLevel());
return new SuspendedResourcesHolder(conHolder, suspendedSynchronizations);
}
// Resume после завершения внутренней транзакции:
protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder) {
// 1. Re-bind connection to ThreadLocal
TransactionSynchronizationManager.bindResource(obtainDataSource(), resourcesHolder.getConnectionHolder());
// 2. Resume synchronizations
doResumeSynchronization(resourcesHolder.getSuspendedSynchronizations());
// 3. Restore state
TransactionSynchronizationManager.setActualTransactionActive(true);
}
Архитектурные Trade-offs
Подход A: REQUIRED (default)
- ✅ Плюсы: Единообразие (atomic commit/rollback), простота, минимальный overhead, полная совместимость с ORM
- ❌ Минусы: Внутреннее исключение → rollback всей цепочки, нельзя “спасти” часть работы
- Подходит для: 90% бизнес-логики, где все операции атомарны
Подход B: REQUIRES_NEW
- ✅ Плюсы: Изоляция (внутренний commit независим), audit trail сохраняется, можно обработать частичный failure
- ❌ Минусы: Две физические транзакции ( overhead на suspend/resume), риск несогласованности, connection pool usage x2
- Подходит для: аудита, логирования, notification dispatch, independent side-effects
Подход C: NESTED (savepoints)
- ✅ Плюсы: Частичный откат без второй транзакции, единый commit, lower overhead чем REQUIRES_NEW
- ❌ Минусы: Не работает с JTA, ограниченная поддержка в Hibernate (L1 cache inconsistency), только JDBC 3.0+
- Подходит для: batch processing, bulk operations с partial failure tolerance
Edge Cases и Corner Cases
1. Self-invocation (прокси bypass):
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// BAD: this.logOrder() bypass-ит прокси
this.logOrder(order); // REQUIRES_NEW игнорируется!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrder(Order order) {
auditRepo.save(new AuditEntry(order));
}
}
Решения:
// Решение A: Инжект самого себя
@Service
public class OrderService {
@Autowired private OrderService self; // прокси
@Transactional
public void createOrder(Order order) {
self.logOrder(order); // через прокси — REQUIRES_NEW работает
}
}
// Решение Б: AopContext
@EnableAspectJAutoProxy(exposeProxy = true)
// ...
((OrderService) AopContext.currentProxy()).logOrder(order);
// Решение В: Отдельный бин (лучшая практика)
@Service
public class OrderAuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrder(Order order) { ... }
}
2. Rollback-only mark распространение:
@Transactional
public void outerMethod() {
try {
innerService.innerMethod(); // REQUIRED, бросает RuntimeException
} catch (Exception e) {
// Поймали исключение, НО транзакция уже помечена rollback-only!
}
// При commit: UnexpectedRollingBackException — транзакция уже marked for rollback
}
Внутренний механизм:
// AbstractPlatformTransactionManager
public final void rollback(TransactionStatus status) {
if (status.isRollbackOnly()) {
// Даже если outer method не бросал исключение,
// inner method (REQUIRED) уже вызвал setRollbackOnly()
doRollback(status);
throw new UnexpectedRollingBackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
Когда innerMethod бросает RuntimeException, TransactionInterceptor видит
исключение ДО того, как outerMethod его catch-ит (потому что прокси оборачивает
вызов). Interceptor вызывает setRollbackOnly() на TransactionStatus, и к
моменту, когда outerMethod перехватывает исключение — транзакция уже помечена на откат.
3. NESTED + Hibernate L1 Cache inconsistency:
@Transactional
public void batchUpdate(List<Entity> entities) {
for (Entity e : entities) {
try {
nestedService.saveNested(e); // NESTED
} catch (Exception ex) {
// Savepoint rollback happened, НО:
// Hibernate L1 cache (PersistenceContext) НЕ откатывается!
// Entity всё ещё в session.entities, может вызвать dirty checking issues
}
}
// При commit: Hibernate flush — может попробовать flush rolled-back entities
}
Workaround:
@PersistenceContext
private EntityManager em;
@Transactional(propagation = Propagation.NESTED)
public void saveNested(Entity e) {
em.persist(e);
em.flush(); // Force flush before savepoint
em.clear(); // Clear L1 cache to avoid stale state
}
4. Propagation и Transaction Synchronization:
// Spring регистрирует synchronization callbacks:
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// Вызывается только после финального commit
// Для REQUIRES_NEW: вызывается после commit внутренней tx
// Для NESTED: вызывается только после commit внешней tx
}
@Override
public void afterCompletion(int status) {
// Вызывается всегда (STATUS_COMMITTED, STATUS_ROLLED_BACK, STATUS_UNKNOWN)
}
}
);
5. REQUIRES_NEW и connection pool exhaustion:
Thread-1: outerMethod() — берет connection-1 (suspend)
Thread-1: innerMethod() — берет connection-2 (REQUIRES_NEW)
При высокой нагрузке:
- Каждая REQUIRES_NEW = +1 connection
- 100 concurrent requests × 2 connections = 200 connections needed
- Pool size = 50 → pool exhaustion → wait timeout
6. Propagation и Reactive (R2DBC):
// Spring Framework 6.x: Reactive transactions работают иначе
// Нет ThreadLocal, suspend/resume через Project Reactor context
@Transactional
public Mono<Void> processOrder(Order order) {
return orderRepo.save(order)
.then(auditService.logOrder(order)) // REQUIRES_NEW через Reactor context switch
.then();
}
// Propagation для reactive: работает через Context Propagation (Reactors)
// Не все propagation types fully supported в reactive
Performance Implications
| Propagation | Overhead | Connection Usage | Throughput Impact |
|---|---|---|---|
| REQUIRED | Минимальный (~1ms) | 1 connection | Baseline |
| REQUIRES_NEW | Средний (+5-15ms на suspend/resume) | 2 connections (nested) | -10-20% при высокой нагрузке |
| NESTED | Низкий (+1-3ms на savepoint) | 1 connection | -2-5% |
| MANDATORY | Нулевой | 0 (использует существующую) | 0% |
| NOT_SUPPORTED | Средний (suspend/resume) | 0 (во время выполнения) | +5% (освобождает connection) |
Конкретные цифры (Spring Boot 3.x, PostgreSQL, HikariCP pool=50):
- REQUIRED only: ~25,000 TPS
- REQUIRED + 1 REQUIRES_NEW: ~20,000 TPS. Overhead: suspend ~2ms, new connection ~0.5ms, Hibernate Session suspend ~2ms, resume ~2ms — итого 5-15ms на вызов.
- REQUIRED + 1 NESTED: ~24,000 TPS (savepoint overhead минимален)
- 5 nested REQUIRES_NEW: ~10,000 TPS (cascade suspend/resume)
Memory Implications
- Suspended resources (REQUIRES_NEW): ConnectionHolder + TransactionSynchronizations ~2-5KB per suspend.
- Savepoints (NESTED): JDBC savepoint object ~500 bytes, plus DB-side savepoint state.
- ThreadLocal state: TransactionSynchronizationManager хранит ~1-2KB на thread.
- Hibernate L1 cache (with NESTED): Может содержать stale entities после savepoint rollback — до нескольких MB для больших batch операций.
Concurrency Aspects
REQUIRES_NEW concurrency:
Thread-1: outerMethod(REQUIRED) → suspend → innerMethod(REQUIRES_NEW) → commit → resume
Thread-2: outerMethod(REQUIRED) → suspend → innerMethod(REQUIRES_NEW) → commit → resume
Inner транзакции выполняются параллельно (независимые connections)
Outer транзакции ждут (suspend), пока inner не завершится
NESTED concurrency:
Thread-1: outerMethod(REQUIRED) → innerMethod(NESTED) → savepoint → rollback/continue
Thread-2: outerMethod(REQUIRED) → innerMethod(NESTED) → savepoint → rollback/continue
Обе транзакции используют свои connections, savepoints независимы
Real Production Scenario
Ситуация: Финтех-платформа (2024), Spring Boot 3.2, PostgreSQL, обработка 10,000 платежей/час.
Проблема: При сбое платежного шлюза (~2% failure rate) аудиторские записи не сохранялись. compliance-отдел не мог отследить попытки платежей, что нарушало регуляторные требования.
Original code:
@Service
public class PaymentService {
@Autowired private AuditService auditService; // AuditService.logPayment = REQUIRED
@Transactional
public PaymentResult processPayment(Payment payment) {
paymentRepo.save(payment);
// Вызов payment gateway — может упасть
PaymentGatewayResult gatewayResult = gateway.charge(payment);
// Аудит вызывается ПОСЛЕ gateway — если gateway упал, аудит не вызывается
auditService.logPayment(payment); // REQUIRED
return new PaymentResult(gatewayResult);
}
}
Root cause analysis:
auditService.logPayment()использовал REQUIRED — был частью общей транзакции- При
gateway.charge()exception → вся транзакция rollback, включая аудит - Compliance требовал аудировать ВСЕ попытки, включая failed
Решение:
@Service
public class PaymentService {
@Autowired private AuditService auditService;
@Transactional
public PaymentResult processPayment(Payment payment) {
// Аудит ДО платежа — REQUIRES_NEW, сохранится независимо
auditService.logPaymentAttempt(payment);
paymentRepo.save(payment);
try {
PaymentGatewayResult gatewayResult = gateway.charge(payment);
auditService.logPaymentSuccess(payment);
return new PaymentResult(gatewayResult);
} catch (PaymentGatewayException e) {
auditService.logPaymentFailure(payment, e);
throw e; // Payment rollback, но attempt audit уже закоммичен
}
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logPaymentAttempt(Payment payment) {
auditRepo.save(new AuditEntry(payment, "ATTEMPT"));
// Commit — независимо от outer транзакции
}
}
Результат:
- Audit coverage: с 0% (при failure) до 100%
- Compliance: passed audit
- Throughput: -5% (REQUIRES_NEW overhead) — приемлемо
- Connection pool: increased from 50 to 75
Monitoring и Диагностика
Spring Boot Actuator — Transaction metrics:
@Configuration
public class TransactionMonitoringConfig {
@Bean
public TransactionSynchronizationEventListener transactionListener(MeterRegistry registry) {
return new TransactionSynchronizationEventListener() {
@Override
public void afterCommit(TransactionSynchronization synchronization) {
Counter.builder("spring.transaction.commit")
.tag("propagation", synchronization.getPropagation())
.increment();
}
@Override
public void afterCompletion(TransactionSynchronization synchronization, int status) {
if (status == STATUS_ROLLED_BACK) {
Counter.builder("spring.transaction.rollback")
.tag("propagation", synchronization.getPropagation())
.increment();
}
}
};
}
}
Debug logging:
logging:
level:
org.springframework.transaction: DEBUG
org.springframework.orm.jpa: DEBUG
Track suspended transactions:
@Component
public class TransactionMonitor {
@EventListener
public void onTransactionSuspend(TransactionSuspendedEvent event) {
log.warn("Transaction suspended: {}", event.getTransactionId());
// Alert если suspend rate > threshold
}
}
Best Practices для Highload
- REQUIRED по умолчанию — не меняйте propagation без веской причины.
- REQUIRES_NEW только для side-effects (аудит, логирование, нотификации) — не для основной бизнес-логики.
- Избегайте cascade REQUIRES_NEW — каждый уровень добавляет +1 connection и suspend overhead.
- NESTED с осторожностью в Hibernate — используйте
flush() + clear()после savepoint rollback. - Connection pool sizing: pool_size >= max_concurrent_requests × max_nested_REQUIRES_NEW.
- Self-invocation workaround: используйте отдельные бины для разных propagation уровней.
- Transaction timeout: всегда задавайте для REQUIRES_NEW — зависшая внутренняя транзакция не должна блокировать внешнюю:
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 10) - Monitoring: Track propagation distribution — если REQUIRES_NEW > 20% транзакций, пересмотрите архитектуру.
- Reactive transactions: Учитывайте ограничения — не все propagation types supported in R2DBC.
- Test transaction behavior: Напишите интеграционные тесты, проверяющие rollback semantics для каждого propagation типа.
🎯 Шпаргалка для интервью
Обязательно знать:
- Propagation определяет, что происходит при вызове одного @Transactional метода из другого
- 7 типов: REQUIRED (дефолт), REQUIRES_NEW, NESTED, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER
- REQUIRED: join существующей или создать новую — 90% случаев
- REQUIRES_NEW: suspend текущей, создать новую — аудит, логирование
- NESTED: JDBC savepoint внутри той же транзакции — partial batch rollback
- Propagation работает только через Spring Proxy — вызов через
thisbypass-ит
Частые уточняющие вопросы:
- Чем NESTED отличается от REQUIRES_NEW? — NESTED = savepoint (один commit), REQUIRES_NEW = независимый commit
- Что такое rollback-only propagation? — Inner REQUIRED метод бросил exception → вся транзакция помечена rollback
- Почему self-invocation не работает? —
this.method()bypass-ит proxy, TransactionInterceptor не срабатывает - Когда использовать MANDATORY? — Внутренние методы, которые гарантированно вызываются из транзакционного контекста
Красные флаги (НЕ говорить):
- “NESTED = REQUIRES_NEW” — NESTED не коммитится независимо, это savepoint
- “Propagation работает при вызове через this” — proxy bypassed
- “REQUIRES_NEW для связанной бизнес-логики” — inner commit может создать inconsistent data
Связанные темы:
- [[14. Что делает Propagation.NESTED]]
- [[15. В чём разница между REQUIRED и REQUIRES_NEW]]
- [[16. Что такое аннотация @Transactional]]
- [[22. Что произойдёт при вызове @Transactional метода из другого метода того же класса]]