Что такое Serializable?
Все аномалии полностью исключены.
🟢 Junior Level
Serializable — это самый строгий уровень изоляции транзакций. Он гарантирует, что результат параллельных транзакций будет эквивалентен какому-то одному последовательному выполнению (не обязательно фактическому хронологическому порядку).
Основные свойства
- Грязное чтение: Запрещено
- Неповторяющееся чтение: Запрещено
- Фантомное чтение: Запрещено
Все аномалии полностью исключены.
Простой пример
Если две транзакции работают с одними данными, на уровне Serializable они выполнятся так, будто работают строго по очереди — одна за другой.
Когда использовать
- Финансовые транзакции
- Системы бронирования
- Когда цена ошибки в данных очень высока
Главный недостаток
Самый медленный уровень. На write-heavy нагрузке throughput может быть в 2-10x ниже, чем на Read Committed. На read-only — разница минимальна. Транзакции могут конфликтовать и отменяться. Приложение должно уметь повторять такие транзакции.
Когда НЕ использовать Serializable
- Read-heavy reporting — избыточные overhead
- >50 concurrent writers — постоянные serialization failures
- Транзакции >1 сек — чем дольше транзакция, тем выше шанс конфликта
- Hot-spot updates (счётчики, очереди) — все пишут в одну строку
🟡 Middle Level
Механизмы реализации
Исторически существует два подхода:
Пессимистичный: Two-Phase Locking (2PL)
- Блокируются все данные, которые транзакция читает или записывает
- Чтение блокирует запись, запись блокирует чтение и запись
- Минус: Низкая производительность и частые Deadlocks
- Используется в старых СУБД (SQL Server по умолчанию для этого уровня)
Оптимистичный: Serializable Snapshot Isolation (SSI)
- Современный метод (PostgreSQL с 9.1)
- Не блокирует чтение/запись, позволяет параллельное выполнение через MVCC
- Отслеживает зависимости между транзакциями
- При обнаружении конфликта одна транзакция прерывается с ошибкой
Главный вызов: Retries
На уровне Serializable СУБД не гарантирует успешное завершение транзакции. База может выдать ошибку:
ERROR: could not serialize access due to read/write dependencies among transactions
Приложение обязано реализовывать механизм повторных попыток (Retry Logic).
Когда использовать Serializable
- Финансовые транзакции: Переводы между множеством счетов с инвариантами
- Системы бронирования: Нельзя допустить двойную продажу
- Сложные бизнес-правила: Логика зависит от набора данных, которые могут измениться
Как минимизировать конфликты
- Делайте транзакции максимально короткими
- Не выполняйте долгие сетевые запросы внутри транзакции
- Обновляйте данные в одном и том же порядке во всех транзакциях
🔴 Senior Level
Serializable Snapshot Isolation (SSI) — Internal Implementation
Dependency Graph Construction
PostgreSQL SSI builds a Serializable Graph tracking:
- RW-conflicts: When transaction T1 reads a row that T2 later writes
- Dangerous structures: Cycles in the dependency graph that could cause anomalies
T1 reads row X (T2 hasn't written yet)
T2 writes row X
T2 reads row Y (T3 hasn't written yet)
T3 writes row Y
T3 reads row X (that T2 wrote)
This forms a cycle → potential anomaly → one transaction must abort.
Цикл опасен, потому что означает: результат зависит от порядка выполнения, и нет serial order, который дал бы тот же результат. Поэтому СУБД прерывает одну транзакцию с ошибкой serialization failure.
Predicate Locks
Unlike traditional row/table locks, SSI uses predicate locks:
- Locks are on index ranges or full table scans
- If predicate overlaps → potential conflict detected
- Locks are tracked in memory, not on disk
- Coarsening: individual page locks may be promoted to relation-level locks to save memory
Conflict Detection Algorithm
On each read:
- Register predicate lock on accessed data
On each write:
- Check for RW-dependencies with other transactions
- If dangerous structure detected:
- Mark one transaction for abortion
- Error code: SQLSTATE 40001 (serialization_failure)
Performance Characteristics
Overhead Analysis
- Memory: Predicate locks stored in shared memory (~few MBs typically)
- CPU: Dependency graph traversal on each read/write
- Abort rate: 1-10% under moderate contention, can spike to 30%+ under heavy writes
- Throughput: 2-10x lower than Read Committed depending on workload
When SSI Performs Well
- Read-heavy workloads (predicate locks don’t conflict)
- Transactions accessing disjoint data sets
- Short transactions with minimal overlap
When SSI Struggles
- High write contention on same rows
- Long-running transactions (more conflict window)
- Hot-spot updates (counters, queues)
Production Retry Implementation
@Configuration
@EnableRetry
public class RetryConfig {
}
@Service
public class TransactionService {
@Retryable(
value = {CannotSerializeTransactionException.class},
backoff = @Backoff(
delay = 50,
maxDelay = 2000,
multiplier = 3,
random = true // Jitter to prevent thundering herd
),
maxAttempts = 5
)
@Transactional(isolation = Isolation.SERIALIZABLE)
public Result performCriticalOperation(Input input) {
// Business logic here
}
@Recover
public Result recover(CannotSerializeTransactionException e, Input input) {
// Fallback or throw business exception
throw new BusinessException("Could not complete after retries", e);
}
}
Serialization Failure Metrics to Monitor
-- PostgreSQL: Track serialization failures
SELECT
datname,
xact_commit,
xact_rollback,
conflicts
FROM pg_stat_database
WHERE datname = 'your_db';
-- Application-level: Track retry rate
// Use Micrometer/Prometheus
Counter.builder("transaction.serialization.retries")
.tag("operation", "transfer")
.register(meterRegistry);
Serializable vs Alternative Approaches
| Approach | Guarantees | Performance | Complexity |
|---|---|---|---|
| Serializable (SSI) | Full ACID | Lowest | Medium (retries) |
| Repeatable Read + FOR UPDATE | Row-level | Medium | Low |
| Optimistic Locking (@Version) | Application-level | High | Medium |
| Application-level locking | Varies | High | High |
Edge Cases and Gotchas
- Read-only transactions: Can still cause serialization failures if they read data being written
- Deferred constraints: Checked at COMMIT time, can trigger serialization failures
- Temporary tables: Not tracked by SSI, not protected
- Advisory locks: Not tracked by SSI, not protected
- Sequence values:
nextval()is NOT rolled back on abort
Migration Strategy from RC to Serializable
- Start with monitoring: track potential conflicts under RC
- Implement retry logic first
- Enable Serializable for new endpoints only
- Gradually migrate critical paths
- Measure abort rates and adjust
🎯 Шпаргалка для интервью
Обязательно знать:
- Serializable — самый строгий уровень, исключает все аномалии (dirty, non-repeatable, phantom)
- PostgreSQL использует SSI (Serializable Snapshot Isolation) с 9.1 версии
- SSI отслеживает RW-dependencies и при обнаружении цикла прерывает транзакцию с ошибкой 40001
- Приложение ОБЯЗАНО реализовывать retry logic при использовании Serializable
- Throughput на write-heavy нагрузке может быть в 2-10x ниже, чем на Read Committed
- Predicate locks используются вместо традиционных row/table locks
Частые уточняющие вопросы:
- Что такое SSI? — Serializable Snapshot Isolation: граф зависимостей + selective abortion
- Когда использовать Serializable? — Финансы, бронирование, где цена ошибки высока
- Когда НЕ использовать? — Read-heavy reporting, >50 concurrent writers, транзакции >1 сек
- Что произойдёт при конфликте? — Ошибка serialization_failure (40001), транзакция abort-ится
Красные флаги (НЕ говорить):
- “Serializable = просто ставлю аннотацию и всё работает” — нужен retry logic
- “Serializable подходит для любых нагрузок” — на write-heavy throughput падает в разы
- “PostgreSQL использует 2PL для Serializable” — используется SSI, не two-phase locking
Связанные темы:
- [[2. Какие уровни изоляции транзакций существуют]]
- [[5. Что такое Repeatable Read]]
- [[11. Какой уровень изоляции по умолчанию в PostgreSQL]]
- [[16. Что такое аннотация @Transactional]]