Вопрос 15 · Раздел 11

В чём разница между REQUIRED и REQUIRES_NEW

4. Всегда задавайте timeout для REQUIRES_NEW: @Transactional(propagation = REQUIRES_NEW, timeout = 5). 5. Избегайте cascade REQUIRES_NEW — каждый уровень удваивает connection us...

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

🟢 Junior Level

REQUIRED и REQUIRES_NEW — это два самых популярных типа распространения транзакций в Spring. Главное отличие: REQUIRED использует уже существующую транзакцию, а REQUIRES_NEW всегда создаёт новую.

Простая аналогия:

  • REQUIRED — как работа над одним документом с коллегой. Вы оба редактируете один файл. Если кто-то один решит всё удалить — потеряют оба.
  • REQUIRES_NEW — как два отдельных документа. Каждый работает в своём файле. Если один испортит свой документ — другой не пострадает.

SQL-пример:

// REQUIRED (по умолчанию)
@Transactional  // propagation = REQUIRED
public void createOrder(Order order) {
    orderRepo.save(order);
    auditService.log(order);  // REQUIRED → тот же commit
    // Если createOrder упадёт — И order, И audit откатятся
}

// REQUIRES_NEW
@Transactional
public void createOrder(Order order) {
    orderRepo.save(order);
    auditService.log(order);  // REQUIRES_NEW → независимый commit
    // Если createOrder упадёт — order откатится, НО audit уже сохранён
}

Ключевые различия:

Характеристика REQUIRED REQUIRES_NEW
Новая транзакция Только если нет текущей Всегда
Приостановка текущей Нет Да
Commit Один общий в конце Независимый
Rollback внутренней Откатывает всю tx Только внутреннюю
Rollback внешней Откатывает внутреннюю НЕ влияет на внутреннюю

Когда использовать REQUIRED:

  • Стандартная бизнес-логика (90% случаев)
  • Когда все операции должны быть атомарны

Когда использовать REQUIRES_NEW:

  • Аудит, логирование
  • Уведомления, которые должны сохраниться независимо
  • Операции, которые не должны зависеть от внешней транзакции

🟡 Middle Level

Как это работает внутри

REQUIRED:

Вызов: serviceA.methodA() → proxy → TransactionInterceptor

1. Interceptor проверяет: есть ли активная транзакция?
   - Да → присоединяется (join), increment reference count
   - Нет → создаёт новую через TransactionManager

2. Выполняется метод
3. При commit:
   - Если это outer tx → commit
   - Если inner tx (join) → ничего не делает (commit только в outer)
4. При rollback:
   - setRollbackOnly() → помечает всю транзакцию для отката
   - Outer tx при commit видит rollback-only → UnexpectedRollingBackException

REQUIRES_NEW:

Вызов: serviceA.methodA() → proxy → innerService.methodB(REQUIRES_NEW)

1. Suspend текущей транзакции:
   - Connection unbind from ThreadLocal
   - Transaction synchronizations suspended
   - TransactionSynchronizationManager state reset

2. Создаётся новая физическая транзакция:
   - Новый connection из pool
   - Новый BEGIN в БД

3. Выполняется метод
4. При commit → commit внутренней транзакции
5. При rollback → rollback внутренней транзакции
6. Resume внешней транзакции:
   - Connection re-bind to ThreadLocal
   - Synchronizations resumed

Практическое применение

Сценарий 1: REQUIRED для атомарной бизнес-логики

@Service
public class OrderService {

    @Autowired private PaymentService paymentService;
    @Autowired private InventoryService inventoryService;

    @Transactional  // REQUIRED
    public OrderResult placeOrder(Order order) {
        // Все операции должны быть атомарны
        inventoryService.reserve(order.getItems());    // REQUIRED — join
        PaymentResult payment = paymentService.charge(order.getTotal());  // REQUIRED — join
        
        if (!payment.isSuccess()) {
            throw new PaymentFailedException();
            // Rollback: И reserve, И charge откатятся — консистентность
        }
        
        orderRepo.save(order);
        return OrderResult.success();
    }
}

Сценарий 2: REQUIRES_NEW для аудита

@Service
public class PaymentService {

    @Autowired private AuditService auditService;

    @Transactional  // REQUIRED
    public PaymentResult processPayment(Payment payment) {
        // Аудит ДО платежа — должен сохраниться даже при сбое
        auditService.logPaymentAttempt(payment);  // REQUIRES_NEW
        
        try {
            PaymentResult result = gateway.charge(payment);
            auditService.logPaymentSuccess(payment);  // REQUIRES_NEW
            return result;
        } catch (GatewayException e) {
            auditService.logPaymentFailure(payment, e);  // REQUIRES_NEW
            throw e;
            // Payment откатится, но attempt audit уже закоммичен
        }
    }
}

@Service
public class AuditService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentAttempt(Payment payment) {
        auditRepo.save(new AuditEntry(payment, "ATTEMPT"));
        // Commit — независимо от внешней транзакции
    }
}

Сценарий 3: REQUIRES_NEW для независимых side-effects

@Service
public class NotificationService {

    @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 5)
    public void sendOrderConfirmation(Order order) {
        // Если отправка email упала — заказ всё равно должен сохраниться
        emailService.send(order.getEmail(), "Order Confirmed");
        notificationRepo.save(new Notification(order, "EMAIL_SENT"));
        // Commit — notification сохранён даже если outer tx rollback
    }
}

Типичные ошибки

Ошибка Последствие Решение
REQUIRED + внутреннее исключение Вся транзакция marked rollback-only try-catch внутри inner метода или REQUIRES_NEW
REQUIRES_NEW для связанных данных Inner commit создаёт несогласованные данные — ЕСЛИ внутренняя транзакция записывает данные, логически зависящие от внешней (например, order_items без order). Для независимых данных (аудит) — это корректное поведение. Использовать REQUIRED для логически связанных операций
REQUIRES_NEW без timeout Зависшая inner tx блокирует outer tx Всегда задавать timeout для REQUIRES_NEW
Предположение, что REQUIRES_NEW = атомарность с outer Inner tx коммитится сразу, outer может откатиться Понимать: REQUIRESНЕED = независимая tx
REQUIRED при вызове через this Propagation игнорируется Использовать отдельный бин или self-injection

Сравнение: REQUIRED vs REQUIRES_NEW

Характеристика REQUIRED REQUIRES_NEW
Физических транзакций 1 2+
Suspend внешней tx Нет Да
Connection usage 1 2 (suspend + new)
Independent commit Нет Да
Rollback propagation На всю цепочку Только своя tx
Outer rollback effect Откатывает inner Не влияет на inner
Throughput overhead Минимальный +10-20%
Connection pool pressure Низкое Высокое (x2 per call)
Deadlock risk Низкий Средний (2 connections)
Use case Основная бизнес-логика Аудит, логирование, нотификации

Когда НЕ стоит использовать REQUIRES_NEW

  • Атомарная бизнес-логика — если все операции должны commit/rollback вместе
  • Высоконагруженные системы — suspend/resume overhead + connection pool pressure
  • Микросервисы с eventual consistency — каждый сервис управляет своей транзакцией
  • Когда inner tx зависит от outer tx данных — inner commit может закоммитить incomplete data

🔴 Senior Level

Internal Implementation: Suspend/Resume и Transaction Synchronization

Suspend Mechanics (REQUIRES_NEW)

// AbstractPlatformTransactionManager.suspend()

protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) {
    if (transaction != null) {
        // 1. Suspend transaction synchronizations (Hibernate session, etc.)
        List<TransactionSynchronization> suspendedSynchronizations = 
            doSuspendSynchronization();
        
        // 2. Unbind connection from ThreadLocal
        // Spring хранит JDBC-соединение в ThreadLocal через ConnectionHolder.
        // unbindResource = отвязать connection от текущего потока
        // bindResource = привязать обратно
        ConnectionHolder conHolder = (ConnectionHolder) 
            TransactionSynchronizationManager.unbindResource(obtainDataSource());
        
        // 3. Reset TransactionSynchronizationManager state
        boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive();
        TransactionSynchronizationManager.setActualTransactionActive(false);
        IsolationLevel currentIsolation = 
            TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
        TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null);
        TransactionSynchronizationManager.setReadOnly(false);
        TransactionSynchronizationManager.setCurrentTransactionName(null);
        
        return new SuspendedResourcesHolder(
            conHolder, suspendedSynchronizations, wasActive, currentIsolation);
    }
    return null;
}

Что suspends-ится:

// doSuspendSynchronization() — suspends all registered synchronizations

// TransactionSynchronization callbacks:
interface TransactionSynchronization {
    void suspend();    // Вызывается при suspend
    void resume();     // Вызывается при resume
    void beforeCommit(boolean readOnly);
    void beforeCompletion();
    void afterCommit();
    void afterCompletion(int status);
}

// Registered synchronizations включают:
// - Hibernate Session synchronization
// - JPA EntityManager synchronization  
// - Custom synchronizations (registered by application code)

Resume Mechanics

// AbstractPlatformTransactionManager.resume()

protected final void resume(@Nullable Object transaction, 
                             @Nullable SuspendedResourcesHolder resourcesHolder) {
    if (resourcesHolder != null) {
        Object suspendedResources = resourcesHolder.getSuspendedResources();
        
        if (suspendedResources != null) {
            // 1. Re-bind connection to ThreadLocal
            TransactionSynchronizationManager.bindResource(
                obtainDataSource(), resourcesHolder.getConnectionHolder());
        }
        
        // 2. Resume synchronizations
        doResumeSynchronization(resourcesHolder.getSuspendedSynchronizations());
        
        // 3. Restore state
        TransactionSynchronizationManager.setActualTransactionActive(
            resourcesHolder.wasActive());
        TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
            resourcesHolder.getSuspendedIsolationLevel());
    }
}

REQUIRED Join Logic

// handleExistingTransaction() — REQUIRED case

case PROPAGATION_REQUIRED:
    // Join существующей транзакции
    // НОВАЯ транзакция НЕ создаётся
    // Reference count increment-ится
    
    return prepareTransactionStatus(
        definition,           // те же параметры
        transaction,          // тот же transaction object
        false,                // newTransaction = false
        false,                // newSynchronization = false
        debugEnabled, 
        null);                // no suspended resources

// Важно: rollback propagation
// Если inner метод (REQUIRED) бросает RuntimeException:
// 1. TransactionInterceptor ловит исключение
// 2. Вызывает: status.setRollbackOnly()
// 3. AbstractPlatformTransactionManager помечает ВСЮ транзакцию
// 4. Outer метод НЕ может это отменить

Rollback-only flag propagation:

// AbstractPlatformTransactionManager

public final void rollback(TransactionStatus status) throws TransactionException {
    if (status.isCompleted()) {
        return;  // Уже завершена
    }
    
    DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
    
    if (defStatus.isNewTransaction()) {
        // Это outer tx → делаем rollback
        doRollback(defStatus);
    } else {
        // Это inner tx (join) → только mark rollback-only
        defStatus.setRollbackOnly();
        // Outer tx при commit увидит этот flag и сделает rollback
    }
}

Архитектурные Trade-offs

Подход A: REQUIRED

  • ✅ Плюсы: Atomicity (всё или ничего), простота, минимальный overhead (1 connection), полная ORM совместимость
  • ❌ Минусы: Нет partial failure tolerance, inner exception → full rollback, rollback-only propagation
  • Подходит для: financial transactions, order processing, any operation requiring full atomicity

Подход B: REQUIRES_NEW

  • ✅ Плюсы: Independent commit, partial failure handling, audit trail, isolation from outer tx failures
  • ❌ Минусы: Suspend/resume overhead (5-15ms), x2 connection usage, risk of data inconsistency (inner committed, outer rolled back)
  • Подходит для: audit logging, notification dispatch, independent side-effects, compliance tracking

Подход C: REQUIRES_NEW + Compensation

@Transactional
public void processWithCompensation(Data data) {
    try {
        sideEffectService.doWork(data);  // REQUIRES_NEW — commit
    } catch (Exception e) {
        // sideEffect уже закоммичен — нужна компенсация
        compensationService.undoSideEffect(data);  // REQUIRES_NEW
        throw e;  // Outer tx rollback
    }
}
  • ✅ Плюсы: Best of both worlds — independence + compensation
  • ❌ Минусы: Сложность, нужно implement undo для каждого side-effect
  • Подходит для: saga patterns, distributed systems, eventual consistency

Edge Cases и Corner Cases

1. Rollback-only propagation (самая частая проблема):

@Transactional
public void outerMethod() {
    try {
        innerService.innerMethod();  // REQUIRED
    } catch (Exception e) {
        // Поймали исключение...
    }
    // ...но транзакция уже marked rollback-only!
    // При commit: UnexpectedRollingBackException
}

@Transactional
public void innerMethod() {  // REQUIRED
    repo.save(entity);
    throw new RuntimeException("fail");
    // TransactionInterceptor: status.setRollbackOnly()
}

Почему так происходит:

innerMethod() — REQUIRED, join существующей транзакции
При исключении:
  → TransactionInterceptor.completeTransactionAfterThrowing()
  → tm.rollback(txInfo.getTransactionStatus())
  → AbstractPlatformTransactionManager.rollback()
  → isNewTransaction() = false (join)
  → defStatus.setRollbackOnly()  // НЕ doRollback!

Outer method при commit:
  → tm.commit(status)
  → status.isRollbackOnly() = true
  → doRollback(status)  // Forced rollback!
  → throw UnexpectedRollingBackException

Решения:

// Решение A: REQUIRES_NEW
innerService.innerMethod();  // REQUIRES_NEW — rollback только своей tx

// Решение Б: try-catch внутри inner метода
@Transactional
public void innerMethod() {
    try {
        repo.save(entity);
        throw new RuntimeException("fail");
    } catch (Exception e) {
        log.error("Inner failed, but won't rollback outer", e);
        // Исключение не propagates → outer tx не marked rollback-only
    }
}

// Решение В: noRollbackFor
@Transactional(noRollbackFor = BusinessException.class)
public void innerMethod() {
    repo.save(entity);
    throw new BusinessException("fail");  // Не вызывает rollback
}

2. REQUIRES_NEW и connection pool exhaustion:

Нагрузка: 100 concurrent requests
Каждый request: 1 outer tx + 2 REQUIRES_NEW calls = 3 connections

Total connections needed: 100 × 3 = 300
HikariCP pool size: 50

Результат: 
  - 50 requests получают connections
  - 250 ждут connectionAcquisitionTimeout (по умолчанию 30 сек)
  - Timeout → request failure

Решение:

spring:
  datasource:
    hikari:
      maximum-pool-size: 150  # >= max_concurrent × max_nested_tx
      connection-timeout: 5000  # faster failure

3. REQUIRES_NEW и Hibernate Session:

@Transactional
public void outerMethod() {
    Entity outer = em.find(Entity.class, 1L);  // loaded in Session 1
    outer.setName("outer-change");
    
    innerService.innerMethod();  // REQUIRES_NEW → suspend Session 1, create Session 2
    
    // Session 2 НЕ видит changes из Session 1 (не flushed)
    // Если innerMethod читает тот же entity → увидит старое значение
    
    // При resume Session 1:
    // Session 1 всё ещё содержит "outer-change" в dirty state
    // Но в БД изменения ещё не записаны
}

Impact: Inner tx может read stale data. Решение: em.flush() перед REQUIRES_NEW вызовом.

4. Transaction Synchronization и REQUIRES_NEW:

// outer tx registers synchronization
TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            // Вызывается после outer tx commit
        }
        
        @Override
        public void afterCompletion(int status) {
            // Вызывается после outer tx completion
        }
        
        @Override
        public void suspend() {
            // Вызывается при REQUIRES_NEW suspend
        }
        
        @Override
        public void resume() {
            // Вызывается при REQUIRES_NEW resume
        }
    }
);

// inner tx (REQUIRES_NEW) имеет свои own synchronizations
// inner afterCommit вызывается после inner commit, НЕ outer

5. REQUIRES_NEW и Isolation Level:

@Transactional
public void outerMethod() {
    // outer tx использует default isolation (или заданное на outer)
    
    innerService.innerMethod();  // REQUIRES_NEW
    // inner tx может задать свой isolation level
}

@Transactional(propagation = Propagation.REQUIRES_NEW, 
               isolation = Isolation.SERIALIZABLE)
public void innerMethod() {
    // inner tx: SERIALIZABLE
    // outer tx: DEFAULT (Read Committed)
    // Эти isolation levels НЕ влияют друг на друга (разные физические tx)
}

6. REQUIRES_NEW и Timeout:

@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 5)
public void innerMethod() {
    // timeout = 5 seconds для inner tx ТОЛЬКО
    // outer tx timeout НЕ affected
    
    longRunningOperation();  // Если > 5 сек → inner tx timeout
    // outer tx продолжает работать (но inner уже откатился)
}

7. Nested REQUIRES_NEW cascade:

@Transactional
public void level0() {
    level1Service.level1();  // REQUIRES_NEW → suspend L0, create L1
    
    @Transactional(propagation = REQUIRES_NEW)
    public void level1() {
        level2Service.level2();  // REQUIRES_NEW → suspend L1, create L2
    }
}

// Connection stack:
// L0: connection-1 (suspended)
// L1: connection-2 (suspended)
// L2: connection-3 (active)
//
// Commit order: L2 → resume L1 → L1 commit → resume L0 → L0 commit
// Rollback order: L2 rollback → resume L1 → L1 может continue или rollback

Performance Implications

Метрика REQUIRED REQUIRES_NEW
Latency (overhead) ~0ms (baseline) +5-15ms (suspend/resume)
Connection usage 1 2+ (per nested call)
Throughput (single call) 25,000 TPS 20,000 TPS (-20%)
Throughput (5 nested) 25,000 TPS 10,000 TPS (-60%)
Connection pool pressure 1x Nx (N = nesting depth)
Hibernate Session sync overhead None Flush + clear per suspend/resume

Цифры приведены для иллюстрации относительных пропорций (Spring Boot 3.2, PostgreSQL 15, HikariCP pool=50). Абсолютные значения зависят от БД, схемы, железа и нагрузки. Ключевой takeaway — REQUIRES_NEW добавляет 20-60% overhead на каждый уровень вложенности.

Scenario: 1 outer tx + 1 REQUIRES_NEW call
  - Suspend: ~2ms
  - New connection acquisition: ~0.5ms  
  - Inner tx execution: baseline
  - Inner commit: ~1ms
  - Resume: ~2ms
  - Total overhead: ~5.5ms per REQUIRES_NEW call
    Suspend включает: unbind Connection из ThreadLocal (~0.1ms), suspend Hibernate
    Session и flush L1 cache (~1.5ms), unregister transaction synchronizations (~0.4ms).

Scenario: 1 outer tx + 3 REQUIRES_NEW calls
  - Total overhead: ~16.5ms
  - Throughput reduction: ~35%

Connection pool sizing:
  - REQUIRED only: pool = max_concurrent_requests
  - With 1 REQUIRES_NEW: pool = max_concurrent_requests × 2
  - With N REQUIRES_NEW: pool = max_concurrent_requests × (N + 1)

Memory Implications

  • Suspended ConnectionHolder: ~2KB (connection reference + state)
  • Suspended TransactionSynchronizations: ~500 bytes – 5KB (зависит от кол-ва)
  • ThreadLocal state per suspend: ~1-2KB
  • Hibernate Session state (suspended): ~100KB – 5MB (зависит от L1 cache size)
  • Inner tx EntityManager: ~100KB – 5MB (новый PersistenceContext)

Total per REQUIRES_NEW call: ~200KB – 10MB (в основном Hibernate Session state)

Concurrency Aspects

REQUIRES_NEW concurrent execution:

Thread-1: outer(REQUIRED) → suspend → inner1(REQUIRES_NEW) → commit → resume → outer commit
Thread-2: outer(REQUIRED) → suspend → inner2(REQUIRES_NEW) → commit → resume → outer commit

inner1 и inner2 выполняются параллельно (разные connections)
outer1 и outer2 ждут на suspend phase, пока inner не завершится

Deadlock potential:
  - Inner tx может заблокировать строки, нужные outer tx после resume
  - Outer tx может заблокировать строки, нужные inner tx
  → Deadlock (detectable by DB deadlock detector)

Deadlock scenario:

T1-outer: UPDATE accounts SET balance = 900 WHERE id = 1;  -- lock row 1
T1-inner(REQUIRES_NEW): UPDATE accounts SET balance = 800 WHERE id = 2;  -- lock row 2
T2-inner(REQUIRES_NEW): UPDATE accounts SET balance = 700 WHERE id = 1;  -- wait row 1
T2-outer: UPDATE accounts SET balance = 600 WHERE id = 2;  -- wait row 2

→ Deadlock! T1-inner wait T2-outer, T2-inner wait T1-outer

Timeline: T=0: T1-outer locks row 1. T=1: T2-inner tries row 1 — BLOCKED.
T=2: T1-inner locks row 2. T=3: T2-outer tries row 2 — BLOCKED.
Deadlock detector: T2-outer → T1-inner → T2-inner → T1-outer → cycle.

Real Production Scenario

Ситуация: Healthcare data platform (2024), Spring Boot 3.1, MySQL 8.0, обработка 5,000 patient records/hour.

Проблема: При обработке записей пациентов, audit log терялся при сбое обработки. Регуляторный аудит обнаружил, что 15% attempted accesses не были залогированы.

Original code:

@Transactional
public void processPatientRecord(PatientRecord record) {
    // Audit с REQUIRED — часть общей транзакции
    auditService.logAccess(record);  // REQUIRED
    
    // Обработка данных — может упасть
    Data validated = validateAndTransform(record);
    patientRepo.save(validated);
    
    // Если validateAndTransform упал → rollback ВСЕГО, включая audit
    // Audit запись теряется → compliance violation
}

Root cause: auditService.logAccess() использовал REQUIRED по умолчанию. При любом сбое в обработке, audit запись откатывалась вместе с основной транзакцией.

Решение:

@Transactional
public void processPatientRecord(PatientRecord record) {
    // Аудит ДО обработки — REQUIRES_NEW
    auditService.logAccess(record);  // REQUIRES_NEW → commit сразу
    
    try {
        Data validated = validateAndTransform(record);
        patientRepo.save(validated);
        auditService.logAccessSuccess(record);  // REQUIRES_NEW
    } catch (ValidationException e) {
        auditService.logAccessFailure(record, e);  // REQUIRES_NEW → commit
        throw e;  // Outer rollback, но access log уже сохранён
    }
}

@Service
public class AuditService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 3)
    public void logAccess(PatientRecord record) {
        auditRepo.save(new AuditEntry(record, "ACCESS"));
        // Commit — независимо от outer tx
    }
}

Результат:

  • Audit coverage: с 85% до 100%
  • Compliance: passed audit
  • Throughput: -8% (REQUIRES_NEW overhead)
  • Connection pool: increased from 30 to 60
  • Mean latency: +6ms per request

Lessons learned:

  1. Все audit-записи должны быть REQUIRES_NEW
  2. Connection pool нужно размерировать с учётом nested tx
  3. Timeout на REQUIRES_NEW критичен (зависший audit не должен блокировать outer)

Monitoring и Диагностика

Track propagation distribution:

@Component
public class PropagationMonitor implements TransactionSynchronization {
    
    private final MeterRegistry registry;
    private final Counter requiredCounter;
    private final Counter requiresNewCounter;
    
    public PropagationMonitor(MeterRegistry registry) {
        this.requiredCounter = Counter.builder("spring.transaction.propagation")
            .tag("type", "REQUIRED").register(registry);
        this.requiresNewCounter = Counter.builder("spring.transaction.propagation")
            .tag("type", "REQUIRES_NEW").register(registry);
    }
    
    @Override
    public void afterCommit() {
        // Track based on transaction attributes
    }
    
    @Override
    public void afterCompletion(int status) {
        if (status == STATUS_ROLLED_BACK) {
            Counter.builder("spring.transaction.rollback")
                .tag("propagation", getCurrentPropagation())
                .increment();
        }
    }
}

Connection pool monitoring:

// HikariCP metrics
Gauge.builder("hikaricp.connections.active", hikariDataSource, 
        ds -> ds.getHikariPoolMXBean().getActiveConnections())
    .register(registry);

// Alert: active_connections > pool_size * 0.8

Suspend/resume tracking:

@Component
public class SuspendMonitor implements TransactionSynchronization {
    
    private final Timer suspendResumeTimer;
    
    @Override
    public void suspend() {
        // Record suspend timestamp
        ThreadLocal<Long> suspendTime = new ThreadLocal<>();
        suspendTime.set(System.currentTimeMillis());
    }
    
    @Override
    public void resume() {
        Long start = suspendTime.get();
        if (start != null) {
            suspendResumeTimer.record(System.currentTimeMillis() - start, 
                TimeUnit.MILLISECONDS);
        }
    }
}

Best Practices для Highload

  1. REQUIRED по умолчанию — не меняйте без обоснования.
  2. REQUIRES_NEW только для side-effects — audit, logging, notification.
  3. Размеряйте connection pool: pool_size >= max_concurrent × (1 + max_REQUIRES_NEW_per_request).
  4. Всегда задавайте timeout для REQUIRES_NEW: @Transactional(propagation = REQUIRES_NEW, timeout = 5).
  5. Избегайте cascade REQUIRES_NEW — каждый уровень удваивает connection usage.
  6. Flush перед REQUIRES_NEW вызовом если inner tx читает те же данные:
    em.flush();  // Перед innerService.method() — REQUIRES_NEW
    innerService.method();
    
  7. Monitor rollback-only propagation rate — если >5%, код имеет architectural issues.
  8. Test under load — connection pool exhaustion manifest-ится только при высокой нагрузке.
  9. Consider async alternatives для audit:
    @Async
    public void logAsync(Event event) {
        // Non-blocking, no REQUIRES_NEW overhead
        auditRepo.save(new AuditEntry(event));
    }
    
  10. Document propagation decisions — каждый REQUIRES_NEW должен быть задокументирован с обоснованием.

🎯 Шпаргалка для интервью

Обязательно знать:

  • REQUIRED: join существующей транзакции или создать новую — одна физическая tx, один commit
  • REQUIRES_NEW: suspend текущей, создать новую — две независимые tx, два commit
  • REQUIRED: inner exception → вся транзакция rollback-only; REQUIRES_NEW: inner rollback не влияет на outer
  • REQUIRES_NEW требует 2 connections (suspend + new), overhead 5-15ms на вызов
  • Rollback-only propagation: inner REQUIRED бросил RuntimeException → outer не может commit-ить
  • REQUIRES_NEW для side-effects (аудит, логирование), REQUIRED для основной бизнес-логики

Частые уточняющие вопросы:

  • Что такое UnexpectedRollbackException? — Outer пытается commit-ить, но inner уже выставил rollback-only
  • Как размерить connection pool для REQUIRES_NEW? — pool_size >= max_concurrent × (1 + nested_REQUIRES_NEW)
  • Почему Hibernate Session может read stale data при REQUIRES_NEW? — Suspended Session 1 не видит changes из Session 2
  • Когда REQUIRES_NEW опасен? — Когда inner tx записывает данные, логически зависящие от outer

Красные флаги (НЕ говорить):

  • “REQUIRES_NEW = атомарность с outer” — inner commit-ится независимо
  • “REQUIRED не влияет на производительность” — inner exception → full rollback
  • “REQUIRES_NEW без timeout — нормально” — зависшая inner tx блокирует outer

Связанные темы:

  • [[13. Что такое Propagation в Spring]]
  • [[14. Что делает Propagation.NESTED]]
  • [[18. Что такое rollback в транзакциях]]
  • [[22. Что произойдёт при вызове @Transactional метода из другого метода того же класса]]