Акция Панель управления ispmanager для VDS — первый месяц бесплатно
до 31.07.2026 Подробнее
Выберите продукт

PostgreSQL 40001 in SERIALIZABLE: concurrent update, диагностика и безопасный retry

Ошибка PostgreSQL 40001 в режиме SERIALIZABLE означает, что СУБД не смогла гарантировать сериализуемость из‑за конкурирующих транзакций. Разберём причины, как найти конфликтующие операции и внедрить правильный retry с backoff, идемпотентностью и outbox.
PostgreSQL 40001 in SERIALIZABLE: concurrent update, диагностика и безопасный retry

Ошибка could not serialize access due to concurrent update в PostgreSQL почти всегда приходит с SQLSTATE 40001 и возникает на уровне изоляции SERIALIZABLE. На практике это выглядит так: «под нагрузкой транзакции начали падать», при этом явных ожиданий блокировок или классического deadlock может и не быть.

Важно понимать: SERIALIZABLE в PostgreSQL — это не «чуть сильнее, чем REPEATABLE READ». Это режим, где СУБД обеспечивает эквивалентность некоторому последовательному (serial) выполнению транзакций. Если PostgreSQL видит, что параллельное выполнение могло бы дать результат, невозможный в последовательном мире, она не обязана «подвешивать» одну из транзакций. Вместо этого одна из транзакций будет отменена с serialization failure, чтобы клиент повторил попытку (retry).

Что означает ошибка 40001 и почему это штатно для SERIALIZABLE

Ключевое слово в сообщении — serialize. PostgreSQL обнаружила конфликт расписаний: две или больше транзакций одновременно читали и/или меняли данные так, что итоговый эффект нельзя корректно «уложить» в последовательное выполнение без изменения результата.

В PostgreSQL это реализовано механизмом SSI (Serializable Snapshot Isolation). Он отслеживает опасные структуры конфликтов (в первую очередь rw-конфликты) и, когда видит, что транзакция стала «опасной» для сериализуемости, завершает одну из них ошибкой 40001.

Для SERIALIZABLE 40001 — это не «сбой базы». Это сигнал приложению: текущую бизнес-операцию нужно повторить, потому что параллельность помешала гарантировать сериализуемость.

Почему именно “concurrent update”

Фраза concurrent update обычно всплывает, когда транзакции пересекаются по одним и тем же строкам или когда обновление опирается на прочитанный набор строк (например, «проверил условие → затем изменил»). В более мягких режимах вы могли бы получить «последний победил» или скрытую потерю обновления; в SERIALIZABLE PostgreSQL обязана это предотвратить.

Типовые причины serialization failure в реальных приложениях

Ниже — ситуации, где serialization failure встречается чаще всего. Если узнаёте свой кейс, обычно правильное решение — комбинация из двух действий: снизить конфликтность транзакций и внедрить корректный retry.

1) «Проверил — потом обновил» (check-then-act) в одной транзакции

Сценарий: «проверить остаток/лимит/статус», затем сделать UPDATE. Две параллельные транзакции читают состояние по снимку, обе решают «можно», и обе пытаются обновить.

2) Очереди и «выбор следующей задачи»

Схема вида SELECT ... WHERE status='new' ORDER BY ... LIMIT 1, затем UPDATE выбранной задачи. Под конкуренцией это легко конфликтует в SERIALIZABLE, особенно если «выбор» зависит от набора строк, который параллельно меняется.

3) Счётчики, балансы и агрегаты

Обновления вида balance = balance - 1 или денормализованный счётчик в одной строке при большом числе конкурентных транзакций создают «горячую точку» (hot spot). В таком месте SERIALIZABLE чаще вынуждена «ронять» транзакции.

4) Длинные транзакции

Чем дольше живёт транзакция, тем выше шанс пересечься с другими транзакциями и попасть в опасную структуру SSI. Типичная ошибка — держать транзакцию открытой, пока приложение делает сетевые запросы, ждёт очередь или выполняет тяжёлые вычисления.

Диагностика конфликтов транзакций: активность сессий и временная шкала

Как отличить 40001 от блокировок и дедлоков

В продакшене часто путают три разных класса проблем:

  • Ожидание блокировки: запрос «висит» и ждёт lock (например, из-за SELECT ... FOR UPDATE или обычного UPDATE по той же строке).

  • Deadlock: PostgreSQL обнаруживает цикл ожиданий и завершает запрос с SQLSTATE 40P01.

  • Serialization failure: транзакция может выполняться быстро, но в конце получает 40001, потому что сериализуемость не гарантируется.

Если вы видите SQLSTATE 40001 и текст could not serialize access, это обычно не про «подкрутить lock_timeout» и не про «увеличить max_connections». Это про конфликтный конкурентный доступ к одним данным при SERIALIZABLE.

Диагностика: где именно возникает конфликт

Цель диагностики — ответить на два вопроса: какие транзакции падают и какие данные являются «точкой конфликта». На бою важно не включать чрезмерно шумные настройки, но несколько практик обычно безопасны.

Шаг 1: убедиться, что проблема именно в SERIALIZABLE

Проверьте, не выставляете ли вы уровень изоляции глобально или на уровне сессии (часто это делается в пуле соединений).

SHOW default_transaction_isolation;
SHOW transaction_isolation;

Если видите serializable, вы в нужной плоскости: без retry такие транзакции будут падать периодически и это ожидаемо.

Шаг 2: логирование медленных запросов не заменяет логирование ошибок

Ошибка serialization failure может случиться и в очень быстром запросе. Поэтому важно, чтобы приложение логировало контекст:

  • SQLSTATE (40001);

  • идентификатор запроса (query name) или шаблон SQL;

  • параметры (с маскировкой чувствительных данных);

  • идентификатор бизнес-операции (например, order_id, user_id, task_id);

  • номер попытки retry и итог (успех/провал).

Без этого в логах PostgreSQL вы часто будете видеть только факт ошибки без ответа на вопрос «какая операция и над какой сущностью конфликтует».

Шаг 3: посмотреть активность и длительность транзакций

Если конфликтность постоянная, снимите срез по активным сессиям: так проще увидеть длинные транзакции и пересечения по времени.

SELECT
  pid,
  usename,
  application_name,
  state,
  wait_event_type,
  wait_event,
  xact_start,
  query_start,
  now() - xact_start AS xact_age,
  LEFT(query, 200) AS query
FROM pg_stat_activity
WHERE datname = current_database()
ORDER BY xact_start NULLS LAST;

Для 40001 вы не обязаны увидеть ожидание блокировок. Но часто увидите длинные транзакции, которые резко повышают вероятность конфликтов SSI.

Шаг 4: подтвердить «горячие» сущности

Если подозреваете «горячую» строку (одна запись, которую обновляют все), самый быстрый путь — прикладной: логировать идентификатор сущности при падении 40001. На практике именно это помогает найти корень проблемы быстрее всего.

Если помимо конфликтов у вас возникают вопросы к общему здоровью кластера (автовакуум, индексы, bloat), держите под рукой материал про базовую настройку: тюнинг autovacuum и индексов в PostgreSQL.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Правильная стратегия: retry, но аккуратно

PostgreSQL в режиме SERIALIZABLE предполагает, что приложение умеет повторять транзакции. Базовая схема простая: поймали SQLSTATE 40001 → откатили → повторили всю транзакцию целиком.

Сколько раз делать retry

Типичный диапазон — 3–5 попыток. Больше имеет смысл только если операция действительно критична и вы готовы платить задержкой. Если после нескольких попыток всё равно падает — конфликтность системная, и нужно менять шаблон доступа к данным.

Нужен backoff (и желательно jitter)

Если два воркера одновременно «бодаются» за одну и ту же сущность, мгновенный retry часто приводит к синхронным повторным столкновениям. Добавьте задержку с ростом (exponential backoff) и случайной составляющей (jitter).

Retry должен оборачивать всю транзакцию

Нельзя «повторить только последний запрос». В SERIALIZABLE ошибка означает, что весь набор чтений/записей нельзя сериализовать. Повторять нужно с самого начала: BEGIN → все запросы → COMMIT.

Идемпотентность: защита от дублей

Самая опасная часть retry — побочные эффекты вне базы: отправка письма, вызов внешнего API, публикация события. Если транзакция в базе откатилась, а внешний эффект уже произошёл, при повторе вы получите дубль.

Практичные меры:

  • всё, что можно, делайте внутри транзакции в БД;

  • внешние эффекты запускайте по паттерну outbox: сначала записали событие в таблицу, затем отдельный воркер доставил;

  • для критичных операций используйте ключи идемпотентности (например, operation_id) и уникальные ограничения.

Примеры retry в коде (псевдокод и практические шаблоны)

Ниже — универсальная логика, не привязанная к конкретному драйверу. Идея в том, где именно ловить ошибку и как правильно повторять транзакцию.

Универсальный псевдокод

max_attempts = 5
attempt = 0

while attempt < max_attempts:
  attempt += 1
  begin transaction isolation level serializable

  try:
    do business logic (select/update/insert)
    commit
    return success
  except error as e:
    rollback
    if e.sqlstate == '40001':
      sleep(backoff_with_jitter(attempt))
      continue
    else:
      raise

raise "serialization failure after retries"

На что обратить внимание при реализации

  • Ловите именно SQLSTATE 40001, а не текст ошибки: текст меняется по версии и локали.

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

  • Логируйте номер попытки и фактическую задержку backoff — это сильно помогает при разборе инцидентов.

Реализация retry для транзакций PostgreSQL при ошибке 40001

Как снизить количество конфликтов (и реже видеть could not serialize access)

Retry — необходимость, но лучше, чтобы он срабатывал редко. Несколько практичных способов снизить частоту 40001 и сообщений could not serialize access due to concurrent update.

1) Делайте транзакции короче

Уберите из транзакции всё «внешнее»: HTTP-запросы, ожидания очередей, тяжёлые вычисления. Подготовьте данные заранее, затем в короткой транзакции сделайте только чтения/записи.

2) Убирайте «горячие строки»

Если у вас один глобальный счётчик — подумайте о шардинге счётчика (несколько строк по хешу), о накоплении событий в отдельной таблице с последующей агрегацией или о пересмотре модели данных.

3) В очередях используйте SKIP LOCKED (если бизнес-логика позволяет)

Для воркеров, которые выбирают задачи, часто лучше использовать явную блокировку строк и пропуск заблокированных, чем заставлять SERIALIZABLE постоянно «разруливать» конфликт выбора через откаты.

BEGIN;
SELECT id
FROM jobs
WHERE status = 'new'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;
UPDATE jobs SET status = 'processing' WHERE id = $1;
COMMIT;

Это не универсальное «лекарство от 40001», но для очередей обычно заметно снижает конкуренцию за одну и ту же задачу.

4) Там, где допустимо, используйте более мягкий уровень изоляции

Иногда SERIALIZABLE включают «на всякий случай» для всего приложения. Это дорого и по конфликтам, и по накладным расходам. Если бизнес-инварианты допускают REPEATABLE READ или READ COMMITTED для части операций, вы снижаете риск 40001 в разы.

5) Проектируйте инварианты через уникальные ограничения

Часть сценариев «проверил — потом вставил/обновил» лучше выражать ограничениями и атомарными запросами. Например, вместо «проверить что записи нет → вставить» используйте уникальный индекс и INSERT ... ON CONFLICT. Это не отменяет SERIALIZABLE, но часто уменьшает конфликтные чтения.

Чек-лист: что делать, когда в логах появился PostgreSQL 40001

  1. Подтвердите, что операции идут в SERIALIZABLE и что это осознанный выбор.

  2. Включите в приложении обработку SQLSTATE 40001 и повтор транзакции целиком.

  3. Добавьте backoff+jitter и лимит попыток retry.

  4. Проверьте, что внешние эффекты не дублируются (outbox/идемпотентность/уникальные ключи).

  5. Найдите «горячие сущности» по прикладным логам (id объекта, тип операции).

  6. Сократите транзакции и пересмотрите конфликтные шаблоны (очереди, счётчики, check-then-act).

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Итог

could not serialize access due to concurrent update — ожидаемое поведение PostgreSQL в SERIALIZABLE, когда параллельные транзакции создают конфликт, несовместимый с сериализуемостью. Рабочий путь в продакшене — не «лечить» это настройками, а проектировать работу так, чтобы:

  • приложение делало корректный retry при SQLSTATE 40001;

  • транзакции были короткими;

  • конфликтные «горячие точки» были минимизированы архитектурно.

Если под нагрузкой количество 40001 растёт и retries перестают помогать — это признак того, что конкретный участок бизнес-логики требует перепроектирования доступа к данным, а не «плохой базы».

Кстати, если вы упираетесь в конкуренцию соединений и хотите стабилизировать работу пула, посмотрите практическое руководство: PgBouncer и пул соединений PostgreSQL.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...