PostgreSQL Concurrency: как на самом деле работает параллелизм

Почему в PostgreSQL "всё работает странно"

Когда несколько транзакций приходят к одним и тем же данным, PostgreSQL приходится решать сразу несколько задач: определить кто что должен видеть, кто когда может писать и что делать, если всё зайдёт в тупик. Эти задачи решаются разными механизмами, которые включаются в разные моменты времени. Есть четыре независимых слоя: MVCC, блокировки, уровни изоляции и детекция конфликтов.

Где заканчивается MVCC и начинаются блокировки

MVCC отвечает за видимость данных, блокировки — за порядок их изменения. MVCC даёт очень сильную гарантию: чтение почти никогда не блокируется. Это возможно потому, что PostgreSQL не перезаписывает строки — он создаёт новые версии, а старые остаются доступны для уже запущенных транзакций. Но как только речь заходит об изменениях, одного MVCC становится недостаточно. Две транзакции не могут одновременно изменить одну и ту же строку — нужно определить их порядок. И этот порядок задаётся блокировками.

Подробный разбор механики блокировок и их типов можно прочитать в статье: Типы блокировок в PostgreSQL: полный практический разбор

Что происходит при двух UPDATE

Возьмём простой сценарий: две транзакции обновляют одну строку.


-- Tранзакция 1
BEGIN;
UPDATE goods SET price = price + 200 WHERE id = 1;

-- Tранзакция 2
BEGIN;
UPDATE goods SET price = price * 1.05 WHERE id = 1;

Схема UPDATE
T1: BEGIN
T1: UPDATE row(id=1)
    ├─ создаётся новая версия строки
    └─ блокируется строка row(id=1)

T2: BEGIN
T2: UPDATE row(id=1)
    └─ ждёт освобождения row(id=1)

T1: COMMIT
    └─ снимается блокировка со строки row(id=1)

T2: resume
    ├─ читает новую версию строки
    └─ делает свой UPDATE

Первая транзакция создаёт новую версию строки и ставит блокировку. Вторая доходит до UPDATE и останавливается — не из-за MVCC, а потому что строка уже захвачена.

После COMMIT первая транзакция освобождает строку, и вторая продолжает работу. И в этом случае она не применит UPDATE к старому значению. Она прочтёт уже новую версию строки и пересчитает выражение.

Полезная статья на эту тему: Почему UPDATE блокирует строки, а не таблицу

Уровни изоляции

Следующий слой — правила видимости. В PostgreSQL транзакция работает со snapshot’ом. В зависимости от уровня изоляции этот snapshot либо фиксирован, либо может обновляться. Это не про то, кто кого блокирует, а про то, какие состояния данных считаются допустимыми.

Подробный разбор с примерами: Isolation levels в PostgreSQL простыми словами

Serialization failure

На уровне SERIALIZABLE PostgreSQL идёт дальше: он позволяет транзакциям выполняться параллельно, а потом проверяет, можно ли представить результат как последовательное выполнение. Если нельзя — одна из транзакций откатывается. Это выглядит как ошибка, но по сути это механизм защиты. База не даёт зафиксировать состояние, которое невозможно объяснить корректным порядком операций.

Подробно: serialization failure: когда возникает и как избежать

Откуда берутся дедлоки

Блокировки решают конфликты записи, но могут привести к циклу ожиданий, когда одна транзакция ждёт ресурс, который держит другая, а та ждёт пока первая транзакция освободит ресурс нужный ей. Чтобы избежать бесконечного ожидания, PostgreSQL отслеживает такие проблемы и разрывает цикл, принудительно завершая одну из транзакций.

Как именно он это делает можно узнать в: Как PostgreSQL обнаруживает дедлоки

Коротко, простыми словами

Если упростить до рабочей модели, получается следующее:

  • MVCC определяет, какие данные ты видишь
  • Блокировки определяют, когда ты можешь их изменить
  • Уровень изоляции определяет, какие состояния допустимы
  • Механизм конфликтов проверяет итог

PostgreSQL просто строго следует своим правилам. Эти правила распределены по разным механизмам, но как только они сложатся в голове в единую картину, поведение системы станет понятным и предсказуемым.