Как база данных обеспечивает сохранение данных транзакции при уровне изоляции repeatable read?
Что проверяют
Знаете ли вы, что repeatable read в разных СУБД устроен по-разному, и можете ли объяснить механизм, а не пересказать определение из стандарта. Следом спрашивают про фантомы, потерянное обновление и что делать Java-приложению, когда база отклонила транзакцию.
Как отвечать
Сначала разведите понятия. Сохранность зафиксированных данных (durability) обеспечивает журнал: WAL в PostgreSQL, redo log в InnoDB. От уровня изоляции она не зависит. Изоляция отвечает за другое: строка, прочитанная в транзакции, при повторном чтении будет такой же.
Обеспечить это можно двумя способами:
- блокировки: прочитанные строки держатся под разделяемой блокировкой до конца транзакции, чужой
UPDATEждёт. Так устроен repeatable read в SQL Server; - MVCC и снимок: транзакция читает версии строк, зафиксированные до момента снимка, писатели читателей не блокируют. Так работают PostgreSQL и InnoDB.
Детали, которые показывают глубину. В PostgreSQL снимок берётся на первом запросе, а не на BEGIN; фантомов на этом уровне нет, но возможен write skew. Если две транзакции меняют одну строку, вторая получает could not serialize access due to concurrent update (SQLSTATE 40001), и её нужно повторить. В InnoDB repeatable read стоит по умолчанию: обычный SELECT читает снимок, а UPDATE и SELECT ... FOR UPDATE берут последнюю версию и ставят next-key блокировки.
В Java уровень задают через @Transactional(isolation = Isolation.REPEATABLE_READ) или Connection.setTransactionIsolation, а повтор при 40001 оформляют через Spring Retry или свой цикл.
Где спотыкаются
Говорят, что repeatable read по стандарту запрещает фантомы. Стандарт их допускает, отсутствие фантомов в PostgreSQL это особенность реализации.
Не закладывают повтор транзакции. В PostgreSQL ошибка сериализации это штатное поведение, а не сбой.
Думают, что в MySQL этот уровень защищает от потерянного обновления при схеме «прочитал, посчитал, записал». Без FOR UPDATE или оптимистичной блокировки через @Version вторая запись затрёт первую.