serialization failure: когда возникает и как избежать

В PostgreSQL есть несколько уровней изоляции. Один из них SERIALIZABLE — он призван полностью исключить аномалии при параллельном выполнении транзакций, если объяснить простым языком, то он имитирует их последовательное выполнение. Для этого используется механизм SSI (Serializable Snapshot Isolation): система отслеживает зависимости между транзакциями и определяет, можно ли их выполнить в какой‑либо последовательности без противоречий (сохранить сериализуемость). Если база данных видит, что сохранить сериализуемость не получится, PostgreSQL прерывает одну из транзакций — это способ защиты целостности данных. В этот момент мы получим ошибку:


SQLSTATE 40001
could not serialize access due to concurrent update -- конфликт при UPDATE

could not serialize access due to read/write dependencies among transactions -- проблема SSI

Эта ошибка не означает сбой в системе. Она означает, что иначе получатся несогласованные данные

Причины возникновения

Возьмём для примера сценарий работы когда сначала проверяется условие, а потом осуществляется действие ("check‑then‑act"). Проверяем остаток товара, потом уменьшаем его количество. Две параллельные транзакции могут прочитать одно и то же состояние (есть на остатках одна единица товара) — и обе решат, что можно остаток уменьшить. В режиме SERIALIZABLE если транзакции читают и изменяют данные так, что их нельзя упорядочить без противоречий, PostgreSQL обнаруживает цикл зависимостей и завершает одну из них с ошибкой.

Ещё одна ситуация — "Горячая строка" (hot spot). Это когда одна из строк становится точкой пересечения множества параллельных операций, например, какой-нибудь счётчик который одновременно обновляется многими пользователями (счётчик просмотров записи), PostgreSQL будет чаще прерывать транзакции.

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

Выявление причины

  1. Оценить частоту ошибки. Разовые 40001 — это нормально. Но если они начинают появляться регулярно под нагрузкой, это сигнал, что надо что-то делать.
  2. Проверить нет ли "check‑then‑act" транзакций (или убедиться что проблема не в них)
  3. Проверить уровень изоляции: SHOW default_transaction_isolation; и SHOW transaction_isolation; Ошибка может быть неожиданной, например, если уровень задан глобально или через ORM.
  4. Проанализировать активные транзакции в pg_stat_activity. Нужно искать долгие транзакции и повторяющиеся запросы к одним и тем же таблицам.
  5. Проверить нет ли строк или таблиц к которым идёт особенно активный конкурентный доступ
Проанализировать активные транзакции в pg_stat_activity

SELECT pid, now() - xact_start AS tx_duration, query
FROM pg_stat_activity
WHERE state = 'active';

PostgreSQL только фиксирует, что транзакции нельзя выполнить в согласованном порядке, без точных причин. Поэтому диагностика заключается в поиске пересечений между транзакциями, а не в разборе какого-то запроса.

Решения

Во-первых, надо подумать, точно ли в кейсе вызвавшем ошибку нужен самый строгий уровень изоляции, может его можно понизить? SERIALIZABLE стоит использовать только в случаях, где важна целостность данных (финансовые операции и важные бизнес-процессы).

Во-вторых, нужно сократить длительность транзакций: чем меньше времени вы держите снимок данных, тем ниже вероятность конфликта. Иногда достаточно просто перестроить порядок операций — и ситуация заметно улучшается.

В-третьих, если есть транзакции "проверка-действие", их можно изменить используя SELECT ... FOR UPDATE или ON CONFLICT DO UPDATE

В-четвёртых, hot spot можно решить если наладить шардинг (разделение данных требующих регулярного параллельного доступа на несколько сегментов)

И последнее — можно и нужно настроить retry — то есть повторять всю транзакцию, но это стоит делать с растущей задержкой. Это снижает шансы, что параллельные процессы снова столкнутся.

Простой пример повторной попытки при serialization failure

$max_attempts = 5;
$attempt = 0;

while ($attempt <= $max_attempts) {
    $attempt++;
    try {
        $pdo->beginTransaction();
        // осуществляем операции транзакции: SELECT, UPDATE и т.п.
        $pdo->commit();
        break;
    } catch (PDOException $e) {
        $pdo->rollBack();
        if ($e->getCode() === '40001' && $attempt !== $max_attempts) {
            // Ждём перед повторной попыткой: экспоненциальная задержка + случайная добавка
            $delay = (int)(pow(2, $attempt) * 100 + rand(0, 100));
            usleep($delay * 1000); // Переводим в микросекунды
            continue;
        }

        throw $e;// После 5 попытки и другие ошибки — выбрасываем
    }
}

Важно: повторять нужно всю транзакцию целиком. Повторить только отдельный запрос не поможет: ошибка 40001 означает, что весь набор операций нельзя сериализовать, и начинать надо заново.

В общем, serialization failure — это не баг, а правильная реакция PostgreSQL на конфликт в режиме SERIALIZABLE. Если число ошибок резко растёт под нагрузкой, скорее всего, дело в логике приложения — лучше поправить её, чем бесконечно копаться в настройках БД.