Что такое Propagation в Spring
Когда метод с аннотацией @Transactional вызывает другой метод с аннотацией @Transactional, Spring должен решить:
🟢 Junior Level
Propagation (распространение транзакций) — это атрибут аннотации @Transactional, определяющий поведение транзакционного менеджера при вызове одного транзакционного метода из другого.
Суть в 30 секундах
Когда метод с аннотацией @Transactional вызывает другой метод с аннотацией @Transactional, Spring должен решить:
- Присоединиться к уже существующей открытой транзакции?
- Приостановить текущую транзакцию и открыть абсолютно новую физическую транзакцию в отдельном соединении с БД?
- Выполнить операцию без транзакции?
- Выбросить исключение, если транзакции нет (или наоборот — если она есть)?
Именно за это решение отвечает параметр propagation:
@Transactional(propagation = Propagation.REQUIRED) // Значение по умолчанию
public void someBusinessMethod() { ... }
7 типов распространения транзакций в Spring
| Тип | Поведение, если транзакция УЖЕ ЕСТЬ | Поведение, если транзакции НЕТ | Типичный сценарий |
|---|---|---|---|
REQUIRED (дефолт) |
Присоединяется к текущей транзакции | Открывает новую транзакцию | 90% стандартных операций бизнес-логики |
REQUIRES_NEW |
Приостанавливает текущую, открывает новую | Открывает новую транзакцию | Независимый аудит, логирование, billing-чеки |
NESTED |
Создает вложенную транзакцию (JDBC Savepoint) | Открывает новую транзакцию | Пакетная обработка (откат части элементов) |
MANDATORY |
Присоединяется к текущей транзакции | Бросает исключение IllegalTransactionStateException |
Вспомогательные сервисы, требующие внешнюю транзакцию |
SUPPORTS |
Присоединяется к текущей транзакции | Выполняется не транзакционно | Оптимизированные запросы на чтение |
NOT_SUPPORTED |
Приостанавливает текущую транзакцию | Выполняется не транзакционно | Тяжелые внешние вызовы (REST/gRPC/Email) |
NEVER |
Бросает исключение IllegalTransactionStateException |
Выполняется не транзакционно | Операции, строго запрещающие блокировки БД |
Наглядный пример: заказ и независимый аудит
package com.example.service;
import com.example.entity.Order;
import com.example.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final AuditService auditService;
@Transactional // REQUIRED по умолчанию: создает Transaction #1
public void placeOrder(Order order) {
orderRepository.save(order);
// Вызов метода другого бина с REQUIRES_NEW
auditService.logAction("Попытка создания заказа #" + order.getId());
if (order.getTotalPrice() == null) {
// При выбросе RuntimeException Transaction #1 откатится,
// но запись аудита из logAction останется в БД!
throw new IllegalArgumentException("Цена заказа не указана!");
}
}
}
package com.example.service;
import com.example.entity.AuditLog;
import com.example.repository.AuditLogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class AuditService {
private final AuditLogRepository auditLogRepository;
// Открывает Transaction #2 в отдельном физическом коннекте к БД,
// приостанавливая Transaction #1
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String message) {
auditLogRepository.save(new AuditLog(message));
}
}
🟡 Middle Level
Архитектурный пайплайн: Spring AOP, Proxy и TransactionInterceptor
Spring реализует управление транзакциями через паттерн AOP Proxy и перехватчик TransactionInterceptor.
Вызов клиента:
service.placeOrder()
|
v
[ CGLIB / Dynamic Proxy ]
|
v
[ TransactionInterceptor ]
1. Читает @Transactional(propagation = ...)
2. Обращается к PlatformTransactionManager
3. Проверяет ThreadLocal ресурсы в TransactionSynchronizationManager
4. Принимает решение: JOIN / SUSPEND / CREATE NEW / SAVEPOINT
|
v
[ Целевой метод сервиса (Target Bean) ]
|
v
[ TransactionInterceptor ]
5. При успехе: commit (или ничего, если транзакция внешняя)
6. При исключении: rollback / rollback to savepoint / setRollbackOnly
Разбор механики каждого типа Propagation
1. Propagation.REQUIRED (дефолт)
Единый логический и физический контекст. Все вызовы объединяются в одну физическую транзакцию БД.
- Опасная ловушка: Если внутренний метод выбросил
RuntimeException, он помечает всю транзакцию статусомrollback-only. Даже если внешний метод поймает исключение черезtry-catch, транзакцию невозможно будет закоммитить!
2. Propagation.REQUIRES_NEW
Внутренний метод всегда выполняется в новой физической транзакции в отдельном соединении из пула.
- Текущая внешняя транзакция приостанавливается (
suspend). - Поток удерживает физический коннект №1 и запрашивает из пула коннект №2.
- Внутренняя транзакция коммитится или откатывается совершенно независимо от судьбы внешней.
- После завершения внутренний коннект возвращается в пул, внешняя транзакция восстанавливается (
resume).
3. Propagation.NESTED
Использует механизм Savepoint (точки сохранения) в рамках одного и того же физического соединения.
- Не открывает второй коннект к БД.
- Перед вызовом внутреннего метода выполняется
connection.setSavepoint("..."). - Если внутренний метод упал, откат происходит только до точки сохранения (
connection.rollback(savepoint)), а внешний метод может перехватить ошибку и успешно завершить внешнюю транзакцию.
4. Propagation.MANDATORY
Метод отказывается выполняться «в одиночку». Если выше по стеку нет открытой транзакции, Spring немедленно выбрасывает:
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'.
5. Propagation.SUPPORTS
«Транзакционный хамелеон». Если транзакция уже открыта — выполняется внутри неё. Если транзакции нет — выполняется без неё (автокоммит JDBC).
6. Propagation.NOT_SUPPORTED
Принудительно приостанавливает существующую транзакцию на время работы метода и выполняет его вне транзакционного контекста. Полезно для тяжелых блокирующих вызовов сторонних REST API или очередей сообщений, чтобы не удерживать транзакционные блокировки строк в БД.
7. Propagation.NEVER
Антипод MANDATORY. Выбрасывает IllegalTransactionStateException, если обнаружена активная транзакция.
🔴 Senior Level
Внутренности ядра: TransactionSynchronizationManager и PlatformTransactionManager
Вся синхронизация транзакционных ресурсов держится на статическом классе TransactionSynchronizationManager, хранящем состояние в ThreadLocal:
public abstract class TransactionSynchronizationManager {
// Карта ресурсов: DataSource -> ConnectionHolder (или SessionFactory -> SessionHolder)
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
// Зарегистрированные колбэки (TransactionSynchronization)
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
}
Механизм Suspend/Resume в AbstractPlatformTransactionManager:
Когда вызывается метод с REQUIRES_NEW, метод suspend() ядра Spring выполняет следующую микрохирургию:
- Отвязывает
ConnectionHolderот текущегоThreadLocalчерезTransactionSynchronizationManager.unbindResource(dataSource). - Приостанавливает все зарегистрированные синхронизации (
TransactionSynchronization.suspend()), включая сессию Hibernate L1 Cache. - Упаковывает всё это состояние в объект
SuspendedResourcesHolder. - Создает новый
TransactionStatus, берет новое соединение из пула HikariCP, привязывает его кThreadLocalи переключаетautoCommit = false. - В блоке
finallyпосле завершения внутреннего метода вызываетсяresume(), возвращающийSuspendedResourcesHolderобратно вThreadLocal.
Риск Connection Pool Deadlock при REQUIRES_NEW в Highload
Использование REQUIRES_NEW в высоконагруженных системах несет в себе скрытую угрозу взаимной блокировки пула соединений (Pool Exhaustion Deadlock).
Размер пула HikariCP = 10 соединений.
Одновременно поступает 10 HTTP-запросов (10 рабочих потоков Tomcat/Undertow):
Поток 1: начал outerMethod (REQUIRED) -> Захватил Connection #1 из пула (удерживает в suspend)
Поток 2: начал outerMethod (REQUIRED) -> Захватил Connection #2 из пула (удерживает в suspend)
...
Поток 10: начал outerMethod (REQUIRED) -> Захватил Connection #10 из пула (удерживает в suspend)
В этот момент в пуле HikariCP осталось 0 свободных соединений!
Поток 1 вызывает innerMethod (REQUIRES_NEW) -> просит Connection #11 -> ЖДЕТ В ПУЛЕ...
Поток 2 вызывает innerMethod (REQUIRES_NEW) -> просит Connection #12 -> ЖДЕТ В ПУЛЕ...
...
Поток 10 вызывает innerMethod (REQUIRES_NEW) -> просит Connection #20 -> ЖДЕТ В ПУЛЕ...
ИТОГ: Все 10 потоков заблокированы навсегда, ожидая соединения из пустого пула!
По истечении connectionTimeout (по умолчанию 30 секунд) все 10 транзакций упадут с:
SQLTransientConnectionException: Connection is not available, request timed out after 30000ms.
Математическая формула размера пула:
Для безопасного использования $N$ уровней вложенности REQUIRES_NEW:
\(\text{PoolSize} \ge \text{MaxThreads} \times (1 + \text{MaxNestedRequiresNew}) + \text{SafetyMargin}\)
Проблема Self-Invocation (самовызова) и методы её решения
Вызов транзакционного метода из того же класса (this.anotherTransactionalMethod()) полностью игнорирует propagation, потому что вызов идет мимо Spring AOP Proxy.
@Service
public class OrderService {
@Transactional
public void outer() {
// ОШИБКА: propagation = REQUIRES_NEW НЕ СРАБОТАЕТ!
// Вызов идет напрямую через указатель this, минуя TransactionInterceptor!
this.innerAudit();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerAudit() {
// Выполнится внутри транзакции outer(), как если бы было REQUIRED!
}
}
Senior-решения проблемы:
- Вынесение в отдельный бин (Best Practice): Разделение ответственности и вынос логики с другим жизненным циклом в
AuditService. - Инжекция самого себя (
Self-injection):@Service public class OrderService { @Autowired private OrderService self; // Инжектится AOP-прокси! @Transactional public void outer() { self.innerAudit(); // Проходит через прокси! } } - Использование
AopContext: Включить@EnableAspectJAutoProxy(exposeProxy = true)и вызывать((OrderService) AopContext.currentProxy()).innerAudit().
4 Tricky Questions
1. Что произойдет, если родительский метод с Propagation.REQUIRED перехватит в блоке try-catch исключение из дочернего метода, который тоже объявлен как Propagation.REQUIRED?
Ответ:
При попытке коммита внешней транзакции Spring выбросит UnexpectedRollbackException:
Transaction silently rolled back because it has been marked as rollback-only.
Механика под капотом:
- Оба метода выполняются в одной и той же физической транзакции и делят один
TransactionStatus. - Когда во внутреннем методе случается
RuntimeException, его перехватываетTransactionInterceptorвнутреннего прокси до того, как управление вернется в блокcatchвнешнего метода. - Поскольку транзакция разделяемая, внутренний интерцептор обязан гарантировать целостность данных: он вызывает
transactionStatus.setRollbackOnly(). - Внешний метод ловит исключение, считает, что успешно обработал ошибку, и выходит без исключений.
- Внешний
TransactionInterceptorпытается выполнитьconnection.commit(), но обнаруживает флагisRollbackOnly() == true. Spring выполняетrollbackи бросаетUnexpectedRollbackException, чтобы вызывающий клиент не подумал ошибочно, что данные успешно сохранились.
2. Поддерживает ли JpaTransactionManager (Hibernate) уровень Propagation.NESTED из коробки?
Ответ:
Нет, по умолчанию не поддерживает. При попытке вызвать метод с Propagation.NESTED под управлением JpaTransactionManager будет выброшено исключение:
org.springframework.transaction.NestedTransactionNotSupportedException: JpaTransactionManager does not support nested transactions by default.
Причина: Hibernate использует контекст персистентности первого уровня (PersistenceContext / L1 Cache) с накоплением мутаций в памяти. JDBC Savepoint откатывает состояние страниц и блокировок в самой базе данных, но EntityManager не умеет восстанавливать свое внутреннее состояние снимков сущностей до состояния Savepoint. Если бы NESTED разрешили, при последующем flush() Hibernate попытался бы отправить в базу неконсистентные сущности, которые в БД уже были откатаны точкой сохранения.
NESTED поддерживается только DataSourceTransactionManager при работе с чистым JDBC / MyBatis / JdbcTemplate.
3. Как ведут себя переменные ThreadLocal транзакции при переходе в асинхронный поток (@Async или CompletableFuture)?
Ответ:
Переменные TransactionSynchronizationManager НЕ передаются в дочерние потоки автоматически.
По умолчанию Spring использует стандартный ThreadLocal, а не InheritableThreadLocal. Если из метода @Transactional запустить дочерний поток через CompletableFuture.runAsync(...) или @Async:
- Дочерний поток стартует с пустым
TransactionSynchronizationManager. - Если в дочернем потоке вызвать
accountRepository.save(account), он выполнится в абсолютно новом независимом соединении в режиме autocommit (или откроет новую собственную транзакцию). - Дочерний поток не видит незакоммиченных изменений родительского потока (на уровнях выше Read Uncommitted).
- Завершение или откат родительской транзакции никак не влияет на операции дочернего потока, что часто приводит к нарушению атомарности.
4. В чем отличие Propagation.NOT_SUPPORTED от выполнения метода без аннотации @Transactional вообще?
Ответ:
- Если метод вообще не аннотирован
@Transactional, и он вызывается внутри существующей транзакции (через бин-прокси), этот метод будет выполняться внутри текущей открытой транзакции, используя ее соединение и контекст синхронизации. - Если метод аннотирован
@Transactional(propagation = Propagation.NOT_SUPPORTED), Spring AOP Proxy принудительно приостанавливает текущую транзакцию (suspend), отвязывает коннект от потока и выполняет тело метода без транзакции (все SQL-команды работают в чистомautoCommit = true). После завершения метода родительская транзакция возобновляется (resume).
🎯 Шпаргалка для интервью
30-секундный ответ
«Propagation в Spring определяет границы транзакций при вызове одного транзакционного метода из другого. Существует 7 типов. Дефолтный —
REQUIRED(присоединяется к текущей или создает новую). Для независимых операций (например, аудит) используетсяREQUIRES_NEW(приостанавливает внешнюю и открывает новую физическую транзакцию в отдельном соединении). Для частичного отката через точки сохранения JDBC —NESTED. Важно помнить: вызовы черезthisвнутри одного класса обходят Spring Proxy, из-за чего настройкиpropagationигнорируются, а использованиеREQUIRES_NEWпри маленьком пуле соединений может вызвать Connection Pool Deadlock».
Чек-лист ключевых понятий
- Дефолт:
Propagation.REQUIRED. - 7 типов:
REQUIRED,REQUIRES_NEW,NESTED,MANDATORY,SUPPORTS,NOT_SUPPORTED,NEVER. - UnexpectedRollbackException: Возникает, когда внешний метод перехватывает исключение из внутреннего
REQUIRED-метода, пометившего транзакциюrollback-only. - Self-invocation: Вызов
this.method()идет мимо AOP Proxy, аннотация не перехватывается. - Connection Pool Deadlock: Вложенные
REQUIRES_NEWблокируют родительские соединения в пуле. - NESTED vs JPA:
JpaTransactionManagerне поддерживаетNESTEDиз коробки из-за невозможности синхронизации Hibernate L1 Cache.
Красные флаги (чего ни в коем случае нельзя говорить)
- ❌ «REQUIRES_NEW и NESTED — это одно и то же» (REQUIRES_NEW коммитится независимо в новом коннекте; NESTED использует Savepoint в текущем коннекте и откатывается, если упадет внешняя транзакция).
- ❌ «Если поймать ошибку внутреннего REQUIRED-метода в try-catch, внешняя транзакция успешно закоммитится» (Нет, будет
UnexpectedRollbackException, так как транзакция уже помеченаrollback-only). - ❌ «Propagation работает при вызове любого метода того же класса» (Вызов через
thisобходит Spring прокси). - ❌ «NESTED поддерживается Hibernate JPA из коробки» (Вызовет
NestedTransactionNotSupportedException).