Learning Platform
Урок 12.03 · 26 мин
Продвинутый
SSISERIALIZABLESIREAD-lockpredicate lockCahillpivotdangerous structure

REPEATABLE READ в Postgres не обеспечивает настоящую сериализуемость: write skew пролетает мимо. До 9.1 (2011) единственный способ получить true SERIALIZABLE в Postgres был — взять LOCK TABLE или SELECT FOR UPDATE на все читаемые строки. Это работало, но убивало concurrency.

В 9.1 появилась реализация SSI — Serializable Snapshot Isolation на основе работы

Cahill, Röhm, Fekete (2008)
. Это и есть SET TRANSACTION ISOLATION LEVEL SERIALIZABLE в современном Postgres. И это сильно отличается от классического блокировочного SERIALIZABLE из DB2.

Граф зависимостей: rw-зависимость как причина проблем

Чтобы понять SSI, нужно научиться видеть транзакции как вершины графа, а конфликты — как рёбра:

Три типа зависимостей между транзакциями

ww — write-write, wr — write-read, rw — read-write (анти-зависимость). SI ловит ww-конфликты (через xmax). А вот rw — нет: и именно rw создаёт write skew и serialization violations.

ww-зависимостьT1 пишет x, T2 пишет x
ловит SIчерез xmax → 40001
wr-зависимостьT1 пишет x, T2 читает x ПОСЛЕ commit T1
нет проблемыT2 видит закоммиченное
rw-зависимость (антиdep)T2 читает x, T1 потом пишет x
НЕ ловит SIT2 в своём snapshot не видит запись T1

Cahill et al. доказали: если в графе rw-зависимостей нет цикла, то расписание транзакций сериализуемо. Если есть цикл — расписание может нарушить инварианты. Минимальная опасная структура — это «pivot».

Pivot и dangerous structure

Pivot
— это транзакция T в графе rw-зависимостей, у которой:

  • есть входящее rw-ребро от T_in (то есть T_in читает то, что T потом пишет)
  • есть исходящее rw-ребро к T_out (T читает то, что T_out потом пишет)
  • T_out коммитит первой из этих трёх
Dangerous structure: pivot + два соседа

Если такая структура существует и T_out закоммитилась первой — расписание может быть несериализуемым. SSI находит её и abort'ит pivot.

T_inreads X (предикат P)
rw →T pivot пишет X (нарушает P)
T pivotreads Y (предикат Q)
rw →T_out пишет Y (нарушает Q)
условиеT_out коммитит ПЕРВОЙ из трёх — тогда serializability нарушена

Конкретный пример с write skew (детально в уроке 4): два врача, оба читают «активных дежурных» (rw), оба обновляют свой статус. Если «активных» становится 0 — инвариант «хотя бы один на смене» нарушен.

SIREAD-locks: легковесные предикатные «блокировки»

Чтобы detect’ить rw-зависимости, Postgres должен знать, что именно читала каждая SERIALIZABLE-транзакция. Для этого вводится

SIREAD-lock
— особая структура, которая ничего не блокирует и существует исключительно для трекинга чтений:

  • На каждое прочитанное место (page, tuple, диапазон по индексу) ставится SIREAD-метка.
  • Метка хранит txid читавшего и идентификатор объекта.
  • Когда другая транзакция пишет в тот же объект, движок проверяет: есть ли SIREAD от активной транзакции? → создаёт rw-ребро в графе.
Гранулярность SIREAD-locks в Postgres

От грубой к мелкой. Чем мельче — тем меньше false positives (лишних abort'ов), но больше памяти. Эскалация: если транзакция читает много — метки могут собраться вверх по иерархии.

relation lockgrain: вся таблица
когдаSeq Scan большой таблицы
page lockgrain: одна heap-page (8 KiB)
когдаIndex Scan, bitmap
tuple lockgrain: одна строка
когдаточечный SELECT по PK
escalationпревышение max_pred_locks_per_relation → page lock; per_transaction → relation lock

Важное свойство: SIREAD-локи переживают COMMIT транзакции. Они хранятся до тех пор, пока хотя бы одна другая активная транзакция могла бы вступить с ними в конфликт. Это нужно, потому что dangerous structure определяется по commit-order, а не по «текущему набору транзакций».

Detection algorithm: когда поднимается 40001

В упрощённом виде Postgres делает следующее:

для каждой записи (UPDATE/INSERT/DELETE) транзакции T:
  найти все SIREAD-локи, покрывающие изменяемый объект
  для каждого такого lock от транзакции Tr:
    добавить rw-ребро Tr → T в граф

на COMMIT транзакции T:
  для каждого pivot-кандидата (T_in -rw-> T -rw-> T_out):
    если T_out уже закоммичена:
      abort одной из (T_in, T, T_out) с ошибкой 40001

Это пессимистично-оптимистичный алгоритм:

  • Чтения не блокируются — только метятся (это часть «оптимистичной» стороны).
  • Конфликт detect’ится при commit или при write — приложение узнаёт об ошибке поздно.
  • Abort’ятся транзакции внутри dangerous structure, не обязательно та, которая последней пыталась commit’нуться.

false positives: цена за легковесность

SSI может abort’ить транзакции, которые объективно были бы сериализуемы — это false positives. Цена за то, что SIREAD-локи имеют ограниченную гранулярность:

  • Если T1 прочитала страницу из 100 строк, а T2 обновила одну строку из тех 100, которая T1 даже не интересовала — SIREAD на странице создаёт rw-ребро.
  • На больших Seq Scan’ах SSI почти всегда сигналит — поэтому в SERIALIZABLE обычно избегают полных сканов.

SET application_name + pg_stat_activity.wait_event помогает диагностировать. Реальный показатель false positive rate — параметр pg_stat_database.serialization_failures vs общий tx-count.

Демо: открываем SERIALIZABLE-транзакцию

Базовый запуск SERIALIZABLE-транзакции в Postgres. SIREAD-локи невидимы для пользователя в обычных SELECT, но можно глянуть pg_locks и фильтровать по mode='SIReadLock'.

PostgreSQL

В реальном Postgres ты увидишь несколько строк типа mode = 'SIReadLock'. В pglite (упрощённой embedded-версии) внутренние lock-структуры экспонированы не полностью, но семантика та же.

Проверка serialization_failures в статистике базы. Этот счётчик растёт при abort'е транзакций с ошибкой 40001. Полезен для мониторинга.

PostgreSQL

В production-Postgres колонки xact_commit, xact_rollback + pg_stat_database.serialization_failures дают полную картину абортов.

Что SSI не делает

Чтобы не питать иллюзий, перечислим, чего SSI не обеспечивает:

  • DDL не сериализуется. ALTER TABLE берёт обычные heavyweight lock’и, и его эффект виден всем сразу.
  • Triggers and functions — выполняются в той же транзакции, и их чтения учитываются. Но если функция вызывает dblink или pg_notify — это внешние эффекты, не откатываемые при 40001.
  • Sequences (nextval) — не сериализуются. Если две транзакции в SERIALIZABLE вызвали nextval('s') и обе откатились — числа всё равно потеряны.
  • Truncate и аналогичные DDL — ведут себя как exclusive lock.

Performance trade-off вперёд

В следующем уроке покажем конкретный пример write skew с дежурными врачами и продемонстрируем, как SSI его ловит. А заодно поговорим о цене: SERIALIZABLE заметно медленнее RC и RR из-за overhead SIREAD-locks. На write-heavy workload разница доходит до 30%.

Проверка знанийKnowledge check
Транзакция T1 в SERIALIZABLE делает `SELECT count(*) FROM accounts WHERE owner_id = 7` (Index Scan по owner_id). Параллельно T2 вставляет новую запись `INSERT INTO accounts(owner_id, ...) VALUES (7, ...)`. Будет ли SSI этот сценарий считать rw-зависимостью? Если да — где конкретно создаётся метка?
ОтветAnswer
Да, это классическая rw-зависимость, и SSI её отследит. T1 при Index Scan ставит предикатные SIREAD-локи на страницы индекса, через которые она прошла, и/или на ключевые диапазоны индекса. Когда T2 делает INSERT, движок при вставке записи в индекс проверяет, есть ли SIREAD на этой странице индекса (или на этом диапазоне). Если есть и принадлежит активной транзакции (T1) — создаётся ребро rw от T1 → T2. Дальше SSI проверяет, образует ли это dangerous structure: если у T1 есть исходящее rw к третьей транзакции, которая уже коммитнулась — одна из транзакций будет abort'нута с 40001. Это намного тоньше, чем range-lock в DB2: предикат «WHERE owner_id = 7» отслеживается через индексные страницы, и широкий Seq Scan ставит SIREAD-локи на всю таблицу (откуда false positives).

Чек-лист

  • SSI (Postgres 9.1+) — реализация SERIALIZABLE через snapshot + SIREAD-locks + detection of dangerous structure.
  • Cahill et al. (2008) показали: если в графе rw-зависимостей нет цикла → расписание сериализуемо. Минимальная опасная структура — pivot с двумя соседями.
  • SIREAD-lock — легковесная метка «эта транзакция читала этот объект», не блокирующая чтения других транзакций. Хранится в shared-memory.
  • Гранулярность SIREAD: tuple → page → relation. Большие сканы = больше false positives.
  • SIREAD-локи переживают COMMIT — они хранятся до тех пор, пока могут конфликтовать с активной транзакцией.
  • При detection — поднимается SQLSTATE 40001 serialization_failure, та же ошибка, что и при ww-конфликте в RR. Retry — единственное правильное действие.
  • SSI быстрее блокировочного SERIALIZABLE, но медленнее RC/RR. Цена платится за честную сериализуемость.
Графы: терминология и зачем DE про них знать ACID-транзакции в Apache ORC

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое SIREAD-lock в PostgreSQL?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5