PostgreSQL

Транзакции и уровни изоляции в PG

Два потока одновременно снимают деньги с одного счёта. Без транзакций - оба прочитали 1000, оба записали 500, итог 500 вместо 0. С транзакциями - второй поток ждёт или получает ошибку сериализации. Выбор уровня изоляции определяет, насколько параллельной и насколько безопасной будет система.

  • **Stripe** использует SERIALIZABLE изоляцию для списаний с баланса - write skew привёл бы к возможности уйти в минус при параллельных списаниях
  • **Booking.com** применяет REPEATABLE READ для бронирования номеров - одна транзакция читает доступные номера и бронирует, другая не должна видеть изменения посередине
  • **Shopify** хранит корзину в REPEATABLE READ транзакции на время checkout - все проверки цен и наличия видят консистентный снимок в 50+ миллисекунд транзакции

ACID и что именно гарантирует PG

Транзакция - это единица работы с четырьмя гарантиями: Atomicity (всё или ничего), Consistency (данные остаются валидными), Isolation (параллельные транзакции не мешают друг другу), Durability (зафиксированное переживёт сбой). В PG каждый отдельный SQL-оператор выполняется в неявной транзакции, если явная не открыта.

В PG транзакция помечается как aborted (в error state) при первой же ошибке - все последующие команды отклоняются с `ERROR: current transaction is aborted`. Выйти можно только через ROLLBACK или ROLLBACK TO SAVEPOINT.

В середине транзакции случился `ERROR: duplicate key value`. Что произойдёт со следующим SELECT?

Read Committed - дефолт PG

Read Committed - уровень изоляции по умолчанию в PG. Каждый SQL-оператор видит снимок данных на момент своего начала (не начала транзакции). Это значит: два SELECT в одной транзакции могут увидеть разные данные если между ними кто-то сделал COMMIT.

Read Committed защищает от Dirty Read (чтения незафиксированных данных), но не защищает от Non-Repeatable Read и Phantom Read. Для отчётов по финансовым балансам используйте REPEATABLE READ.

В Read Committed транзакция делает два SELECT count(*) FROM orders WHERE status='pending' с разрывом в 1 секунду. Могут ли они вернуть разные числа?

Repeatable Read

В Repeatable Read транзакция фиксирует снимок данных при первом обращении к данным (первом DML или SELECT) и держит его до конца. Все SELECT в транзакции видят одинаковое состояние базы. При UPDATE/DELETE строки, изменённые другой транзакцией с момента начала снимка, вызывают ошибку сериализации - PG откатывает транзакцию.

PG реализует Repeatable Read через MVCC - без блокировок чтения. Другие транзакции продолжают писать в таблицу; текущая просто не видит их изменений. Это гораздо эффективнее, чем read locks в MySQL.

Транзакция в REPEATABLE READ делает UPDATE строки, которую другая транзакция уже изменила и закоммитила. Что произойдёт?

Serializable и SSI

Serializable - наивысший уровень изоляции. PG использует Serializable Snapshot Isolation (SSI) - алгоритм, обнаруживающий зависимости чтения-записи между транзакциями (rw-антизависимости). Если обнаружен цикл зависимостей (две транзакции взаимно зависят через read-write), одна из них принудительно откатывается. При этом читающие транзакции не блокируют пишущих.

SSI в PG требует хранить predicate locks (pg_predicate_lock) - метаданных о том, что читалось. Параметр `max_pred_locks_per_transaction` (128 по умолчанию) контролирует гранулярность. При превышении PG эскалирует lock с row-уровня до page и table - это может вызвать ложные сериализационные ошибки.

Write Skew аномалия - почему Repeatable Read не защищает от неё?

Savepoints - частичный откат

Savepoint позволяет откатить часть транзакции, сохранив предыдущую работу. Это полезно в длинных транзакциях, где один шаг может упасть с ошибкой, но остальное должно остаться. JDBC и SQLAlchemy используют savepoints под капотом для `nested transactions`.

RELEASE SAVEPOINT sp1 освобождает память, занятую savepoint-метаданными, но не меняет состояние транзакции. После RELEASE откатиться к этому savepoint нельзя - только к более ранним.

SAVEPOINT = отдельная вложенная транзакция с независимым COMMIT

Savepoint не создаёт независимой транзакции. COMMIT финализирует всё, ROLLBACK откатывает всё. ROLLBACK TO возвращает к точке, но не коммитит.

В SQL нет nested transactions с независимыми COMMIT. То, что ORMы называют 'вложенными транзакциями' - это savepoints; COMMIT вложенной транзакции в ORM делает RELEASE SAVEPOINT, а не реальный COMMIT.

ROLLBACK TO SAVEPOINT sp1 при ошибке внутри транзакции - транзакция после этого жива?

Ключевые идеи

  • Read Committed (дефолт): каждый оператор видит свежий снимок - быстро, но non-repeatable reads возможны
  • Repeatable Read: снимок фиксируется при первой операции - одинаковые SELECTs, но write skew не защищает
  • Serializable SSI: обнаруживает циклы rw-зависимостей без блокировок чтения - изоляция без write skew, с overhead на predicate locks

Связанные темы

Уровни изоляции тесно связаны с внутренними механизмами PG:

  • MVCC и версионирование строк — Уровни изоляции реализованы через MVCC - snaphot visibility rules определяют какие tuple versions видит транзакция
  • Блокировки PG — Serializable использует predicate locks дополнительно к обычным row locks - pg_locks показывает оба типа
  • VACUUM и dead tuples — Долгоживущие транзакции в REPEATABLE READ удерживают старые версии строк от очистки VACUUM - bloat растёт

Вопросы для размышления

  • Почему SERIALIZABLE в PG не блокирует читателей, хотя и обнаруживает конфликты? Как SSI отличается от S2PL (strict two-phase locking)?
  • ORM открывает транзакцию при старте HTTP-запроса и коммитит в конце. При Read Committed: что произойдёт если за 200 мс обработки другая транзакция поменяла данные?
  • В каком сценарии savepoints дают явное преимущество перед retry всей транзакции с нуля?

Связанные уроки

  • db-03-acid
Транзакции и уровни изоляции в PG

0

1

Войти