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