Вопрос 17 · Раздел 5

Что делает аннотация @Transactional?

4. Избегайте REQUIRES_NEW без причины 5. Не делайте длинные транзакции 6. TransactionalEventListener → для событий после коммита 7. Self-invocation → не работает!

Версии по языкам: English Russian Ukrainian

🟢 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

  1. REQUIRED — по умолчанию
  2. rollbackFor = Exception.class — всегда явно
  3. readOnly = true — для чтения
  4. Избегайте REQUIRES_NEW без причины
  5. Не делайте длинные транзакции
  6. TransactionalEventListener → для событий после коммита
  7. 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 создаёт прокси]]