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

В PostgreSQL 16 транзакция на уровне SERIALIZABLE может завершиться ошибкой сериализации — даже если каждый запрос по отдельности выглядит правильным. А READ COMMITTED, уровень по умолчанию, не спасает от всех гонок. Выбор зависит от инварианта данных: что именно нельзя допустить и сколько приложение готово платить ожиданием блокировок или повторными попытками.
Сначала назовите ошибку, которую предотвращаете
Уровень изоляции часто выбирают по принципу «поставим самый строгий — так безопаснее». На практике это может добавить конфликты и повторы, не закрыв ошибку в логике приложения. До настройки сформулируйте инвариант: например, остаток товара не должен стать отрицательным или два клиента не должны занять одно место.
В PostgreSQL 16 доступны четыре стандартных уровня, но READ UNCOMMITTED работает как READ COMMITTED. На READ COMMITTED каждый оператор получает свой снимок зафиксированных данных. Поэтому два последовательных SELECT в одной транзакции могут увидеть разные результаты. REPEATABLE READ сохраняет снимок на время транзакции, но не исключает все аномалии сериализации. SERIALIZABLE отслеживает опасные зависимости и может отменить транзакцию, чтобы результат оставался эквивалентен последовательному выполнению.
| Подход | Что даёт | Цена и ограничение |
|---|---|---|
| READ COMMITTED и атомарное изменение | Защищает конкретное условие в одном SQL-операторе | Не обеспечивает единый снимок для нескольких запросов |
| Блокировка строк | Не даёт конкурентной транзакции изменить выбранные строки до завершения текущей | Транзакции ждут друг друга; возможны взаимные блокировки |
| REPEATABLE READ | Повторные чтения видят один снимок | Запись при конфликте может завершиться ошибкой; сложные межстрочные инварианты не всегда защищены |
| SERIALIZABLE | Защищает от аномалий, несовместимых с последовательным выполнением | Возможны отмены транзакций и необходимость повторять их целиком |
Термин. Снимок транзакции — представление данных, которые транзакция может прочитать с учётом фиксаций других транзакций. В READ COMMITTED новый снимок создаётся для каждого оператора, в REPEATABLE READ — сохраняется для транзакции.
Приём 1. Проверьте условие прямо в UPDATE
Для простого ограничения — например, не продавать больше товара, чем есть на складе, — не читайте остаток отдельно, чтобы затем обновить его. Между SELECT и UPDATE другая транзакция успеет продать те же единицы. Сведите проверку и изменение в один оператор:
UPDATE inventory
SET available = available - 2
WHERE sku = 'ITEM-42'
AND available >= 2
RETURNING available;Предполагается, что в таблице есть столбцы sku и available. Если запрос вернул строку, резерв создан; если не вернул — товара не хватает или такого SKU нет. Если нужно различить эти причины, обработайте это отдельно, но не возвращайтесь к небезопасной схеме «прочитать, потом решить». При конкурентном обновлении PostgreSQL дождётся завершения другой операции и проверит условие для актуальной версии строки.
Этот приём хорошо работает, когда инвариант выражается одной операцией над строкой. Если правило зависит от набора строк или нескольких таблиц, одной такой проверки может быть недостаточно.
Приём 2. Блокируйте строки, которые собираетесь менять
Когда бизнес-правило требует прочитать несколько известных записей, а затем принять решение, возьмите блокировки до проверки. Например, перед переводом денег нужно проверить оба счёта и изменить их согласованно. Запрос блокировки выглядит так:
BEGIN;
SELECT id, balance
FROM accounts
WHERE id IN (101, 202)
ORDER BY id
FOR UPDATE;
-- Приложение проверяет баланс и выполняет изменения.
-- Затем транзакция фиксируется.
COMMIT;Это фрагмент для транзакции приложения, а не полный алгоритм перевода: проверка и изменения должны пройти до COMMIT в той же транзакции. ORDER BY задаёт одинаковый порядок захвата строк. Это снижает риск взаимной блокировки, но не устраняет его полностью: другая часть системы может брать те же блокировки в другом порядке.
Блокировка удерживается до конца транзакции. Не делайте внутри неё долгие вычисления, не ждите ответа внешнего API и не держите соединение без необходимости: пока приложение ждёт, другие запросы могут стоять в очереди.
Приём 3. Используйте REPEATABLE READ или SERIALIZABLE осознанно
REPEATABLE READ подходит, если несколько чтений должны опираться на один и тот же снимок. Например, отчёт не должен видеть разные состояния данных в начале и конце транзакции. Но фиксированный снимок сам по себе не означает, что любые записи безопасны: конкурирующее изменение строки может привести к ошибке, а межстрочные правила требуют отдельного внимания.
SERIALIZABLE полезен, когда корректность зависит от последовательности нескольких чтений и записей, а перепроектировать операцию в атомарный UPDATE или явные блокировки неудобно. Уровень задают до выполнения запросов:
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- Запросы, составляющие одну логическую операцию.
COMMIT;Обрабатывайте SQLSTATE 40001: это ошибка сериализации, при которой нужно повторить всю транзакцию с начала, а не только последний запрос. На повторной попытке приложение должно заново прочитать данные и принять решение. Повторять транзакцию с теми же устаревшими значениями — значит снова создать ту же гонку.
Выбирайте не «самую строгую изоляцию», а самое простое средство, которое защищает конкретный инвариант. Для одного остатка часто достаточно условного UPDATE; для сложного правила может понадобиться сериализация и корректный повтор операции.
Когда что выбирать
- Одно условие на одной строке. Используйте UPDATE с проверкой в WHERE и проверяйте, вернулась ли строка. Так вы избегаете окна между чтением и записью.
- Несколько известных строк. Блокируйте их через SELECT FOR UPDATE, захватывайте в одинаковом порядке и держите транзакцию короткой.
- Нужен стабильный снимок для чтений. Рассмотрите REPEATABLE READ, но отдельно проверьте, не строится ли инвариант на изменении нескольких строк.
- Корректность зависит от сочетания конкурентных чтений и записей. Рассмотрите SERIALIZABLE и заранее реализуйте повтор транзакции при SQLSTATE 40001.
Уникальные ограничения тоже важны: если нельзя создать две записи с одной комбинацией значений, это правило лучше закрепить ограничением уникальности в базе, а не только предварительной проверкой в приложении. Такая проверка уязвима для гонки: оба запроса могут увидеть, что записи ещё нет.
Что ломается в приложении и в проде
Повторяют только упавший запрос. После ошибки сериализации транзакцию нужно начать заново: прежние чтения и решения уже нельзя считать актуальными.
Слишком долго держат блокировки. Длительная транзакция задерживает конкурентов и увеличивает вероятность тайм-аутов и взаимных блокировок. Фиксируйте результат как можно скорее.
Считают, что SERIALIZABLE исключает ошибки. Он обеспечивает требуемую семантику, но может отменить участника конфликта. Если приложение не умеет повторять операцию, пользователь всё равно увидит сбой.
Слепо повторяют любые ошибки. Ошибка сериализации и взаимная блокировка — разные ситуации. Для взаимной блокировки PostgreSQL возвращает SQLSTATE 40P01. Политика повторов должна учитывать тип ошибки, ограничивать число попыток и не запускать повтор для бизнес-ошибок вроде недостатка средств.
В проде измеряйте не только общее время ответа, но и число ошибок 40001 и 40P01, длительность транзакций и ожидание блокировок. Сравнивайте метрики до и после при похожем потоке запросов: без одинаковых условий нельзя понять, помог ли новый уровень изоляции или нагрузка просто изменилась.
Что делать дальше
Для самой конфликтной операции запишите инвариант и найдите место, где приложение сначала читает данные, а потом меняет их. Попробуйте выразить правило одним атомарным запросом; если это не подходит, решите, какие строки нужно блокировать или требуется ли стабильный снимок. Затем добавьте обработку повторов и проверьте под конкурентной нагрузкой, как часто возникают ошибки сериализации и ожидания блокировок. Инвалидация кэша и согласованность данных между разными сервисами — отдельные задачи: уровни изоляции одной базы их не решают.
Антиблок-аналитик 115-ФЗ: Анализ выписок, поиск рисков блокировки счёта, инструкции по безопасным транзакциям.
Спросить за 41 ₽Источники: PostgreSQL 16: уровень изоляции транзакций, PostgreSQL 16: явные блокировки
Частые вопросы
Какой уровень изоляции в PostgreSQL стоит по умолчанию?
READ UNCOMMITTED действительно слабее READ COMMITTED?
Нужно ли всегда включать SERIALIZABLE?
Что делать, если транзакция завершилась ошибкой 40001?
Эти правила отличаются в другой версии PostgreSQL?
Что может пойти не так в проде при переходе на SERIALIZABLE?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.