$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогPostgreSQL транзакции и уровни изоляции: практические ловушки под нагрузкой

PostgreSQL транзакции и уровни изоляции: практические ловушки под нагрузкой

$ sudo teach IT
·22 июля 2026 г.·11 мин·60
PostgreSQL транзакции и уровни изоляции: практические ловушки под нагрузкой

Разберём Read Committed/Repeatable Read/Serializable на примерах аномалий и реальных паттернов. Дадим чек-лист, как безопасно читать и писать в конкурентной среде.

Содержание
Как PostgreSQL понимает изоляцию: не только «уровень», но и модель MVCCТермины аномалий, которые встречаются чаще всегоRead Committed: быстрый и «коварный» по отношению к повторным чтениямАномалия 1: Non-repeatable readАномалия 2: Phantom read (в наборе)Практический вывод для Read CommittedRepeatable Read: «один снимок на всю транзакцию» — но не магияНе всё, что кажется «фантомом», устранено полностьюWrite skew: пример, который ломает «проверили — значит можно»Почему так происходитПрактический вывод для Repeatable ReadSerializable: защита от невозможных историй — но с учётом отказовЧто это означает на практикеМинимальный пример перезапускаSerializable — не замена правильному дизайнуЦенаКак понять, какой уровень выбрать: матрица «что вы делаете» и «какие риски допустимы»Когда достаточно Read CommittedКогда нужен Repeatable ReadКогда стоит рассматривать SerializableЛовушки при конкурентной нагрузке: типовые анти-паттерныЛовушка 1: «Проверили, потом записали» в несколько statement под Read CommittedЛовушка 2: отсутствие явных уникальных ограниченийЛовушка 3: блокировки «как попало»Ловушка 4: SERIALIZABLE без ретраев и идемпотентностиБезопасные паттерны чтения и записи в конкурентной средеПаттерн A: Атомарная запись через UPSERT и constraintПаттерн B: Валидация через блокировку строкПаттерн C: Проверка инварианта через constraints вместо транзакционной магииПаттерн D: Serializable для критичных операций + повторПрактический чек-лист: как читать и писать безопасноЧтениеЗаписьУровень изоляции

В PostgreSQL транзакции выглядят «просто» на уровне интерфейса: BEGIN, набор SELECT/INSERT/UPDATE/DELETE, COMMIT. Но под нагрузкой начинаются проблемы, которые сложно отладить постфактум: пропадающие строки в отчётах, повторяющиеся записи, неверные итоговые суммы, гонки между задачами очереди, «невозможные» состояния, которые появляются только при конкуренции.

Центр этой боли — уровни изоляции и аномалии, которые допускаются на каждом уровне. PostgreSQL по умолчанию использует Read Committed — он хорош в большинстве случаев, но не гарантирует того, что ожидают авторы бизнес-логики из других систем (или после прочтения старых статей, где акцент на одной «теоретически идеальной» настройке).

Ниже разберём Read Committed, Repeatable Read, Serializable, посмотрим на конкретные аномалии и реальные паттерны чтения/записи, которые чаще всего ломаются. В конце будет практический чек-лист.


Как PostgreSQL понимает изоляцию: не только «уровень», но и модель MVCC

PostgreSQL использует MVCC (Multi-Version Concurrency Control): строки версионируются, и каждый запрос видит «снимок» данных в зависимости от уровня изоляции.

Ключевой практический момент: в PostgreSQL «изоляция» означает, какие аномалии возможны при параллельных транзакциях. В реальности вы сталкиваетесь не с абстрактными «теоретическими» эффектами, а с тем, что ваши бизнес-вычисления и проверочные запросы перестают соответствовать ожиданиям.

Термины аномалий, которые встречаются чаще всего

  • Dirty read — чтение незакоммиченных данных. В PostgreSQL на всех стандартных уровнях изоляции этого нет (из-за MVCC).
  • Non-repeatable read — повторное чтение одной и той же строки даёт разные значения (например, после коммита другой транзакции).
  • Phantom read — «призрачные» строки: повторный запрос, который возвращает набор строк по условию, начинает возвращать новые строки, появившиеся из-за параллельных вставок/изменений.
  • Write skew / lost update — потеря логической целостности при конкурентных проверках и последующих действиях (часто это не «ошибка СУБД», а ошибка композиции проверок и записи).
  • Serialization failure — на Serializable транзакция может быть прервана для предотвращения «невозможных» аномалий.

Read Committed: быстрый и «коварный» по отношению к повторным чтениям

Read Committed (по умолчанию) обеспечивает: каждый отдельный оператор (statement) видит консистентный снимок на момент начала этого statement. Между двумя запросами в одной транзакции параллельная транзакция может закоммитить изменения — и второй запрос увидит их.

Аномалия 1: Non-repeatable read

Рассмотрим ситуацию: вы в транзакции дважды читаете счётчик и ожидаете, что он не изменится.

code
-- Транзакция A (уровень Read Committed)
BEGIN;

SELECT balance FROM accounts WHERE id = 1;  -- statement #1

-- В этот момент транзакция B меняет balance и делает COMMIT

SELECT balance FROM accounts WHERE id = 1;  -- statement #2

COMMIT;

На уровне Read Committed второе чтение может вернуть другое значение, даже если логически вы ожидали «тот же мир». Это и есть non-repeatable read.

Почему это важно в бизнесе: отчёты, расчёты комиссий, проверки «если баланс >= X, то …» могут стать логически несогласованными, если вы делаете несколько шагов через отдельные SELECT и при этом рассчитываете на неизменность данных во время транзакции.

Аномалия 2: Phantom read (в наборе)

code
BEGIN;

-- statement #1
SELECT sum(amount) 
FROM payments 
WHERE user_id = 42 AND status = 'pending';

-- параллельно другая транзакция добавляет/меняет pending-платежи и делает COMMIT

-- statement #2
SELECT sum(amount) 
FROM payments 
WHERE user_id = 42 AND status = 'pending';

COMMIT;

sum(...) может отличаться в зависимости от времени выполнения statement #2. Это особенно заметно в задачах биллинга, проверки лимитов, построении инкрементальных представлений.

Практический вывод для Read Committed

Если вы используете Read Committed, то в транзакции нужно исходить из правила:

Поведение оператора — консистентно, поведение всей транзакции — нет.

Соответственно:

  • нельзя полагаться на то, что повторный SELECT даст одинаковый результат;
  • нужно либо фиксировать данные на уровне логики (например, делать один запрос вместо двух), либо использовать блокировки/дизайн транзакции.

Repeatable Read: «один снимок на всю транзакцию» — но не магия

Repeatable Read обеспечивает: в рамках одной транзакции все statement видят один и тот же снимок данных (snapshot), сделанный на момент старта транзакции.

Это уже значительно меняет картину: non-repeatable read и классические phantom read внутри одного транзакционного снимка устраняются на уровне «видимости версий».

Не всё, что кажется «фантомом», устранено полностью

Важно различать:

  • фантомы из-за новых строк, появившихся после старта;
  • фантомы из-за взаимодействия нескольких условий и обновлений (write skew).

Поскольку на Repeatable Read снимок один на транзакцию, простые сценарии «повторил SELECT — набор другой» обычно исчезают. Но появляется другая категория проблем — логическая несовместимость, когда транзакции проверяют условия на одном снимке и меняют данные так, что итоговая система попадает в недопустимое состояние.

Write skew: пример, который ломает «проверили — значит можно»

Предположим, есть два врача/лаборатории A и B, и правило бизнеса такое: в сумме должно оставаться не меньше 1 доступного ресурса. На уровне базы нет жёсткого ограничения, которое гарантирует это выражение напрямую.

Схема условная:

  • resources(id, available boolean)
  • Транзакции выбирают свою запись, проверяют условие «в другом месте всё ещё занято/доступно», и затем освобождают/занимают.

Сценарий:

  • Условие: available должно удовлетворять A + B >= 1 (упрощённо).
  • Транзакция T1 делает UPDATE resources SET available=false WHERE id=1 после проверки, что resources.id=2 доступно.
  • Транзакция T2 параллельно делает то же для resources.id=2.

На Repeatable Read обе транзакции видят одинаковый снимок: в начале обе записи доступны (available=true). Каждая на основе снимка считает проверку истинной и делает свой UPDATE. В результате обе записи становятся false, и правило нарушается.

Это write skew — классическая аномалия, которую Repeatable Read не предотвращает.

Почему так происходит

Потому что транзакции:

  1. читают данные на одном снимке;
  2. принимают решение, исходя из прочитанного;
  3. пишут так, что ограничение для системы в целом больше не выполняется.

В терминах строгой теории это не «невозможная» последовательность, которую модель может отсечь на Repeatable Read. PostgreSQL гарантирует консистентность чтения, но не «невозможность» таких логических противоречий.

Практический вывод для Repeatable Read

Repeatable Read полезен, когда:

  • вы делаете несколько чтений и хотите стабильность видимости;
  • вы собираете отчёт/список в рамках одной транзакции.

Но он не гарантирует что композиция «прочитал — проверил — изменил» будет логически корректной при конкурентных изменениях, если у вас есть условные бизнес-ограничения, не выраженные в виде жёстких constraints или не защищённые правильными блокировками.


Serializable: защита от невозможных историй — но с учётом отказов

Serializable — это попытка обеспечить эффект, эквивалентный строгой последовательной обработке транзакций. В PostgreSQL это достигается не простым блокированием «всего всего», а с помощью механизма, который отслеживает конфликты и может прервать транзакцию с ошибкой serialization failure.

Что это означает на практике

  • СУБД будет стараться предотвратить аномалии вроде write skew и других «неэквивалентных» историй.
  • Если конфликт логики не удаётся разрешить автоматически, транзакция будет отменена, и вам нужно повторить её.

Типичная ошибка: ERROR: could not serialize access due to read/write dependencies among transactions (SQLSTATE 40001).

Минимальный пример перезапуска

В приложении обычно делают цикл с повтором для serializable:

code
import time
import psycopg

def run_serializable(conn, work):
    for attempt in range(1, 6):
        try:
            with conn.transaction(isolation_level="serializable"):
                return work()
        except psycopg.errors.SerializationFailure:
            # небольшая пауза снижает шанс бесконечного конфликта
            time.sleep(0.05 * attempt)
    raise RuntimeError("Serializable транзакция не завершилась после повторов")

Идея: логика работы должна быть идемпотентной или безопасной к повтору, а перезапуск должен учитываться на уровне домена.

Serializable — не замена правильному дизайну

Если у вас бизнес-операция требует строгого ограничения, лучше выражать его максимально явно:

  • unique constraints,
  • foreign keys,
  • check constraints,
  • constraints на комбинации полей.

Но когда ограничение сложное и относится к нескольким строкам/условиям, Serializable — один из способов гарантировать корректность.

Цена

Чаще всего Serializable:

  • увеличивает накладные расходы на отслеживание зависимостей;
  • увеличивает вероятность serialization failure под нагрузкой;
  • требует обработки ретраев на уровне приложения.

Поэтому в реальных системах Serializable применяют выборочно:

  • там, где ошибка бизнес-логики недопустима;
  • где есть конфликтующая конкуренция;
  • где корректность важнее пропускной способности.

Как понять, какой уровень выбрать: матрица «что вы делаете» и «какие риски допустимы»

Ни один «универсальный» ответ невозможен, но есть устойчивые практические правила.

Когда достаточно Read Committed

  • Операции, где вы делаете один запрос для чтения и сразу на его основе пишете, без «повторного чтения ради контроля».
  • Сценарии с жёсткими ограничениями в БД (unique, not null, FK), которые защищают от неконсистентного результата.
  • Пайплайны, где итоговый эффект допускает «eventual correctness» (например, переагрегации по расписанию).

Когда нужен Repeatable Read

  • Отчёты и выгрузки, где важно, чтобы набор данных не менялся между несколькими запросами.
  • Сложные расчёты в одной транзакции, где повторяемость чтения критична.
  • Ситуации, где вы понимаете возможность write skew и либо исключаете его другими средствами (блокировки, constraints), либо принимаете риск.

Когда стоит рассматривать Serializable

  • Операции «проверь условие и сделай действие» на основе пересекающихся данных, где write skew может нарушить инвариант.
  • Очереди/распределение ресурсов при высокой конкуренции, где важно гарантировать «ровно один выигравший» с корректным распределением.
  • Критичные финансовые/балансовые операции, особенно при отсутствии прямых constraints.

Ловушки при конкурентной нагрузке: типовые анти-паттерны

Ниже — то, что чаще всего ломается именно в production.

Ловушка 1: «Проверили, потом записали» в несколько statement под Read Committed

code
BEGIN;

SELECT COUNT(*)
FROM orders
WHERE user_id = 42 AND status = 'active';

-- ожидаем, что результат в момент UPDATE тот же
UPDATE orders
SET status = 'active'
WHERE id = 123;

COMMIT;

Между SELECT и UPDATE другая транзакция может изменить данные, и условие потеряет смысл.

Что делать:

  • свести проверку и действие к одной атомарной операции, если возможно;
  • использовать SELECT ... FOR UPDATE (или FOR SHARE) по ключевым строкам;
  • либо повышать уровень изоляции.

Ловушка 2: отсутствие явных уникальных ограничений

Иногда логика «гарантируем уникальность программно» не выдерживает гонок. Даже при Serializable можно получить проблемные эффекты, если запись не защищена constraint-ом (или если вы делаете несколько таблиц/шагов без единого инварианта).

Обязательные вещи:

  • уникальность — через UNIQUE index;
  • корректные ссылки — через FK;
  • базовые инварианты — через CHECK.

Ловушка 3: блокировки «как попало»

Если вы добавляете FOR UPDATE, но:

  • выбираете строки в разном порядке в разных местах,
  • держите транзакции дольше,
  • блокируете лишнее,

— вы можете устроить deadlock или резко просадить throughput.

Правило: блокируйте минимально необходимые строки и делайте одинаковый порядок доступа.

Ловушка 4: SERIALIZABLE без ретраев и идемпотентности

На Serializable «иногда транзакция падает» — это нормальная часть защиты. Если приложение не повторяет, вы получаете ошибки пользователям и/или ломаете целостность.


Безопасные паттерны чтения и записи в конкурентной среде

Рассмотрим несколько практичных схем.

Паттерн A: Атомарная запись через UPSERT и constraint

Если бизнес-операция по смыслу «создать или обновить» по ключу, используйте INSERT ... ON CONFLICT.

code
INSERT INTO balances(user_id, amount)
VALUES (42, 100)
ON CONFLICT (user_id)
DO UPDATE
SET amount = balances.amount + EXCLUDED.amount;

Это переносит конкуренцию на уровень индекса и делает поведение детерминированным. Важный эффект: вы избегаете «SELECT then UPDATE».

Паттерн B: Валидация через блокировку строк

Если есть смысл «один клиент на один ресурс» (например, распределить free-лейбл), используйте блокировку выборки:

code
BEGIN;

-- выбираем одну доступную строку и блокируем её
WITH cte AS (
  SELECT id
  FROM inventory
  WHERE status = 'free'
  ORDER BY id
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
UPDATE inventory i
SET status = 'reserved'
FROM cte
WHERE i.id = cte.id
RETURNING i.id;

COMMIT;

FOR UPDATE SKIP LOCKED помогает строить очередь воркеров: один воркер не ждёт заблокированную строку, а берёт другую. Этот подход часто лучше, чем «постоянно сканировать и пытаться».

Паттерн C: Проверка инварианта через constraints вместо транзакционной магии

Если инвариант формализуется в виде SQL constraint, используйте его. Пример — ограничение суммы в реляционной форме бывает сложно, но часто ограничение сводится к:

  • уникальным ключам,
  • проверкам диапазонов,
  • обязательности связей.

Когда инвариант выразим, СУБД сделает «правильность» без гонок.

Паттерн D: Serializable для критичных операций + повтор

Когда ваш инвариант зависит от нескольких строк, а write skew — реальная угроза, используйте Serializable точечно, с ретраями.

Пример на SQL уровне (без приложения)

В SQL вы можете вручную повторять, но обычно это делает код.

code
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- критичная бизнес-операция:
-- 1) чтение
-- 2) проверка инварианта
-- 3) обновление

COMMIT;

Если получите serialization failure, транзакцию нужно откатить и запустить снова.


Практический чек-лист: как читать и писать безопасно

Ниже — компактный список, который стоит держать перед глазами при проектировании.

Чтение

  • Не предполагайте, что повторный SELECT в одной транзакции даст одинаковые результаты на Read Committed.
  • Для консистентных отчётов/выгрузок с несколькими запросами рассмотрите Repeatable Read (но анализируйте возможный write skew при последующих update’ах).
  • В критичных местах старайтесь делать «чтение → действие» так, чтобы оно было атомарным (через UPSERT/constraints) или защищённым блокировками.

Запись

  • Защитите инварианты, которые можно выразить в БД, constraints/unique indexes.
  • Если вы делаете «проверили — решили — записали», подумайте: может ли параллельный процесс изменить данные между шагами?
    • Если да — переходите к атомарной операции, FOR UPDATE, или Serializable.
  • Сокращайте время жизни транзакции и объём заблокированных данных.
  • Всегда фиксируйте порядок блокировки строк (особенно если вы блокируете несколько таблиц/ключей).

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

  • Начните с

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

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630
Python для аналитики на практике: как строить признаки и проверять их воспроизводимость
python

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость

Поймём, где чаще всего ломается воспроизводимость (версии данных, фильтры, дрейф) и как оформить пайплайн признаков так, чтобы результаты совпадали между запусками и окружениями.

17 сентября 2026 г.
1050
Что такое переменные, типы и условия в коде: объясню без матана на примерах
такое

Что такое переменные, типы и условия в коде: объясню без матана на примерах

Разберём ключевые понятия для старта — переменные, типы данных, сравнения и условные операторы — простыми словами и на бытовых примерах. В конце соберём мини-скрипт, который принимает решение по введённым данным.

30 сентября 2026 г.
660

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Как PostgreSQL понимает изоляцию: не только «уровень», но и модель MVCCТермины аномалий, которые встречаются чаще всегоRead Committed: быстрый и «коварный» по отношению к повторным чтениямАномалия 1: Non-repeatable readАномалия 2: Phantom read (в наборе)Практический вывод для Read CommittedRepeatable Read: «один снимок на всю транзакцию» — но не магияНе всё, что кажется «фантомом», устранено полностьюWrite skew: пример, который ломает «проверили — значит можно»Почему так происходитПрактический вывод для Repeatable ReadSerializable: защита от невозможных историй — но с учётом отказовЧто это означает на практикеМинимальный пример перезапускаSerializable — не замена правильному дизайнуЦенаКак понять, какой уровень выбрать: матрица «что вы делаете» и «какие риски допустимы»Когда достаточно Read CommittedКогда нужен Repeatable ReadКогда стоит рассматривать SerializableЛовушки при конкурентной нагрузке: типовые анти-паттерныЛовушка 1: «Проверили, потом записали» в несколько statement под Read CommittedЛовушка 2: отсутствие явных уникальных ограниченийЛовушка 3: блокировки «как попало»Ловушка 4: SERIALIZABLE без ретраев и идемпотентностиБезопасные паттерны чтения и записи в конкурентной средеПаттерн A: Атомарная запись через UPSERT и constraintПаттерн B: Валидация через блокировку строкПаттерн C: Проверка инварианта через constraints вместо транзакционной магииПаттерн D: Serializable для критичных операций + повторПрактический чек-лист: как читать и писать безопасноЧтениеЗаписьУровень изоляции