Что такое аннотация @Transactional?
@Transactional — это аннотация Spring, которая позволяет управлять транзакциями декларативно. Вместо ручного BEGIN/COMMIT/ROLLBACK вы просто ставите аннотацию — Spring делает вс...
🟢 Junior Level
@Transactional — это аннотация Spring, которая позволяет управлять транзакциями декларативно. Вместо ручного BEGIN/COMMIT/ROLLBACK вы просто ставите аннотацию — Spring делает всё автоматически.
Простейший пример
@Service
public class UserService {
@Transactional
public void createUser(User user) {
userRepository.save(user);
// Spring автоматически сделает COMMIT при успехе
// или ROLLBACK при RuntimeException
}
}
Аналогия
Представьте банковский перевод: вы либо переводите деньги целиком, либо не переводите вовсе. @Transactional гарантирует, что половина перевода не “зависнет” в базе.
Ключевые параметры
| Параметр | Что делает | Значение по умолчанию |
|---|---|---|
propagation |
Как транзакция связана с другими | REQUIRED |
isolation |
Уровень изоляции | DEFAULT (уровень БД) |
readOnly |
Оптимизация для чтения | false |
rollbackFor |
Какие исключения вызывают откат | RuntimeException, Error |
timeout |
Таймаут в секундах | -1 (нет таймаута) |
Когда использовать
Любой метод, который выполняет несколько операций с БД, которые должны быть атомарными: переводы денег, создание заказа с позициями, обновление связанных сущностей.
Важные правила
- Работает только на public методах
- Работает только при вызове извне класса (не через
this) - По умолчанию откатывает только на RuntimeException и Error
🟡 Middle Level
Как работает: AOP Proxy механизм
Внешний вызов → Proxy → TransactionInterceptor → PlatformTransactionManager
↓
Открыть транзакцию → Выполнить метод → COMMIT/ROLLBACK
- Spring создаёт Proxy-объект вокруг бина (JDK Dynamic Proxy или CGLIB)
- При вызове метода управление попадает в
TransactionInterceptor - Перехватчик обращается к
PlatformTransactionManager, который открывает соединение и начинает транзакцию - Выполняется ваш метод
- При успехе —
commit, при исключении —rollback
Где можно размещать аннотацию
| Уровень | Работает? | Рекомендация |
|---|---|---|
| Метод | ✅ Да | Лучший вариант — точный контроль |
| Класс | ✅ Да | Все public методы транзакционны |
| Интерфейс | ⚠️ Зависит | Только с JDK Dynamic Proxy |
| Private метод | ❌ Нет | Proxy не перехватывает |
Пример с параметрами
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
timeout = 30,
readOnly = false,
rollbackFor = Exception.class,
noRollbackFor = NotFoundException.class
)
public void processOrder(Order order) { ... }
Распространённые ошибки
| Ошибка | Что происходит | Как исправить |
|---|---|---|
@Transactional на private методе |
Аннотация игнорируется | Сделать метод public |
Вызов this.method() внутри класса |
Proxy bypassed, нет транзакции | Вынести в отдельный бин или self-injection |
Checked Exception без rollbackFor |
COMMIT вместо ROLLBACK | Добавить rollbackFor = Exception.class |
@Transactional на Controller |
Соединение с БД держится при сериализации JSON | Перенести на Service слой |
| Catch и swallow исключения | Spring не видит исключение → COMMIT | Перебросить исключение или вызвать setRollbackOnly() |
Когда НЕ использовать
| Ситуация | Почему не нужно | Альтернатива |
|---|---|---|
| Метод только читает одну запись | Излишняя сложность | readOnly = true или без транзакции |
| HTTP-вызов внутри метода | Держит соединение с БД unnecessarily | Вынести HTTP-вызов за транзакцию |
| Логирование/аудит | Не требует атомарности с бизнес-логикой | REQUIRES_NEW в отдельном бине |
| Фоновая обработка сообщений | Может требовать другой стратегии | Programmatic transactions |
Сравнение: декларативный vs программный подход
| Подход | Плюсы | Минусы | Когда использовать |
|---|---|---|---|
@Transactional |
Чистый код, декларативность | Ограничен proxy | Стандартные сценарии |
TransactionTemplate |
Полный контроль, работает внутри this |
Больше boilerplate | Условная логика транзакций |
🔴 Senior Level
Spring Transaction AOP — Internal Architecture
Proxy Creation Flow
// InfrastructureAdvisorAutoProxyCreator (BeanPostProcessor)
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (isInfrastructureClass(bean.getClass())) return bean;
if (AnnotationUtils.findAnnotation(bean.getClass(), Transactional.class) != null
|| hasTransactionalMethods(bean.getClass())) {
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvice(new TransactionInterceptor(tm, transactionAttributeSource));
return proxyFactory.getProxy();
}
return bean;
}
TransactionInterceptor Execution
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(invocation.getThis());
TransactionAttribute txAttr = tas.getTransactionAttribute(
invocation.getMethod(), targetClass);
PlatformTransactionManager tm = determineTransactionManager(txAttr);
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, invocation.getMethod().toString());
Object retVal;
try {
retVal = invocation.proceed();
} catch (Throwable ex) {
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
JDK Dynamic Proxy vs CGLIB
| Характеристика | JDK Dynamic Proxy | CGLIB |
|---|---|---|
| Механизм | Реализует интерфейсы | Наследует класс |
| Скорость создания | Быстрее (~5-10μs) | Медленнее (~20-50μs) |
| Overhead вызова | Низкий | Немного выше |
@Transactional на интерфейсе |
✅ Работает | ❌ Игнорируется |
| Final методы | ✅ Не проблема | ❌ Нельзя переопределить |
| Память | Меньше | Больше (генерирует класс) |
// JDK Dynamic Proxy
UserService proxy = (UserService) Proxy.newProxyInstance(
classLoader,
new Class[]{UserService.class},
handler
);
// CGLIB
UserService proxy = (UserService) Enhancer.create(UserService.class, handler);
Spring Boot по умолчанию использует CGLIB если нет интерфейсов. Можно форсировать: @EnableTransactionManagement(proxyTargetClass = true).
Thread-Local Transaction State
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
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");
}
Именно поэтому @Transactional не работает между потоками при стандартной конфигурации — ThreadLocal не передаётся. @Async методы получают отдельный transaction context. При использовании кастомных TaskExecutor с ThreadLocal propagation или Virtual Threads (Java 21+) возможно пробрасывание контекста, но это требует явной настройки.
Edge Cases (минимум 3)
-
UnexpectedRollbackException: В цепочке REQUIRED → REQUIRED внутренний метод выбросил RuntimeException. Внешний перехватил, но TransactionInterceptor уже выставил
rollback-onlyфлаг. При попытке commit — выбрасываетсяUnexpectedRollbackException. -
@Transactional на final методе: CGLIB не может переопределить final метод → аннотация silently ignored. JDK Proxy тоже не перехватывает final методы интерфейса.
-
Наследование и @Transactional: Если子类 переопределяет метод без повторения аннотации — поведение зависит от типа proxy. С CGLIB аннотация суперкласса может не обнаружиться.
-
Несколько TransactionManager: Если в приложении два
PlatformTransactionManager,@Transactionalбез qualifier использует@Primary. Вызов двух разных методов с разными менеджерами = две независимые транзакции, не распределённая транзакция. Нужен JTA/Atomikos для 2PC. JTA (Java Transaction API) и Atomikos (реализация JTA). 2PC (Two-Phase Commit) — протокол для атомарного коммита транзакций на нескольких ресурсах.
Performance Numbers
| Операция | Время |
|---|---|
| Proxy invocation overhead | ~1-2 μs |
| Transaction setup (Spring side) | ~5-10 μs |
| Connection acquisition (HikariCP) | ~50-200 μs |
| Typical DB query | 1-10 ms |
Overhead Spring — пренебрежимо мал. Доминирует работа с БД.
Memory Implications
Каждая активная транзакция удерживает:
- JDBC Connection из пула (~2-4 KB)
- Hibernate L1 cache (Persistence Context) — растёт с количеством загруженных entities
- ThreadLocal entries в
TransactionSynchronizationManager - Undo log в БД (чем дольше транзакция, тем больше)
Долгие транзакции = exhaustion пула соединений + bloat в БД.
Thread Safety
@Transactional не thread-safe сам по себе. Каждый поток получает:
- Отдельную транзакцию
- Отдельное соединение с БД
- Отдельный Hibernate Session
Сам сервисный бин — singleton. Но состояние транзакции хранится в ThreadLocal, поэтому конкурентные вызовы одного @Transactional метода изолированы.
Production War Story
В production сервис с @Transactional ловил checked BusinessException. Разработчик не указал rollbackFor. Транзакция делала COMMIT, но часть данных уже была записана. Клиент получал ошибку, но половина заказа оставалась в базе. Решение: rollbackFor = Exception.class на всех write-методах + интеграционные тесты на rollback поведение.
Почему Spring закоммитил: по умолчанию rollback только для RuntimeException + Error. Checked exception (наследуется от Exception, но не от RuntimeException) воспринимается TransactionInterceptor как «нормальное завершение» → commit. Даже если метод бросил исключение, Spring не видит его как сигнал к откату — потому что исключение перехвачено в catch-блоке, а rollbackFor не указан.
Monitoring
Spring Boot Debug Logging:
logging:
level:
org.springframework.transaction: DEBUG
org.springframework.orm.jpa: DEBUG
Actuator Metrics:
@Bean
public TransactionManagerCustomizers transactionManagerCustomizers(MeterRegistry registry) {
return new TransactionManagerCustomizers(tm -> {
// Custom metrics
});
}
Micrometer:
spring.datasource.connections.active
spring.datasource.connections.idle
Highload Best Practices
- Минимальный scope транзакции: Только DB-операции. HTTP-вызовы, файловый I/O — за пределами.
- readOnly для чтения: Отключает dirty checking (Hibernate сравнивает entities с snapshot-ом при flush; при readOnly = true этот механизм отключается). Может роутить на read-реплику.
- Connection pool tuning: HikariCP
maximumPoolSize=CPU cores * 2 + disk spindles(формула-ориентир от создателя HikariCP. Для SSD «disk spindles» = 1. Starting point — 10-20, затем tune под нагрузку). - Avoid long transactions: Разбивайте batch-операции на чанки.
- Use REQUIRES_NEW для side-effects: Логирование, нотификации — не должны блокировать основную транзакцию.
- Consider programmatic transactions: Для сложной условной логики
TransactionTemplateгибче.
🎯 Шпаргалка для интервью
Обязательно знать:
- @Transactional — декларативное управление транзакциями через AOP Proxy (JDK Dynamic Proxy или CGLIB)
- Работает только на public методах и только при вызове извне класса (не через
this) - По умолчанию rollback только для RuntimeException и Error, checked exceptions → commit
- Параметры: propagation (REQUIRED), isolation (DEFAULT), readOnly (false), timeout (-1), rollbackFor
- Состояние хранится в ThreadLocal — не работает across threads (@Async = отдельный context)
- @Transactional на интерфейсе работает только с JDK Dynamic Proxy, игнорируется с CGLIB
Частые уточняющие вопросы:
- Почему @Transactional не работает при вызове через this? — Proxy bypassed, TransactionInterceptor не участвует
- Чем JDK Proxy отличается от CGLIB? — JDK: interface-based, CGLIB: subclass-based (final методы не перехватываются)
- Что будет если @Transactional на Controller? — Соединение с БД держится при сериализации JSON — anti-pattern
- Как работает UnexpectedRollbackException? — Inner REQUIRED выставил rollback-only, outer пытается commit
Красные флаги (НЕ говорить):
- “@Transactional на private методе работает” — proxy не перехватывает private методы
- “Checked exception вызывает rollback” — по умолчанию Spring делает commit
- “Можно ставить на Controller” — connection held during JSON serialization, pool exhaustion
Связанные темы:
- [[13. Что такое Propagation в Spring]]
- [[17. На каком уровне можно использовать @Transactional]]
- [[19. Какие исключения по умолчанию вызывают rollback]]
- [[22. Что произойдёт при вызове @Transactional метода из другого метода того же класса]]