#Что означает ошибка
SQLSTATE 40001 could not serialize access due to concurrent update появляется, когда транзакция с уровнем REPEATABLE READ или SERIALIZABLE столкнулась с изменением данных другой транзакцией и не может безопасно продолжиться в прежнем снимке. PostgreSQL отменяет транзакцию намеренно; правильная реакция приложения — ROLLBACK и повтор всей бизнес-операции с повторным чтением данных, а не повтор одной строки SQL.
#Как различить причины
| Возможная причина | Как узнать | Что делать |
|---|---|---|
| Два запроса меняют одну записьДругая транзакция успела зафиксировать изменение строки, которую пытается изменить старый снимок. | 40001 появляется при параллельной записи, а не при одном последовательном запросе. | Добавить ограниченные повторы целой транзакции после ROLLBACK. |
| Используется REPEATABLE READ или SERIALIZABLEЭти уровни защищают согласованность, но допускают откат из-за конкуренции. | SHOW transaction_isolation показывает repeatable read или serializable в соединении ошибки. | Проверить, нужен ли выбранный уровень; не понижать его без анализа требований. |
| Слишком длинная транзакцияМежду чтением и записью проходит много времени, в том числе из-за сетевого ожидания. | Транзакция держится секунды или минуты, конфликт возникает чаще при нагрузке. | Укоротить транзакцию и убрать внешние операции. |
#Пошаговая диагностика и устранение
- Уточните код и не продолжайте прерванную транзакциюВ журнале приложения скопируйте
could not serialize access due to concurrent update, SQLSTATE 40001 и идентификатор операции. В psql после ошибки выполнитеROLLBACK;— до этого следующие команды дадутcurrent transaction is aborted. Если журнал показывает 40P01 deadlock detected, это иной код, хотя ограниченный повтор транзакции тоже может понадобиться. - Проверьте используемый уровень изоляцииОткройте psql либо pgAdmin → нужная база → Tools → Query Tool. В отдельном соединении выполните
SHOW default_transaction_isolation;и внутри проверяемой транзакцииSHOW transaction_isolation;. Если приложение явно указывает уровень при BEGIN, значение может отличаться от значения по умолчанию — ищите его в конфигурации драйвера. Не меняйте уровень изоляции глобально. - Найдите конфликтующую бизнес-операциюВ исходном коде найдите весь участок между BEGIN и COMMIT, где меняется одна и та же запись. Запишите, какие значения были прочитаны, какие проверки сделаны и что пересчитывается перед UPDATE. Если несколько работников одновременно списывают остаток, повторять старый расчёт нельзя: во второй попытке надо ещё раз получить остаток и заново проверить условия.
- Воспроизведите ошибку на собственной тестовой таблицеВ отдельной пустой тестовой базе создайте таблицу из примера и откройте два окна psql, подключённые к ней. В каждом выполните BEGIN ISOLATION LEVEL REPEATABLE READ и SELECT строки id=1, чтобы зафиксировать снимок. В А измените строку и COMMIT. В Б попытайтесь UPDATE той же строки — ожидается 40001; завершите Б командой ROLLBACK.
- Оберните всю операцию в ограниченное повторениеВ драйвере приложения перехватывайте ошибку по SQLSTATE 40001, откатывайте соединение, ждите небольшую случайную задержку и начинайте новый BEGIN. Повтор должен включать все чтения, расчёты и выбор SQL; ограничьте число повторов, например тремя попытками, затем сообщите вызывающему коду о перегрузке. Не используйте бесконечный цикл.
- Защитите от повторных внешних действийПроверьте, не отправляет ли код письмо, платёж или запрос стороннему API внутри повторяемой транзакции. У такого действия может быть эффект, даже если SQL откатился. Перенесите отправку на этап после успешного COMMIT либо примените механизм идемпотентности (уникальный ключ операции) и транзакционную очередь отправки, если система это поддерживает.
- Сократите время конкурентной транзакцииУберите из BEGIN медленные сетевые запросы и пользовательское ожидание; используйте короткие SQL-операции и индекс для поиска записи. Если бизнес-правила позволяют, пересмотрите необходимость REPEATABLE READ/SERIALIZABLE только после тестов на потерянные обновления и другие аномалии. Не объявляйте переход к READ COMMITTED универсальным лечением.
- Проверьте повтор и завершение операцийВ тестовой среде запустите две параллельные операции и проверьте итоговые строки SELECT-ом: значения должны соответствовать ровно одному подтверждённому эффекту каждой успешной бизнес-операции. В журнале допускается зарегистрированный 40001 с успешным ограниченным повтором; если конфликт повторяется много раз, снизьте конкуренцию и исследуйте долгие транзакции.
- Команды для отдельных ситуацийВыберите только подходящий вариант. Это не одна последовательность: часть команд диагностирует проблему, а часть меняет настройки или данные.
#Воспроизведение и проверка только в отдельной тестовой базе
-- Окно A, отдельная тестовая база
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT n FROM whelp_serial_demo WHERE id=1;
UPDATE whelp_serial_demo SET n=n+1 WHERE id=1;
COMMIT;-- Окно Б: выполнить BEGIN и SELECT ДО COMMIT в окне А
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT n FROM whelp_serial_demo WHERE id=1;
-- После COMMIT в окне A:
UPDATE whelp_serial_demo SET n=n+1 WHERE id=1;
-- Ожидается 40001; завершите ROLLBACK;#Как убедиться в исправлении и что делать дальше
Успех — две конкурентные операции проходят за ограниченное число попыток и итоговые значения верны, а повторно выполняемые запросы не приводят к двойным списаниям. Если ошибка остаётся после нескольких попыток, журналируйте число повторов, длительности транзакций, SQLSTATE и идентификатор операции; затем оптимизируйте конкурентный доступ вместе с разработчиком.