Что делает аннотация @Transactional
Транзакция гарантирует соблюдение требований ACID (Атомарность, Согласованность, Изолированность, Надежность). Если в процессе выполнения бизнес-метода происходит сбой, все изме...
🟢 Junior Level
@Transactional — это декларативная аннотация Spring, которая возлагает управление транзакциями базы данных на IoC-контейнер, избавляя разработчика от ручного написания шаблонного кода открытия, фиксации (commit) и отката (rollback) транзакций.
Зачем нужна аннотация
Транзакция гарантирует соблюдение требований ACID (Атомарность, Согласованность, Изолированность, Надежность). Если в процессе выполнения бизнес-метода происходит сбой, все изменения данных, сделанные этим методом в БД, гарантированно откатятся к исходному состоянию.
package com.example.service;
import com.example.model.Order;
import com.example.repository.OrderRepository;
import com.example.repository.PaymentRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
public OrderService(OrderRepository orderRepository, PaymentRepository paymentRepository) {
this.orderRepository = orderRepository;
this.paymentRepository = paymentRepository;
}
@Transactional
public void placeOrder(Order order, Long amount) {
// Шаг 1: Сохранить заказ в БД (INSERT)
orderRepository.save(order);
// Шаг 2: Списать средства (UPDATE)
paymentRepository.charge(order.getUserId(), amount);
// Если здесь упадет RuntimeException, оба шага откатятся (Rollback)
}
}
Как это работает под капотом (AOP-прокси)
- Spring создает динамический AOP-прокси вокруг бина.
- При вызове метода интерцептор
TransactionInterceptorзапрашивает соединение уPlatformTransactionManagerи выполняетconnection.setAutoCommit(false). - Вызывается целевой бизнес-метод.
- Если метод завершился успешно →
connection.commit(). - Если метод выбросил
RuntimeExceptionилиError→connection.rollback(). - Соединение возвращается в пул (HikariCP).
🟡 Middle Level
Параметры аннотации @Transactional
Аннотация позволяет тонко настроить поведение транзакции:
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
timeout = 5,
readOnly = false,
rollbackFor = {Exception.class},
noRollbackFor = {CustomBusinessWarningException.class}
)
Стратегии распространения транзакций (Propagation)
Spring определяет, как метод должен вести себя, если транзакция уже открыта выше по стеку вызовов:
| Стратегия | Поведение при наличии активной транзакции | Поведение при отсутствии транзакции |
|---|---|---|
REQUIRED (по умолчанию) |
Присоединяется к существующей | Создает новую физическую транзакцию |
REQUIRES_NEW |
Приостанавливает текущую, создает независимую новую | Создает новую транзакцию |
NESTED |
Создает Savepoint внутри текущей транзакции | Создает новую транзакцию |
MANDATORY |
Присоединяется к существующей | Выбрасывает NoTransactionException |
SUPPORTS |
Выполняется внутри транзакции | Выполняется без транзакции (non-transactional) |
NOT_SUPPORTED |
Приостанавливает текущую, выполняется без нее | Выполняется без транзакции |
NEVER |
Выбрасывает IllegalTransactionStateException |
Выполняется без транзакции |
Уровни изоляции (Isolation)
Защищают от аномалий параллельного чтения данных:
DEFAULT: уровень изоляции СУБД по умолчанию (в PostgreSQL —READ_COMMITTED, в MySQL InnoDB —REPEATABLE_READ).READ_COMMITTED: защищает от «грязного» чтения (Dirty Reads).REPEATABLE_READ: защищает от грязного и неповторяющегося чтения (Non-repeatable Reads).SERIALIZABLE: полная изоляция ценой блокировок и отката конфликтующих транзакций (защищает от Phantom Reads).
Правила отката (Rollback Rules): ловушка Checked Exceptions
Критическое правило Spring по умолчанию: Автоматический откат (
rollback) выполняется только дляUnchecked Exceptions(наследникиRuntimeException) иError.
Если метод выбросит проверяемое исключение (Checked Exception, например IOException или SQLException), транзакция НЕ откатится, а успешно зафиксируется (commit)!
// ОШИБКА: транзакция закоммитится, несмотря на исключение!
@Transactional
public void processFile() throws IOException {
orderRepo.save(new Order());
if (error) {
throw new IOException("Диск недоступен"); // Checked exception: транзакция НЕ откатится!
}
}
// ПРАВИЛЬНО: явное указание rollbackFor
@Transactional(rollbackFor = Exception.class)
public void processFileCorrect() throws IOException {
orderRepo.save(new Order());
if (error) {
throw new IOException("Диск недоступен"); // Откатится корректно
}
}
🔴 Senior Level
Архитектура: TransactionSynchronizationManager и привязка к потоку
Управление транзакциями в традиционном Spring MVC / JDBC основано на привязке ресурсов к потоку через ThreadLocal:
TransactionSynchronizationManagerдержит вThreadLocalмапу ресурсов (текущее JDBCConnectionили HibernateSession).- Все репозитории и
JdbcTemplateизвлекают одно и то же соединение из контекста потока. - Коллбэки синхронизации (
TransactionSynchronization) позволяют регистрировать слушатели событий коммита и отката.
Флаг readOnly = true: внутренняя оптимизация
Указание @Transactional(readOnly = true) активирует глубокие оптимизации на нескольких уровнях:
- Уровень Hibernate / JPA:
- Hibernate переводит
SessionвFlushMode.MANUAL. - Отключается Dirty Checking: Hibernate не сохраняет снапшоты сущностей в Persistence Context и не сканирует поля на изменения при закрытии сессии. Это колоссально экономит оперативную память и CPU при выборке тысяч записей!
- Hibernate переводит
- Уровень JDBC / СУБД:
- Spring вызывает
connection.setReadOnly(true). - Некоторые СУБД (например, PostgreSQL, Oracle) и пулы соединений могут перенаправлять такие запросы на Read-Replica узлы кластера.
Важно:
readOnly = trueне гарантирует абсолютную защиту отINSERTна уровне драйвера, если СУБД не валидирует статус транзакции строго.
- Spring вызывает
Смертельная ловушка REQUIRES_NEW: Deadlock пула соединений
Сценарий, регулярно валящий production-системы под нагрузкой:
@Service
public class OrderService {
@Autowired
private AuditService auditService;
@Transactional // Транзакция 1 (Берет Connection A из пула)
public void placeOrder() {
// ... бизнес-логика ...
auditService.logAction(); // Внутренний вызов REQUIRES_NEW
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // Транзакция 2 (Берет Connection B)
public void logAction() {
auditRepository.save(new AuditLog());
}
}
Механика взаимной блокировки (Deadlock):
- Пул HikariCP настроен на 10 соединений (
maximum-pool-size = 10). - Приходит всплеск трафика: ровно 10 параллельных запросов вызывают
placeOrder(). - Каждый из 10 потоков забирает по одному соединению (все 10 соединений исчерпаны).
- Теперь каждый поток доходит до строки
auditService.logAction(). - Для
REQUIRES_NEWкаждому потоку требуется второе независимое соединение. - Все 10 потоков блокируются в ожидании свободного соединения из пула.
- Ни один поток не может завершить внешнюю транзакцию и вернуть соединение.
- Результат: 100% Deadlock приложения до истечения
connection-timeout!
Надежная публикация событий: TransactionalEventListener
Если внутри транзакции нужно отправить сообщение в Apache Kafka или RabbitMQ:
- Отправлять напрямую из
@Transactionalнельзя: если сообщение уйдет брокеру, а база данных упадет на этапе коммита, возникнет рассинхронизация (Phantom Event). - Решение: использовать
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)либо паттерн Transactional Outbox.
package com.example.event;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class OrderNotificationListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
// Выполняется СТРОГО ПОСЛЕ успешного коммита транзакции в БД!
kafkaProducer.send("orders-topic", event);
}
}
🎯 Шпаргалка для интервью
30-секундный ответ
@Transactional— аннотация декларативного управления транзакциями, работающая через Spring AOP Proxy иPlatformTransactionManager.
- Оборачивает вызов метода в открытие транзакции,
commitпри успехе иrollbackпри ошибке.- По умолчанию: откат происходит только для
RuntimeExceptionиError. Проверяемые исключения (Checked Exception) транзакцию не откатывают (требуетсяrollbackFor = Exception.class).- Основные параметры:
propagation(по умолчаниюREQUIRED),isolation(изоляция СУБД),readOnly(отключает Dirty Checking в Hibernate),timeout.- Не работает при вызове внутри того же класса (
self-invocation) и наprivate/finalметодах.
4 каверзных вопроса с ответами
-
Откатится ли транзакция, если метод выбросит
IOException? Ответ: Нет, по умолчанию транзакция зафиксируется (commit). Spring откатывает транзакции только при возникновении непроверяемых исключений (RuntimeException) или системных ошибок (Error). Чтобы транзакция откатывалась при любых исключениях, необходимо явно указывать@Transactional(rollbackFor = Exception.class). -
В чём разница между
Propagation.REQUIRES_NEWиPropagation.NESTED? Ответ:REQUIRES_NEWприостанавливает внешнюю транзакцию и открывает полностью независимую новую транзакцию, требующую второго физического соединения с БД. Сбой во внешней транзакции не откатит новую, и наоборот.NESTEDиспользует то же самое физическое соединение и создает точку сохранения (Savepoint). Откат вложенной части происходит до точки сохранения без отката всей внешней транзакции. -
Как использование
REQUIRES_NEWможет положить сервер при росте нагрузки? Ответ: Каждый вызовREQUIRES_NEWудерживает сразу два физических соединения из пула (одно заморожено во внешней транзакции, второе активно во внутренней). Если пул соединений ограничен, а число параллельных потоков равно размеру пула, наступает взаимная блокировка (Deadlock пула соединений): все потоки ждут второе соединение, удерживая первое. -
Что дает флаг
readOnly = trueв Hibernate? Ответ: Hibernate отключает механизм отслеживания грязных сущностей (Dirty Checking) и переводит сессию в режимFlushMode.MANUAL. Hibernate не сохраняет копии снимков сущностей в памяти, что значительно снижает потребление RAM и нагрузку на CPU при выборке больших объемов данных.
Красные флаги (чего категорически нельзя говорить)
- ❌ «Spring откатывает транзакцию при абсолютно любой ошибке» — грубая ошибка. Checked Exceptions по умолчанию игнорируются транзакционным механизмом.
- ❌ «
REQUIRES_NEWоткрывает транзакцию в том же соединении» — дляREQUIRES_NEWобязательно выделяется отдельное соединение из пула. - ❌ «
readOnly = trueгарантированно запрещает SQL-запросыINSERTна уровне синтаксиса базы» — это оптимизация сессии Hibernate и подсказка драйверу, а не строгий триггер прав СУБД. - ❌ «Можно делать длинные HTTP-вызовы к внешним сервисам прямо внутри
@Transactional» — антипаттерн, приводящий к исчерпанию пула соединений БД и падению микросервиса.