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