Что такое аннотация @Transactional
Пометив метод аннотацией @Transactional, вы поручаете Spring Framework: 4. Если метод выбросил RuntimeException или Error — автоматически выполнить ROLLBACK.
🟢 Junior Level
@Transactional — это ключевая аннотация Spring Framework, предназначенная для декларативного управления транзакциями. Она избавляет разработчика от необходимости вручную открывать соединение с базой данных, выполнять команды BEGIN, COMMIT или ROLLBACK и закрывать ресурсы в блоках try-finally.
Суть в 30 секундах
Пометив метод аннотацией @Transactional, вы поручаете Spring Framework:
- Автоматически открыть транзакцию в базе данных перед выполнением метода.
- Выполнить бизнес-логику метода.
- Если метод завершился успешно — выполнить
COMMIT. - Если метод выбросил
RuntimeExceptionилиError— автоматически выполнитьROLLBACK.
[ Клиентский вызов ]
|
v
[ Spring AOP Proxy ] -------------> BEGIN TRANSACTION (берет коннект из пула)
|
v
[ Вызов метода Service ] ---------> Выполняются SQL-запросы в БД
|
+-- Без ошибок? -----------> COMMIT (изменения зафиксированы)
|
+-- RuntimeException? -----> ROLLBACK (все изменения отменены)
Простейший пример в Spring Boot
package com.example.service;
import com.example.entity.Account;
import com.example.repository.AccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
@RequiredArgsConstructor
public class BankTransferService {
private final AccountRepository accountRepository;
@Transactional // Атомарный перевод средств
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId)
.orElseThrow(() -> new IllegalArgumentException("Отправитель не найден"));
Account to = accountRepository.findById(toId)
.orElseThrow(() -> new IllegalArgumentException("Получатель не найден"));
from.debit(amount);
to.credit(amount);
accountRepository.save(from);
// Если здесь произойдет сбой или выброс исключения,
// списание со счета 'from' гарантированно откатится!
accountRepository.save(to);
}
}
Главные атрибуты аннотации @Transactional
| Атрибут | Назначение | Значение по умолчанию |
|---|---|---|
propagation |
Поведение при вызове из другой транзакции | Propagation.REQUIRED |
isolation |
Уровень изоляции транзакции в СУБД | Isolation.DEFAULT (дефолт СУБД) |
readOnly |
Оптимизация для запросов только на чтение | false |
timeout |
Максимальное время выполнения в секундах | -1 (без таймаута) |
rollbackFor |
Классы исключений, вызывающие ROLLBACK |
RuntimeException.class, Error.class |
noRollbackFor |
Исключения, при которых транзакция коммитится | {} (пустой массив) |
🟡 Middle Level
Архитектурный механизм: Spring AOP Proxy
Spring управляет транзакциями с помощью паттерна AOP Proxy (динамический прокси-объект):
Application Context при старте:
Bean (BankTransferService) ---> Оборачивается в CGLIB/JDK Proxy
|
При вызове метода: v
proxy.transferMoney() ---> [ TransactionInterceptor ]
|
1. Читает @Transactional метаданные
2. Обращается к PlatformTransactionManager
3. Проверяет ThreadLocal ресурсы
4. Выполняет целевой метод реального бина
5. Обрабатывает результат / Rollback / Commit
Размещение аннотации: Метод vs Класс vs Интерфейс
- Над методом: Самый точный и рекомендуемый вариант. Переопределяет настройки класса для конкретного метода.
- Над классом: Применяется ко всем
publicметодам этого класса. - Над интерфейсом: ⚠️ Опасный антипаттерн! Работает только при использовании устаревших JDK Dynamic Proxies на базе интерфейсов. Если в проекте используется CGLIB (стандарт по умолчанию в Spring Boot), аннотация на интерфейсе будет полностью проигнорирована!
Распространенные ошибки разработчиков
| Ошибка | Последствие | Архитектурно верное решение |
|---|---|---|
Вызов через this.method() |
Вызов идет мимо AOP Proxy, транзакция не открывается | Вынести метод в отдельный сервис или использовать self-injection |
Аннотация на private методе |
Прокси не может перехватить private-метод, аннотация игнорируется | Делать метод public |
Аннотация на final методе |
CGLIB не может переопределить final-метод, проксирование не работает |
Убрать ключевое слово final |
Checked Exception без rollbackFor |
При выбросе IOException или SQLException происходит COMMIT |
Всегда указывать rollbackFor = Exception.class |
| Перехват исключения без проброса | try-catch проглатывает ошибку, Spring выполняет COMMIT |
Пробрасывать исключение или вызывать TestTransaction.flagForRollback() |
@Transactional на контроллере |
Удерживает коннект к БД во время парсинга JSON и сетевого I/O | Ставить аннотацию строго на слой @Service |
🔴 Senior Level
Внутренности ядра: TransactionInterceptor и TransactionAttributeSource
В ядре Spring процесс перехвата транзакционного метода инкапсулирован в классе TransactionAspectSupport (метод invokeWithinTransaction):
// Упрощенная архитектурная логика Spring Framework 6.x / 7.x
protected Object invokeWithinTransaction(Method method, Class<?> targetClass,
InvocationCallback invocation) throws Throwable {
TransactionAttributeSource tas = getTransactionAttributeSource();
TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 1. Создание транзакции (если необходимо согласно Propagation)
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal;
try {
// 2. Вызов реального метода целевого бина
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// 3. Обработка исключения: проверка правил rollbackFor / noRollbackFor
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// 4. Очистка контекста в ThreadLocal
cleanupTransactionInfo(txInfo);
}
// 5. Фиксация транзакции при успешном возврате
commitTransactionAfterReturning(txInfo);
return retVal;
}
CGLIB vs JDK Dynamic Proxy: генерация байткода
Spring Boot начиная со 2.x по умолчанию использует CGLIB-проксирование (spring.aop.proxy-target-class=true):
- JDK Dynamic Proxy: Создает прокси-класс через
java.lang.reflect.Proxy. Требует обязательного наличия интерфейса. Не может проксировать вызовы к классам без интерфейсов. - CGLIB (через ByteBuddy): Генерирует байткод подкласса-наследника на лету (
TargetService$$SpringCGLIB$$0 extends TargetService).- Не требует интерфейсов.
- Критическое ограничение: не может переопределить методы с модификатором
final(потому что Java запрещает переопределение final-методов в наследниках). Если вызвать final-метод, управление пойдет напрямую в оригинальный класс без вызова интерцептора транзакций.
Жизненный цикл соединения: Lazy Connection Acquisition
Многие разработчики ошибочно полагают, что аннотация @Transactional немедленно физически захватывает сокетное соединение из пула HikariCP при входе в метод:
- По умолчанию в связке с JPA/Hibernate захват соединения происходит лениво (
Lazy Connection Acquisition): физический коннект из пула запрашивается только тогда, когда Hibernate отправляет первый реальный SQL-запрос (Statement.execute()) или обращается к sequence генератору. - Исключение: Если в аннотации указан нестандартный уровень изоляции (например,
@Transactional(isolation = Isolation.REPEATABLE_READ)илиSERIALIZABLE), Spring обязан захватить соединение немедленно при входе в прокси, чтобы выполнить вызовconnection.setTransactionIsolation(...).
Программное управление: TransactionTemplate как альтернатива
В высоконагруженных распределенных системах для максимального сужения границ транзакции Senior Engineers часто заменяют декларативную аннотацию на TransactionTemplate:
package com.example.service;
import com.example.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@Service
@RequiredArgsConstructor
public class HighloadOrderService {
private final TransactionTemplate transactionTemplate;
private final OrderRepository orderRepository;
private final ExternalPaymentClient paymentClient;
public void processOrderWithExternalCall(Long orderId) {
// 1. Тяжелый внешний сетевой вызов (БЕЗ удержания коннекта к БД!)
PaymentResponse response = paymentClient.executePayment(orderId);
// 2. Короткая транзакция ровно на время обновления записи в БД:
transactionTemplate.execute(status -> {
orderRepository.updateStatus(orderId, response.getStatus());
return true;
});
}
}
4 Tricky Questions
1. Что произойдет, если пометить метод @Transactional модификатором final при стандартной конфигурации Spring Boot (CGLIB)?
Ответ:
Код успешно скомпилируется и приложение успешно запустится, однако транзакция открыта не будет!
При использовании CGLIB (ByteBuddy) Spring создает динамический класс-наследник TargetClass$$SpringCGLIB. Согласно спецификации виртуальной машины Java (JVM), метод с модификатором final не может быть переопределен в подклассе.
В результате CGLIB-прокси не может внедрить свой перехватывающий байткод (MethodInterceptor), и вызов final-метода передается напрямую в оригинальный экземпляр класса. TransactionInterceptor никогда не вызывается, соединение с БД не открывается, а изменения выполняются либо в режиме autocommit, либо приводят к TransactionRequiredException.
2. Почему в Spring аннотация @Transactional по умолчанию откатывает транзакцию только при RuntimeException и Error, но фиксирует ее при Checked Exception?
Ответ: Это фундаментальное архитектурное решение создателя Spring Рода Джонсона, опирающееся на философию языка Java:
RuntimeException(unchecked): Сигнализирует о непредвиденной технической или программной аварии (NullPointerException,DataAccessException, баг в алгоритме). В этом случае продолжение работы невозможно, транзакция считается скомпрометированной и должна быть откатана.Checked Exception(checked): Рассматривается как штатный, ожидаемый альтернативный результат бизнес-логики (например,InsufficientFundsExceptionилиUserNotFoundException). Предполагается, что вызывающий код обязан знать о таком исходе, перехватить его и продолжить корректное выполнение программы, сохранив предыдущие изменения. Если бизнес-требования требуют отката при любых ошибках, разработчик обязан явно указать:@Transactional(rollbackFor = Exception.class)или@Transactional(rollbackFor = Throwable.class).
3. В какой физический момент времени запрашивается соединение из пула HikariCP при входе в метод с @Transactional?
Ответ: Это зависит от настроек изоляции и DataSource:
- По умолчанию (
Isolation.DEFAULT): Соединение запрашивается лениво (Lazy Connection Acquisition) в момент первого реального обращения к базе данных (выполнение первого SQL-запроса через JDBC или генерация sequence). До этого момента метод может выполнять тяжелые вычисления в JVM без удержания сокета БД. - При явном указании уровня изоляции (
isolation = Isolation.REPEATABLE_READи т.д.): Spring обязан запросить физическое соединение из пула немедленно в точке входа в AOP Proxy, так как для изменения уровня изоляции необходимо выполнить методjava.sql.Connection.setTransactionIsolation(...). Это снижает масштабируемость при высоком RPS, если метод долго готовится перед отправкой запроса в БД.
4. Почему размещение @Transactional на методе @RestController является критической ошибкой в Highload?
Ответ: Это грубейший архитектурный антипаттерн, приводящий к быстрому истощению пула соединений (Connection Pool Exhaustion):
- Метод контроллера удерживает открытую транзакцию и коннект к БД на протяжении всей фазы формирования HTTP-ответа.
- Если клиент скачивает ответ через медленное мобильное соединение (slow client), или контроллер сериализует большой JSON-массив из 50 000 объектов, JDBC-соединение остается заблокированным потоком контроллера.
- 50–100 медленных клиентов полностью парализуют пул HikariCP, заставляя все остальные сервисы падать по
ConnectionTimeout. Транзакции должны открываться только на уровне сервисного слоя (@Service) и жить минимально возможное время.
🎯 Шпаргалка для интервью
30-секундный ответ
«
@Transactional— это инструмент декларативного управления транзакциями в Spring, реализованный через паттерн AOP Proxy и перехватчикTransactionInterceptor. При вызове метода прокси открывает транзакцию черезPlatformTransactionManager, выполняет метод, фиксирует его при успехе и откатывает при выбросеRuntimeExceptionилиError. По умолчанию аннотация работает только на public-методах при внешнем вызове. Вызовы черезthisобходят прокси. Checked-исключения по умолчанию не откатывают транзакцию, если не задан атрибутrollbackFor = Exception.class».
Чек-лист ключевых понятий
- Механизм реализации: Spring AOP Proxy (CGLIB по умолчанию в Spring Boot).
- Ограничения прокси: Не работает на
privateиfinalметодах, не работает при вызове внутри класса черезthis. - Правило отката по умолчанию: Только
RuntimeExceptionиError. Для checked исключений нуженrollbackFor = Exception.class. - Слой использования: Строго
@Service, ни в коем случае не@RestController. - Thread-Safety: Метаданные транзакции привязаны к
ThreadLocal, поэтому@Transactionalне передается в дочерние потоки (@Async/CompletableFuture).
Красные флаги (чего ни в коем случае нельзя говорить)
- ❌ «@Transactional работает на private методах» (AOP Proxy перехватывает только public методы).
- ❌ «Любое исключение в Java автоматически откатывает транзакцию» (Checked exceptions по умолчанию коммитятся).
- ❌ «Если вызвать метод этого же сервиса через this, откроется новая транзакция» (Вызов через
thisидет мимо прокси). - ❌ «Можно ставить @Transactional на контроллеры» (Это антипаттерн, вызывающий pool exhaustion).