BEGIN TRAN UPDATE tst SET y = -1 WHERE x = 3 |
а потом попробовать изменить ту же запись из snapshot-транзакции:
SET TRANSACTION ISOLATION LEVEL SNAPSHOTBEGIN TRAN UPDATE tst SET y = 3 WHERE x = 3 COMMIT TRAN |
То snapshot-транзакция повиснет в задумчивости. При взгляде на блокировки, наложенные на таблицу tst, можно наблюдать картину, характерную для обычного блокировочика.
Тип | Описание | Объект | Режим | Статус | spid |
TAB | 1963154039 | IX | GRANT | 51 | |
RID | 1:1357:2 | 72057594057326592 | U | WAIT | 51 |
PAG | 1:1357 | 72057594057326592 | IU | GRANT | 51 |
TAB | 1963154039 | IX | GRANT | 52 | |
RID | 1:1357:2 | 72057594057326592 | X | GRANT | 52 |
PAG | 1:1357 | 72057594057326592 | IX | GRANT | 52 |
Таблица 3
Snapshot-транзакция (spid 52) ожидает на блокировке (U – WAIT), пока освободится нужная запись (RID 1:1357:2), заблокированная другой транзакцией (spid 51) монопольно (X - GRANT).
Если сейчас вернуться в первое окно и откатить блокирующую транзакцию, то snapshot совершенно спокойно выполнит свое обновление и зафиксируется. Однако если блокирующую транзакцию зафиксировать, то, в отличие от блокировочного поведения, snapshot-транзакция будет отменена, и клиентское приложение получит сообщение об ошибке:
.Net SqlClient Data Provider: Msg 3960, Level 16, State 1, Line 1Cannot use snapshot isolation to access table 'tst' in database 'AdventureWorks'. Snapshot transaction aborted due to update conflict. Retry transaction. |
Более того, для отката snapshot-транзакции ей даже не нужно ожидать снятия блокировки. Чтобы избежать несогласованного изменения, необходимо производить откат даже в том случае, если после старта snapshot-транзакции одна из записей, необходимых для пишущего запроса, была изменена другой транзакцией, успевшей зафиксироваться.
Если в одном из подключений начать snapshot-транзакцию, сделав простую выборку:
SET TRANSACTION ISOLATION LEVEL SNAPSHOTBEGIN TRANSELECT * FROM tst |
Затем в другом подключении изменить какую-нибудь запись:
BEGIN TRAN UPDATE tst SET y=3 WHERE x=3COMMIT TRAN |
А потом попытаться изменить эту же запись из snapshot-транзакции:
UPDATE tst SET y=3 WHERE x=3 COMMIT TRAN |
То snapshot транзакция успехом не увенчается, а будет отменена по причине конфликта версий.
Дело в том, что если существует транзакция, зафиксированная после начала работы snapshot-транзакции, и изменения этой транзакции попадают в условия выборки, сделанной snapshot-транзакцией, то вполне могут существовать записи, которые были изменены этой транзакцией после того, как snapshot-запрос их просмотрел, таким образом, что они стали удовлетворять условию snapshot-выборки. То есть получается, что snapshot-транзакция может увидеть только часть изменений, сделанных другой транзакцией, а этого допустить нельзя.
В данном случае поведение Yukon очень похоже на версионное. Но на самом деле он все равно ведет себя немного «по-блокировочному». Проявляется это, например, все в той же ситуации с отсутствием индексов.
Если еще раз выполнить блокирующую транзакцию, не фиксируя ее:
BEGIN TRAN UPDATE tst SET y=3 WHERE x=3 |
А потом попробовать изменить другую запись из snapshot транзакции:
SET TRANSACTION ISOLATION LEVEL SNAPSHOTBEGIN TRAN UPDATE tst SET y=-1 WHERE x=4 COMMIT TRAN |
То snapshot транзакция все равно окажется заблокированной.
Тип | Описание | Объект | Режим | Статус | spid |
RID | 1:1357:2 | 72057594057326592 | U | WAIT | 51 |
Таблица 4
И, что характерно, блокировка произойдет на той же самой записи. Иными словами, в отсутствии индексов Yukon осуществляет поиск по таблице как самый обычный блокировочник, даже при уровне изоляции snapshot. Версионник в данной ситуации сначала сканирует всю таблицу обычным версионным запросом, выясняя нужные записи, а потом обращается с обновлением только к тем, которые подходят под условие выборки, поэтому подобного там не происходит. Yukon же сканирует таблицу, накладывая блокировку обновления на каждую запись, и снимая ее, если запись не удовлетворяет условию выборки, или конвертируя в эксклюзивную, если удовлетворяет. Таким образом, даже если заблокированная запись не удовлетворяет условию выборки обновляющего оператора snapshot-транзакции, в случае отсутствия индексов транзакция все равно застрянет на блокировке.
Результат подобного ожидания snapshot-транзакции вполне закономерен. Если блокирующую транзакцию зафиксировать, то snapshot-транзакция будет отменена, несмотря на то, что формально измененная запись ни до, ни после запроса под условия выборки не попадала. Оборачивается это тем, что если в момент изменения записей в таблице без индексов из snapshot-транзакции будет изменена любая запись, то snapshot-транзакция не выполнится.
Судя по всему, сделать согласованное неблокирующее сканирование при обновлении записей – задача нетривиальная. Поэтому все сделано единообразно, «по-блокировочному». С одной стороны, подобное блокировочное сканирование обещает довольно мрачные перспективы попыткам записи из snapshot-транзакций в неиндексированные таблицы. Но, с другой стороны, это предотвращает некоторые экзотические случаи фантомов, которые пропускает классический snapshot.
Несмотря на то, что версионный snapshot помогает избавиться от большинства побочных эффектов, связанных с параллельным выполнением транзакций, вероятность появления некоторых феноменов по-прежнему остается. Чтобы избавиться от подобных эффектов, при работе с классическим версионником и некоторыми гибридами приходится предпринимать дополнительные меры в виде фиктивных изменений или специальных блокировок некоторых записей.
Но Yukon, как и предыдущие версии SQL Server от Microsoft, поддерживает уровень изоляции serializable, который по определению не допускает никаких феноменов. По механизму этот уровень изоляции является чисто блокировочным и никакие версионные запросы, даже на чтение, здесь не поддерживаются, если конечно, не давать специальных указаний оптимизатору.
Новая функциональность, безусловно, окажется очень полезной. Самый заметный эффект – это отсутствие блокировок между читающими и пишущими запросами. То есть читающие запросы не блокируют пишущие, и наоборот. Собственно, вся версионность ради этого и затевалась.
У чистых блокировочников, каковым до недавнего времени являлся и Microsoft SQL Server, при наступлении того печального момента, когда читающие запросы начинают довольно сильно конфликтовать с пишущими, используется стандартный архитектурный прием. Механизм работы с данными делится на две составляющие, OLAP и OLTP. OLTP реализует работу пишущих транзакций на относительно небольшом объеме актуальных данных, а OLAP содержит основные данные, доступные только для чтения, причем в силу того, что каждый механизм оптимизирован исключительно под свои задачи, подобное решение, как правило, оказывается очень эффективным. При этом совершенно необязательно сразу строить большую систему и покупать дополнительное оборудование. Если задача не требует излишней громоздкости, то начинается все обычно с построения промежуточных агрегирующих таблиц и материализованных представлений, в дальнейшем возможен перенос одной из составляющих в отдельную базу, экземпляр сервера и, наконец, если в том возникнет необходимость, на отдельный сервер или даже группу серверов.
Может показаться, что класс задач, для которых реально нужна версионность, достаточно узок. С одной стороны, использование версионности имеет смысл, если в блокировочнике конфликт читающих и пишущих запросов начинает оказывать заметное влияние на производительность. Но с другой стороны, при дальнейшем увеличении нагрузки, производительность версионного механизма довольно быстро начнет снижаться из-за частых обновлений по причине больших накладных расходов на поддержку версионности, она ведь тоже обходится не даром.
Но тут можно вспомнить, что существуют задачи, где необходимо выполнять большое количество малопрогнозируемых запросов (ad-hoc queries). В этом случае конфликты читающих и пишущих запросов выходят на первое место, поскольку в подобной ситуации очень высока вероятность возникновения взаимоблокировок.
Также стоит заметить, что, несмотря на всю полезность агрегатных таблиц, они обладают одним большим недостатком, сильно сужающим область их применения. Если в исходной таблице активность запросов можно физически разнести по разным страницам данных, то в агрегатной таблице большое количество изменений создаст совершенно ненужный ажиотаж в очень маленьком объеме.
Немаловажно также и то, что писать приложения для БД с поддержкой версионности попросту удобнее. Отсутствие необходимости отслеживать возможные конфликты читающих и пишущих запросов здорово облегчает жизнь, особенно не слишком опытным разработчикам, и повышает масштабируемость системы (хотя в этом случае выше вероятность появления некоторых неочевидных эффектов).
И наконец, введенная поддержка версионности позволит легче адаптироваться к новому серверу тем, кто привык работать с подобной функциональностью.
Преимущества версионности.
Читающие запросы не блокируют пишущие, и наоборот.
Уменьшатся вероятность возникновения взаимоблокировок.
Есть возможность выполнять большие согласованные чтения, не предусмотренные структурой базы (ad-hoc queries), без риска сильного падения производительности остальных запросов.
Можно выполнять согласованные агрегатные запросы без повышения уровня изоляции.
Во что обходится версионность.
Пишущие транзакции обязаны создавать копию записи в хранилище версий, даже если на данный момент нет читающих запросов, которым нужна эта запись.