Spring FrameworkSeniorTechnical

Что такое декларативное управление транзакциями в 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» в реальном сервисе.

Sources

Related topics