Почему SERIALIZABLE в Postgres требует retry-логики в коде

REPEATABLE READ даёт snapshot на старте транзакции и убирает non-repeatable read, но не видит write skew — ситуацию, когда две транзакции читают непересекающиеся строки, а потом обе пишут на основе прочитанного, и итог оказывается некорректным вместе.

SERIALIZABLE в Postgres реализован через Serializable Snapshot Isolation: движок следит за read-write зависимостями между конкурентными транзакциями и ищет цикл, который нарушил бы сериализуемость. Найдя такой цикл, он не блокирует одну из транзакций заранее, а прерывает её в момент commit с ошибкой SQLSTATE 40001.

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

for (int attempt = 0; attempt < 3; attempt++) { try { transactionTemplate.execute(status -> { // читаем и пишем внутри // SERIALIZABLE транзакции return null; }); break; } catch (DataAccessException e) { if (!isSerializationFailure(e)) { throw e; } // 40001 — повторяем всю транзакцию } }

Спросят следом: чем write skew отличается от lost update, и почему optimistic locking через @Version его не ловит. Ответ — @Version защищает конкретную строку, а write skew возникает между разными строками, которые логически связаны, но физически не пересекаются.

SERIALIZABLE не предотвращает конфликт заранее — он обнаруживает его post factum и заставляет транзакцию проиграть заново. Цена корректности здесь retry-петля в коде, а не lock wait в базе.

Тренажёр: 600 вопросов, мок с таймером, план повторов

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки