Process 53 acquiring U lock on RID: 6:1:17495:0 |
Обратите внимание, здесь, считывая запись для проверки, сервер накладывает блокировку обновления. Если запись удовлетворит условию выборки, можно будет сконвертировать блокировку в эксклюзивную, не опасаясь возможной взаимоблокировки. Как только сервер убеждается, что запись не подходит, блокировка снимается. На данном уровне изоляции сервер имеет на это полное право, добиваясь тем самым более высокой степени параллелизма.
Process 53 releasing lock on RID: 6:1:17495:0 |
Как только находится нужная запись, блокировка на страницу конвертируется в IX:
Process 53 acquiring IX lock on PAG: 6:1:17495 |
затем блокировка на запись конвертируется в X:
Process 53 acquiring X lock on RID: 6:1:17495:3 |
и производится изменение записи. Блокировка этой записи, естественно, до конца транзакции не снимается.
Поскольку сервер не знает, что он уже выбрал все записи, удовлетворяющие условию, то он продолжает перебирать по очереди все, что осталось, накладывая и снимая соответствующую блокировку на каждую запись. Если после выполнения этой части транзакции посмотреть на наложенные блокировки, то мы увидим эксклюзивную блокировку записи x = 4 (RID 1:17495:3) и эксклюзивные блокировки намерения выше по иерархии, на страницу и таблицу:
dbid ObjId ObjName Type Resource Mode Status ------ ----------- ------- ---- ---------- ----- -------6 2034106287 Tbl RID 1:17495:3 X GRANT6 2034106287 Tbl PAG 1:17495 IX GRANT6 2034106287 Tbl TAB IX GRANT |
Как и следовало ожидать, больше подходящих записей не нашлось, ничего не обновилось, а значит, других блокировок удерживать не надо.
Теперь переключимся во второе окно и выполним T2, также с выставленным флагом отслеживания порядка наложения блокировок:
DBCC TRACEON(1200, 3604, -1)BEGIN TRAN UPDATE Tbl SET X = 2 WHERE X = 2COMMIT TRANDBCC TRACEOFF(1200, 3604, -1) |
Затем переключимся обратно в первое окно и завершим выполнение первой транзакции:
UPDATE Tbl SET X = 6 WHERE X = 6COMMIT TRANDBCC TRACEOFF(1200, 3604, -1) |
После этого произойдет мертвая блокировка, о чем сервер нам и сообщит.
Переключимся опять в окно с T2 (надеюсь, эти скачки не слишком утомительны) и посмотрим, каков был порядок наложения блокировок в T2:
Process 51 acquiring IX lock on TAB: 6:2034106287 [] (class bit2000000 ref1) result: OKProcess 51 acquiring IU lock on PAG: 6:1:17495 (class bit0 ref1) result: OKProcess 51 acquiring U lock on RID: 6:1:17495:0 (class bit0 ref1) result: OKProcess 51 releasing lock on RID: 6:1:17495:0Process 51 acquiring U lock on RID: 6:1:17495:1 (class bit0 ref1) result: OKProcess 51 acquiring IX lock on PAG: 6:1:17495 (class bit2000000 ref1) result: OKProcess 51 acquiring X lock on RID: 6:1:17495:1 (class bit2000000 ref1) result: OKProcess 51 acquiring U lock on RID: 6:1:17495:2 (class bit0 ref1) result: OKProcess 51 releasing lock on RID: 6:1:17495:2Process 51 acquiring U lock on RID: 6:1:17495:3 (class bit0 ref1) result: OKWAITProcess 51 releasing lock on RID: 6:1:17495:3Process 51 acquiring U lock on RID: 6:1:17495:4 (class bit0 ref1) result: OKProcess 51 releasing lock on RID: 6:1:17495:4.......... |
Начало такое же, как и в T1, что, в общем, закономерно: IX – на таблицу, IU – на страницу. Затем U на первую запись, чтобы затем прочитать и выяснить, подходит она нам или нет. Если не подходит, снимаем блокировку и переходим к следующей записи.
Следующая запись подходит, и начинается тот же процесс, что и в первом запросе T1. Конвертируем блокировку на страницу в IX, на запись – в эксклюзивную (X), и производим обновление. Эта блокировка, как и в T1, не снимается. Опять же, поскольку сервер не знает, что он выбрал все записи, удовлетворяющие условию, указанному в WHERE, то он продолжает перебирать оставшиеся записи по очереди. Вот тут и начинаются отличия: Запись X = 4 (RID 6:1:17495:3) удерживается эксклюзивной блокировкой (X), наложенной T1, ведь T1 мы не зафиксировали. И как только T2 доберется до этой записи, то она будет вынуждена ждать на блокировке до тех пор, пока T1 не отменится или не зафиксируется, так как U и X блокировки не совместимы, что мы и наблюдаем:
Process 51 acquiring U lock on RID: 6:1:17495:2 (class bit0 ref1) result: OKWAIT |
Это ожидание длится до фиксации или, как в нашем случае, до отмены T1. Дальнейшие записи уже относятся к состоянию, когда T1 отменена, и в них нет ничего интересного, такой же последовательный перебор записей входящих в таблицу.
Теперь переключимся в первое окно к T1 и посмотрим, что происходило после старта второй части этой транзакции:
Process 53 acquiring IU lock on PAG: 6:1:17495 (class bit0 ref1) result: OKProcess 53 acquiring U lock on RID: 6:1:17495:0 (class bit0 ref1) result: OKProcess 53 releasing lock on RID: 6:1:17495:0Process 53 acquiring U lock on RID: 6:1:17495:1 (class bit0 ref1) result: TIMEDOUTProcess 53 sleeping for lockProcess 53 acquiring U lock on RID: 6:1:17495:1 (class bit0 ref1) result: DEADLOCK |
Запрашивается IU на страницу и U на первую запись, все для того же самого – чтобы убедиться, подходит запись или нет. Первая запись не подходит, и блокировка снимается. А вторая запись уже эксклюзивно заблокирована транзакцией T2, которая успела влезть между двумя Update'ами. Вот тут-то и происходит взаимоблокировка.
Рисунок 2. Взаимоблокировка из-за многократного перебора записей.
Без излишних подробностей можно описать происходящее примерно так:
T1 перебирает все записи по очереди, сначала блокируя их (U), убеждается, что запись не нужна и снимает блокировку, до тех пор пока не найдет нужную (x = 4), после чего, поднимает блокировку до X и производит запись. И, что важно, эта блокировка уже не снимается, а висит до конца транзакции.
T2 делает тоже самое. Она начинает перебирать записи, ставя и снимая блокировки, пока не находит нужную (x = 2). После этого она выполняет те же самые действия, что и первая транзакция - конвертацию блокировки в X, а затем запись. Опять-таки, эта блокировка (X) (и только эта) удерживается до фиксации или отмены T2. После этого перебор записей продолжается, так как не известно, все ли подходящие записи выбраны. Рано или поздно T2 натыкается на запись, уже заблокированную T1 (x=4), и вынуждена ждать либо фиксации, либо отмены T1.
Стартует второй оператор T1, со своим перебором, и натыкается на запись, уже заблокированную эксклюзивно (X) транзакцией T2 (x = 2).
Таким образом, T1 ждет T2, которая ждет T1 – взаимоблокировка.
Примечательно, что если на втором шаге T2 наткнется на запись, заблокированную T1, раньше чем найдет хотя бы одну запись, удовлетворяющую ее условию, то никакой мертвой блокировки не случится, так как все блокировки обновления (U), снимаются тут же, не дожидаясь конца транзакции. А значит, T2 ничего не удерживает, и не сможет выполняться дальше, пока T1 не завершиться.
Убедиться в этом можно, поменяв UPDATE в T2 таким образом:
UPDATE Tbl SET X=10 WHERE X=10 |
и запустив заново скрипты. Перебирая записи по очереди, T2 раньше доберется до x=4, чем до x=10, и не сможет заблокировать x=10, а будет ждать, пока освободится x=4.
С другой стороны, если и T1 и T2 нашли нужную запись и успешно сконвертировали блокировку, то, что найдет раньше второй оператор в T1, уже значения не имеет. Он все равно вынужден перебрать все записи, и рано или поздно вынужден будет ожидать T2, которая уже ждет T1.
Здесь опять-таки причина взаимоблокировки заключается в нарушении порядка доступа. Первая транзакция обращается дважды к одним и тем же записям, отпуская и снова накладывая блокировку. Если другая транзакция успевает захватить любой ресурс, вступив при этом в конфликт с первой, происходит взаимоблокировка. Это диктует способ лечения. Надо либо запретить второй транзакции втискиваться между первыми двумя, либо сделать так, чтобы они вообще не пересекались. В данном случае возможны следующие варианты:
Повысить уровень изоляции до SERIALIZABLE. При этом уровне изоляции все блокировки держатся до конца транзакции, таким образом, первый оператор T1 заблокирует до фиксации или отмены даже те записи, которые отбирались просто для проверки и под условие поиска не подпадали. Транзакция T2 будет вынуждена ждать в самом начале, не успев наложить ни одной блокировки, и не сможет помешать второму обновлению T1. А значит, сначала отработает T1 целиком, а потом уже T2.
С помощью специальной подсказки (hint) указать оптимизатору, что при обновлении записи блокировка должна производиться не по записям, а потаблично. Эффект будет тем же самым, что и при повышении уровня изоляции до SERIALIZABLE. T1 при первом же обращении заблокирует всю таблицу и будет удерживать блокировку до фиксации или отмены, а T2 не сможет захватить ни одну запись до тех пор, пока не отработает T1.
Построить индекс по X. В этом случае не будет никакой необходимости перебирать все записи по очереди. Пользуясь информацией из индекса, транзакции будут сразу обращаться к нужной записи. Таким образом, T1 и T2 никогда не понадобится одна и та же запись, и они не передерутся за ресурсы.