Spring FrameworkSeniorTechnical
Какие поведения propagation транзакций существуют в Spring?
Spring поддерживает 7 режимов propagation: REQUIRED (присоединяется или создаёт), REQUIRES_NEW (всегда новая), NESTED, SUPPORTS, NOT_SUPPORTED, MANDATORY, NEVER — определяют поведение транзакций при вложенных вызовах.
Propagation транзакций в Spring
Атрибут propagation аннотации @Transactional определяет, как метод должен вести себя относительно существующей транзакции вызывающего кода. Spring использует прокси-механизм, поэтому propagation применяется только при вызовах через прокси (межбиновые вызовы).
Все режимы propagation
- REQUIRED (по умолчанию) — присоединяется к существующей транзакции; если её нет — создаёт новую
- REQUIRES_NEW — всегда создаёт новую транзакцию, приостанавливая существующую
- NESTED — создаёт вложенную транзакцию с savepoint; откат вложенной не откатывает внешнюю
- SUPPORTS — выполняется в транзакции если есть, без транзакции если нет
- NOT_SUPPORTED — всегда выполняется без транзакции, приостанавливая существующую
- MANDATORY — требует существующей транзакции; без неё бросает
IllegalTransactionStateException - NEVER — требует отсутствия транзакции; при наличии бросает исключение
Примеры
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Autowired
private AuditService auditService;
// REQUIRED: присоединяется к транзакции вызывающего кода
@Transactional(propagation = Propagation.REQUIRED)
public void placeOrder(Order order) {
// Сохранение заказа
orderRepository.save(order);
// REQUIRES_NEW: лог аудита фиксируется НЕЗАВИСИМО от результата заказа
auditService.log("Order placed", order.getId());
// Если здесь exception — заказ откатится, но лог уже в БД
paymentService.charge(order);
}
}
@Service
public class AuditService {
// Всегда новая транзакция — commit не зависит от внешней
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String action, Long entityId) {
auditRepository.save(new AuditEntry(action, entityId, LocalDateTime.now()));
}
}
@Service
public class BulkImportService {
@Autowired
private ItemService itemService;
// Внешняя транзакция
@Transactional
public ImportResult importAll(List<ItemDto> items) {
ImportResult result = new ImportResult();
for (ItemDto dto : items) {
try {
// NESTED: savepoint перед каждым элементом
itemService.importOne(dto);
result.success();
} catch (Exception e) {
// Откат только этого элемента, внешняя транзакция продолжается
result.fail(dto.getId(), e.getMessage());
}
}
return result;
}
}
@Service
public class ItemService {
@Transactional(propagation = Propagation.NESTED)
public void importOne(ItemDto dto) {
itemRepository.save(dto.toEntity());
// exception здесь откатит только этот savepoint
}
}
MANDATORY и NEVER для защиты контракта
@Service
public class CriticalService {
// Метод должен вызываться только внутри транзакции
@Transactional(propagation = Propagation.MANDATORY)
public void criticalOperation() {
// IllegalTransactionStateException если транзакции нет
}
// Метод не должен вызываться в транзакции (например, долгая операция)
@Transactional(propagation = Propagation.NEVER)
public void standaloneReport() {
// TransactionSynchronizationException если транзакция есть
}
}
Подводные камни
- Self-invocation: вызов
@Transactional-метода из другого метода того же класса не проходит через прокси — propagation игнорируется, транзакция не создаётся. Обходное решение: вынести метод в отдельный бин. REQUIRES_NEWприостанавливает внешнюю транзакцию и открывает новое соединение с БД — при использовании в цикле это исчерпает пул соединений.NESTEDподдерживается не всеми JDBC-драйверами (требует savepoint); с JPA это проблематично, так как JPA EntityManager не имеет прямого понятия savepoint.- Откат при
REQUIRED: если внутренний метод кидает исключение, он помечает транзакцию как rollback-only; внешний метод при попытке commit получитUnexpectedRollbackException. @Transactionalпо умолчанию откатывает толькоRuntimeExceptionиError— checked exceptions не вызывают откат без явногоrollbackFor.- Уровень изоляции (
isolation) применяется только при создании новой транзакции; приREQUIREDс уже существующей транзакцией запрошенная изоляция игнорируется. - Тестирование с
@Transactionalна тест-методе: транзакция откатывается после теста, ноREQUIRES_NEWвнутри тестируемого кода создаёт независимую транзакцию, которая фиксируется в БД — данные остаются после теста. - При
NOT_SUPPORTEDсуществующая транзакция приостанавливается, а не откатывается; данные, изменённые до вызова, не будут видны незавершённой транзакцией.
Common mistakes
- Путать термин «transaction propagation» с соседним механизмом Spring Framework.
- Не называть границу lifecycle, transaction, thread или request для «transaction propagation».
- Игнорировать production-эффекты «transaction propagation»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «transaction propagation» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «transaction propagation».
- Уточнить, какие настройки или API меняют «transaction propagation» в реальном сервисе.