Що таке анотація @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 методу з іншого методу того ж класу]]