Что такое снимок данных в PostgreSQL и как он работает
Что проверяют
Понимаете ли вы, что именно видит транзакция и почему, и выбираете ли уровень изоляции осознанно. Для Go-разработчика вопрос прикладной: уровень задаётся в коде при старте транзакции, а ошибка сериализации приходит в приложение и обрабатывать её тоже там.
Как отвечать
Снимок это не копия данных, а описание видимости: номер текущей транзакции, граница по уже завершённым и список тех, что ещё выполняются. PostgreSQL хранит несколько версий каждой строки (MVCC), у версии есть служебные поля xmin (транзакция, создавшая её) и xmax (транзакция, удалившая или заменившая). Сверяя их со снимком, сервер решает, видна версия или нет. Поэтому читатели не блокируют писателей, а писатели читателей.
Когда берётся снимок, зависит от уровня изоляции:
READ COMMITTED, значение по умолчанию: новый снимок на каждый оператор, поэтому два одинаковых SELECT внутри одной транзакции могут вернуть разное.
REPEATABLE READ: снимок берётся один раз, на первом операторе, и держится до конца транзакции.
SERIALIZABLE: то же плюс отслеживание зависимостей между транзакциями, конфликт завершается ошибкой сериализации.
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})Старые версии убирает vacuum, но только те, что не нужны ни одному живому снимку.
Где спотыкаются
Говорят «транзакция видит базу на момент своего начала», не называя уровень. На умолчательном READ COMMITTED это прямо неверно и даёт плавающие расхождения в отчётах и проверках внутри одной транзакции.
Открывают транзакцию и держат её на время долгой обработки или похода во внешний сервис. Снимок удерживает горизонт очистки, vacuum не может убрать мёртвые версии, таблица и индексы пухнут, планы портятся.
Переключаются на REPEATABLE READ или SERIALIZABLE и не пишут повтор. Ошибку сериализации нельзя обработать внутри той же транзакции: её нужно откатить и выполнить заново, поэтому вся логика обязана лежать в функции, которую безопасно позвать второй раз.