Кейс: отчёт по остаткам на складе считался по разным цифрам в двух вкладках одного пользователя
Пользователь открывал два отчёта подряд в разных вкладках браузера — один и тот же склад, разница в секундах. Цифры расходились на десятки единиц. Разработчики сначала подумали на кэш: решили, что где-то отдаётся устаревшая версия.
Оказалось наоборот — кэша не было вообще. Каждый запрос отчёта уходил напрямую в Postgres, читал остатки по товарам join'ом из трёх таблиц. Проблема была в уровне изоляции по умолчанию — Read Committed — и в том, что отчёт состоял из нескольких последовательных запросов внутри одной транзакции, а не одного большого SQL.
Первый запрос считал сумму остатков по категории A, второй — по категории B. Между этими двумя запросами параллельно завершалась транзакция перемещения товара со склада: товар уходил из категории A и появлялся в категории B. Read Committed даёт каждому отдельному запросу свой снепшот на момент его выполнения — значит первый запрос мог не увидеть уход товара, а второй уже увидел его приход. Итог — товар посчитан два раза внутри одного отчёта.
Починили переводом транзакции отчёта на Repeatable Read: снепшот фиксируется на старте транзакции и не меняется до её конца, все запросы внутри видят одну и ту же картину данных.
@Transactional(isolation = Isolation.REPEATABLE_READ) public StockReport buildReport(Long warehouseId) { long categoryA = stockRepo.sumByCategory(warehouseId, CAT_A); long categoryB = stockRepo.sumByCategory(warehouseId, CAT_B); return new StockReport(categoryA, categoryB); }
Разбор на собесе обычно доходит до вопроса: почему Repeatable Read в Postgres не решает всё до конца. Он убирает non-repeatable read — но не убирает phantom read при вставке новых строк, попадающих под тот же фильтр после старта транзакции. Для отчёта с фиксированным набором товаров это было неважно, но если бы фильтр допускал появление новых строк — понадобился бы Serializable с retry-логикой.
Read Committed даёт каждому SQL-запросу собственный снепшот — если отчёт строится несколькими запросами подряд, картина между ними может измениться незаметно для кода.
Тренажёр: 600 вопросов, мок с таймером, план повторов
senior·base — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки