Что такое dirty read, non-repeatable read и phantom read
Что проверяют
Понимаете ли вы, что аномалии чтения это не баги СУБД, а плата за параллелизм, и умеете ли подобрать уровень изоляции под задачу. В Go вопрос продолжается практикой: как уровень задаётся в database/sql и что делать с ошибкой сериализации.
Как отвечать
Три аномалии по возрастанию тонкости. Грязное чтение: транзакция видит незафиксированные изменения чужой, и если та откатится, прочитанного никогда не существовало. Неповторяющееся чтение: одна и та же строка, прочитанная дважды внутри транзакции, даёт разные значения, потому что между чтениями её изменили и зафиксировали. Фантомное чтение: повторный запрос по тому же условию возвращает другой набор строк, так как появились или исчезли подходящие записи.
Уровни стандарта закрывают их по очереди: Read Uncommitted допускает всё, Read Committed убирает грязное чтение, Repeatable Read добавляет повторяемость строки, Serializable закрывает и фантомы. Важная оговорка про реальные системы: в PostgreSQL грязного чтения нет ни на одном уровне, а Repeatable Read сделан на снимке и фантомов уже не даёт, зато допускает перекос записи, который снимается только на Serializable.
В Go уровень задаётся на транзакцию, а не глобально:
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})и к нему обязателен цикл повтора: на строгих уровнях драйвер вернёт ошибку сериализации, транзакцию откатывают и выполняют заново.
Где спотыкаются
Перечисляют уровни и молчат про цену. Serializable на горячей таблице означает поток отказов сериализации, и без ретраев сервис начинает отдавать ошибки пользователю. Обратная крайность тоже частая: под Read Committed делают чтение и запись двумя шагами вместо одного UPDATE с условием и теряют обновления.
Смешивают стандарт и конкретную СУБД. Ответ про то, что Repeatable Read допускает фантомы, верен для учебника и неверен для PostgreSQL, где уровень работает по снимку. Стоит сразу сказать, о какой системе речь.
Роняют дисциплину работы с транзакцией в Go. Забытый defer tx.Rollback() держит соединение из пула до таймаута, а при повторе берут тот же объект транзакции, хотя после ошибки он мёртв и нужен новый BeginTx.