В 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 будет чаще прерывать транзакции.
Наконец, длинные транзакции — даже если сама операция быстрая, долгая транзакция повышает шанс пересечься с другими по данным. Иногда её держат открытой, пока приложение выполняет сетевые запросы или тяжёлые вычисления, — а это только усугубляет ситуацию.
Выявление причины
- Оценить частоту ошибки. Разовые
40001— это нормально. Но если они начинают появляться регулярно под нагрузкой, это сигнал, что надо что-то делать. - Проверить нет ли "check‑then‑act" транзакций (или убедиться что проблема не в них)
- Проверить уровень изоляции:
SHOW default_transaction_isolation;иSHOW transaction_isolation;Ошибка может быть неожиданной, например, если уровень задан глобально или через ORM. - Проанализировать активные транзакции в
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 — то есть повторять всю транзакцию, но это стоит делать с растущей задержкой. Это снижает шансы, что параллельные процессы снова столкнутся.
$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. Если число ошибок резко растёт под нагрузкой, скорее всего, дело в логике
приложения — лучше поправить её, чем бесконечно копаться в настройках БД.