Что такое декларативное управление транзакциями в Spring и как работает @Transactional?
@Transactional реализуется через AOP-прокси (JDK dynamic proxy или CGLIB): Spring перехватывает вызов метода через TransactionInterceptor, открывает транзакцию в PlatformTransactionManager и делает commit или rollback в зависимости от исхода.
Как работает @Transactional
Когда Spring обнаруживает бин с методом, помеченным @Transactional, он создаёт AOP-прокси вокруг бина. При каждом вызове аннотированного метода управление сначала попадает в TransactionInterceptor, который взаимодействует с PlatformTransactionManager (или ReactiveTransactionManager для реактивного стека). Менеджер открывает новую транзакцию либо присоединяется к существующей в соответствии с propagation-стратегией. После выхода из метода: если исключений нет — commit(), если выброшено непроверяемое (RuntimeException) или явно указанное исключение — rollback().
Ключевые атрибуты аннотации
propagation— стратегия распространения транзакции:REQUIRED(по умолчанию),REQUIRES_NEW,NESTED,SUPPORTS,NOT_SUPPORTED,MANDATORY,NEVER.isolation— уровень изоляции:DEFAULT,READ_COMMITTED,REPEATABLE_READ,SERIALIZABLE.rollbackFor/noRollbackFor— классы исключений, управляющие rollback.readOnly— подсказка для JPA/Hibernate: отключить dirty checking, использовать read-replica.timeout— максимальное время транзакции в секундах.
Пример с propagation и rollbackFor
@Service
@Transactional(readOnly = true) // все методы класса — read-only по умолчанию
public class OrderService {
private final OrderRepository orderRepo;
private final PaymentService paymentService;
public OrderService(OrderRepository orderRepo, PaymentService paymentService) {
this.orderRepo = orderRepo;
this.paymentService = paymentService;
}
// Переопределяем на read-write + откат на любое исключение
@Transactional(
rollbackFor = Exception.class,
isolation = Isolation.READ_COMMITTED,
timeout = 30
)
public Order placeOrder(CreateOrderRequest req) throws PaymentException {
Order order = orderRepo.save(new Order(req));
// paymentService.charge работает в ОТДЕЛЬНОЙ транзакции
paymentService.charge(order.getId(), req.getAmount());
return order;
}
public List<Order> findByUser(Long userId) {
// readOnly=true — Hibernate пропускает dirty check, выигрыш производительности
return orderRepo.findByUserId(userId);
}
}
@Service
public class PaymentService {
private final PaymentRepository paymentRepo;
// REQUIRES_NEW: всегда новая транзакция, независимо от вызывающей
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void charge(Long orderId, BigDecimal amount) throws PaymentException {
Payment payment = new Payment(orderId, amount);
paymentRepo.save(payment);
// если выброшено исключение — откатится только эта транзакция
gateway.process(payment);
}
}
Self-invocation: главная ловушка
Вызов @Transactional-метода из того же класса (self-invocation) минует прокси — транзакция не будет открыта. Решения: инжектировать бин в себя (@Autowired ApplicationContext или @Lazy-ссылка на себя), вынести метод в отдельный бин, либо включить AspectJ weaving вместо прокси-режима.
Настройка менеджера транзакций
@Configuration
@EnableTransactionManagement // включает обработку @Transactional
public class DataConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource ds) {
return new DataSourceTransactionManager(ds);
}
// Для JPA:
// return new JpaTransactionManager(entityManagerFactory);
}
Подводные камни
- Self-invocation: метод вызывает другой
@Transactional-метод того же класса напрямую — прокси не задействован, транзакция не открывается. - Checked exceptions не откатывают по умолчанию: только
RuntimeExceptionиError. Для проверяемых нуженrollbackFor = Exception.class. - private-методы игнорируются Spring AOP — аннотация на
private void doSomething()не работает. - Транзакция на уровне контроллера:
@Transactionalв@RestControllerработает, но антипаттерн — HTTP-слой не должен управлять транзакциями; граница транзакции должна быть в сервисном слое. - LazyInitializationException: при использовании JPA с
@Transactional(readOnly = true)ленивые коллекции доступны внутри метода, но после его завершения сессия закрыта. - NESTED propagation требует поддержки savepoint от JDBC-драйвера; не все БД и драйверы это поддерживают.
- readOnly = true не гарантирует изоляции: это лишь подсказка для провайдера, реальная read-only изоляция зависит от БД.
- Многопоточность: транзакция привязана к потоку через
ThreadLocal(TransactionSynchronizationManager). Запуск задачи в новом потоке изнутри транзакции не распространяет её на новый поток.
Common mistakes
- Путать термин «declarative transactions» с соседним механизмом Spring Framework.
- Не называть границу lifecycle, transaction, thread или request для «declarative transactions».
- Игнорировать production-эффекты «declarative transactions»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «declarative transactions» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «declarative transactions».
- Уточнить, какие настройки или API меняют «declarative transactions» в реальном сервисе.