Що робить анотація @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). - Викликається цільовий бізнес-метод.
- Якщо метод завершився успішно $\to$ викликається
connection.commit(). - Якщо метод викинув
RuntimeExceptionабоError$\to$ викликається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 каверзних питання з відповідями
1. Чи відкотиться транзакція, якщо метод викине IOException?
Відповідь: Ні, за замовчуванням транзакція зафіксується (commit). Spring відкочує транзакції лише при виникненні неперевіряних винятків (RuntimeException) або системних помилок (Error). Щоб транзакція відкочувалася при будь-яких помилках, необхідно явно вказувати @Transactional(rollbackFor = Exception.class).
2. У чому різниця між Propagation.REQUIRES_NEW та Propagation.NESTED?
Відповідь: REQUIRES_NEW призупиняє зовнішню транзакцію і відкриває повністю незалежну нову транзакцію, що вимагає другого фізичного з’єднання з БД. Помилка у зовнішній транзакції не відкотить нову, і навпаки. NESTED використовує те саме фізичне з’єднання і створює точку збереження (Savepoint). Відкат вкладеної частини відбувається до Savepoint без відкату всієї зовнішньої транзакції.
3. Як використання REQUIRES_NEW може покласти сервер при зростанні навантаження?
Відповідь: Кожен виклик REQUIRES_NEW утримує одночасно два фізичні з’єднання з пулу (одне заморожене у зовнішній транзакції, друге активне у внутрішній). Якщо розмір пулу обмежений, а кількість паралельних потоків дорівнює розміру пулу, настає взаємне блокування (Deadlock пулу з’єднань): усі потоки чекають друге з’єднання, утримуючи перше.
4. Що дає прапорець readOnly = true в Hibernate?
Відповідь: Hibernate вимикає механізм відстеження брудних сутностей (Dirty Checking) і переводить сесію в режим FlushMode.MANUAL. Hibernate не зберігає знімки стану сутностей у пам’яті, що суттєво знижує навантаження на Garbage Collector та процесор при читанні великих масивів даних.
Червоні прапорці (чого категорично не можна говорити)
- ❌ «Spring відкочує транзакцію при абсолютно будь-якій помилці» — груба помилка. Checked Exceptions за замовчуванням ігноруються механізмом відкату.
- ❌ «
REQUIRES_NEWвідкриває транзакцію в тому самому з’єднанні» — дляREQUIRES_NEWобов’язково виділяється окреме фізичне з’єднання з пулу. - ❌ «
readOnly = trueгарантовано блокує операціїINSERTна рівні драйвера» — це оптимізація сесії Hibernate та підказка драйверу, а не суворий тригер прав СУБД. - ❌ «Можна робити довгі HTTP-виклики до зовнішніх сервісів прямо всередині
@Transactional» — антипатерн, що призводить до вичерпання пулу з’єднань БД і падіння мікросервісу.