Введение в управление версиями строк
Когда происходит обновление, то при использовании оптимистических уровней изоляции SQL Server сохраняет старые версии строк в специальной области базы tempdb, которое называется хранилище версий. Для исходной строки в базе данных добавляется 14-ти байтный указатель, который ссылается на старую версию этой строки (в зависимости от ситуации, может быть несколько версий). Рисунок 1 иллюстрирует это поведение.
Теперь, когда операция чтения (иногда записи) обратится к строке, для которой установлена монопольная (X) блокировка, то конфликта блокировки не произойдет, а будет считана старая версия строки из хранилища версий, как показано на рисунке 2.
Рисунок 2. Операции чтения и хранилище версий
Как вы уже догадались, оптимистические уровни изоляции помогают уменьшить влияние блокировок, но есть несколько нюансов. Наиболее значительный из них это то, что создается дополнительная нагрузка на базу tempdb. Использование оптимистических уровней изоляции на системах с большим количеством изменений данных может привести к интенсивному использованию базы tempdb и значительному ее увеличению в объеме. Этот момент будет рассмотрен позже в этой статье.
Также появляются дополнительные накладные расходы при изменении данных и их извлечении. SQL Server приходится копировать данные в базу tempdb и поддерживать связанный список версий записей, а при чтении данных нужно этот список обойти. Это добавляет дополнительную нагрузку на процессор и систему ввода/вывода.
Наконец, использование оптимистических уровней изоляции увеличивает фрагментации индексов. При изменении строки SQL Server увеличивает размер строки на 14 байт, чтобы хранить указатель на версию. Если страница полностью заполнена, и измененная строка не помещается на страницу, то страница разбивается, что приводит к фрагментации. Эти 14 байт будут занимать место, до тех пор, пока индекс не перестроится.
Совет. При использовании оптимистических уровней изоляции рекомендуется устанавливать параметр FILLFACTOR меньше 100, чтобы оставалось свободное место на страницах индекса. Это уменьшит разбиение страниц, когда добавляется указатель на версию и увеличивается размер строки.
Оптимистические уровни изоляции транзакций
Есть два оптимистических уровня изоляции транзакций: READ COMMITTED SNAPSHOT и SNAPSHOT. Если быть точнее, то SNAPSHOT это полноценный уровень изоляции, в то время как READ COMMITTED SNAPSHOT это параметр базы даных, который влияет на поведение операций чтения с уровнем изоляции READ COMMITTED.
Рассмотрим эти уровнее подробнее.
Уровень изоляции READ COMMITTED SNAPSHOT
Оба оптимистических уровня изоляции включаются на уровне базы данных. READ COMMITTED SNAPSHOT (RCSI) включается командой ALTER DATABASE SET READ_COMMITTED_SNAPSHOT ON.
Примечание. Изменение этого параметра требует монопольного доступа к базе. Команда не выполнится, если есть другие подключения к базе. Вы можете переключить базу данных в однопользовательский режим или использовать команду ALTER DATABASE SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK AFTER X SECONDS. При этом откатятся все активные транзакции и завершаться все подключения к базе.
Как уже говорилось, RCSI изменяет поведение операций чтения с уровнем изоляции READ COMMITTED. Но при этом не влияет на операции записи.
Как видно из рисунка 3, вместо установки совмещаемой (S) блокировки, которая привела бы к конфликту с монопольной (X) блокировкой, происходит чтение старой версии данных из хранилища версий. Операции записи устанавливают монопольные (X) блокировки и блокировки обновления (U) в пессиместических уровнях изоляции, поэтому они могут блокировать друг друга. Но при этом они уже не блокируют операции чтения, как это происходит с уровнем изоляции READ UNCOMMITTED.
Рисунок 3. Поведение уровня изоляции Read Committed Snapshot
Однако, существует большая разница между уровнями изоляции READ UNCOMMITTED и READ COMMITTED SNAPSHOT. Уровень изоляции READ UNCOMMITTED не устанавливает совмещаемые (S) блокировки, при этом возможны разные проблемы несогласованности данных (грязное чтение, неповторяющееся чтение и др.). С другой стороны, уровень изоляции READ COMMITTED SNAPSHOT дает нам полную согласованность данных на уровне команды (операции). В этом случае невозможно чтение незафиксированных данных, а также данных, которые изменялись с момента начала транзакции.
Совет. Переключение базы данных в режим использования уровня изоляции READ COMMITTED SNAPSHOT может быть экстренным выходом из ситуации, когда в системе много проблем с блокировками данных. Это позволит избежать блокировок для операций чтения/записи, если чтение происходит с уровнем изоляции READ COMMITTED. Но вы должны понимать, что лишь временная мера. Необходимо обнаружить и устранить причину возникновения таких блокировок.
Уровень изоляции SNAPSHOT
SNAPSHOT является отдельным уровнем изоляции. Он должен быть явно задан в коде с помощью команды SET TRANSACTION ISOLATION LEVEL SNAPSHOT или с помощью табличного указания WITH (SNAPSHOT).
По умолчанию, использование уровня изоляции SNAPSHOT запрещено. Его необходимо включить с помощью команды ALTER DATABASE SET ALLOW_SNAPSHOT_ISOLATION ON. Эта команда не требует монопольного доступа к базе и ее можно выполнять когда есть активные пользователи.
Уровень изоляции SNAPSHOT обеспечивает согласованность данных на уровне транзакции. Транзакции будут работать с версией данными, зафиксированной на начало транзакции вне зависимости от того, сколько транзакция активна и какие изменения происходили с данными в других транзакциях в это время.
В примере, показанном на рисунке 4, Сессия 1 начинает транзакцию и читает строку в момент времени T1. В момент времени T2 Сессия 2 изменяет строку в неявной транзакции. При этом старая (оригинальная) версия строки помещается во временное хранилище базы tempdb.
Рисунок 4. Уровень изоляции Snapshot и операции чтения
Следующим этапом появляется Сессия 3, которая начинает еще одну транзакцию и читает ту же строку в момент T3. Она читает версию строки зафиксированную Сессией 2 (в момент времени T2). Сессия 4 снова меняет строку в неявной транзакции в момент времени T4. В итоге в хранилище окажутся две версии строки — одна, которая была записана между моментами T2 и T4, и другая, что была записана до момента T2. Теперь, если Сессия 3 опять прочитает те же самые данные, то будет считана версия строки из хранилища, которая была записана между моментами T2 и T4, т.к. эта версия была создана при начале транзакции Сессии 3. Аналогично, Сессия 1 будет использовать версию строки, которая существовала до момента T2. После фиксации транзакций в Сессии 1 и Сессии 3 хранилище версий запустит задачу по удалению обеих версий строк, конечно, если эти данные не нужны другим транзакциям.
Уровень изоляции SNAPSHOT предоставляет аналогичную согласованность данных, что и уровень SERIALIZABLE, но без установки блокировок, хотя может генерировать огромное количество данных в базе tempdb. Если у вас есть сессия, которая удаляет миллионы строк из таблиц, то все они будут скопированы в хранилище версий. Даже если сама инструкция выполняется с пессиместическим уровнем изоляции. Данные будут сохраняться для возможных транзакций, использующий уровень изоляции snapshot или RCSI.
Теперь рассмотри поведение операций записи.Предположим, что Сессия 1 начинает транзакцию и обновляет одну из строк. Эта сессия удерживает монопольную (X) блокировку, как показано на рисунке 5.
Рисунок 5. Уровень изоляции snapshot и операции записи (1)
Сессия 2 хочет обновить все строки, у которых колонка Cancelled = 1. Она начинает сканирование таблицы, и когда должны быть считаться данные с orderid = 10, то читаются строки из хранилища версий, то есть последней зафиксированной версии до начала транзакции Сессии 2. Это оригинальная версия строки (не обновленная), у нее значение колонки Cancelled = 0, поэтому Сессия 2 не обновляет ее. Сессия 2 продолжает сканирование таблицы, не устанавливая на эту строку блокировки обновления (U) и монопольные (X) блокировки.
Аналогично, Сессия 3 хочет обновить все строки, у которых Amount = 29.95. Когда она читает версию строки из хранилища версий, то определяет, что строка должна быть обновлена. При этом неважно, что Сессия 1 в это время меняет колонку Amount. "Новая версия" строки еще не зафиксирована, и ее не видят другие сессии. Сессия 3 хочет обновить строку в базе данных, пытается установить монопольную (X) блокировку, но получает отказ, потому что Сессия 1 уже установила на этой строке аналогичную блокировку.
Однако, возможен еще один случай. Давайте рассмотрим сценарий, в котором подразумевается, что согласованность данных обеспечивается уровнем изоляции snapshot.
В примере, показанном на рисунке 6, Сессия 1 начинает транзакцию и меняет одну из строк. Следующим шагом Сессия 2 начинает другую транзакцию. На самом деле, не так уж и важно, что сессия начинается раньше транзакции, при условии, что новая версия строки с OrderId = 10 не фиксируется.
Рисунок 6. Уровень изоляции snapshot и операции записи (2)
В любом случае Сессия 1 завершит транзакцию. В этот момент монопольная (X) блокировка строки снимается. Если Сессия 2 попытается прочитать эту строку, то по прежнему будет получать версию из хранилища версий, потому что это последняя зафиксированная версия при начале транзакции Сессией 2. Но если транзакция попытается изменить эту версию, то получит ошибку 3960 и транзакция откатится. Пример ошибки показан на рисунке 7.
Совет. Ошибку 3960 можно обрабатывать с помощью операторов TRY/CATCH.
Не нужно забывать об этой проблеме, когда вы обновляете данные с уровнем изоляции SNAPSHOT в системе, в которой данные часто меняются. Один из возможных обходных путей, это использовать READCOMMITTED или другой пессиместический уровень изоляции с помощью табличных указаний операции обновления, как показано в листинге 1.
Листинг 1. Предотвращение ошибки 3960 с помощью табличного указания READCOMMITTED
set transaction isolation level snapshot begin tran select count(*) from Delivery.Drivers update Delivery.Orders with (readcommitted) set Cancelled = 1 where OrderId = 10 rollback
Уровень изоляции SNAPSHOT может изменить поведение системы. Предположим, мы имеем таблицу dbo.Colors, в которой две строки: Black и White. Код создания таблицы приведен в листинге 2.
Листинг 2. Поведение операции обновления с уровнем изоляции SNAPSHOT: Создание таблицы
create table dbo.Colors ( Id int not null, Color char(5) not null ) go insert into dbo.Colors(Id, Color) values(1,'Black'),(2,'White')
Теперь запустим две сессии одновременно. В первой сессии мы запустим обновление, которое установит в колонке Color цвет White для строк, в которых текущее значение равно Black. Код показан в листинге 3.
Листинг 3. Поведение операции обновления с уровнем изоляции SNAPSHOT: Код Сессии 1
begin tran update dbo.Colors set Color = 'White' where Color = 'Black' commit
Во второй сессии, выполним противоположные операции, как показано в листинге 4.
Листинг 4. Поведение операции обновления с уровнем изоляции SNAPSHOT: Код Сессии 2
begin tran update dbo.Colors set Color = 'Black' where Color = 'White' commit
Давайте запустим обе сессии одновременно с уровнем изоляции READ COMMITTED или любым другим пессиместическим уровнем. На первом шаге, как показано на рисунке 8, у нас есть конкуренция за ресурс. Одна из сессий установит монопольную (X) блокировку на строку и обновит ее. В тоже время другая сессия не сможет установить блокировку обновления (U) на эту же строку.
Рисунок 8. Пессимистические уровни изоляции: Шаг 1
Когда первая сессия зафиксирует транзакцию, то снимет монопольную (X) блокировку. В таблице окажутся две записи с одинаковым значением в колонке Color, потому что первая сессия уже произвела изменения (как показано на рисунке 9). В итоге в таблице всегда окажутся две одинаковые строки (Black или White), в зависимости от того, какая сессия первой успеет установить блокировку.
Рисунок 9. Пессимистические уровни изоляции: Шаг 2
С уровнем изоляции snapshot все работает немного по-другому (рисунок 10). Когда сессия обновит строку, то она помещает старую версию в хранилище версий. Вторая сессия при этом будет считывать строки из хранилища. В результате цвета поменяются местами.
Рисунок 10. Уровень изоляции snapshot
Вы должны быть в курсе, как работают уровни изоляции RSCI и SNASPSHOT, особенно если имеется код, который работает с блокировками. Например, имеется триггер, реализующий ссылочную целостность. Это может быть триггер ON DELETE, удаляющий данные из связанных таблиц. Этот триггер использует операцию select, чтобы проверить, имеются ли строки, связанные с удаляемой строкой. Используя оптимистические уровни изоляции можно пропустить строки, которые были изменены после начала транзакции. В этом случае правильнее использовать пессиместические уровни изоляции, например, READCOMMITTED.
Примечание. SQL Server использует уровень изоляции READ COMMITTED при проверке ограничения внешнего ключа. Это означает, что вы можете получить блокировку между операциями записи и чтения даже с оптимистическими уровнями изоляции, особенно если нет индексов ссылающихся столбцов, что приводит к сканированию таблицы.
Хранилище версий
Как уже говорилось ранее, необходимо следить за тем, как оптимистические уровни изоляции влияют на систему. Для примера, выполните следующий код, который удаляет все строки из таблицы Delivery.Orders (листинг 5).
Листинг 5. Удаление всех заказов из таблицы
set transaction isolation level read uncommitted begin tran delete from Delivery.Orders commit
Стоит отметить, что сессия запущена в режиме READ UNCOMMITTED. Даже если нет других транзакций, использующих оптимистические уровни изоляции, есть вероятность, что они возникнут перед фиксированием транзакции. В результате SQL Server должен поддерживать хранилище версий в не зависимости от того, запущены ли другие транзакции, использующие оптимистические уровни изоляции, или нет.
На рисунке 11 показано свободное место tempdb и размер хранилища версий. Видно, что после начала операции удаления размер хранилища версий растет, занимая все пространство tempdb.
Рисунок 11. Свободное место tempdb и размер хранилища версий
На рисунке 12 можно увидеть показатели формирования записей в хранилище версий и его очистки.
По умолчанию задача очистки выполняется раз в минуту, а также перед автоматическим увеличением размера базы tempdb.
Рисунок 12. Свободное место tempdb и размер хранилища версий
Есть еще 3 счетчика производительности, связанных с оптимистическими уровнями изоляции:
- Snapshot Transactions. Счетчик показывает количество активных транзакций, использующих уровень изоляции snapshot.
- Update Conflict Ratio. Показывает соотношение количества конфликтов обновления к общему количеству операций обновления с уровней изоляции snapshot.
- Longest Transaction Running Time. Показывает длительность в секундах самой старой активной транзакции, использующей версии строк.
Существует несколько динамических представлений (Dynamic Management Views — DMVs), которые могут быть полезны при решении вопросов, связанных с хранилищем версий и транзакций в целом. Подробнее на http://technet.microsoft.com/en-us/library/ms178621.aspx (Transaction Related Dynamic Management Views and Functions section).
Резюме
SQL Server в оптимистических уровнях изоляции использует версионирование строк. Транзакции не блокируются из-за несовместимости разделяемых (S) блокировок с блокировками обновления (U) и монопольными (X) блокировками, а используют "старые", зафиксированные ранее версии строк. Существуют два оптимистических уровня изоляции транзакции: READ COMMITTED SNAPSHOT и SNAPSHOT.
READ COMMITTED SNAPSHOT — это параметр базы данных, который влияет на поведение операций чтения, использующих режим READ COMMITTED. При этом он не влияет на операции записи — по прежнему сохраняются несовместимости блокировок (U)/(U) и (U)/(X). READ COMMITTED SNAPSHOT не требует изменений в код и может быть использован как "волшебная палочка", если в системе наблюдаются конфликты блокировок.
READ COMMITTED SNAPSHOT обеспечивает согласованность данных на момент выполнения операции, т.е. запрос читает данные, которые были зафиксированы на момент, когда запрос начался.
Уровень изоляции SNAPSHOT — отдельный уровень изоляции транзакций, который нужно явно указывать в коде. Этот уровень обеспечивать согласованность данных на уровне транзакции. Это значит, что запрос обращается к версии данных, которая была зафиксирована на момент начала транзакции.
При использовании уровня изоляции SNAPSHOT операции записи не блокируют друг друга, за исключением тех случаев, когда они меняют одни и те же строки. Это приводит либо к блокировке, либо к ошибке 3960.
Оптимистические уровни изоляции позволяют уменьшить блокировки, но при этом они могут значительно увеличить нагрузку на базу tempdb. Особенно в OLTP-системах, где данные постоянно меняются. Необходимо принять во внимание все возможные варианты их использования на стадии реализации, провести оптимизацию tempdb, и провести мониторинг системы, чтобы убедить, что нет лишнего и ненужного обращения к хранилищу версий.
———————————
При написании статьи использовались материалы из книги Дмитрия Короткевича «Pro SQL Server Internals» (2014 г.)
Хороший цикл статей, в конце статьи не хватает ссылок на другие статьи цикла.
И хорошо бы раскрыть аналогичные темы (цикла статей), но для PostrgreSQL
«При использовании уровня изоляции SNAPSHOT операции записи не блокируют друг друга, за исключением тех случаев, когда они меняют одни и те же строки. Это приводит либо к блокировке, либо к ошибке 3960»
Странная фраза. Непересекающиеся наборы строк и так друг друга не блокируют.
Я из Вашей статьи про SNAPSHOT понял следующее: убирается несовместимость Х-блокировок. То есть, если запись какого-то набора строк привела к наложению Х-блокировки в одном сеансе, в другом сеансе другие строки смогут записаться, если они отличны от первых.
Не поленился и провел эксперимент. Включил в своей базе снапшот. Также у меня в базе включен RCSI. Затем в первом сеансе в транзакции записываю большой набор строк (600 тыс). Это привело к наложению Х-блокировки на всю таблицу (у меня периодический регистр сведений, цены номенклатуры). Во втором сеансе пытаюсь записать другой набор строк с другим периодом, то есть первый и второй наборы точно не пересекаются. Транзакция второго сеанса отваливается с ошибкой ожидания на транзакционной блокировке. Точно также, как это происходит в чистом RCSI.
Таким образом, непонятно, что Вы имеет ввиду, когда говорите что в снапшот «записи друг друга не блокируют».
Расшифруйте подробнее, плз. Какой натурный тест можно выполнить, чтобы воспроизвести ваши слова?
Кстати, с точки зрения чтения, при включении снапшота дополнительно к RCSI — ничего не поменялось. Запрос внутри транзакции по прежнему получает данные на момент выполнения самого запроса, а не на момент начала транзакции, как в «чистом» снапшот.