В чому різниця між REQUIRED та REQUIRES_NEW
4. Завжди задавайте timeout для REQUIRES_NEW: @Transactional(propagation = REQUIRES_NEW, timeout = 5). 5. Уникайте cascade REQUIRES_NEW — кожен рівень подвоює connection usage....
🟢 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_NEW = незалежна 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 під навантаженням — 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 методу з іншого методу того ж класу]]