Питання 18 · Розділ 11

Що таке rollback в транзакціях?

Як кнопка "Скасувати" (Ctrl+Z) в текстовому редакторі — все, що ви зробили після останнього збереження, повертається назад.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Rollback (відкат) — це операція скасування всіх змін поточної транзакції та повернення бази даних до стану до її початку.

Механізм: СУБД використовує undo log (журнал старих значень). При rollback кожен змінений рядок відновлюється з undo log. В Spring TransactionInterceptor викликає Connection.rollback(), який сигналізує СУБД відкотити поточну транзакцію.

Простий приклад

BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
UPDATE accounts SET balance = balance + 10 WHERE id = 2;
-- Щось пішло не так!
ROLLBACK;  -- Обидві зміни скасовані, баланси повернулися до початкових

Аналогія

Як кнопка “Скасувати” (Ctrl+Z) в текстовому редакторі — все, що ви зробили після останнього збереження, повертається назад.

Навіщо потрібен

Rollback гарантує атомарність (літера A в ACID): або всі зміни зберігаються, або жодної.

Rollback в Spring

Spring автоматично робить rollback, коли @Transactional метод викидає виняток.

@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
    Account fromAcc = repo.findById(from);
    Account toAcc = repo.findById(to);
    fromAcc.setBalance(fromAcc.getBalance().subtract(amount));
    toAcc.setBalance(toAcc.getBalance().add(amount));
    repo.save(fromAcc);
    repo.save(toAcc);
    // Якщо тут вилетить RuntimeException — все автоматично відкотиться
}

За замовчуванням

Spring відкочує тільки при RuntimeException та Error. Checked Exceptions (нащадки Exception) не викликають відкат — Spring зробить COMMIT.

Чому Spring так вирішив: RuntimeException = програмна помилка (NPE, IAE), від якої неможливо відновитися → rollback. Checked Exception = очікувана бізнес-ситуація (користувача не знайдено), яку викликач може обробити → commit.

Коли НЕ варто робити rollback

  1. Невідновлювані побічні ефекти — email вже надіслано, платіж вже пройшов через зовнішній API. Rollback БД не скасує зовнішню дію.
  2. Довгі транзакції з частковим успіхом — краще записати partial result і обробити пізніше, ніж втратити все.
  3. Логування/аудит — ці дані мають вижити навіть при rollback основної транзакції (використовуйте REQUIRES_NEW).

🟡 Middle Level

Як працює Rollback на рівні БД

  1. Всі зміни записуються в Undo Log
  2. При ROLLBACK СУБД читає Undo Log у зворотному порядку
  3. Виконуються компенсуючі дії, що відновлюють старі значення
  4. Блокування знімаються тільки після завершення відкату

Rollback в Spring Framework

Автоматичний (декларативний)

Відбувається при викиданні винятку з @Transactional метода.

Програмний (manual)

@Transactional
public void someMethod() {
    try {
        // логіка
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus()
            .setRollbackOnly();
    }
}

Стан Rollback-Only

Якщо в ланцюжку викликів REQUIRED → REQUIRED один метод кинув RuntimeException, Spring помічає транзакцію прапором rollback-only.

Навіть якщо зовнішній метод перехопить виняток і спробує завершитися успішно — при виході Spring побачить флаг і викине UnexpectedRollbackException.

Частковий відкат (Savepoints)

Через Propagation.NESTED можна відкотити не всю транзакцію, а тільки її частину до savepoint.

Коли НЕ використовувати rollback

Ситуація Чому Альтернатива
Компенсуюча транзакція (Saga) Дані потрібні для зворотної операції Явна компенсуюча операція
Audit logging Лог має зберегтися навіть при помилці REQUIRES_NEW для логів
Частковий успіх в batch Один failed item ≠ rollback всього batch Обробити помилку, продовжити

Поширені помилки

Помилка Що відбувається Як виправити
Catch і swallow винятку Spring не бачить виняток → COMMIT Перекинути або setRollbackOnly()
@Transactional без rollbackFor на checked exception COMMIT замість ROLLBACK rollbackFor = Exception.class
Очікування повного rollback в REQUIRED chain Вкладений метод вже виставив rollback-only Використовувати REQUIRES_NEW для незалежних операцій
Rollback і подальше використання EntityManager Session в неконсистентному стані Не перевикористовувати EntityManager після rollback

Порівняння: автоматичний vs ручний rollback

Підхід Плюси Мінуси Коли використовувати
Автоматичний (@Transactional) Чистий код, мінімум boilerplate Тільки при виході винятку Стандартні сценарії
Ручний (setRollbackOnly) Умовний rollback, без викидання Boilerplate, coupling до Spring API Бізнес-логіка з condition

🔴 Senior Level

Rollback Mechanics — Database Level

PostgreSQL Rollback Process

1. Transaction enters abort state
2. All tuple modifications marked as dead:
   - Inserted tuples: xmin = aborted txid → invisible to all future txns
   - Updated tuples: old version remains (xmax not set), new version marked aborted
   - Deleted tuples: deletion is undone (xmax cleared)
3. Row-level locks released
4. Transaction state changed to ABORTED
5. XLOG record written: XACT_ABORT

Key insight: В PostgreSQL MVCC rollback майже повністю логічний — tuple versions позначаються невалідними через статус транзакції. Фізичне очищення відбувається пізніше через VACUUM.

InnoDB Rollback Process

1. Transaction enters rollback state
2. Undo log records read in reverse order
3. For each undo record:
   - Restore old value to data page
   - Release associated locks
   - Update undo log pointer
4. Transaction state → ROLLED BACK
5. Trx sys history updated

InnoDB rollback більш фізичний — data pages реально відновлюються в попередній стан через undo log.

Spring Rollback Decision Tree

// Simplified AbstractPlatformTransactionManager
private void processRollback(DefaultTransactionStatus status, Throwable ex) {
    // 1. Check if rollback is actually needed
    if (ex != null && !status.getTransactionAttribute().rollbackOn(ex)) {
        return;  // Don't rollback — will commit instead!
    }

    // 2. Check if this is a nested transaction (savepoint)
    if (status.hasSavepoint()) {
        status.rollbackToHeldSavepoint();  // Rollback to savepoint only
        return;
    }

    // 3. Check if this is a new transaction
    if (status.isNewTransaction()) {
        status.getTransactionManager().doRollback(status);
    }
    // 4. Participating in existing transaction
    else {
        status.setRollbackOnly();  // Can't actually rollback, just mark
    }
}

UnexpectedRollbackException — Full Analysis

@Transactional  // REQUIRED — starts Tx1
public void outer() {
    try {
        inner();  // REQUIRED — joins Tx1
    } catch (RuntimeException e) {
        // Exception caught, але Tx1 вже помічений rollback-only
    }
    // Spring tries to commit Tx1 → sees rollback-only flag
    // Throws UnexpectedRollbackException
}

@Transactional  // REQUIRED — joins Tx1
public void inner() {
    repo.save(entity);
    throw new RuntimeException();  // TransactionInterceptor → setRollbackOnly()
}

Flow:

  1. outer() starts physical transaction Tx1
  2. inner() joins Tx1 (same connection)
  3. inner() throws RuntimeException
  4. TransactionInterceptor catches, calls setRollbackOnly() on Tx1
  5. outer() catches exception, returns normally
  6. TransactionInterceptor tries to COMMIT Tx1
  7. TransactionManager sees rollback-only flag
  8. Throws UnexpectedRollbackException

Рішення:

// Option 1: Не перехоплювати — нехай прокидається
@Transactional
public void outer() {
    inner();  // Якщо inner падає — outer теж rollbacks
}

// Option 2: REQUIRES_NEW для незалежних операцій
@Transactional
public void outer() {
    try {
        independentService.doWork();  // REQUIRES_NEW — окрема транзакція
    } catch (Exception e) {
        // Безпечно — independent tx вже оброблена
    }
}

// Option 3: Перехопити і перекинути
@Transactional
public void outer() {
    try {
        inner();
    } catch (RuntimeException e) {
        throw new BusinessException("Failed", e);  // Все одно викликає rollback
    }
}

Edge Cases (мінімум 3)

  1. Rollback та Sequence: SELECT nextval('seq') НЕ відкочується. Sequence просувається незалежно від rollback. Після rollback sequence value вже збільшений — наступний виклик поверне наступне значення. Це призводить до gaps в sequence.

  2. Rollback та Hibernate @Version: Версія сутності НЕ відновлюється при rollback. Entity вважається stale — при подальших операціях OptimisticLockException.

  3. Rollback та L1 Cache: Після rollback Hibernate Persistence Context залишається з dirty entities. Entity все ще в L1 cache, але стан не відповідає БД. Spring створює новий EntityManager для наступної транзакції, але якщо ви вручну перевикористовуєте — буде проблема.

  4. Partial flush перед rollback: Hibernate міг зробити flush деяких змін до виникнення винятку. На рівні БД вони будуть відкачені, але в Hibernate session вони залишаються як persisted.

Performance Numbers: Rollback vs Commit

Операція PostgreSQL InnoDB
Rollback latency O(1) — змінюється статус транзакції O(N) — undo кожної зміни
Commit latency fsync WAL (~1-5 ms) fsync redo log (~1-5 ms)
Lock release Fast (in-memory) Fast (in-memory)
Connection cleanup ~10-50 μs ~10-50 μs

Rollback в PostgreSQL швидший за commit (не потрібен fsync). В InnoDB — залежить від кількості змін.

Memory Implications

  • Undo log: Росте пропорційно об’єму змін в транзакції. Довга транзакція з великим rollback = великий undo log.
  • Hibernate L1 cache: Після rollback entities залишаються в cache — memory не звільняється до закриття EntityManager.
  • Connection pool: Connection повертається в пул тільки після завершення rollback. Довгий rollback = connection зайнятий довше.

Thread Safety

Rollback завжди відбувається в тому ж потоці, де виконувалась транзакція. ThreadLocal стан (TransactionSynchronizationManager) гарантує ізоляцію. @Async методи мають окремий transaction context — їх rollback не впливає на caller.

Production War Story

Сервіс обробки платежів: outer() викликав inner() з REQUIRED. inner() падав з DataIntegrityViolationException. outer() ловив Exception, логував, і продовжував — очікуючи, що транзакція жива. Замість цього Spring кинув UnexpectedRollbackException на commit. Всі платежі падали з 500. Fix: inner() переведено на REQUIRES_NEW — його rollback не впливає на outer транзакцію.

Monitoring

Debug logging:

logging:
  level:
    org.springframework.transaction: DEBUG
    org.hibernate.SQL: DEBUG

Micrometer metrics:

@Bean
public TransactionSynchronization transactionMetrics(MeterRegistry registry) {
    return new TransactionSynchronization() {
        @Override
        public void afterCompletion(int status) {
            if (status == STATUS_ROLLED_BACK) {
                registry.counter("transaction.rollback").increment();
            }
        }
    };
}

Actuator:

GET /actuator/metrics/transaction.rollback
GET /actuator/metrics/transaction.commit

Highload Best Practices

  1. Мінімальна тривалість транзакції: Чим довша транзакція, тим більше undo log і вища вартість rollback.
  2. Batch processing з savepoints: Розбивайте великі batch-операції на чанки. При помилці одного чанку — rollback тільки його.
  3. Avoid catch-and-swallow: Якщо перехопили Exception — явно викликайте setRollbackOnly() або перекидайте.
  4. Use REQUIRES_NEW для side-effects: Логування, нотифікації — не повинні блокувати основну транзакцію при rollback.
  5. Monitor rollback rate: Високий % rollback = проблема в бізнес-логіці або валідації, не в транзакціях.
  6. Test rollback behavior: Integration tests мають перевіряти що дані дійсно відкотилися, а не просто що виняток було викинуто.

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Rollback = скасування всіх змін транзакції, повернення до стану до BEGIN (через undo log)
  • Spring автоматично rollback-ить при RuntimeException та Error, checked exceptions → commit
  • Rollback-only flag: inner REQUIRED метод кинув RuntimeException → вся транзакція помічена на відкат
  • UnexpectedRollbackException: outer намагається commit-ити, але транзакція вже marked rollback-only
  • Savepoints (Propagation.NESTED) дозволяють частковий rollback без відкату всієї транзакції
  • Sequence values (nextval) НЕ відкочуються — це призводить до gaps в sequence

Часті уточнюючі запитання:

  • Що буде якщо поймати exception в @Transactional методі? — Spring не бачить exception → commit, потрібен setRollbackOnly()
  • Чим rollback в PostgreSQL відрізняється від InnoDB? — PG: логічний (статус tx), InnoDB: фізичний (undo log reverse)
  • Коли rollback небажаний? — Невідновлювані side effects (email, external API), partial success в batch
  • Як реалізувати умовний rollback? — TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()

Червоні прапорці (НЕ говорити):

  • “Rollback відкочує sequence” — nextval не транзакційний, gaps неминучі
  • “Піймав exception = rollback” — Spring не бачить caught exceptions
  • “Після rollback EntityManager можна继续使用” — Session в неконсистентному стані

Пов’язані теми:

  • [[19. Які винятки за замовчуванням викликають rollback]]
  • [[20. Як налаштувати rollback для checked винятків]]
  • [[14. Що робить Propagation.NESTED]]
  • [[1. Розшифруйте кожну літеру ACID]]