Что такое rollback в транзакциях?
Как кнопка "Отменить" (Ctrl+Z) в текстовом редакторе — всё, что вы сделали после последнего сохранения, возвращается назад.
🟢 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
- Невосстановимые побочные эффекты — email уже отправлен, платёж уже прошёл через внешний API. Rollback БД не отменит внешнее действие.
- Долгие транзакции с частичным успехом — лучше записать partial result и обработать позже, чем потерять всё.
- Логирование/аудит — эти данные должны выжить даже при rollback основной транзакции (используйте REQUIRES_NEW).
🟡 Middle Level
Как работает Rollback на уровне БД
- Все изменения записываются в Undo Log
- При ROLLBACK СУБД читает Undo Log в обратном порядке
- Выполняются компенсирующие действия, восстанавливая старые значения
- Блокировки снимаются только после завершения отката
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:
outer()starts physical transaction Tx1inner()joins Tx1 (same connection)inner()throws RuntimeExceptionTransactionInterceptorcatches, callssetRollbackOnly()on Tx1outer()catches exception, returns normallyTransactionInterceptortries to COMMIT Tx1TransactionManagersees rollback-only flag- 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)
-
Rollback и Sequence:
SELECT nextval('seq')НЕ откатывается. Sequence advances независимо от rollback. После rollback sequence value уже увеличен — следующий вызов вернёт следующее значение. Это приводит к gaps в sequence. -
Rollback и Hibernate
@Version: Версия сущности НЕ восстанавливается при rollback. Entity считается stale — при последующих операцияхOptimisticLockException. -
Rollback и L1 Cache: После rollback Hibernate Persistence Context остаётся с dirty entities. Entity всё ещё в L1 cache, но состояние не соответствует БД. Spring создаёт новый EntityManager для следующей транзакции, но если вы вручную переиспользуете — будет проблема.
-
Partial flush перед rollback: Hibernate мог сделать flush некоторых изменений до возникновения исключения. На уровне БД они будут откачены, но в Hibernate session они остаются как persisted.
Performance Numbers: Rollback vs Commit
| Операция | PostgreSQL | InnoDB |
|---|---|---|
| Rollback latency | O(1) — меняется статус транзакции | O(N) — undo каждый change |
| 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
- Минимальная длительность транзакции: Чем дольше транзакция, тем больше undo log и выше стоимость rollback.
- Batch processing с savepoints: Разбивайте большие batch-операции на чанки. При ошибке одного чанка — rollback только его.
- Avoid catch-and-swallow: Если перехватили Exception — явно вызывайте
setRollbackOnly()или перебрасывайте. - Use
REQUIRES_NEWдля side-effects: Логирование, нотификации — не должны блокировать основную транзакцию при rollback. - Monitor rollback rate: Высокий % rollback = проблема в бизнес-логике или валидации, не в транзакциях.
- Test rollback behavior: Integration tests должны проверять что данные действительно откатились, а не просто что exception был выброшен.
🎯 Шпаргалка для интервью
Обязательно знать:
- 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 exceptions]]
- [[14. Что делает Propagation.NESTED]]
- [[1. Расшифруйте каждую букву ACID]]