Реферат: Версионность в Yukon
BEGIN TRAN
SELECT * FROM tst
COMMIT
Этот запрос совершенно спокойно выполнится, никого не потревожив, и вернет при этом состояние таблицы на момент, предшествующий изменениям, так как ни одна из изменяющих таблицу транзакций на время выполнения этого запроса еще не зафиксирована. Все дело в том, что обычно версионник выполняет изменения данных примерно по такому сценарию:
Делается версионное сканирование таблицы, и выясняется, какие записи необходимо изменить.
Предпринимаются попытки изменить отобранные записи.
Если запись изменилась с момента версионного скана, то проверяется, не перестала ли она удовлетворять критерию отбора, и если не перестала, то запись меняется, если перестала, то пропускается. (Некоторые реализации применяют здесь более хитрые алгоритмы, но этот вопрос выходит за рамки данной статьи).
Если запись меняется в настоящий момент, то сервер дожидается конца изменений и опять-таки проверяет соответствие записи условиям выборки.
То есть ожидание , если и происходит, то только из-за того, что в процессе обновления встретилась запись, которая меняется в данный момент.
Чистый же блокировочник работает немного по другому сценарию. Сканирование данных ему не имеет смысла делать, так как все запросы на чтение у него блокирующие. Поэтому он просто перебирает все записи в таблице по очереди (напомню, речь идет о таблице без индексов), проверяя их на соответствие условию выборки, и накладывая при этом блокировку обновления (update lock). Такая блокировка совместима с блокировками чтения, но несовместима сама с собой и с монопольными блокировками. Таким образом, читающим запросам подобный перебор не мешает, но другие блокировки обновления и монопольные будут помехой этому запросу. Следовательно, если в момент перебора в таблице монопольно заблокирована хотя бы одна запись (что и имеет место в данном примере, так как запись была изменена, но транзакция еще не зафиксирована), то рано или поздно изменяющий запрос до нее доберется и зависнет на блокировке, ожидая фиксации «вражеской» транзакции.
Несмотря на возможность версионных запросов, Yukon все равно при записи данных поступает как блокировочник, что и приводит к вышеописанному эффекту.
Repeatable read
Уровень изоляции repeatable read в базе с включенной поддержкой версионности работает точно так же, как и на базе без оной. Совершенно спокойно накладываются и удерживаются должное время все положенные по статусу разделяемые (share) блокировки. Да в общем-то, вряд ли тут вообще что-то могло измениться. Но появилась одна полезная возможность: Если запрос выполняется по базе с включенной поддержкой версионности, то при указании оптимизатору хинта READCOMMITTED в читающем запросе, выборка будет версионной. Возможность действительно довольно полезная - в связи с некоторыми особенностями уровня изоляции snapshot.
Snapshot
Уровень изоляции snapshot, является чисто версионным, в отличие от предыдущего, чисто блокировочного, и вообще совершенно новым для SQL Server.
Читающие запросы при этом уровне изоляции выполняются так, как и положено им выполняться в честном версионнике при этом уровне изоляции. Если вернуться все к той же тестовой табличке и в одном из подключений начать транзакцию, в которой изменить какую-нибудь запись, но саму транзакцию не фиксировать…
BEGIN TRAN UPDATE tst SET y = 2 WHERE x = 4 |
А в другом подключении начать snapshot транзакцию с читающим запросом к той же табличке…
SET TRANSACTION ISOLATION LEVEL SNAPSHOT BEGIN TRAN SELECT * FROM tst |
То snapshot-транзакция, как, впрочем, и версионный read committed, совершенно спокойно отработает, вернув предыдущее значение измененной записи. Однако если сейчас зафиксировать первую, изменяющую транзакцию…
COMMIT TRAN |
А затем повторить ту же самую выборку из snapshot транзакции…
SELECT * FROM tst |
То эта выборка вернет все еще старые значения записей, существовавшие до фиксации первой транзакции. То есть здесь чтение полностью воспроизводимо, в отличие от read committed. Более того, этот уровень изоляции не допускает появления фантомов, в отличие от блокировочного repeatable read. Например, если выполнить третью транзакцию, в которой в таблицу добавляются записи…
BEGIN TRAN INSERT INTO tst (x, y) VALUES (6, 0) COMMIT TRAN |
То очередная выборка всех записей таблицы tst из snapshot-транзакции вернет все те же записи, что и в первый раз, просто потому, что на момент первого запроса нового значения еще не было. В общем, с таблицей можно делать все что угодно, все выборки из snapshot-транзакции будут одними и теми же.