45802 (Версионность в Yukon), страница 2
Описание файла
Документ из архива "Версионность в Yukon", который расположен в категории "". Всё это находится в предмете "информатика" из , которые можно найти в файловом архиве . Не смотря на прямую связь этого архива с , его также можно найти и в других разделах. Архив можно найти в разделе "рефераты, доклады и презентации", в предмете "информатика, программирование" в общих файлах.
Онлайн просмотр документа "45802"
Текст 2 страницы из документа "45802"
Физически механизм версионности в Yukon отслеживает изменение данных отдельно для каждой записи. Для этого к записи добавляется кусочек длиной в 14 байт, в котором хранится идентификатор транзакции, изменившей эту запись, и ссылка на предыдущую зафиксированную версию. Таким образом, все версии одной записи образуют связный список, и серверу в случае необходимости не составляет никакого труда спуститься по цепочке ссылок к нужным данным.
ПРИМЕЧАНИЕ В том случае, если на странице данных не хватает места под дополнительные 14 байт на каждую запись, то во время перевода базы в «версионное» состояние данные будут автоматически перераспределены по страницам. |
Все версии записи собираются в специальном хранилище (version store heaps), которое расположено в tempdb. При любом изменении записи предыдущая версия копируется в это хранилище.
Благодаря особенностям tempdb и отсутствию журналирования для хранилища версий при обслуживании и чтении копий данных, нагрузка на операции ввода/вывода обещает быть минимальной.
Те устаревшие копии данных, которые уже точно не будут нужны ни одной транзакции, удаляются из tempdb автоматически. При этом отслеживается старейшая заинтересованная транзакция. Поэтому исключается вероятность того, что сервер в какой-то момент не найдет нужную версию. Удаление производит специальный механизм, который учитывает текущую нагрузку, через количество активных версионных транзакций, и процент свободного места в tempdb.
Для оценки предполагаемого размера хранилища версий предлагается следующая формула: Size (KB) = частота обновления версий (Version generation rate) Kb/sec. * время выполнения самой длинной транзакции (the longest transaction time) sec.
Как это работает
Здесь опять-таки проще всего разобрать по очереди все уровни изоляции предлагаемые Yukon снизу вверх.
Read uncommitted
В чистом версионнике, как уже говорилось, read uncommitted обычно не предусмотрен, да и не нужен. В Yukon при запросах к БД с включенной поддержкой версионности такой фокус тоже провернуть не получится. Даже при явном указании соответствующих подсказок оптимизатору в запросе, возвращается предыдущая зафиксированная версия, а не грязные данные транзакции, заблокировавшей запись от изменений.
Read committed
С read committed все несколько сложнее. После включения поддержки версионности все запросы на чтение при этом уровне изоляции автоматически начинают работать как версионные. То есть если транзакция при чтении натыкается на заблокированную запись, то она читает предыдущую версию этой записи из tempdb.
ПРИМЕЧАНИЕ На самом деле в alpha-версии Yukon для поддержки версионности при read committed необходимо включить специальный трейс-флаг (3970). Но Microsoft торжественно клянется, что в финальном продукте все будет происходить автоматически. |
Можно провести простенький эксперимент. Пусть есть небольшая табличка tst в БД с поддержкой версионности, например, AdventureWork, созданная с помощью вот такого скрипта:
CREATE TABLE tst(x int, y int) GO INSERT INTO tst(x, y) VALUES(1, 5) INSERT INTO tst(x, y) VALUES(2, 4) INSERT INTO tst(x, y) VALUES(3, 3) INSERT INTO tst(x, y) VALUES(4, 2) INSERT INTO tst(x, y) VALUES(5, 1) |
Сначала откроем новое подключение, откроем read committed-транзакцию и сделаем выборку, транзакцию при этом закрывать не будем.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED BEGIN TRAN SELECT * FROM tst WHERE x = 3 |
Получим то, что и ожидалось: x = 3, y = 3.
Теперь в другой транзакции попытаемся обновить эту запись, также не фиксируя транзакцию.
BEGIN TRAN UPDATE tst SET y = -1 WHERE x = 3 |
Если после этого взглянуть на блокировки, наложенные на табличку tst, то, как и при использовании предыдущих версий SQL Server, можно заметить эксклюзивную блокировку на запись и две блокировки намерения выше по иерархии, на страницу и таблицу.
Тип | Описание | Объект | Режим | Статус | spid |
TAB | 1963154039 | IX | GRANT | 52 | |
RID | 1:1357:2 | 72057594057326592 | X | GRANT | 52 |
PAG | 1:1357 | 72057594057326592 | IX | GRANT | 52 |
Таблица 1
То есть картина совпадает с той, которую можно видеть при использовании предыдущей версии SQL Server или БД без поддержки версионности.
Однако дальше начинаются отличия. Если сейчас вернуться к первой транзакции и попытаться опять выполнить тот же самый запрос, то он совершенно спокойно отработает.
SELECT * FROM tst WHERE x = 3 |
И результат будет точно таким же: x=3, y=3. Если попробовать сделать то же самое на БД без включенной поддержки версионности, то второй запрос из первой транзакции не выполнится. Он будет ожидать фиксации или отката второй транзакции. То есть он попросту не сможет прочитать нужную запись, поскольку она заблокирована. Но в данном случае блокировка нисколько не мешает прочитать версию данных, существовавшую на момент начала выборки.
Более того, в силу особенностей работы с неиндексированными таблицами (а для тестовой таблицы индексов не создавалось), в базе без поддержки версионности второй запрос в первой транзакции не смог бы выбрать не только заблокированную запись, но и любую другую. Из-за блокировки ему все равно бы пришлось ждать завершения работы первой транзакции. Говоря проще, у блокировочника, в случае отсутствия индексов, блокировка одной записи превращается фактически в блокировку всей таблицы.
Если теперь зафиксировать изменения тестовой таблицы, произведенные второй транзакцией:
COMMIT TRAN |
и сделать опять выборку тех же данных в транзакции номер один:
SELECT * FROM tst WHERE x = 3 |
то мы уже получим результат x = 3, y = (-1), феномен неповторимого чтения (non-repeatable read) в действии. Нам удалось дважды обратиться к одним и тем же данным из одной транзакции и получить различные результаты.
Итак, для читающих запросов все работает просто здорово, перейдем теперь к пишущим запросам все при том же уровне изоляции.
Допустим, версионник пытается обновить несколько записей из read committed-транзакции и натыкается на то, что кто-то в данный момент из другой транзакции обновил, но еще не зафиксировал одну из нужных записей. Просто подождать, пока вражеская транзакция зафиксируется, и затем поменять запись, нельзя, так как к этому времени запись может быть изменена таким образом, что перестанет удовлетворять критериям запроса. Чтобы этого не произошло, необходимо, как минимум, перечитать эту запись заново - чтобы изменять актуальную версию, а не устаревшую.
Таким образом возникает некоторый парадокс – при одном и том же уровне изоляции читающие запросы получаются согласованнее, чем пишущие. Формально все в порядке – требования уровня изоляции не нарушены. Чтобы избежать такого парадокса, в некоторых коммерческих реализациях в таких случаях делается откат запроса, а затем запрос выполняется заново, чтобы обеспечить обновление на согласованном срезе данных.
Разработчики Yukon не стали добавлять таких сложностей, и все изменения делаются по старинке, точно так же, как их делает обычный блокировочник. Вплоть до побочного эффекта, связанного с блокированием всей таблицы по причине отсутствия индекса.
Если, например, в одном подключении выполнить часть транзакции, изменив в тестовой табличке одну запись, но не фиксировать транзакцию, удерживая тем самым блокировку…
SET TRANSACTION ISOLATION LEVEL READ COMMITTED BEGIN TRAN UPDATE tst SET y=3 WHERE x=3 |
А в другой транзакции попытаться изменить другую запись…
SET TRANSACTION ISOLATION LEVEL READ COMMITTED BEGIN TRAN UPDATE tst SET y=-1 WHERE x=4 COMMIT |
То вторая транзакция не завершится, а подвиснет в задумчивости. Если же в этот момент взглянуть на блокировки, наложенные на таблицу 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 COMMITTED 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-транзакции будут одними и теми же.