Что такое Serializable
Он гарантирует, что результат параллельного выполнения любого количества транзакций будет в точности эквивалентен некоторому строго последовательному их выполнению (одна за друг...
🟢 Junior Level
Serializable (Сериализуемый) — это самый высокий и строгий уровень изоляции транзакций в реляционных базах данных.
Он гарантирует, что результат параллельного выполнения любого количества транзакций будет в точности эквивалентен некоторому строго последовательному их выполнению (одна за другой, без какого-либо перекрытия во времени).
Ключевые свойства:
- Полное отсутствие аномалий: Устраняет абсолютно все известные аномалии параллелизма — грязное чтение (Dirty Read), неповторяющееся чтение (Non-Repeatable Read), фантомы (Phantom Read), потерю обновлений (Lost Update) и перекос записи (Write Skew).
- Цена надежности: Самый ресурсоемкий уровень. В условиях высокой конкуренции на запись транзакции могут конфликтовать, и СУБД будет принудительно отменять одну из них.
- Обязанность приложения: Приложение обязано реализовывать логику повтора (Retry) при получении ошибок сериализации.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TicketBookingService {
// Гарантирует абсолютную защиту от овербукинга (двойной продажи одного места)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void bookSeat(Long flightId, String seatNumber, Long passengerId) {
boolean isTaken = seatRepository.isSeatTaken(flightId, seatNumber);
if (isTaken) {
throw new IllegalStateException("Место уже занято!");
}
seatRepository.assignSeat(flightId, seatNumber, passengerId);
}
}
Аналогия: Представьте примерочную кабинку в магазине с узким проходом, куда физически может войти только один человек. Даже если перед магазином собралась толпа из 100 покупателей, они заходят строго по одному, закрывают за собой дверь на ключ, и никто не может помешать или увидеть примерку другого.
🟡 Middle Level
Два фундаментальных подхода к реализации Serializable
Исторически и архитектурно СУБД реализуют уровень Serializable двумя совершенно разными способами:
ПОДХОДЫ К РЕАЛИЗАЦИИ SERIALIZABLE
│
┌────────────────────────┴────────────────────────┐
▼ ▼
ПЕССИМИСТИЧЕСКИЙ (2PL) ОПТИМИСТИЧЕСКИЙ (SSI)
- Двухфазная блокировка (Two-Phase Locking) - Serializable Snapshot Isolation
- Читатели блокируют писателей - Читатели НЕ блокируют писателей
- Писатели блокируют читателей - Построен поверх MVCC
- Блокировки предикатов (Predicate Locks) - Отслеживание графа зависимостей (rw-antidependencies)
- Пример: MS SQL Server (по умолчанию), MySQL - Пример: PostgreSQL (начиная с 9.1)
Главный вызов: Ошибки сериализации и необходимость Retry
На уровне Serializable успешное завершение метода не гарантируется базой данных. Если СУБД обнаруживает риск нарушения сериализуемости, она аварийно прерывает транзакцию-жертву со стандартизированным кодом ошибки:
\(\text{SQLSTATE 40001 (serialization\_failure)}\) \(\text{Текст ошибки: "could not serialize access due to read/write dependencies among transactions"}\)
Приложение обязано перехватывать исключение и повторять транзакцию с самого начала:
import org.springframework.dao.CannotSerializeTransactionException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SafeFinancialService {
@Retryable(
retryFor = { CannotSerializeTransactionException.class },
maxAttempts = 5,
backoff = @Backoff(delay = 50, multiplier = 2.0, random = true)
)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void executeCriticalTransfer(Long fromId, Long toId, BigDecimal amount) {
// Бизнес-логика финансового перевода
}
}
Когда использовать Serializable:
- Финансовые расчеты и балансовые инварианты (где сумма нескольких счетов должна удовлетворять сложному условию).
- Бронирование ресурсов (авиабилеты, гостиничные номера, складские остатки), где недопустим овербукинг.
- Защита от Write Skew при невозможности использования явных строковых блокировок (
SELECT FOR UPDATE).
🔴 Senior Level
Архитектура Serializable Snapshot Isolation (SSI) в PostgreSQL
До версии 9.1 PostgreSQL поддерживал Serializable лишь номинально (как синоним Repeatable Read). Начиная с 9.1, был внедрен революционный алгоритм SSI (Кэхилл, Рём, Фекете, 2008).
1. Неблокирующие SIREAD-замки
Вместо захвата тяжелых блокировок строк и предикатов PostgreSQL использует легковесные структуры в общей оперативной памяти (shared memory) — SIREAD locks:
- Устанавливаются на кортежи, страницы памяти или целые таблицы при любом
SELECT. - SIREAD-замки НИКОГДА никого не блокируют! Ни один поток не засыпает в ожидании SIREAD-замка. Они служат лишь метками в памяти: «Транзакция $T$ прочитала этот диапазон данных».
2. Граф зависимостей и поиск опасных структур (Dangerous Structures)
СУБД непрерывно анализирует граф зависимостей между транзакциями. Особое внимание уделяется rw-антизависимостям (rw-antidependencies):
- Ситуация $T_1 \xrightarrow{rw} T_2$ возникает, когда $T_1$ прочитала версию данных, которую $T_2$ затем обновила или удалила. В гипотетическом последовательном порядке $T_1$ обязана была бы завершиться до $T_2$.
- Потенциальная аномалия возникает при формировании так называемой опасной структуры (Dangerous Structure / Pivot) — двух последовательных rw-стрелок в графе:
\(T_{in} \xrightarrow{rw} T_{pivot} \xrightarrow{rw} T_{out}\)
Если в графе образуется цикл или риск цикла, ядро PostgreSQL объявляет центральную транзакцию ($T_{pivot}$) жертвой и принудительно прерывает ее (
abort) с ошибкой40001.
3. Эскалация блокировок предикатов (Lock Coarsening)
Количество SIREAD-замков ограничено размером блока shared memory (параметр max_pred_locks_per_transaction).
Если транзакция считывает слишком много строк:
- Множество кортежных замков схлопываются в один страничный замок (
Page Lock). - Если и этого мало — в замок на всю таблицу (
Relation Lock). Побочный эффект: Огрубление предикатных блокировок резко увеличивает вероятность ложных срабатываний (False Positives) и необоснованных отмен неконфликтующих транзакций.
4 Tricky Questions
1. Что такое SIREAD-блокировки в PostgreSQL и почему они никогда не вызывают ожиданий потоков?
Ответ: SIREAD-блокировки в алгоритме SSI не являются классическими блокировками ресурсов (Lock/Mutex). Они представляют собой информационные метки (флаги в shared memory), регистрирующие факт чтения данных транзакцией. При записи данных пишущая транзакция не ждет освобождения SIREAD-замка (как в 2PL), а мгновенно создает новую версию строки, попутно отмечая в памяти возникновение ребра антизависимости $T_{reader} \xrightarrow{rw} T_{writer}$. SIREAD-замки служат исключительно для математического анализа ацикличности графа сериализации ядром СУБД.
2. Может ли транзакция, выполняющаяся строго в режиме «только для чтения» (READ ONLY), быть отменена на уровне SERIALIZABLE с ошибкой 40001?
Ответ:
Да, абсолютно.
Это классическая ловушка для архитекторов. В алгоритме SSI даже чистые читатели участвуют в графе зависимостей.
Если транзакция READ ONLY считывает промежуточное состояние данных, связанное с двумя другими параллельно пишущими транзакциями, она может замкнуть опасный цикл зависимостей. Чтобы сохранить иллюзию сериализуемости для всей системы, PostgreSQL имеет полное право прервать читающую транзакцию.
(Решение в PostgreSQL: объявлять транзакцию как SET TRANSACTION READ ONLY DEFERRABLE; — тогда СУБД задержит старт транзакции до момента, пока не появится гарантированно безопасный глобальный снимок).
3. Откатываются ли значения счетчиков генераторов последовательностей (Sequence.nextval) при возникновении ошибки сериализации?
Ответ:
Нет, никогда.
Генераторы последовательностей (sequence) в реляционных базах данных (PostgreSQL, Oracle) работают вне транзакционного контекста ради высокой производительности.
Функция nextval() атомарно сдвигает счетчик в памяти и на диске без привязки к откату транзакции. Если транзакция получила ID 105, а затем была прервана по serialization_failure, значение 105 навсегда теряется, образуя «дыру» в последовательности идентификаторов. Попытка повтора транзакции (@Retryable) получит уже следующий ID — 106.
4. В чем разница между классическим Serializable в MySQL InnoDB и PostgreSQL?
Ответ:
- В MySQL InnoDB: При установке
SET TRANSACTION ISOLATION LEVEL SERIALIZABLEдвижок неявно конвертирует все обычные запросыSELECTв блокирующие запросыSELECT ... FOR SHARE(накладывает Shared S-блокировки и Gap Locks на прочитанные диапазоны). Писатели блокируются читателями, а читатели блокируются писателями (пессимистический подход, близкий к 2PL). - В PostgreSQL:
SELECTостается абсолютно неблокирующим и выполняется по механизму многоверсионности MVCC. Конфликты выявляются оптимистически постфактум через граф SSI.
🎯 Шпаргалка для интервью
- Суть Serializable: Самый строгий уровень; гарантирует результат, эквивалентный строго последовательному выполнению транзакций.
- Исключает все аномалии: Dirty Read, Non-Repeatable Read, Phantom Read, Lost Update, Write Skew.
- Реализация в PostgreSQL (SSI): Не блокирует чтение; строит граф
rw-antidependenciesчерез неблокирующиеSIREAD locks. - Реализация в MySQL: Пессимистическая (неявно превращает все
SELECTвSELECT ... FOR SHAREс Gap Locks). - Ошибка 40001: Неизбежный спутник
Serializable. Приложение обязано иметь механизм@Retryableс экспоненциальным backoff. - READ ONLY транзакции: Тоже могут прерываться на уровне
Serializable, если не использоватьDEFERRABLE. - Производительность: На write-heavy нагрузке пропускная способность может проседать в 2–10 раз из-за конфликтов и повторов.