Уровни изоляции транзакций: практический выбор без гонок
В PostgreSQL уровень изоляции влияет на то, какие изменения увидит транзакция и может ли она завершиться ошибкой из-за параллельной записи. Разбираем, когда достаточно Read Committed, а когда нужны Repeatable Read или Serializable.

В PostgreSQL 16 транзакции по умолчанию работают в режиме Read Committed: каждый запрос видит актуальный снимок данных, но два запроса внутри одной транзакции могут увидеть разное состояние. Для обычного списания со склада этого часто достаточно; для расчёта, который должен сохранить общий инвариант между несколькими строками, такой режим может допустить ошибку. Выбор уровня — это компромисс между свободой параллельной работы и вероятностью повторить транзакцию.
Сначала определите, что должно оставаться правдой
Не выбирайте самый строгий уровень «на всякий случай». Сформулируйте инвариант — условие, которое обязано выполняться после каждой успешной операции. Например: остаток товара не уходит ниже нуля; либо в смене всегда работает хотя бы один сотрудник. Затем проверьте, может ли параллельный запрос нарушить это условие, даже если каждый запрос по отдельности выглядит корректным.
В PostgreSQL 16 есть три практически разных режима. Read Uncommitted тоже можно указать, но PostgreSQL трактует его как Read Committed. Поэтому обычный выбор — между Read Committed, Repeatable Read и Serializable.
| Уровень | Что видит транзакция | Когда подходит | Цена |
|---|---|---|---|
| Read Committed | Каждый запрос получает новый снимок: видит данные, зафиксированные до начала этого запроса. | Обычные короткие операции, атомарное изменение одной строки. | Повторное чтение может вернуть другие данные; многозапросная логика может столкнуться с гонкой. |
| Repeatable Read | Запросы читают один снимок, установленный первым запросом транзакции. | Несколько чтений должны согласованно видеть состояние базы на один момент. | Конфликтующие записи могут прервать транзакцию; некоторые аномалии между несколькими строками всё ещё возможны. |
| Serializable | База отслеживает опасные зависимости и отклоняет транзакции, если результат нельзя объяснить последовательным выполнением. | Критичные правила зависят от набора строк или результатов проверочного запроса. | Возможны отмены и повторы; приложению нужна обработка ошибок. |
Термин: снимок данных. Это набор изменений, который транзакция считает уже зафиксированным и доступным для чтения. В Repeatable Read один снимок используется повторно, в Read Committed снимок обновляется для каждого запроса.
Read Committed: начинайте с него, но пишите атомарно
Для простого списания остатка не нужно сначала прочитать значение, проверить его в приложении, а затем отдельным запросом сохранить новое. Между чтением и записью другой процесс может изменить ту же строку. Поместите проверку и изменение в один UPDATE:
BEGIN;
UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = 42
AND quantity > 0
RETURNING quantity;
COMMIT;
В PostgreSQL при конкурирующем изменении строки UPDATE ждёт завершения первой операции, а затем проверяет условие WHERE на актуальной версии строки. Если запас уже исчерпан, запрос не вернёт строку. Приложение должно считать число изменённых строк: ноль означает, что списание не состоялось. Не делайте вывод об успехе только по тому, что запрос не завершился ошибкой.
Такой подход работает, когда условие и изменение можно выразить для одной строки. Если правило зависит от нескольких строк, агрегата или отсутствия подходящей строки, отдельная проверка в Read Committed не гарантирует, что параллельная транзакция не изменит результат сразу после неё.
Repeatable Read: один снимок для нескольких чтений
Используйте этот уровень, когда отчёт или операция должны прочитать несколько связанных наборов данных так, будто они относятся к одному моменту. В PostgreSQL новые фиксации других транзакций не меняют снимок внутри вашей транзакции. Это делает повторное чтение предсказуемым, но не превращает транзакцию в универсальную блокировку от конкурентных изменений.
Например, приложение проверяет две строки — доступность двух ресурсов — и затем обновляет одну из них. Другая транзакция может одновременно принять решение, опираясь на тот же снимок, и обновить вторую. Обе операции способны завершиться, хотя вместе нарушат правило «хотя бы один ресурс должен оставаться доступным». Это пример write skew: каждая транзакция меняет свою строку, поэтому простого конфликта записи по одной строке может не быть.
Repeatable Read в PostgreSQL предотвращает фантомное чтение, но допускает такие аномалии сериализации. Поэтому согласованный снимок — не доказательство, что итог эквивалентен последовательному выполнению всех операций.
Serializable: когда правило охватывает несколько строк
Если корректность зависит от совместного состояния нескольких записей, Serializable — самый прямой способ поручить базе проверку конфликтующих зависимостей. Установите уровень до первого запроса, который читает или меняет данные:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT employee_id, on_shift
FROM shifts
WHERE shift_date = DATE '2025-04-01';
-- Проверка и изменения, сохраняющие правило смены
COMMIT;
Пример показывает только установку режима и чтение: бизнес-правило и запись должны соответствовать схеме конкретного приложения. PostgreSQL может отменить транзакцию с ошибкой сериализации. Это не поломка базы, а сигнал: приложение должно повторить всю транзакцию — от чтения исходных данных до фиксации, а не только последний UPDATE.
Более строгая изоляция не отменяет конкуренцию. Она меняет её цену: вместо незаметно неверного результата приложение получает явную ошибку и должно уметь повторить операцию.
Практический порядок выбора
- Одна строка и простое условие. Оставьте Read Committed и объедините проверку с изменением в один SQL-запрос. Проверьте по числу возвращённых или изменённых строк, выполнилась ли операция.
- Несколько чтений должны видеть одно состояние. Рассмотрите Repeatable Read. Проверьте, что приложению подходят отмены транзакций при конфликтующих изменениях.
- Инвариант зависит от нескольких строк. Рассмотрите Serializable либо спроектируйте явную блокировку или ограничение базы, подходящее модели данных. Проверьте конкурентным тестом, что правило не нарушается.
- Добавьте повтор транзакции там, где возможна отмена. Для ошибки сериализации PostgreSQL использует SQLSTATE 40001. Повторяйте всю бизнес-операцию ограниченное число раз, с небольшой задержкой между попытками; не запускайте бесконечный цикл.
- Разделите повторяемые ошибки и постоянные. Ошибка сериализации может пройти при новой попытке. Ошибка валидации, неверные данные или нарушение бизнес-правила обычно требуют другого решения, а не автоматического повтора.
Повтор должен быть безопасен и на уровне приложения. Если внутри транзакции отправляется письмо, списываются деньги через внешний сервис или публикуется сообщение, повтор SQL-транзакции не отменит уже совершённое внешнее действие. Такие эффекты выносят за границы повторяемого участка или делают идемпотентными.
Что чаще всего ломается
Приложение повторяет только последний запрос
После serialization failure исходные чтения уже не подходят для принятия решения. Повторите всю транзакцию на новом снимке. Иначе запрос использует устаревшие значения и может снова прийти к неверному результату.
Длинная транзакция держит старый снимок
Чем дольше транзакция открыта, тем дольше база должна сохранять версии строк, нужные её снимку. Не держите её открытой во время пользовательского ввода, долгого сетевого вызова или фоновой работы. Фиксируйте транзакцию сразу после завершения нужных операций.
Повтор превратился в бесконечный цикл
При постоянной конкуренции транзакции могут регулярно отменяться. Задайте лимит попыток, добавьте задержку и логируйте число повторов и SQLSTATE. Если отмен много, выясните, какие запросы конфликтуют: повышение числа повторов само по себе проблему не устраняет.
Serializable включили, но приложение не готово к отменам
Операция, которая раньше обычно завершалась, теперь может вернуть ошибку сериализации. Проверьте, что приложение распознаёт SQLSTATE 40001 и повторяет всю транзакцию. Deadlock — отдельный тип ошибки; его также иногда повторяют, но сначала убедитесь, что повтор безопасен и ограничен.
Что измерять в проде
Не оценивайте выбор изоляции только по среднему времени ответа. Смотрите на частоту serialization failure и deadlock, число повторов на одну бизнес-операцию, долю исчерпанных попыток и задержку транзакций. Сравнивайте одинаковые интервалы и одинаковую нагрузку до и после изменения. Если конфликты сосредоточены вокруг нескольких строк, проверьте, можно ли сузить критическую секцию или выразить правило ограничением данных.
Следующий шаг — выписать инварианты для самых дорогих операций и проверить каждый конкурентным тестом с двумя параллельными транзакциями. Для каждой операции зафиксируйте ожидаемый результат, включая случай, когда одна из транзакций получает отказ. Поведение уровней изоляции в других СУБД может отличаться: переносить эти выводы на MySQL или SQL Server без сверки документации нельзя.
Антиблок-аналитик 115-ФЗ: Анализ выписок, поиск рисков блокировки счёта, инструкции по безопасным транзакциям.
Спросить за 41 ₽Источники: PostgreSQL 16: уровни изоляции транзакций, PostgreSQL 16: SET TRANSACTION
Частые вопросы
Какой уровень изоляции выбрать по умолчанию в PostgreSQL?
Достаточно ли Repeatable Read, чтобы избежать всех гонок?
Что делать, если транзакция получила ошибку сериализации?
Можно ли повысить уровень изоляции посреди транзакции?
Подойдут ли эти правила для другой версии или другой СУБД?
Что делать, если повтор транзакции не помогает?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.