Що таке анотація @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 |
Прокидати виняток або викликати TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() |
@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).