Что делает аннотация @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. Что такое прокси в Spring]]
- [[18. Почему @Transactional не работает при self-invocation]]
- [[19. Как решить проблему с self-invocation]]
- [[14. Когда Spring создаёт прокси]]