По материалам статьи Craig Freedman: Serializable vs. Snapshot Isolation Level
Уровни изоляции транзакций Serializable и Snapshot обеспечивают согласованное чтение из базы данных. На любом из этих уровней изоляции транзакция может читать только зафиксированные данные. Более того, транзакция может читать одни и те же данные несколько раз, не заботясь о каких-либо параллельных транзакциях, вносящих изменения в эти же данные. Те нежелательные эффекты, которые были продемонстрированы в предыдущих статьях при Read Committed и Repeatable Read, на уровнях изоляции Serializable и Snapshot просто невозможны.
Обратите внимание, что я использовал фразу «не заботясь о каких-либо … вносящих изменения». Такой подбор слов является преднамеренным. На уровне изоляции Serializable ядро SQL Server накладывает блокировку диапазона ключей и удерживает её до окончания транзакции. Блокировка диапазона ключей гарантирует, что после того, как транзакция прочитает данные, никакая другая транзакция не сможет изменить эти данные (даже для вставки фантомных строк) до тех пор, пока не завершится транзакция, удерживающая блокировку. На уровне изоляции Snapshot ядро SQL Server не накладывает никаких блокировок. Таким образом, одновременная транзакция может изменять данные, которые уже прочитаны второй транзакцией. Вторая транзакция просто не замечает этих изменений и продолжает использовать старую версию данных.
Уровень изоляции Serializable основан на пессимистическом контроле параллелизма. Он гарантирует согласованность, предполагая, что две транзакции могут пытаться обновить одни и те же данные, и использует блокировки, чтобы гарантировать, что они этого не cделают, но (за счет уменьшения параллелизма) одна транзакция должна ждать завершения другой, и две транзакции могут заблокироваться. Уровень изоляции Snapshot основан на оптимистичном управлении параллелизмом. Это позволяет транзакциям выполняться без блокировок и с максимальным параллелизмом, но может произойти сбой и последующий откат транзакции, если две транзакции одновременно попытаются изменить одни и те же данные.
Как видим, существуют различия между уровнями изоляции Serializable и Snapshot в уровне параллелизма (которого можно достичь), и в наборе возможных проблем (взаимоблокировки и конфликты обновлений).
Рассмотрим, чем работа в Serializable и Snapshot отличаются с точки зрения обеспечиваемой ими изоляции транзакции. При Serializable всё довольно просто. Чтобы результат двух транзакций считался сериализуемым, они должны выполняться в некотором порядке по одной транзакции за раз.
Snapshot не гарантирует такой уровень изоляции. Несколько лет назад Джим Грей предложил прекрасный пример, демонстрирующий различия этих уровней. Представьте, что у нас есть мешок, содержащий смесь белых и чёрных шаров. Предположим, мы хотим запустить две транзакции. Одна транзакция перекрашивает каждый белый шар в чёрный шар. Вторая транзакция перекрашивает каждый чёрный шар в белый шар. Если мы запускаем эти транзакции с изоляцией Serializable, они будут исполняться поочерёдно. После первой транзакции останется мешок с шарами только одного цвета. После этого вторая транзакция изменит все эти шары на другой цвет. Есть только два возможных исхода: мешок только с белыми шарами или мешок только с чёрными шарами.
Если мы запускаем эти транзакции с изоляцией Snapshot, появляется третий результат, который невозможен при изоляции Serializable. Каждая транзакция может одновременно делать снимок мешка с шарами в том виде, в каком он был до внесения изменений. Теперь одна транзакция находит белые шары и перекрашивает их в чёрные шары. В то же время другие транзакции находят чёрные шары (но только те шары, которые были чёрными, когда мы сделали снимок, а не те шары, которые первая транзакция изменила на чёрные) и перекрашивает их в белый цвет. В результате, мешок будет содержать смесь белых и чёрных шаров. На самом деле, с этим уровнем изоляции мы правильно поменяли цвет каждого шара.
Следующий рисунок иллюстрирует эти различия:
Мы можем продемонстрировать подобное поведение средствами SQL Server. Обратите внимание, что Snapshot изоляция доступна только с SQL Server 2005 и должна быть явно включена для используемой базы данных:
alter database database_name set allow_snapshot_isolation on
Начнем с создания простой таблицы с двумя строками, обозначающими два шара разных цветов:
create table marbles (id int primary key, color char(5))
insert marbles values(1, 'Black')
insert marbles values(2, 'White')
Затем в первом сеансе начните транзакцию с уровнем изоляции Snapshot:
set transaction isolation level snapshot
begin tran
update marbles set color = 'White' where color = 'Black'
Теперь, прежде чем зафиксировать изменения, запустите во втором сеансе следующее:
set transaction isolation level snapshot
begin tran
update marbles set color = 'Black' where color = 'White'
commit tran
Наконец, зафиксируйте транзакцию в первом сеансе и проверьте данные в таблице:
commit tran
select * from marbles
Вот какой получился результат:
id color
-- -----
1 White
2 Black
Как можно видеть, шар 1, который изначально был чёрным, теперь стал белым, а шар 2, который изначально был белым, стал чёрным. Если вы попробуете тот же эксперимент с уровнем изоляции Serializable, одна транзакция будет ждать завершения другой, и, в зависимости от порядка, оба шара будут белыми или чёрными.
Комментарии (12)
AlexSpaizNet
24.04.2022 15:03А кто реально видел что бы кто-то использовал эти настройки на живой базе в продакшене ? Я вот не видел :/ А хотелось бы...
edo1h
неплохо бы добавить, что snapshot isolation level нет в стандарте sql.
вроде бы нигде, кроме как в ms sql, его не найти.
nekkie
у оракла формально нет отдельного snapshot isolation level, но он именно так (оптимистично) ведёт себя по-умолчанию.
SergeyPyankov
Аналогичный уровень имеется в Firebird и Interbase.
mvv-rus
Про дела давно минувших дней, Snapshot и многоверсионность в СУБД.
Изначально (вторая половина 90-х) уровень изоляции Snapshot в БД для x86 был в Interbase — ну, и в Firebird, как в его потомке, потом остался. И, вообще-то, Snapshot, если смотреть с точки зрения уровней изоляции стандарта SQL-92 — это реализация Repeatable Read.
В MS SQL механизм версионирования записей, необходимый для создания снимков (AFAIK версионирование в MS SQL прикрутили «сбоку», за счет создания версий записей в tempdb), появился довольно поздно, до этого (SQL 2000 и ранее) уровень изоляции Repeatable Read реализовывался за счет блокировок записей. И для тех, кто работал в те времена на Delphi, вроде меня, разница была очень заметна: если сделать типовую клиент-серверную программу без изысков (а идеологически в Delphi архитектура такой программы, хоть она и использовала СУБД на сервере, была сделана под работу с настольной БД — dBase, Paradox, и программа могла долго-долго держать откытой «текущую» запись) для работы с базой под Interbase, то в ней было очень сложно наткнуться на блокировку, а вот под MS SQL наткнуться было запросто, а потому программу нужно было делать по-другому.
Ну, и в PostgreSQL, тоже уже давно есть поддержка версионировния записей, Snapshot Isolation тоже реализован. Но появилась эта поддержка все-таки позже, чем в Interbase.
PS Про Oracle — элементарно не в курсе.
qideil
SI есть почти во всех базах которые используют Write-Ahead-Logging (а это наверное 90% всех DB), так как он довольно просто реализуется.
mvv-rus
Неверное утверждение. Write-Ahead-Logging в MS SQL был с незапамятных времен (в 7.0 точно был), а вот SI появилась только в SQL 2005.
qideil
Может быть не было feature request? Если есть WAL, то SI делается очень легко. На чтение просто откат записей WAL на данных, без каких-либо блокировок, а на запись только проверка конфликта на commit.
mvv-rus
А что вы будете делать, если транзакция на чтение со снимка начала выполнятья, и ещё выполняется, а за это время была выполнена другая транзакция, которая успешно изменила какие-то записи, была успешно завершена, о чем выполнившее ее приложение было извещено и пошло выполняться дальше — и тут транзакции со снимком потребовалась старая версия измененных данных? Как это приложение узнает, что его транзакция была, на самом деле, откачена?
qideil
Откатываются только WAL логи конкретной страницы, которая нужна SI транзакции, до той точки, когда транзакциия началась. Почитайте AERIS публикацию C. Mohan-a, там все очень хорошо расписано.
mvv-rus
То есть, от движка СУБД нужен селективный откат части завершенной транзакции? Это IMHO несколько больше, чем то, для чего обычно использется WAL.
И что тогда должна увидеть третья транзакция (допустим, с уровнем изоляции Read Committed ), запущенная после уже завершенной второй, но до завершения первой (которой нужен снимок)? Наверняка ведь она должна увидеть результат второй транзакции. То есть, движок СУБД должен хранить несколько версий одной страницы в своем кэше (но не на диске, раз структура БД этого не позволяе), правильно? То есть, в кэше появляется запись, не имеющая аналога в структуре БД на диске. А куда тогда движок ее запишет, если потребуется эту часть кэша освободить? Нужно какое-то общесистемное хранилище под это дело. То есть, это — не только WAL. В Interbase, к примеру, такого хранилища не было. И потом возникают вопросы — размер этого хранилища, синхронизация доступа к нему (Interbase был родом из Unix и создавал по отдельному рабочему процессу на соединение через fork, так что с синхронизацией ему было непросто)? И — решение вопроса, чтобы это хранилище не стало узким местом во всей СУБД.
Короче, схему создания снимков реализовать-то можно и без изменения структуры БД на диске — но требуется очень серьезная доработка движка.
PS IMHO когда в комментарии его автор ссылается на что-то не общеизвестное, то хорошим тоном является дать гиперссылку.
funny_falcon
Исторически Serializable в PostgreSQL был именно Snapshot Isolation (SI). И Repeatable Read (RR) тоже (они не отличались). Но во времена 8.x добавили настоящий Serializable через расширение SI - SSI (Serializable Snapshot Isolation).
Теперь в PostgreSQL SI остался только в RR.
Что любопытно, RR и SI не эквивалентны друг другу, но ни один не слабее другого. Каждый из них допускает разные "аномалии".