Що таке rollback в транзакціях
Rollback є прямою реалізацією принципу атомарності (Atomicity в ACID):
🟢 Junior Level
Rollback (відкат транзакції) — це фундаментальна операція СУБД, яка скасовує всі зміни даних, виконані в межах поточної транзакції, та повертає базу даних до вихідного стану, в якому вона перебувала до виконання команди BEGIN / START TRANSACTION.
Суть у 30 секундах
Rollback є прямою реалізацією принципу атомарності (Atomicity в ACID):
«Усі операції всередині транзакції або фіксуються цілком і незворотно (
COMMIT), або в разі виникнення найменшої помилки повністю скасовуються (ROLLBACK), ніби їх ніколи не існувало».
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
-- Сталася помилка (наприклад, збій зовнішнього сервісу або порушення check-constraint)
ROLLBACK;
-- Баланси обох рахунків миттєво повернулися до початкових значень!
Автоматичний відкат у Spring Boot
У Spring Framework анотація @Transactional автоматично перехоплює винятки через AOP-проксі та делегує відкат менеджеру транзакцій:
package com.example.service;
import com.example.entity.Wallet;
import com.example.repository.WalletRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
@RequiredArgsConstructor
public class PaymentProcessingService {
private final WalletRepository walletRepository;
@Transactional // За замовчуванням: відкат при RuntimeException та Error
public void processPayment(Long walletId, BigDecimal amount) {
Wallet wallet = walletRepository.findById(walletId).orElseThrow();
wallet.setBalance(wallet.getBalance().subtract(amount));
walletRepository.save(wallet);
if (wallet.getBalance().compareTo(BigDecimal.ZERO) < 0) {
// Викидання винятку автоматично тригерить connection.rollback()
throw new IllegalStateException("Баланс гаманця не може бути від'ємним!");
}
}
}
Головне правило Spring за замовчуванням
⚠️ За замовчуванням Spring виконує
ROLLBACKтільки в разі виникнення неперевірюваних винятків (RuntimeException) та фатальних системних помилок (Error). Якщо метод викине перевірюваний виняток (Checked Exception, наприкладIOException,SQLExceptionабо іншого спадкоємцяjava.lang.Exception), Spring вважатиме це штатним бізнес-результатом і виконаєCOMMIT!
🟡 Middle Level
Способи виклику Rollback у Spring
1. Декларативний відкат (через виняток)
Стандартний та найбільш декларативний підхід:
@Transactional(rollbackFor = Exception.class)
public void order() throws Exception { ... }
2. Програмний відкат (без викидання винятку)
Якщо бізнес-сценарій вимагає скасувати зміни в базі даних, але повернути клієнту зрозумілий об’єкт відповіді без помилки 500 Internal Server Error:
package com.example.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
@Service
@RequiredArgsConstructor
public class SafeOrderService {
private final ExternalPaymentGateway externalGateway;
@Transactional
public OrderResponse placeOrderSafely(OrderRequest request) {
saveOrder(request);
boolean paymentSuccess = externalGateway.charge(request.getAmount());
if (!paymentSuccess) {
// Програмно позначаємо транзакцію для відкату без викидання винятку:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return new OrderResponse(false, "Платіж відхилено банком");
}
return new OrderResponse(true, "Успішно");
}
}
Прапорець Rollback-Only та UnexpectedRollbackException
У ланцюжку методів із розповсюдженням Propagation.REQUIRED:
- Метод
outer()викликає методinner(). - Обидва методи виконуються в межах однієї фізичної транзакції.
- У методі
inner()виникаєRuntimeException. TransactionInterceptorвнутрішнього методу перехоплює помилку й викликаєtransactionStatus.setRollbackOnly().- Якщо метод
outer()перехопить цей виняток через блокtry-catchі спробує штатно завершитися, Spring під час виходу виявить прапорецьisRollbackOnly() == trueі примусово виконає відкат, викинувши коду, що викликає, винятокUnexpectedRollbackException.
[ outer() REQUIRED ] ───────────────────────────┐
| | (Єдина транзакція)
v |
[ inner() REQUIRED ] |
| |
+-- Викидає RuntimeException! |
+-- TransactionStatus.setRollbackOnly() ────┘
|
[ outer() перехопив помилку в catch і намагається зробити commit ]
|
v
Spring бачить прапорець Rollback-Only:
-> connection.rollback()
-> throw new UnexpectedRollbackException("Transaction marked as rollback-only");
🔴 Senior Level
Фізична реалізація Rollback у ядрах СУБД: PostgreSQL проти MySQL
Різні СУБД реалізують відкат транзакцій із принципово різною алгоритмічною складністю:
Порівняння механіки ROLLBACK:
PostgreSQL (Логічний відкат O(1)):
1. Сервер отримує команду ROLLBACK (XACT_ABORT).
2. Записує 2 біти в Commit Log (pg_xact / CLOG): статус = TRANSACTION_STATUS_ABORTED.
3. Записує один запис у WAL (Write-Ahead Log) про скасування транзакції.
4. Звільняє всі блокування рядків і таблиць в оперативній пам'яті.
-> Час відкату: частки мілісекунди, НЕ ЗАЛЕЖИТЬ від обсягу оновлених рядків!
-> Фізичне сміття (dead tuples) пізніше фоново утилізує процес autovacuum.
MySQL InnoDB (Фізичний відкат O(N)):
1. Сервер переводить транзакцію у стан ROLLED BACK.
2. Сканує ланцюжок записів в Undo Log У ЗВОРОТНОМУ ПОРЯДКУ.
3. Для кожного зміненого рядка фізично відновлює старі значення
у сторінках Clustered Index (B+Tree) у пам'яті та на диску!
4. Знімає Next-Key Locks.
-> Час відкату: пропорційний обсягу змін!
-> Відкат транзакції на 10 млн рядків може заблокувати MySQL на кілька хвилин!
Поведінка сутностей у пам’яті JVM (Пастка Hibernate L1 Cache)
Найбільш підступна проблема відкату на рівні Java-застосунку:
СУБД скасовує стан рядків у базі даних, але віртуальна машина Java (JVM) НЕ ВІДНОВЛЮЄ значення полів об’єктів у купі (Heap)!
@Transactional
public void modifyAccount(Long accountId) {
Account account = accountRepository.findById(accountId).orElseThrow();
account.setBalance(BigDecimal.ZERO); // Мутація об'єкта в пам'яті JVM!
// Припустимо, тут стався SQL-збій або викинуто виняток:
throw new RuntimeException("Crash");
}
Що відбулося насправді:
- База даних виконала
ROLLBACK: у таблиці на диску баланс залишився рівним, наприклад,10 000. - В об’єкті Java
accountполеbalanceвсе ще містить значення0! - Сесія Hibernate (
PersistenceContext) після відкату перебуває в невалідному (брудному) стані. - Якщо застосунок не знищить цей екземпляр
EntityManager(наприклад, у довгих пакетних обробниках або фонових задачах), наступні операції можуть прочитати викривлені дані прямо з кешу L1. - Senior Рішення: Після будь-якого непередбачуваного відкату сесія Hibernate має бути негайно закрита або примусово очищена викликом
entityManager.clear().
4 Tricky Questions
1. Що відбувається зі значеннями генераторів автоінкременту (SEQUENCE у PostgreSQL / AUTO_INCREMENT у MySQL) під час ROLLBACK?
Відповідь: Лічильники автоінкременту та послідовностей НЕ відкочуються назад. Генератори sequence спроєктовані поза транзакційною моделлю MVCC заради високої продуктивності:
- Запит
nextval('seq')атомарно інкрементує лічильник через блокування спінлока в спільній пам’яті й одразу фіксує нове число. - Якщо транзакція, що отримала ID 105, відкочується, номер 105 втрачається назавжди. Наступна транзакція отримає номер 106.
- У таблицях утворюються «прогалини» в ідентифікаторах (ID gaps: 1, 2, 5, 8). Це абсолютно нормальна й очікувана поведінка реляційних баз даних. Спроба зробити генератор послідовності строго неперервним вимагала б ексклюзивного блокування всієї таблиці до фіксації кожного запису, що повністю знищило б паралелізм.
2. Чому в PostgreSQL відкат мільйона рядків триває 1 мілісекунду, а в MySQL InnoDB може тривати кілька хвилин?
Відповідь:
- У PostgreSQL відкат є суто логічним ($O(1)$). Сервер просто виставляє стан
TRANSACTION_STATUS_ABORTEDу кільцевому буферіpg_xact (CLOG)і записує подіюXACT_ABORTу WAL. PostgreSQL фізично не торкається змінених сторінок таблиць. Усі нові версії рядків (dead tuples) просто стають невидимими для інших транзакцій за правилами MVCC і згодом очищаються процесомautovacuum. - У MySQL InnoDB відкат є фізичним компенсуючим процесом ($O(N)$). Рушій зобов’язаний послідовно розмотати сегмент
Undo Logтранзакції у зворотному порядку та фізично застосувати старі значення полів поверх змінених сторінок даних у буферному пулі та кластерному індексі. Якщо транзакція оновила 5 мільйонів рядків, рушій виконає 5 мільйонів операцій фізичного відновлення, створюючи величезне навантаження на процесор і дискову підсистему.
3. Як впливає ROLLBACK на версійність сутностей (@Version / Optimistic Locking) у Spring Data JPA?
Відповідь:
Під час відкату транзакції поле @Version у Java-об’єктів у пам’яті не повертається до вихідного значення.
Якщо сутність мала версію 1, Hibernate під час підготовки до коміту інкрементує її в пам’яті до 2. Якщо потім на етапі виконання SQL-оператора виникає збій і транзакція відкочується:
- У БД версія залишилася рівною 1.
- У Java-об’єкті в полі
@Versionуже зафіксована цифра 2. - Якщо спробувати зберегти цей самий Java-об’єкт у наступній транзакції, Hibernate виконає
UPDATE ... WHERE id = ? AND version = 2і не знайде жодного рядка, що спричинить хибний викидOptimisticLockException(StaleObjectStateException). Об’єкт після відкату необхідно вичитувати з бази даних наново.
4. Що станеться, якщо всередині методу @Transactional перехопити виняток через try-catch, але ніяк не повідомити Spring?
Відповідь:
Відбудеться тихий коміт транзакції (Silent Commit) з частковим збереженням даних!
Spring AOP Proxy перехоплює винятки лише тоді, коли вони вилітають за межі цільового методу. Якщо всередині методу виняток було перехоплено й приховано (catch (Exception e) { log.error("Error", e); }), Spring вважає, що метод завершився успішно. Інтерцептор викличе connection.commit(). Зміни, виконані до моменту збою, назавжди зафіксуються в базі даних, що призведе до грубого порушення цілісності бізнес-інваріантів.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
«Rollback — це операція скасування всіх змін поточної транзакції для гарантування принципу атомарності (ACID). У Spring
@Transactionalавтоматично ініціює відкат тільки за умови виникненняRuntimeExceptionтаError, тоді як перевірювані (checked) винятки за замовчуванням призводять до коміту. На рівні СУБД відкат у PostgreSQL відбувається миттєво ($O(1)$) шляхом позначення статусу транзакції якABORTEDу CLOG, а в MySQL InnoDB вимагає фізичного застосування журналу відкату Undo Log ($O(N)$). Критично пам’ятати: відкат у БД не повертає старі значення полів Java-об’єктів у купі JVM і не скасовує видані значення автоінкрементних Sequence».
Чек-лист ключових понять
- Поведінка Spring за замовчуванням: Відкат на
RuntimeException+Error, коміт на checkedException. - UnexpectedRollbackException: Виникає, коли внутрішній метод із
REQUIREDпозначив транзакцію якrollback-only, а зовнішній метод перехопив помилку й спробував зафіксувати транзакцію (commit). - Програмний відкат:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(). - Sequence / Auto-Increment: Ніколи не відкочуються під час
ROLLBACK, спричиняючи дірки (gaps) у нумерації. - L1 Cache Inconsistency: Об’єкти в пам’яті JVM зберігають мутації після відкату; сесія Hibernate стає брудною, потрібен
entityManager.clear().
Червоні прапорці (чого в жодному разі не можна говорити)
- ❌ «Rollback повертає лічильники Sequence назад до колишніх значень» (Лічильники не є транзакційними, утворюються прогалини).
- ❌ «Якщо перехопити RuntimeException у try-catch всередині сервісу, транзакція все одно відкотиться» (Якщо виняток не залишив межі проксі-методу й не викликано
setRollbackOnly, Spring виконає коміт). - ❌ «Після відкату транзакції Java-об’єкт автоматично відновить свої старі значення полів» (Стан об’єктів у Heap JVM не контролюється СУБД і не відкочується).