Repeatable Read замораживает снимок и убирает неповторяющиеся и фантомные чтения. Но он пропускает аномалию посложнее: две транзакции, каждая из которых читает данные, изменяемые другой. Классический пример - перекос записи (write skew): оба врача снимают себя с дежурства, каждый видит, что второй ещё на смене, и в итоге дежурных не остаётся.
Serializable закрывает и это. В PostgreSQL он реализован как SSI - Serializable Snapshot Isolation.
Не блокировать, а наблюдать
SSI не заставляет транзакции ждать. Они идут на обычном снимке Repeatable Read (см. isolation-levels), а сервер параллельно следит за зависимостями между ними. Чтобы знать, кто что прочитал, он ставит предикатные блокировки - SIRead. Это не настоящие блокировки: они никого не останавливают, а лишь помечают «эта транзакция прочитала вот эти строки».
-- под нагрузкой Serializable предикатные блокировки видны как SIReadLock
SELECT locktype, mode FROM pg_locks WHERE mode = 'SIReadLock';
Опасная структура и откат
SSI ищет в графе зависимостей опасный узор: транзакцию, которая и читает данные, изменённые другой, и сама изменяет данные, читаемые третьей. Когда такой цикл грозит нарушить сериализуемость, сервер жертвует одной из транзакций и откатывает её:
ERROR: could not serialize access due to read/write dependencies among transactions
Код тот же 40001, что и у конфликта записи на Repeatable Read, но причина
другая: не «кто-то опередил с записью», а «совместный порядок чтений и
записей нельзя свести к последовательному». Реакция приложения та же -
повторить транзакцию.
Цена и компромиссы
- Гранулярность. Предикатные блокировки бывают на уровне строки, страницы или таблицы. Под нехваткой памяти они укрупняются, и тогда появляются ложные срабатывания - лишние откаты там, где реального конфликта не было.
- Короткие транзакции. Чем дольше живёт транзакция, тем больше она накопит зависимостей и тем выше шанс попасть под откат.
- Готовность повторять. Serializable снимает с разработчика ручные
блокировки
SELECT FOR UPDATE, но взамен требует цикла повтора по40001.
SSI - это сделка: пишешь код так, будто транзакции выполняются строго по очереди, а движок сам ловит случаи, где параллельность это нарушила бы, и заставляет повторить.