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

В PostgreSQL 16 транзакция на уровне READ COMMITTED может дважды прочитать одну запись и получить разные значения, а REPEATABLE READ всё ещё не защищает от некоторых конфликтов между строками. Ошибка в выборе уровня обходится не только неверным отчётом: можно продать последний товар дважды или сохранить состояние, нарушающее бизнес-правило. Ниже — практические сценарии и способы не перепутать изоляцию с гарантией отсутствия гонок.
Сначала договоритесь, что именно должна защищать транзакция
По умолчанию PostgreSQL использует READ COMMITTED: каждый SQL-оператор получает снимок данных на момент своего старта. Поэтому два SELECT внутри одной транзакции могут увидеть разные зафиксированные изменения. Это нормально для этого уровня, но неожиданно, если код рассчитывает на неизменность прочитанных данных.
В PostgreSQL READ UNCOMMITTED работает как READ COMMITTED: грязное чтение не появляется. Для практического выбора обычно остаются три уровня — READ COMMITTED, REPEATABLE READ и SERIALIZABLE — плюс явные блокировки, когда нужно защитить конкретные строки.
Термин: снимок данных — набор изменений, которые транзакция видит при чтении. При MVCC* PostgreSQL хранит версии строк, поэтому читатели обычно не блокируют писателей, но разные уровни изоляции выбирают эти версии по-разному.
Выбирайте не самый строгий уровень «на всякий случай», а гарантию, которая соответствует инварианту: что не должно стать неверным после двух одновременных запросов.
Четыре подхода и цена каждого
| Подход | Что защищает | Цена и ограничение |
|---|---|---|
| READ COMMITTED | Каждый оператор видит зафиксированные данные на момент его старта | Повторный запрос может увидеть новые значения; связанные проверки и изменения нужно проектировать отдельно |
| REPEATABLE READ | Повторные чтения в транзакции используют один снимок | В PostgreSQL возможны аномалии сериализации между разными строками; транзакция может получить ошибку сериализации |
| SERIALIZABLE | Успешно завершившиеся транзакции эквивалентны некоторому последовательному порядку | PostgreSQL может прервать транзакцию; приложение должно повторить её целиком |
| Явная блокировка строк | Не даёт параллельной транзакции изменить выбранные строки до завершения текущей | Увеличивает ожидания и риск взаимной блокировки; не блокирует отсутствующую строку или произвольное условие |
Таблица — не рейтинг безопасности. Например, для уменьшения остатка на одной строке атомарный UPDATE с условием часто точнее и дешевле, чем перевод всей операции на SERIALIZABLE. А если бизнес-правило зависит от нескольких строк, REPEATABLE READ может дать стабильное чтение, но не гарантирует сохранение самого правила.
Типичные ошибки: где ломается привычный код
1. Читать, решать, затем записывать без защиты
Представьте остаток в одну единицу. Два запроса читают его, оба решают, что товар доступен, и начинают оформление заказа. Если проверка и уменьшение остатка разделены, приложение может принять два заказа. Повышение уровня изоляции не всегда лучший первый шаг: если вся проверка относится к одной строке, объедините условие и изменение в одном операторе.
Для таблицы inventory с полями sku и quantity условное списание выглядит так:
UPDATE inventory
SET quantity = quantity - 1
WHERE sku = 'item-42' AND quantity > 0
RETURNING quantity;
Если запрос вернул строку, единица списана. Если не вернул — товара нет или остаток уже изменил другой запрос. Проверьте, что sku уникален: иначе UPDATE может затронуть несколько строк. Это решает задачу одного остатка, но не заменяет транзакцию, если оформление заказа меняет ещё и другие таблицы.
2. Считать REPEATABLE READ защитой от любых гонок
Допустим, в смене должны оставаться дежурными хотя бы два врача. Две транзакции на REPEATABLE READ читают одни и те же две активные записи, затем каждая снимает с дежурства своего врача. Они меняют разные строки, поэтому обе могут завершиться, хотя правило уже нарушено. Это аномалия сериализации: отдельные чтения стабильны, но совокупный результат нельзя объяснить безопасным последовательным порядком.
Если инвариант охватывает несколько записей и его нельзя выразить одним условным обновлением или надёжной блокировкой общей строки, рассмотрите SERIALIZABLE. Но обязательно обработайте возможный отказ транзакции, а не отправляйте его пользователю как необъяснимую ошибку сервера.
3. Повторять только последний UPDATE после ошибки
В PostgreSQL SERIALIZABLE и REPEATABLE READ могут завершиться ошибкой сериализации с SQLSTATE 40001. Повторить нужно всю транзакцию: начать заново, повторить чтения, вычисления и записи. Старое решение могло опираться на снимок, который уже не актуален. Повтор только последнего запроса не восстанавливает логику операции.
Повтор должен быть ограниченным и безопасным. Продумайте, что произойдёт, если операция включает внешний вызов или отправку письма: такой побочный эффект нельзя бездумно выполнить повторно вместе с транзакцией. Обычно внешние действия выносят за её границы или организуют отдельным надёжным механизмом доставки.
4. Блокировать строку, которой ещё нет
SELECT ... FOR UPDATE блокирует найденные строки, а не абстрактное условие и не отсутствующую запись. Если два запроса одновременно проверяют, что пользователя ещё нет, и затем вставляют его, блокировать результат пустого SELECT нечего. Для защиты от дублей задайте UNIQUE-ограничение и обрабатывайте конфликт вставки. Ограничение в базе — последняя линия защиты, даже если приложение предварительно проверяет данные.
5. Проверять конкурентность на одном соединении
Последовательный тест не показывает гонку: второй запрос стартует уже после первого. Для проверки откройте два соединения и синхронно выполните чтение и запись, например через барьер в тесте. Фиксируйте, что случилось с обеими транзакциями: ожидание блокировки, успешный COMMIT или SQLSTATE 40001. Одного измерения времени недостаточно — оно не показывает корректность результата.
Когда какой подход выбирать
- Одно числовое поле и простое условие. Начните с атомарного UPDATE ... WHERE ... RETURNING. Проверьте уникальность строки и обработайте случай, когда UPDATE ничего не вернул.
- Нужно прочитать и изменить конкретную существующую строку. В READ COMMITTED используйте SELECT ... FOR UPDATE внутри транзакции, затем принимайте решение и обновляйте запись. Это удерживает блокировку до конца транзакции. Если строка может отсутствовать, отдельно обеспечьте уникальность или другой способ координации.
- Несколько чтений должны видеть один снимок. REPEATABLE READ подходит, если важна стабильность чтения и вы понимаете, что межстрочное бизнес-правило он не всегда защитит. Обрабатывайте ошибки сериализации.
- Нужно сохранить правило сразу для нескольких строк. Рассмотрите SERIALIZABLE, особенно если правило трудно выразить через ограничения и блокировки. Проверьте корректность повторного запуска всей транзакции под конкурентной нагрузкой.
Явные блокировки берите в одинаковом порядке во всех путях кода. Иначе две транзакции могут захватить строки в разной последовательности и ждать друг друга. PostgreSQL обнаружит взаимную блокировку и прервёт одну транзакцию; приложение должно уметь безопасно обработать этот исход. Чем дольше транзакция держит блокировку, тем дольше ждут остальные.
Что проверить перед выпуском
- Запишите бизнес-инвариант простыми словами: например, остаток товара не должен стать отрицательным.
- Проверьте, можно ли защитить его одним оператором или ограничением базы данных.
- Если нужны блокировки, убедитесь, что все конкурентные пути блокируют одни и те же строки и делают это в одинаковом порядке.
- Для SERIALIZABLE и REPEATABLE READ предусмотрите обработку SQLSTATE 40001 и повтор всей транзакции.
- В тесте запускайте операции одновременно через разные соединения, затем проверяйте конечные данные и число успешных операций.
Следующий шаг — выбрать один критичный сценарий в вашем приложении и проверить его двумя параллельными транзакциями на PostgreSQL 16. Сначала зафиксируйте ожидаемый инвариант, затем измерьте долю ошибок сериализации, ожидания блокировок и время транзакций. Если используете другую версию PostgreSQL, сверяйте поведение и синтаксис с документацией именно этой версии.
* MVCC — многоверсионное управление параллельным доступом: механизм, при котором чтение обычно обращается к подходящей версии строки, а не ждёт завершения каждой записи.
Антиблок-аналитик 115-ФЗ: Анализ выписок, поиск рисков блокировки счёта, инструкции по безопасным транзакциям.
Спросить за 41 ₽Источники: PostgreSQL 16: уровни изоляции транзакций, PostgreSQL 16: явные блокировки
Частые вопросы
Какой уровень изоляции выбрать по умолчанию в PostgreSQL?
REPEATABLE READ гарантирует, что транзакции не испортят данные?
Что делать, если транзакция завершилась с SQLSTATE 40001?
SELECT FOR UPDATE заблокирует запись, если SELECT ничего не нашёл?
Нужно ли использовать SERIALIZABLE для списания остатка товара?
Изменилось ли поведение в другой версии PostgreSQL?
Что обязательно проверить в production?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.