Есть один баг, который выдает ошибку такого рода:
Transaction (Process ID 51) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
Так вот, нашел интересную статью на RSDN`е.
И для того что юы не забыть её содержимое когда дочитаю до конца, сюда я буду записывать найболее значимые тезы.
Поехали.
Статья называется
Deadlocks Что такое взаимоблокировки и как с ними бороться
«Блокировка», в отличие от «взаимоблокировки», явление совершенно обычное, и означает лишь то, что транзакция получит некий ресурс в свое распоряжение не сразу, а чуть-чуть подождав, пока другая транзакция не снимет с этого ресурса блокировку, наложенную ранее.
Синхронизация происходит благодаря тому, что прежде чем прозвести с объектом какие-то действия (прочитать или изменить), на него накладывается блокировка. Она запрещает изменять или даже читать объект другим транзакциям до тех пор, пока транзакция, наложившая блокировку, не завершит работу с этим объектом.
Типы блокировок
Поскольку запрос может быть как на чтение, так и на запись, то блокировки для этих случаев так же отличаются, вдобавок существует еще и промежуточный тип блокировки.
Read Lock – блокировка чтения, она же «коллективная», она же «разделяемая». Смысл этой блокировки в том, что она совместима с точно такими же блокировками. Иными словами, на один и тот же ресурс может быть наложено сколь угодно много коллективных блокировок. В терминологии MSSQL эта блокировка называется Shared Lock, или сокращенно S.
Write Lock – блокировка записи, она же «монопольная», она же «эксклюзивная». Эта блокировка не совместима ни с Read Lock, ни сама с собой, ни с каким либо другим типом блокировок. То есть в один момент времени на один объект может быть наложена только одна монопольная блокировка. Эта блокировка в терминологии MSSQL называется Exclusive Lock, или же сокращенно X.
Update Lock – это промежуточная блокировка, блокировка «обновления». Она совместима с Read Lock, но не совместима с Write Lock и сама с собой. Иными словами на один объект могут быть одновременно наложены одна блокировка обновления, ни одной монопольной блокировки и сколь угодно много коллективных блокировок. Этот тип блокировок введен как раз для снижения риска возникновения взаимоблокировки. Каким именно образом, будет объяснено ниже.
Взаимоблокировка
Большинство способов обеспечения параллелизма, хотя бы отчасти основанных на блокировках, подвержено взаимоблокировкам (deadlock). И хотя известны достаточно остроумные алгоритмы, позволяющие не допускать подобных ситуаций в принципе, в коммерческих приложениях они почти не встречаются. Microsoft SQL Server здесь не является исключением, и также подвержен взаимоблокировкам (они же «мертвые блокировки» или «тупиковые ситуации»).
Взаимоблокировка, как можно понять из названия – это ситуация, когда транзакции блокируют друг друга таким образом, что дальнейшее выполнение невозможно. В силу протокола двухфазной блокировки ни одна из участвующих во взаимоблокировке транзакций не может отпустить уже захваченные ей ресурсы до того, как наложит блокировки на все, что ей необходимо. А получить все необходимые ресурсы мешают уже наложенные блокировки. Таким образом, получается замкнутый круг. Естественно, и транзакций, и объектов в общем случае может быть сколь угодно много. Разорвать такую блокировку без внешнего вмешательства невозможно, и если не предпринимать специальных усилий, то транзакции будут находиться в состоянии ожидания бесконечно долго. Разрешить подобную ситуацию можно лишь путем отмены хотя бы одной из транзакций.
imestamp based
Существуют механизмы, позволяющие вообще не допускать тупиковых ситуаций при использовании протокола двухфазной блокировки, например, на основе временных меток (timestamp).
ПРИМЕЧАНИЕ
Здесь главное – не запутаться. Существует как способ обеспечения параллелизма на основе временных меток – это одна из альтернатив протоколу двухфазной блокировки, так и способ предотвращения тупиковых ситуаций. Это два совершенно разных механизма, и сейчас мы обсуждаем именно способ предотвращения взаимоблокировок.
Принцип, положенный в основу этого способа, достаточно прост. Каждой транзакции присваивается временная метка, а далее возможно два варианта развития событий в зависимости от конкретной реализации.
- «ожидание-гибель» (wait-die). Если транзакция T1 «старше» Т2, тогда транзакции Т1 разрешается пребывать в состоянии ожидания на блокировке. Если же Т1 «младше» T2, тогда Т1 откатывается.
- «ранение-ожидание» (wound-wait). Если транзакция T1 «старше» T2, тогда T1 «ранит» T2; ранение обычно носит «смертельный» характер – транзакция Т2 откатывается, если только к моменту получения «ранения» T2 не оказывается уже завершенной. В этом случае Т2 «выживает» и отката не происходит. Если же Т1 «младше» Т2, тогда Т1 разрешается находиться в состоянии ожидания на блокировке.
-
Timeout based
Самый простой способ – это ввести некоторое фиксированное время ожидания (timeout), и если транзакция оказалась заблокированной больше этого времени, то считать, что она вошла в тупиковую ситуацию и отменять ее. Недостатки этого способа очевидны – нет стопроцентной гарантии, что отмененная транзакция была одной из участниц взаимоблокировки. Увеличение времени тайм-аута повышает эту вероятность, но одновременно увеличивает время обнаружения действительно намертво заблокированных транзакций. А это, в свою очередь, ведет к увеличению времени простоя запросов, не участвующих во взаимоблокировке, но ожидающих ресурсов, захваченных намертво заблокированными транзакциями.
Wait-for graph based
Существуют и более удачный способ определения взаимоблокировок (хотя и более трудоемкий). Для этого менеджер блокировок строит направленный граф, который называется «графом ожидания» (wait-for graph). В вершинах этого графа находятся транзакции, а в ребрах – зависимости. Например, ребро Ti->Tj появляется в том случае, если Ti ждет, пока Tj освободит какой-нибудь объект. Таким образом, если в графе ожидания возникает цикл (T1->T2->…->Tn->T1), то T1 ждет сама себя, как и все остальные n транзакций в цикле, следовательно, транзакции заблокированы намертво. В данном случае обнаружение взаимоблокировок сводится к нахождению замкнутых циклов в графе ожидания. Сами зависимости в граф добавляются и уничтожаются по мере получения и снятия блокировок, технически в этом ничего сложного нет. Сложность лишь в том, как часто менеджер блокировок должен проверять граф ожидания на наличие циклов. Теоретически это можно делать каждый раз при добавлении новой зависимости, однако делать проверки так часто слишком накладно, поскольку, как правило, количество обычных блокировок намного выше мертвых, к тому же сама взаимоблокировка никуда не денется и дождется, пока за ней придут. Поэтому проверять наличие циклов можно либо когда в граф добавляется какое-то фиксированное количество граней, либо опять же, по истечении некоего таймаута. Но здесь, в отличие от предыдущего способа, гарантируется, что будет найдена именно мертвая блокировка, а также, что мы обнаружим все мертвые блокировки, а не только те, которые продержались достаточно долго.
Вдобавок здесь сервер обладает куда большей информацией о тупиковой ситуации, что позволяет ему с меньшими потерями разрешить конфликт. Разорвать взаимоблокировку, как уже говорилось, можно лишь отменив одну из транзакций, входящих в замкнутый цикл. Сервер при выборе жертвы может руководствоваться следующими соображениями:
- Объем работы, проделанный транзакцией (вся эта работа будет утеряна в случае отмены).
- Количество работы, которое придется проделать, чтобы произвести отмену транзакции. Менеджер должен стараться избегать отмены транзакции, которая практически завершена.
- Количество циклов, в которых участвует транзакция. Теоретически транзакция может входить в несколько циклов в графе ожидания, таким образом, отмена одной транзакции может привести к снятию нескольких взаимоблокировок.
- Одна и та же транзакция может несколько раз подряд войти в тупиковую ситуацию и быть отмененной, перезапуститься и опять попасть в цикл. Чтобы избежать подобного циклического рестарта, алгоритм выбора жертвы должен также учитывать, сколько раз транзакция была отменена из-за мертвых блокировок.
Timeout based
Самый простой способ – это ввести некоторое фиксированное время ожидания (timeout), и если транзакция оказалась заблокированной больше этого времени, то считать, что она вошла в тупиковую ситуацию и отменять ее. Недостатки этого способа очевидны – нет стопроцентной гарантии, что отмененная транзакция была одной из участниц взаимоблокировки. Увеличение времени тайм-аута повышает эту вероятность, но одновременно увеличивает время обнаружения действительно намертво заблокированных транзакций. А это, в свою очередь, ведет к увеличению времени простоя запросов, не участвующих во взаимоблокировке, но ожидающих ресурсов, захваченных намертво заблокированными транзакциями.
Wait-for graph based
Существуют и более удачный способ определения взаимоблокировок (хотя и более трудоемкий). Для этого менеджер блокировок строит направленный граф, который называется «графом ожидания» (wait-for graph). В вершинах этого графа находятся транзакции, а в ребрах – зависимости. Например, ребро Ti->Tj появляется в том случае, если Ti ждет, пока Tj освободит какой-нибудь объект. Таким образом, если в графе ожидания возникает цикл (T1->T2->…->Tn->T1), то T1 ждет сама себя, как и все остальные n транзакций в цикле, следовательно, транзакции заблокированы намертво. В данном случае обнаружение взаимоблокировок сводится к нахождению замкнутых циклов в графе ожидания. Сами зависимости в граф добавляются и уничтожаются по мере получения и снятия блокировок, технически в этом ничего сложного нет. Сложность лишь в том, как часто менеджер блокировок должен проверять граф ожидания на наличие циклов. Теоретически это можно делать каждый раз при добавлении новой зависимости, однако делать проверки так часто слишком накладно, поскольку, как правило, количество обычных блокировок намного выше мертвых, к тому же сама взаимоблокировка никуда не денется и дождется, пока за ней придут. Поэтому проверять наличие циклов можно либо когда в граф добавляется какое-то фиксированное количество граней, либо опять же, по истечении некоего таймаута. Но здесь, в отличие от предыдущего способа, гарантируется, что будет найдена именно мертвая блокировка, а также, что мы обнаружим все мертвые блокировки, а не только те, которые продержались достаточно долго.
Вдобавок здесь сервер обладает куда большей информацией о тупиковой ситуации, что позволяет ему с меньшими потерями разрешить конфликт. Разорвать взаимоблокировку, как уже говорилось, можно лишь отменив одну из транзакций, входящих в замкнутый цикл. Сервер при выборе жертвы может руководствоваться следующими соображениями:
- Объем работы, проделанный транзакцией (вся эта работа будет утеряна в случае отмены).
- Количество работы, которое придется проделать, чтобы произвести отмену транзакции. Менеджер должен стараться избегать отмены транзакции, которая практически завершена.
- Количество циклов, в которых участвует транзакция. Теоретически транзакция может входить в несколько циклов в графе ожидания, таким образом, отмена одной транзакции может привести к снятию нескольких взаимоблокировок.
- Одна и та же транзакция может несколько раз подряд войти в тупиковую ситуацию и быть отмененной, перезапуститься и опять попасть в цикл. Чтобы избежать подобного циклического рестарта, алгоритм выбора жертвы должен также учитывать, сколько раз транзакция была отменена из-за мертвых блокировок.