Преимущества этого способа заключаются в первую очередь в том, что взаимоблокировки не допускаются в принципе. Этот способ несколько проще в реализации, нежели построение и отслеживание графа ожидания. И, наконец, отсутствует вероятность циклического рестарта отмененной транзакции, так как при откате временная метка сохраняется, и любая транзакция со временем гарантировано станет самой «старшей», а значит, ее не откатят.
Недостаток же этого способа заключается в том, что число откатов здесь гораздо больше, чем в реализации на основе графа ожидания.
Реализация в Microsoft SQL Server
В Microsoft SQL Server используется механизм устранения взаимоблокировок на основе графа ожидания. Граф строится при каждом запросе блокировки. По истечении некоего тайм-аута просыпается монитор блокировок, и если он обнаруживает, что какая-то транзакция ждет слишком долго, инициируется процесс нахождения замкнутого цикла в графе ожидания. В случае обнаружения мертвой блокировки происходит откат одной из транзакций, участвующих в цикле. «Жертва» вычисляется в зависимости от объема проделанной работы, которая в свою очередь определяется по количеству записей в журнале транзакций, которые необходимо откатить. Однако есть возможность указать серверу, какую транзакцию предпочтительнее видеть в качестве «жертвы», с помощью команды:
SET DEADLOCK_PRIORITY { LOW | NORMAL | @deadlock_var } |
Здесь @deadlock_var – переменная в диапазоне от 1 до 12, чем меньше число, тем ниже приоритет; LOW соответствует 3, а NORMAL – 6.
Но как бы сервер не старался, все, что он сможет сделать по своей инициативе – это отменить одну из транзакций. Самостоятельно Microsoft SQL Server отмененную транзакцию заново не запускает, а возвращает сообщение об ошибке. Поэтому в клиентском приложении необходимо предусмотреть обработку данной ситуации и, возможно, перезапуск отмененной транзакции. Однако по ряду причин целиком полагаться на обработку подобных ошибок в приложении не следует, это последний рубеж обороны по защите нервов конечного пользователя. Недостатки от перекладывания всей ответственности на клиента в данном случае таковы:
Возрастает нагрузка на журнал транзакций, так как каждая неудачная попытка тоже будет приводить к записи в журнале, включая запись об откате. Для интенсивно работающей системы это может быть критично, ввиду того, что самым узким местом в таких случаях часто оказывается именно журнал транзакций.
Монитор, отслеживающий замкнутые циклы в графе ожидания, не работает непрерывно, таким образом, взаимоблокировка не обнаруживается мгновенно. Это снижает производительность системы в целом из-за того, что ни в чем неповинные транзакции вынуждены ждать, пока менеджер не отменит одну из намертво заблокированных.
Подобное решение в общем случае отвратительно масштабируется, так как исходная причина не устранена, и с увеличением нагрузки число взаимоблокировок будет возрастать, причем не линейно. А следовательно, будет расти и количество ожидающих транзакций, что в итоге может привести к полной неработоспособности системы.
Возможные причины возникновения взаимоблокировок
В свете всего вышеописанного почетной обязанностью разработчика является сведение вероятности мертвой блокировки к минимуму, а в идеале – к нулю, что является достаточно сложной, но вполне разрешимой задачей.
Строго говоря, все случаи взаимоблокировки сводятся к нарушению порядка доступа к объектам. Далее разберем несколько примеров транзакций, потенциально способных привести к тупиковой ситуации. Но перед этим, чтобы примеры были более наглядными, надо выбрать базу для экспериментов (например стандартную Northwind) и создать в ней табличку для дальнейших опытов. Для этого достаточно выполнить в Query Analyzer’е вот такой скрипт:
--- Выбор тестовой базыUSE Northwind GO--- Создание таблицыif exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[Tbl]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)drop table [dbo].[Tbl]GOCREATE TABLE [dbo].[Tbl] ( [X] [int] NULL, [Y] [int] NULL,[value] [varchar] (50))GO--- Заполнение тестовыми даннымиinsert into Tbl(X, Y, Value) VALUES (1, 10, 'Алма-Ата')insert into Tbl(X, Y, Value) VALUES (2, 9, 'Алушта')insert into Tbl(X, Y, Value) VALUES (3, 8, 'Алупка')insert into Tbl(X, Y, Value) VALUES (4, 7, 'Анкара')insert into Tbl(X, Y, Value) VALUES (5, 6, 'Агра')insert into Tbl(X, Y, Value) VALUES (6, 5, 'Анапа')insert into Tbl(X, Y, Value) VALUES (7, 4, 'Альбукерке')insert into Tbl(X, Y, Value) VALUES (8, 3, 'Алансон')insert into Tbl(X, Y, Value) VALUES (9, 2, 'Авиньен')insert into Tbl(X, Y, Value) VALUES (10, 1, 'Абакан') |
Самый тривиальный случай, нарушение порядка доступа в чистом виде, представляют собой две транзакции примерно следующего содержания:
Первая транзакция - T1.
BEGIN TRAN UPDATE Tbl SET X = 1 WHERE X = 1 UPDATE Tbl SET X = 3 WHERE X = 3COMMIT TRAN |
Вторая транзакция - T2.
BEGIN TRAN UPDATE Tbl SET X = 3 WHERE X = 3 UPDATE Tbl SET X = 1 WHERE X = 1COMMIT TRAN |
Если эти транзакции стартуют одновременно, то произойдет взаимоблокировка по причине очевидного нарушения порядка доступа. T1 сначала обращается к записи X = 1, а затем к записи X = 3. Т2 же, наоборот, сначала обращается к записи X = 3, а затем к X = 1.
Рисунок 1. Порядок обращения к записям, приводящий к взаимоблокировке.
При одновременном старте Т1 захватывает запись X = 1, в это время Т2 успевает захватить запись X = 3. Затем T1 хочет захватить запись X = 3, но она уже захвачена T2, поэтому T1 ожидает T2 на блокировке, и в граф добавляется ребро T1->T2. Примерно в это же время T2 хочет захватить запись X = 1, которая также уже захвачена T1. В графе ожидания появляется второе ребро T2->T1 и он становится цикличным. Ну а поскольку подобная ситуация без грубого вмешательства неразрешима, то одна из транзакций будет отменена, другая же, пользуясь тем, что блокировка исчезла, спокойно завершит свою работу.
Способы лечения здесь также очевидны, достаточно просто поменять порядок операторов в одной из транзакций. В общем случае необходимо добиться того, чтобы все транзакции обращались к объектам в одном и том же порядке.
Следующие примеры я постараюсь разобрать более подробно, эмулируя поведение сервера в реальной ситуации.
Очень часто встречается примерно такая последовательность операторов в одной транзакции:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ BEGIN TRAN SELECT @Var = Y FROM Tbl WHERE X = 2 --- --- здесь выполняются какие-нибудь вычисления над @Var.--- UPDATE Tbl SET Y = @Var WHERE X = 2COMMIT TRAN |
Если два экземпляра такой транзакции запустить одновременно из двух разных потоков, то с очень большой вероятностью все закончится взаимоблокировкой.
Выполним вышеприведенный T-SQL-код из транзакций T1 и T2. Чтобы имитировать возможное развитие событий при параллельной работе будем выполнять транзакции по частям. Сначала половину T1, затем целиком T2, а потом оставшуюся часть T1. Эффект будет точно таким же, как если бы в реальном приложении между двумя операторами T1 успела бы пролезть транзакция T2. На самом деле для получения взаимоблокировки достаточно, чтобы между двумя операторами T1 успел втиснуться только первый оператор T2, дальнейший порядок операций уже не важен.
Итак, выполним первую часть T1 в одном из окон Query Analyser’а:
--- установим необходимый уровень изоляцииSET ISOLATION LEVEL REPEATABLE READ BEGIN TRANSACTION SELECT * FROM Tbl WHERE X = 2 |
Все выполнилось успешно, но транзакция все еще считается активной, мы ее не отменили и не зафиксировали. Если в этот момент посмотреть на блокировки, наложенные на таблицу Tbl, можно увидеть примерно следующую картину, с точностью до констант:
spid dbid ObjId ObjName IndId Type Resource Mode Status ------ ------ ---------- ------- ----- ---- --------- ----- ------ 54 6 2034106287 Tbl 0 PAG 1:17495 IS GRANT54 6 2034106287 Tbl 0 RID 1:17495:1 S GRANT54 6 2034106287 Tbl 0 TAB IS GRANT |
Иными словами, мы наложили коллективную блокировку (S) на конкретную запись (RID 1:17495:1), и две коллективные блокировки намерения (IS) выше по иерархии, на страницу и таблицу. Откроем новое соединение с той же базой в новом окне QA и попытаемся выполнить эту же транзакцию целиком:
--- установим необходимый уровень изоляцииSET ISOLATION LEVEL REPEATABLE READ BEGIN TRAN SELECT * FROM Tbl WHERE X = 2 UPDATE Tbl SET Y = 3 WHERE X = 2COMMIT TRAN |
Блокировок, естественно, добавилось:
spid dbid ObjId ObjName IndId Type Resource Mode Status ------ ------ ----------- ------- ------ ---- ---------- ----- ------- 54 6 2034106287 Tbl 0 PAG 1:17495 IS GRANT54 6 2034106287 Tbl 0 RID 1:17495:1 S GRANT54 6 2034106287 Tbl 0 TAB IS GRANT61 6 2034106287 Tbl 0 PAG 1:17495 IX GRANT61 6 2034106287 Tbl 0 RID 1:17495:1 X CNVT61 6 2034106287 Tbl 0 TAB IX GRANT |
Те, что (в моем случае) от spid 54 – это наложенные ранее, от первой транзакции, а те, у которых spid 61 - от второй. С блокировками намерения все то же самое, они запрошены и успешно получены. А вот с эксклюзивными ситуация такая: сначала, выполняя SELECT, мы получили разделяемую блокировку на ту же запись (RID 1:17495:1), что и первая транзакция. Затем нам понадобилось туда же записать, а для этого надо сконвертировать коллективную блокировку S до X. Однако сделать это не получается, так как мешает S-блокировка на ту же запись от первой транзакции. Что мы и видим в третьей снизу строчке, статус эксклюзивной блокировки (X) CNVT – конвертирование. То есть SELECT выполнился, но до UPDATE дело не дошло, T2 ждет, пока T1 освободит запись X=2, чтобы наложить эксклюзивную блокировку.
Переключимся обратно в первое окно и попытаемся завершить T1:
UPDATE Tbl SET Y=3 WHERE X=2COMMIT TRAN |
Теперь и T1 будет ждать, пока T2 освободит свою коллективную блокировку. Таким образом, транзакции будут ожидать друг друга, цикл в графе ожидания замкнется и, некоторое время спустя, когда менеджер блокировок это обнаружит, одна из транзакций будет отменена. Приложение, запустившееее, получитсообщение 1205 овзаимоблокировке (Transaction (Process ID 61) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction), а другая транзакция завершится успешно.