Що таке 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
| Підхід | Гарантії | Продуктивність | Складність |
|---|---|---|---|
| Serializable (SSI) | Full ACID | Найнижча | Medium (retries) |
| Repeatable Read + FOR UPDATE | Row-level | Середня | Low |
| Optimistic Locking (@Version) | Application-level | Висока | Medium |
| Application-level locking | Varies | Висока | 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]]