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

Что делает Propagation.NESTED

Spring реализует NESTED через JDBC Savepoints (не через новые транзакции):

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

🟢 Junior Level

Propagation.NESTED — это тип распространения транзакций в Spring, который создаёт “вложенную” транзакцию внутри уже существующей. Если внешняя транзакция откатывается, вложенная тоже откатится. Но если вложенная откатится — внешняя может продолжить работу.

Простая аналогия: Вы пишете документ (внешняя транзакция). Периодически вы делаете “сохранить версию” (savepoint). Если новая правка испортила документ, вы можете откатиться к последней версии (savepoint), не теряя всю предыдущую работу. Но если вы решите выбросить весь документ — все версии тоже исчезнут.

SQL-пример (JDBC Savepoint):

@Service
public class BatchImportService {

    @Autowired private NestedImportService nestedImportService;

    @Transactional
    public void importAll(List<Data> dataList) {
        for (Data data : dataList) {
            try {
                // Каждая запись — вложенная "мини-транзакция"
                nestedImportService.importOne(data);
            } catch (Exception e) {
                // Только эта запись откатится, остальные продолжат
                log.warn("Failed to import: {}", data.getId());
            }
        }
        // В конце — один общий COMMIT для всех успешных записей
    }
}

@Service
public class NestedImportService {

    @Transactional(propagation = Propagation.NESTED)
    public void importOne(Data data) {
        repo.save(data);
        // Если exception → rollback to savepoint (не весь batch)
    }
}

Ключевое отличие от REQUIRES_NEW:

  • NESTED: Один COMMIT в самом конце, вложенная — это savepoint внутри той же транзакции
  • REQUIRES_NEW: Два независимых COMMIT, внутренняя транзакция фиксируется сразу

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

  • Обработка batch-данных с допустимыми partial failures
  • Когда нужно “спасти” часть работы, но финальный commit должен быть атомарным

🟡 Middle Level

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

Spring реализует NESTED через JDBC Savepoints (не через новые транзакции):

Вызов: outerMethod() → innerMethod(NESTED)

1. Spring проверяет: есть ли активная транзакция? → Да
2. Spring создаёт JDBC Savepoint:
   
   Connection conn = dataSource.getConnection();
   Savepoint sp = conn.setSavepoint("NESTED_SAVEPOINT_1");
   
3. Выполняется innerMethod()
4. Если успех → savepoint released (но НЕ commit!)
5. Если исключение → conn.rollback(sp) → откат к savepoint
6. В конце outerMethod() → один общий conn.commit()

Важно: NESTED работает только с DataSourceTransactionManager и JpaTransactionManager. Он НЕ работает с JTA в режиме savepoint. JTA (Java Transaction API) — API для распределённых транзакций, охватывающих несколько БД или систем. Spring автоматически fallback-ит к REQUIRES_NEW поведению. Поэтому для JTA лучше указывать REQUIRES_NEW явно.

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

Сценарий 1: Batch import с partial failure tolerance

@Service
public class CsvImportService {

    @Autowired private RowImportService rowImportService;

    @Transactional
    public ImportResult importCsv(MultipartFile file) {
        int success = 0;
        int failed = 0;

        List<String> rows = parseCsv(file);
        for (String row : rows) {
            try {
                rowImportService.importRow(row);  // NESTED
                success++;
            } catch (ValidationException e) {
                failed++;
                // Эта строка откатилась к savepoint, продолжаем
            }
        }
        return new ImportResult(success, failed);
        // COMMIT — все успешные строки сохраняются
    }
}

@Service
public class RowImportService {

    @Transactional(propagation = Propagation.NESTED)
    public void importRow(String row) {
        Data data = validateAndParse(row);  // может бросить ValidationException
        repo.save(data);
    }
}

Сценарий 2: Оркестрация с compensation

@Service
public class OrderOrchestrator {

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

    @Transactional
    public OrderResult createOrder(Order order) {
        try {
            inventoryService.reserve(order.getItems());     // NESTED
        } catch (Exception e) {
            return OrderResult.failed("No stock");
        }

        try {
            paymentService.charge(order.getTotal());        // NESTED
        } catch (Exception e) {
            // Payment failed — reservation тоже откатится (общая транзакция)
            throw new OrderException("Payment failed", e);
        }

        // Всё успешно — notification (REQUIRED, часть общей транзакции)
        notificationService.sendOrderConfirmation(order);

        return OrderResult.success();
        // COMMIT — все изменения атомарны
    }
}

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

Ошибка Последствие Решение
NESTED без активной внешней транзакции Ведёт себя как REQUIRED (создаёт новую) Убедиться, что внешний метод @Transactional
NESTED + Hibernate L1 cache inconsistency Stale entities после savepoint rollback em.flush() + em.clear() после rollback
NESTED с JTA Не поддерживается — fallback к REQUIRES_NEW Использовать REQUIRES_NEW явно для JTA
Предположение, что NESTED = REQUIRES_NEW Внутренние изменения НЕ видны другим до общего commit Понимать разницу: NESTED = savepoint, REQUIRES_NEW = новая tx
Длинная транзакция с множеством savepoints Накопление undo log, memory pressure Лимитировать количество nested вызовов

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

Характеристика NESTED REQUIRES_NEW
Физических транзакций 1 2
Механизм JDBC Savepoint Suspend + новая транзакция
Commit внутренней Нет (только release savepoint) Да, немедленный
Rollback внутренней To savepoint Полная внутренняя tx
Видимость данных другим tx Только после общего commit Сразу после внутреннего commit
Rollback внешней tx Откатывает внутреннюю Не влияет на внутреннюю
Поддержка JTA Нет Да
Hibernate L1 cache Проблемы при rollback Без проблем
Connection usage 1 connection 2 connections (suspend + new)
Overhead Низкий (~1-3ms) Средний (~5-15ms)

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

  • JTA / distributed transactions — не поддерживается
  • Когда внутренняя транзакция должна коммититься независимо — используйте REQUIRES_NEW
  • С Hibernate без flush/clear — risk of L1 cache inconsistency
  • Для аудита — аудит обычно нужен независимо от outer tx, лучше REQUIRES_NEW

Когда NESTED vs REQUIRES_NEW

  • Аудит/логирование = REQUIRES_NEW (данные должны выжить при outer rollback)
  • Batch processing с partial failure = NESTED (единый commit, один connection)
  • Distributed transaction = REQUIRES_NEW (NESTED не поддерживает JTA)
  • Нужно видеть inner data из других tx = REQUIRES_NEW (NESTED виден только после общего commit)

🔴 Senior Level

Internal Implementation: Savepoint Mechanics и Spring Integration

JdbcTransactionObjectSupport — Savepoint Management

// org.springframework.jdbc.datasource.JdbcTransactionObjectSupport
// Spring Framework 6.x

public class JdbcTransactionObjectSupport implements SavepointManager, SmartTransactionObject {
    
    private ConnectionHolder connectionHolder;
    private Savepoint currentSavepoint;
    private Integer previousIsolationLevel;
    private boolean rollbackOnly = false;
    private boolean savepointAllowed = false;

    @Override
    public Object createSavepoint(String name) throws TransactionException {
        try {
            if (!this.savepointAllowed) {
                throw new NestedTransactionNotSupportedException(
                    "Cannot create savepoint — no active transaction or not supported");
            }
            // JDBC 3.0 API
            this.currentSavepoint = getConnection().setSavepoint(name);
            return this.currentSavepoint;
        } catch (SQLException ex) {
            throw new CannotCreateSavepointException("Could not create JDBC savepoint", ex);
        }
    }

    @Override
    public void rollbackToSavepoint(Object savepoint) throws TransactionException {
        try {
            Savepoint sp = (Savepoint) savepoint;
            // JDBC rollback to savepoint
            getConnection().rollback(sp);
            // Важно: connection остаётся активным, транзакция продолжается
            this.currentSavepoint = null;
        } catch (SQLException ex) {
            throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex);
        }
    }

    @Override
    public void releaseSavepoint(Object savepoint) throws TransactionException {
        try {
            Savepoint sp = (Savepoint) savepoint;
            getConnection().releaseSavepoint(sp);
            // Non-fatal if DB auto-releases on commit (MySQL)
        } catch (SQLException ex) {
            // Log but don't fail — some databases release savepoints automatically
        }
    }
}

AbstractPlatformTransactionManager — NESTED Handling

// Ключевой фрагмент из handleExistingTransaction()

case PROPAGATION_NESTED:
    if (!isNestedTransactionAllowed()) {
        throw new NestedTransactionNotSupportedException(
            "Transaction manager does not allow nested transactions");
    }
    
    if (useSavepointForNestedTransaction()) {
        // DataSourceTransactionManager, JpaTransactionManager → savepoint
        DefaultTransactionStatus status = 
            prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
        
        // Создаём savepoint
        status.createAndHoldSavepoint();
        
        return status;
    } else {
        // JTA → нет savepoint support → fallback к REQUIRES_NEW
        SuspendedResourcesHolder suspendedResources = suspend(transaction);
        // ... создаёт новую транзакцию
    }

useSavepointForNestedTransaction() Decision

// DataSourceTransactionManager → true (поддерживает savepoints)
// JpaTransactionManager → true (делегирование к JDBC)
// JtaTransactionManager → false (JTA не поддерживает savepoints)
// HibernateTransactionManager → true (через JDBC connection)

protected boolean useSavepointForNestedTransaction() {
    return (getDataSource() != null);  // Упрощённо
}

Savepoint Implementation в различных БД

PostgreSQL:

BEGIN;
  SAVEPOINT sp1;
  INSERT INTO orders (id, amount) VALUES (1, 100);
  
  -- Rollback to savepoint
  ROLLBACK TO SAVEPOINT sp1;
  -- orders пуст — insert отменён, но транзакция активна
  
  INSERT INTO orders (id, amount) VALUES (2, 200);
COMMIT;
-- orders: (2, 200)

-- PostgreSQL savepoints:
-- - Хранятся в памяти (не на диске)
-- - Могу быть nested (savepoint внутри savepoint)
-- - Auto-released на commit

MySQL/InnoDB:

START TRANSACTION;
  SAVEPOINT sp1;
  INSERT INTO orders VALUES (1, 100);
  
  ROLLBACK TO SAVEPOINT sp1;
  -- В MySQL: auto-release savepoint после rollback!
  -- Повторный ROLLBACK TO SAVEPOINT sp1 → ERROR
  
  INSERT INTO orders VALUES (2, 200);
COMMIT;

-- MySQL savepoints:
-- - Auto-released после ROLLBACK TO (нельзя reuse)
-- - Не логируются в binlog (statement-based replication safe)
-- - Это поведение стабильно across all supported versions MySQL 5.7+ и PostgreSQL 9+.
--   Spring генерирует уникальные имена savepoint-ов начиная с версии 3.x.

Oracle:

-- Oracle savepoints:
-- - Можно reuse имя (в отличие от MySQL)
-- - Хранятся в PGA (memory)
-- - Поддерживают nested savepoints

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

Подход A: NESTED (savepoints)

  • ✅ Плюсы: Один connection, единый commit, partial rollback, lower overhead than REQUIRES_NEW
  • ❌ Минусы: Не работает с JTA, Hibernate L1 cache issues, savepoint limitations (MySQL auto-release), нет независимого commit
  • Подходит для: batch processing, bulk operations, compensation patterns внутри одной БД

Подход B: REQUIRES_NEW

  • ✅ Плюсы: Полная изоляция, работает с JTA, независимый commit, clean Hibernate semantics
  • ❌ Минусы: Два connection, suspend/resume overhead, риск несогласованности (inner committed, outer rolled back) Подходит для: audit, notification, cross-database operations

Подход C: Manual savepoint management

@Transactional
public void manualSavepoint() {
    Connection conn = DataSourceUtils.getConnection(dataSource);
    Savepoint sp = conn.setSavepoint("my_sp");
    
    try {
        // business logic
    } catch (Exception e) {
        conn.rollback(sp);  // Ручной rollback
    }
    // ...
}
  • ✅ Плюсы: Полный контроль, можно интегрировать с любой логикой
  • ❌ Минусы: Boilerplate, error-prone, bypass-ит Spring transaction management
  • Подходит для: сложных сценариев, где Spring propagation недостаточен

Edge Cases и Corner Cases

1. MySQL Savepoint Auto-Release:

@Transactional
public void mysqlSavepointIssue() {
    nestedService.doWork();  // NESTED → SAVEPOINT sp1
    // Если внутри doWork() был rollback to sp1 → MySQL auto-release sp1
    
    nestedService.doWork2();  // NESTED → пытается создать SAVEPOINT sp1 снова
    // PostgreSQL: OK (savepoint можно reuse)
    // MySQL: OK (новое имя sp2)
    
    // Но если Spring reuse-ит то же имя → MySQL ERROR:
    // "Savepoint 'NESTED_SAVEPOINT_1' does not exist"
}

Spring решает это генерацией уникальных имён:

// DefaultTransactionStatus
private String generateSavepointName() {
    return "NESTED_SAVEPOINT_" + System.identityHashCode(this);
}

2. Hibernate L1 Cache и Savepoint Rollback:

@Transactional
public void hibernateSavepointIssue() {
    Entity e1 = new Entity("first");
    em.persist(e1);
    em.flush();  // INSERT в БД
    
    try {
        nestedService.failOperation();  // NESTED → бросает exception
    } catch (Exception ex) {
        // Savepoint rollback: БД откатила INSERT
        // НО: Hibernate L1 cache (PersistenceContext) всё ещё содержит e1!
        // e1.status = MANAGED, но в БД строки нет
    }
    
    // При flush/commit:
    // Hibernate может попробовать UPDATE e1 → ERROR (строки нет)
    // Или: Hibernate dirty checking → INSERT e1 снова → дубликат
}

Решение:

@Transactional(propagation = Propagation.NESTED)
public void failOperation() {
    Entity e = new Entity("will-fail");
    em.persist(e);
    em.flush();
    // После flush, если exception → savepoint rollback удалит INSERT из БД
    // Но L1 cache всё ещё содержит e → нужен em.clear()
    throw new RuntimeException("fail");
}

// Outer method:
@Transactional
public void safeNestedOperation() {
    try {
        nestedService.failOperation();
    } catch (Exception e) {
        em.clear();  // Очистить L1 cache от potentially rolled-back entities
    }
}

Пошагово: (1) em.persist() регистрирует entity в PersistenceContext как MANAGED.
(2) em.flush() отправляет INSERT в БД. (3) Исключение триггерит savepoint rollback 
INSERT удаляется из БД. (4) Но PersistenceContext всё ещё содержит entity со
статусом MANAGED. (5) При финальном commit Hibernate пытается снова flush  ошибка.
em.clear() решает проблему, discarding весь PersistenceContext.

3. Nested Savepoints (savepoint внутри savepoint):

@Transactional
public void deeplyNested() {
    level1Service.doWork();  // NESTED → SAVEPOINT sp1
    
    try {
        level2Service.doWork();  // NESTED → SAVEPOINT sp2 (внутри sp1)
    } catch (Exception e) {
        // Rollback to sp2 — sp1 всё ещё активен
    }
    
    try {
        level3Service.doWork();  // NESTED → SAVEPOINT sp3
    } catch (Exception e) {
        // Rollback to sp3 — sp1 и sp2 (если не released) активны
    }
}

// Limit: PostgreSQL — unlimited nested savepoints
// MySQL — unlimited, но каждый rollback to auto-releases
// Oracle — unlimited

4. Savepoint и Sequence (PostgreSQL):

BEGIN;
  SAVEPOINT sp1;
  SELECT nextval('my_seq');  -- вернёт 1
  ROLLBACK TO SAVEPOINT sp1;
  
  -- Sequence value НЕ откатывается!
  SELECT nextval('my_seq');  -- вернёт 2, не 1
  
  -- Это потому что sequence — не транзакционная структура

Impact: Если бизнес-логика полагается на sequence values внутри nested транзакций, будут gaps.

5. Savepoint и Temporary Tables:

-- PostgreSQL: temporary tables НЕ rollback-ятся к savepoint
BEGIN;
  CREATE TEMP TABLE tmp_data (id INT);
  SAVEPOINT sp1;
  INSERT INTO tmp_data VALUES (1);
  ROLLBACK TO SAVEPOINT sp1;
  
  SELECT * FROM tmp_data;  -- ПУСТО — rollback сработал
  
  -- НО: DROP TEMP TABLE не rollback-ится
  SAVEPOINT sp2;
  DROP TABLE tmp_data;
  ROLLBACK TO SAVEPOINT sp2;
  
  SELECT * FROM tmp_data;  -- ERROR: table doesn't exist!

6. Savepoint и Triggers:

-- Если триггер вызывает exception внутри nested транзакции:
CREATE TRIGGER order_before_insert
    BEFORE INSERT ON orders
    FOR EACH ROW
    EXECUTE FUNCTION validate_order();

-- Если триггер падает:
-- Savepoint rollback откатывает INSERT
-- НО: side-effects триггера (например, sequence nextval) не откатываются

Performance Implications

Операция Latency Throughput Impact
Savepoint creation ~0.1ms Negligible
Savepoint release ~0.05ms Negligible
Rollback to savepoint ~0.5-2ms (зависит от кол-ва изменений) Низкий
10 nested savepoints ~1-3ms total -2-5%
100 nested savepoints ~10-30ms -10-15% (undo log growth)

Конкретные цифры (Spring Boot 3.x, PostgreSQL 15, HikariCP pool=50):

  • REQUIRED only: ~25,000 TPS
  • REQUIRED + 1 NESTED: ~24,000 TPS (savepoint overhead ~4%)
  • REQUIRED + 10 NESTED: ~22,000 TPS (cumulative savepoint overhead)
  • REQUIRED + 1 REQUIRES_NEW: ~20,000 TPS (suspend/resume ~20%)

Memory overhead:

  • Savepoint object: ~500 bytes (JDBC) + DB-side state
  • PostgreSQL: savepoint в shared memory, ~1KB per savepoint
  • MySQL: savepoint в InnoDB trx struct, ~2KB per savepoint
  • Undo log retention: при длинной транзакции с savepoints — dead tuples accumulate

Memory Implications

  • JDBC Savepoint: ~500 bytes per savepoint object (client-side)
  • DB-side savepoint state: 1-2KB per savepoint в shared memory
  • Undo log / dead tuples: Каждая операция внутри savepoint создаёт undo records. При rollback — undo records не освобождаются до основного commit.
  • Hibernate L1 cache: Может содержать stale entities после savepoint rollback — до нескольких MB для больших операций.
  • Connection holder state: Spring хранит savepoint reference в TransactionStatus — ~200 bytes per nested level.

Concurrency Aspects

NESTED concurrency model:

Thread-1: outerMethod(REQUIRED)
  → nestedService.doWork(NESTED) → savepoint sp1
    → Если exception: rollback to sp1 (только Thread-1 affected)
  → nestedService.doWork2(NESTED) → savepoint sp2
    → commit sp2 (release)
  → commit outer (commit all)

Thread-2: outerMethod(REQUIRED)
  → nestedService.doWork(NESTED) → savepoint sp3
  
Thread-1 и Thread-2 используют разные connections → независимы
Savepoints видны только в рамках своей транзакции

Savepoint и lock behavior:

-- Row locks, acquired внутри savepoint, release-ятся при rollback to savepoint
BEGIN;
  SAVEPOINT sp1;
  UPDATE accounts SET balance = 100 WHERE id = 1;  -- row lock acquired
  ROLLBACK TO SAVEPOINT sp1;
  -- Row lock released!

-- НО: DDL внутри savepoint (PostgreSQL):
  SAVEPOINT sp2;
  ALTER TABLE orders ADD COLUMN new_col TEXT;  -- DDL
  ROLLBACK TO SAVEPOINT sp2;
  -- DDL НЕ откатывается к savepoint! (DDL implicit commit)

Real Production Scenario

Ситуация: Log aggregation platform (2024), Spring Boot 3.1, PostgreSQL, ingestion 50,000 events/sec.

Проблема: При batch insert логов (1000 events/batch), один malformed event вызывал rollback всего batch. Throughput падал на 30% из-за повторной обработки.

Original code:

@Transactional
public void ingestBatch(List<LogEvent> events) {
    for (LogEvent event : events) {
        LogEvent validated = validate(event);  // Может бросить ValidationException
        logRepo.save(validated);
    }
    // Один ValidationException → rollback всего batch (1000 events lost)
}

Попытка №1: try-catch внутри REQUIRED

@Transactional
public void ingestBatch(List<LogEvent> events) {
    for (LogEvent event : events) {
        try {
            LogEvent validated = validate(event);
            logRepo.save(validated);
        } catch (ValidationException e) {
            failedCount++;
            // Проблема: transaction marked rollback-only!
            // При commit: UnexpectedRollingBackException
        }
    }
}

Финальное решение: NESTED

@Autowired private EventImportService eventImportService;

@Transactional
public void ingestBatch(List<LogEvent> events) {
    int success = 0, failed = 0;
    
    for (LogEvent event : events) {
        try {
            eventImportService.importEvent(event);  // NESTED
            success++;
        } catch (ValidationException e) {
            failed++;
            // Только этот event откатился к savepoint
        }
    }
    
    metrics.record(success, failed);
    // COMMIT — все успешные events сохранены
}

@Service
public class EventImportService {
    
    @Transactional(propagation = Propagation.NESTED)
    public void importEvent(LogEvent event) {
        LogEvent validated = validate(event);  // ValidationException → savepoint rollback
        logRepo.save(validated);
        // No exception → savepoint released
    }
}

Результат:

  • Batch completion rate: с 70% (30% rollback) до 99.5%
  • Throughput: с 35,000 до 48,000 events/sec (+37%)
  • Latency p99: с 200ms до 50ms (меньше retries)
  • Trade-off: ~3% overhead от savepoint management

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

Track savepoint creation and rollback:

@Component
public class SavepointMonitor implements TransactionSynchronization {
    
    private final MeterRegistry registry;
    
    @Override
    public void beforeCommit(boolean readOnly) {
        // Track savepoint usage
    }
    
    @Override
    public void afterCompletion(int status) {
        if (status == STATUS_ROLLED_BACK) {
            Counter.builder("transaction.savepoint.rollback")
                .increment();
        }
    }
}

// Register
TransactionSynchronizationManager.registerSynchronization(new SavepointMonitor(registry));

Hibernate L1 cache monitoring:

@PersistenceUnit
private EntityManagerFactory emf;

public void checkL1CacheSize() {
    SessionFactoryImpl sf = emf.unwrap(SessionFactoryImpl.class);
    for (Session s : sf.getSessions()) {
        PersistenceContext pc = s.getPersistenceContext();
        int entityCount = pc.getEntitiesByKey().size();
        if (entityCount > THRESHOLD) {
            log.warn("Large L1 cache after nested operations: {}", entityCount);
        }
    }
}

PostgreSQL savepoint monitoring:

-- Нет direct savepoint stats, но можно monitor через:
SELECT 
    xact_commit,
    xact_rollback,
    tup_inserted,
    tup_updated,
    tup_deleted
FROM pg_stat_database
WHERE datname = 'your_db';

-- High rollback rate + high insert rate → potential savepoint rollback pattern

Best Practices для Highload

  1. Лимитируйте глубину nesting — не более 10-20 nested savepoints per transaction.
  2. Используйте NESTED только для partial failure tolerance — не для бизнес-логики.
  3. Flush + Clear после nested rollback с Hibernate:
    try {
        nestedService.doWork();
    } catch (Exception e) {
        em.clear();  // Обязательно после savepoint rollback
    }
    
  4. Избегайте sequence依赖 внутри nested транзакций — gaps неизбежны.
  5. Monitor savepoint rollback rate — если >20%, проблема с данными, а не с транзакциями.
  6. Для batch processing: chunk-based подход с NESTED на chunk level:
    // Chunk size 100, NESTED per chunk
    for (List<Event> chunk : Lists.partition(events, 100)) {
        try {
            importChunk(chunk);  // NESTED
        } catch (Exception e) {
            // Только chunk откатился
        }
    }
    
  7. Connection pool: NESTED использует 1 connection → не требует увеличения pool size.
  8. Test with production-like data: Savepoint behavior может различаться с разным volume данных.
  9. Consider alternative для JTA: Если нужна distributed transaction — используйте REQUIRES_NEW + saga pattern.
  10. Document savepoint semantics: Убедитесь, что команда понимает разницу между NESTED и REQUIRES_NEW.

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

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

  • NESTED создаёт JDBC savepoint внутри существующей транзакции — не новую транзакцию
  • При rollback внутренней — откат к savepoint, внешняя продолжается
  • При rollback внешней — откатывает всё, включая nested (savepoint откатывается)
  • Один COMMIT в самом конце — все успешные nested operations сохраняются атомарно
  • Не работает с JTA (fallback к REQUIRES_NEW) — только JDBC 3.0+ savepoints
  • Hibernate L1 cache inconsistency после savepoint rollback — нужен em.flush() + em.clear()

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

  • Чем NESTED отличается от REQUIRES_NEW? — NESTED: один commit, savepoint; REQUIRES_NEW: независимый commit, 2 connection
  • Что произойдёт если нет внешней транзакции? — NESTED ведёт себя как REQUIRED (создаёт новую)
  • Почему Hibernate L1 cache ломается? — PersistenceContext не откатывается при savepoint rollback
  • Когда использовать NESTED? — Batch processing с partial failure tolerance, chunk-based operations

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

  • “NESTED = REQUIRES_NEW” — принципиально разные механизмы (savepoint vs suspend/new tx)
  • “NESTED работает с JTA” — JTA не поддерживает savepoints
  • “Inner data видна другим транзакциям сразу” — только после общего commit

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

  • [[13. Что такое Propagation в Spring]]
  • [[15. В чём разница между REQUIRED и REQUIRES_NEW]]
  • [[18. Что такое rollback в транзакциях]]
  • [[16. Что такое аннотация @Transactional]]