Що робить анотація @Transactional?
4. Уникайте REQUIRES_NEW без причини 5. Не робіть довгі транзакції 6. TransactionalEventListener → для подій після комміту 7. Self-invocation → не працює!
🟢 Junior Level
@Transactional — анотація, яка автоматично керує транзакціями БД.
Проста аналогія: Банківський переказ. Або гроші списалися І надійшли, або нічого не сталося. Не можна, щоб гроші списалися, але не надійшли.
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderRepository.save(order); // SQL INSERT
paymentRepository.charge(order); // SQL UPDATE
// Якщо помилка → все відкотиться!
}
}
Як працює:
Proxy → BEGIN TRANSACTION → викликає метод → COMMIT
↓ (помилка)
ROLLBACK
🟡 Middle Level
Propagation (поширення)
| Propagation | Поведінка |
|---|---|
| REQUIRED (за замовч.) | Використовує існуючу або створює нову |
| REQUIRES_NEW | Завжди створює нову (призупиняє поточну) |
| NESTED | Вкладена через Savepoint |
| MANDATORY | Вимагає існуючу (інакше помилка) |
Isolation (ізоляція)
| Isolation | Що запобігає |
|---|---|
| READ_COMMITTED | Dirty Reads |
| REPEATABLE_READ | Non-repeatable Reads |
| SERIALIZABLE | Phantom Reads (але повільно!) |
Rollback правила
// За замовчуванням: тільки RuntimeException та Error (unchecked exceptions). Checked exception (Exception і нижче) НЕ викликають rollback.
@Transactional
public void save() throws IOException { }
// IOException → НЕ відкотиться!
// ✅ Явне вказання
@Transactional(rollbackFor = Exception.class)
public void save() throws IOException { }
// IOException → відкотиться!
readOnly оптимізація
@Transactional(readOnly = true)
public List<User> findAll() {
// → Hibernate вимикає Dirty Checking
// → Економія CPU та пам'яті
}
🔴 Senior Level
REQUIRES_NEW ризик
@Transactional
public void outer() {
inner(); // REQUIRES_NEW → нова транзакція!
}
@Transactional(propagation = REQUIRES_NEW)
public void inner() {
// Призупиняє зовнішню транзакцію
// Бере ІНШЕ з'єднання з пулу!
// → Ризик deadlock при малому пулі
}
Connection Release проблема
@Transactional
public void process() {
repository.save(order); // З'єднання взято
httpClient.callExternal(); // 5 секунд → з'єднання тримається!
repository.save(payment);
}
// → З'єднання може утримуватися протягом усієї транзакції. Поведінка залежить від ConnectionReleaseMode: за замовчуванням у Spring + Hibernate з'єднання утримується до commit/rollback.
// Рішення: розбити на два методи
TransactionalEventListener
// Надіслати в Kafka ТІЛЬКИ після комміту!
@TransactionalEventListener(phase = AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
kafkaTemplate.send("orders", event);
}
// → Якщо транзакція відкотиться → подія НЕ надішлеться!
// → Якщо метод, що публікує подію, НЕ в транзакції — подія надішлеться негайно, як якби це був звичайний ApplicationEvent.
Production Experience
Реальний сценарій: довга транзакція
@Transactional метод:
1. Зберегти замовлення (10 мс)
2. HTTP виклик зовнішнього API (3 секунди)
3. Зберегти платіж (10 мс)
→ З'єднання зайняте 3+ секунди!
→ При 100 RPS → 300 з'єднань потрібно!
→ Пул з 20 → всі зайняті → таймаути!
Рішення: розбити на два методи
Best Practices
- REQUIRED — за замовчуванням
- rollbackFor = Exception.class — завжди явно
- readOnly = true — для читання
- Уникайте REQUIRES_NEW без причини
- Не робіть довгі транзакції
- TransactionalEventListener → для подій після комміту
- Self-invocation → не працює!
Резюме для Senior
- Proxy → TransactionInterceptor керує транзакцією
- Propagation → REQUIRED/REQUIRES_NEW/NESTED
- rollbackFor → явно вказуйте Exception.class
- readOnly → вимикає Dirty Checking
- Connection Release → не тримайте довго
- TransactionalEventListener → AFTER_COMMIT
- Self-invocation → не працює!
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- @Transactional = автоматичне керування транзакціями: BEGIN → метод → COMMIT / ROLLBACK
- За замовчуванням: propagation = REQUIRED, rollback тільки для RuntimeException та Error
- Propagation: REQUIRED (використовує/створює), REQUIRES_NEW (завжди нова), NESTED (через Savepoint)
- Isolation: READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE — що запобігають
- rollbackFor = Exception.class — завжди явно вказуйте для checked exceptions
- readOnly = true — вимикає Hibernate Dirty Checking, економить CPU/пам’ять
- REQUIRES_NEW бере ІНШЕ з’єднання з пулу → ризик deadlock при малому пулі
- TransactionalEventListener(AFTER_COMMIT) — подія надішлеться тільки після комміту
- Self-invocation НЕ працює — виклик через this повз проксі
Часті уточнюючі запитання:
- Які виключення викликають rollback за замовчуванням? → Тільки unchecked (RuntimeException, Error). Checked Exception — НЕ відкочують
- Чим REQUIRES_NEW відрізняється від NESTED? → REQUIRES_NEW = окрема транзакція, NESTED = вкладена через Savepoint в рамках тієї ж
- Чому REQUIRES_NEW небезпечний? → Бере інше з’єднання, при малому пулі — deadlock/timeout
- Що робити з довгими транзакціями? → Розбити на методи, прибрати HTTP-виклики з транзакції
Червоні прапорці (НЕ говорити):
- «@Transactional відкочує всі виключення» — тільки unchecked за замовчуванням
- «REQUIRES_NEW = вкладена транзакція» — це NESTED, REQUIRES_NEW — окрема
- «readOnly = true забороняє запис» — це оптимізація Hibernate, не заборона
- «Транзакція звільняє з’єднання між викликами» — утримує до commit/rollback
Пов’язані теми:
- [[13. Що таке proxy в Spring]]
- [[18. Чому @Transactional не працює при self-invocation]]
- [[19. Як вирішити проблему з self-invocation]]
- [[14. Коли Spring створює proxy]]