Уровни изоляции в PostgreSQL: ошибки и как их избежать
В PostgreSQL уровень изоляции меняет то, какие параллельные транзакции видит ваш запрос и когда база может его прервать. Разбираем типичные ошибки на примерах для PostgreSQL 16 — от повторного чтения до сериализационных сбоев.

В PostgreSQL 16 транзакция на уровне Read Committed может увидеть разные данные в двух запросах подряд, а Serializable — завершиться ошибкой, хотя каждый запрос по отдельности выглядит корректно. Ошибка обычно не в выборе «неправильного» уровня, а в предположении, что он решает за приложение задачу блокировок, уникальности или повторных попыток.
Что именно меняет уровень изоляции
Уровень изоляции задаёт правила видимости параллельных изменений и то, какие конфликты PostgreSQL считает основанием для отмены транзакции. Для типичных ошибок важны три уровня: Read Committed, Repeatable Read и Serializable.
В PostgreSQL Read Uncommitted работает как Read Committed: грязное чтение невозможно. Уровень по умолчанию — Read Committed. В нём каждый SQL-оператор получает собственный снимок данных: второй SELECT в той же транзакции может увидеть изменения, зафиксированные другой транзакцией после первого SELECT.
Repeatable Read сохраняет снимок для всей транзакции, поэтому повторное чтение не покажет более поздние фиксации. Но это не означает, что транзакция защищена от всех логических конфликтов: PostgreSQL допускает аномалии сериализации. Serializable обнаруживает такие конфликты и может отменить одну из транзакций.
Уровень изоляции не делает последовательность «прочитать — проверить — записать» атомарной сам по себе. Если условие можно выразить в одном UPDATE или закрепить ограничением базы, обычно это надёжнее, чем повышать изоляцию наугад.
Снимок транзакции — набор версий строк, которые запрос может видеть с учётом момента начала чтения. В PostgreSQL он связан с MVCC — механизмом хранения версий строк, благодаря которому чтение обычно не блокирует запись.
Три ошибки, которые встречаются чаще всего
Считать, что Read Committed даёт один снимок на всю транзакцию
Предположим, приложение сначала читает остаток товара, а затем решает, можно ли принять заказ. Между этими операторами другая транзакция может изменить остаток и зафиксировать изменения. Следующий запрос в Read Committed увидит уже новое состояние. Это не сбой PostgreSQL: так устроен уровень.
Если требуется изменить строку только при выполнении условия, поместите проверку в оператор записи. Например, для счётчика остатков условное обновление не даст двум запросам списать больше, чем есть:
UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = 42
AND quantity > 0
RETURNING quantity;
Если запрос вернул строку, списание прошло; если не вернул — подходящей строки с остатком больше нуля не нашлось. В реальном приложении результат нужно проверить и обработать. При работе с несколькими строками также важно определить порядок обновления, чтобы снизить риск взаимных блокировок.
Выбирать Repeatable Read и ждать полной сериализуемости
Repeatable Read в PostgreSQL предотвращает неповторяемое чтение и фантомы в рамках снимка, но не запрещает любую ситуацию, которую нельзя представить как последовательное выполнение транзакций. Классический риск возникает, когда две транзакции читают общий набор строк, каждая решает, что условие выполнено, и меняет разные строки. Конфликта записи одной и той же строки может не быть, а бизнес-правило всё равно нарушится.
Переход на Serializable помогает обнаруживать такие опасные сочетания чтений и записей. Цена — транзакция иногда завершается ошибкой сериализации, и приложение должно повторить её целиком. Переход не заменяет уникальный индекс или ограничение CHECK: структурные правила данных лучше закреплять в самой базе.
Не повторять транзакцию после ошибки Serializable
PostgreSQL может вернуть SQLSTATE 40001 (serialization_failure). Это штатный исход конкурентной работы, а не повод повторять только последний UPDATE: предыдущие чтения и решения тоже могли опираться на снимок, который уже нельзя использовать.
Повторяйте всю транзакцию от начала, ограничив число попыток и добавив небольшую случайную задержку между ними, если это нужно для вашей нагрузки. Не включайте в повторяемый блок необратимые внешние действия — например, отправку письма или списание через платёжный сервис. Выполняйте их после успешной фиксации или используйте отдельный надёжный механизм доставки.
Как выбрать уровень и не переплатить конфликтами
Для каждой транзакции выбирайте самый простой механизм, который гарантирует конкретное бизнес-правило. В таблице — не рейтинг уровней, а практический выбор с его ограничениями.
| Ситуация | Что обычно выбрать | Цена и ограничение |
|---|---|---|
| Независимое чтение или условное изменение одной строки | Read Committed и атомарный UPDATE с условием | Для сложного правила на нескольких строках одного оператора может быть недостаточно |
| Нужен стабильный снимок при нескольких чтениях | Repeatable Read | Конфликтующие записи могут привести к отмене транзакции; логическая сериализуемость не гарантируется |
| Правило зависит от сочетания чтений и записей нескольких транзакций | Serializable | Нужны повторные попытки всей транзакции и контроль их частоты |
| Нельзя допустить повтор одного значения, например адреса электронной почты | Уникальное ограничение или уникальный индекс | Приложение должно корректно обработать конфликт уникальности |
Уникальность нельзя надёжно обеспечить схемой «сначала SELECT, затем INSERT»: два клиента могут одновременно не найти запись и оба попытаться её создать. Добавьте уникальное ограничение, а конфликт обработайте в приложении. Это работает независимо от того, успели ли транзакции увидеть запись друг друга.
Если решение зависит от текущего значения строки и должно исключить одновременное изменение именно этой строки, рассмотрите явную блокировку строки. В PostgreSQL для этого используют, например, SELECT ... FOR UPDATE. Но блокировка удерживается до конца транзакции, увеличивает ожидания и не решает автоматически конфликт правил, зависящих от набора строк, которые ещё не существуют.
Что ломается на границах
- Транзакцию открыли слишком поздно. Если проверка уже выполнена отдельным запросом вне транзакции, последующий BEGIN не включает её в снимок и не защищает принятое решение.
- Уровень задали после первого запроса. Для текущей транзакции команду
SET TRANSACTIONнужно выполнить до первого запроса к данным; иначе PostgreSQL вернёт ошибку. Уровень можно также указать при начале транзакции. - Поймали deadlock. Это не то же самое, что ошибка сериализации. PostgreSQL отменяет одну из транзакций, чтобы разорвать цикл ожидания. Проверьте, что транзакции захватывают строки в одинаковом порядке, а обработчик не считает любой сбой безопасным для повторения.
- В Repeatable Read столкнулись с конкурентным обновлением. Транзакция может завершиться ошибкой, если другая транзакция изменила строку после создания её снимка. Перечитайте данные и повторите всю операцию в новой транзакции.
- Долго держите транзакцию открытой. Долгие транзакции удерживают старые снимки и могут мешать очистке устаревших версий строк. Не ждите внутри транзакции ответа пользователя или внешнего API.
Не повышайте уровень изоляции глобально в надежде «починить гонки»: это может добавить отмены и ожидания всем операциям, но не заменить корректные ограничения данных. Для PostgreSQL 16 детали поведения и синтаксис сверяйте с документацией именно этой версии.
Что проверить дальше
Возьмите одну критичную операцию — например, резервирование товара или создание пользователя — и запишите её инвариант простыми словами: что ни при какой конкуренции не должно стать истинным. Затем проверьте, можно ли выразить его ограничением базы или условным UPDATE. Только если правило зависит от сочетания чтений и записей, сравните Repeatable Read и Serializable.
В тесте запускайте две транзакции параллельно и проверяйте не только итоговые строки, но и ошибки SQLSTATE, число повторов, время ожидания и частоту deadlock. В проде отслеживайте те же показатели: рост ошибок сериализации может означать не только перегрузку, но и транзакции, которые слишком широко читают и меняют данные. Следующий шаг — сверить границы транзакции и стратегию повторов с документацией PostgreSQL 16.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: PostgreSQL 16: Transaction Isolation, PostgreSQL 16: Explicit Locking
Частые вопросы
Какой уровень изоляции PostgreSQL использует по умолчанию?
Достаточно ли Repeatable Read, чтобы две параллельные операции не нарушили бизнес-правило?
Что делать, если Serializable вернул SQLSTATE 40001?
Можно ли перейти на более высокий уровень изоляции без изменений в приложении?
Почему уникальный SELECT перед INSERT не защищает от дублей?
Чем ошибка сериализации отличается от deadlock?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.