Смекни!
smekni.com

Версионность в Yukon (стр. 3 из 5)

SELECT * FROM tst WHERE x = 3

то мы уже получим результат x = 3, y = (-1), феномен неповторимого чтения (non-repeatable read) в действии. Нам удалось дважды обратиться к одним и тем же данным из одной транзакции и получить различные результаты.

Итак, для читающих запросов все работает просто здорово, перейдем теперь к пишущим запросам все при том же уровне изоляции.

Допустим, версионник пытается обновить несколько записей из read committed-транзакции и натыкается на то, что кто-то в данный момент из другой транзакции обновил, но еще не зафиксировал одну из нужных записей. Просто подождать, пока вражеская транзакция зафиксируется, и затем поменять запись, нельзя, так как к этому времени запись может быть изменена таким образом, что перестанет удовлетворять критериям запроса. Чтобы этого не произошло, необходимо, как минимум, перечитать эту запись заново - чтобы изменять актуальную версию, а не устаревшую.

Таким образом возникает некоторый парадокс – при одном и том же уровне изоляции читающие запросы получаются согласованнее, чем пишущие. Формально все в порядке – требования уровня изоляции не нарушены. Чтобы избежать такого парадокса, в некоторых коммерческих реализациях в таких случаях делается откат запроса, а затем запрос выполняется заново, чтобы обеспечить обновление на согласованном срезе данных.

Разработчики Yukon не стали добавлять таких сложностей, и все изменения делаются по старинке, точно так же, как их делает обычный блокировочник. Вплоть до побочного эффекта, связанного с блокированием всей таблицы по причине отсутствия индекса.

Если, например, в одном подключении выполнить часть транзакции, изменив в тестовой табличке одну запись, но не фиксировать транзакцию, удерживая тем самым блокировку…

SET TRANSACTION ISOLATION LEVEL READ COMMITTEDBEGIN TRAN UPDATE tst SET y=3 WHERE x=3

А в другой транзакции попытаться изменить другую запись…

SET TRANSACTION ISOLATION LEVEL READ COMMITTEDBEGIN TRAN UPDATE tst SET y=-1 WHERE x=4COMMIT

То вторая транзакция не завершится, а подвиснет в задумчивости. Если же в этот момент взглянуть на блокировки, наложенные на таблицу tst:

Тип Описание Объект Режим Статус spid
PAG 1:1357 72057594057326592 IU GRANT 54
PAG 1:1357 72057594057326592 IX GRANT 53
RID 1:1357:2 72057594057326592 X GRANT 53
RID 1:1357:2 72057594057326592 U WAIT 54
TAB 1963154039 IX GRANT 54
TAB 1963154039 IX GRANT 53

Таблица 2

то можно наблюдать картину, характерную для самого обычного блокировочника, хотя версионности никто не отменял, в чем можно убедиться, выполнив в еще одной параллельной read committed-транзакции читающий запрос:

SET TRANSACTION ISOLATION LEVEL READ COMMITTEDBEGIN TRAN SELECT * FROM tstCOMMIT

Этот запрос совершенно спокойно выполнится, никого не потревожив, и вернет при этом состояние таблицы на момент, предшествующий изменениям, так как ни одна из изменяющих таблицу транзакций на время выполнения этого запроса еще не зафиксирована. Все дело в том, что обычно версионник выполняет изменения данных примерно по такому сценарию:

Делается версионное сканирование таблицы, и выясняется, какие записи необходимо изменить.

Предпринимаются попытки изменить отобранные записи.

Если запись изменилась с момента версионного скана, то проверяется, не перестала ли она удовлетворять критерию отбора, и если не перестала, то запись меняется, если перестала, то пропускается. (Некоторые реализации применяют здесь более хитрые алгоритмы, но этот вопрос выходит за рамки данной статьи).

Если запись меняется в настоящий момент, то сервер дожидается конца изменений и опять-таки проверяет соответствие записи условиям выборки.

То есть ожидание , если и происходит, то только из-за того, что в процессе обновления встретилась запись, которая меняется в данный момент.

Чистый же блокировочник работает немного по другому сценарию. Сканирование данных ему не имеет смысла делать, так как все запросы на чтение у него блокирующие. Поэтому он просто перебирает все записи в таблице по очереди (напомню, речь идет о таблице без индексов), проверяя их на соответствие условию выборки, и накладывая при этом блокировку обновления (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 SNAPSHOTBEGIN TRANSELECT * 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-транзакции будут одними и теми же.

Таким образом, чтение при уровне изоляции snapshot в Yukon практически ничем не отличается от обычного версионного. Оно работает по тем же принципам, и «внешний» эффект точно такой же. Транзакция получает согласованный срез данных, начиная с первого обращения к данным, и все последующие изменения ее не касаются.

Все это очень хорошо работает при читающих запросах, однако при записи могут возникать конфликты. Если при выполнении обновления snapshot-транзакция доберется до записи, заблокированной другой транзакцией, то, возникнет конфликт версий. Если блокирующая транзакция успешно фиксируется, в чистом версионнике snapshot-транзакция откатывается, поскольку если она изменит данные более «молодой» транзакции и продолжит работу, вполне возможен феномен утерянного обновления. Сместить эти транзакции друг относительно друга во времени и считать snapshot-транзакцию более «молодой» тоже не получится, так как блокирующая транзакция могла добавить записи, удовлетворяющие условию выборки snapshot-транзакции, а значит, все snapshot-запросы должны были эти записи увидеть. То есть snapshot-транзакция все равно должна выполниться заново, с более поздней временной меткой, чтобы увидеть все изменения, внесенные блокирующей транзакцией.

И в данном случае Yukon мало чем отличается от версионника. Если при уровне изоляции read committed в случае изменения он может себе позволить вести себя как блокировочник, то при уровне изоляции snapshot такой фокус не пройдет. Как минимум при этом snapshot скатится все до того же read committed. Дело в том, что блокировочник уровни изоляции выше read committed обеспечивает удержанием коллективных (share) блокировок при запросах на чтение до конца транзакции. Версионник же подобных блокировок для обеспечения уровня изоляции snapshot не использует, у него принцип совсем другой. А поскольку к моменту конфликта snapshot-транзакция уже могла выполнить несколько версионных чтений, то поступать как блокировочник уже поздно, поэтому Yukon, так же как и версионник, в случае конфликта откатывает snapshot-транзакцию.

Если продолжить издевательства над таблицей tst, и изменить в ней какую-нибудь запись, не фиксируя транзакцию: