Каковы основные причины тупиков и их можно предотвратить?

55

Недавно одно из наших приложений ASP.NET показало ошибку взаимоблокировки базы данных, и меня попросили проверить и исправить ошибку. Мне удалось найти причину тупика - хранимой процедуры, которая строго обновляла таблицу внутри курсора.

Я впервые вижу эту ошибку и не знаю, как ее эффективно отследить и исправить. Я перепробовал все возможные способы и, наконец, обнаружил, что обновляемая таблица не имеет первичного ключа! К счастью, это был столбец идентификации.

Позже я обнаружил, что разработчик, который написал сценарий для базы данных, запутался. Я добавил первичный ключ, и проблема была решена.

Я чувствовал себя счастливым и вернулся к своему проекту, и провел некоторое исследование, чтобы выяснить причину этого тупика ...

По-видимому, это было круглое условие ожидания, которое вызвало тупик. Обновления, по-видимому, занимают больше времени без первичного ключа, чем с первичным ключом.

Я знаю, что это не очень четкий вывод, поэтому я публикую здесь ...

  • Проблема с отсутствующим первичным ключом?
  • Существуют ли другие условия, которые вызывают взаимоблокировку, кроме (взаимное исключение, удержание и ожидание, отсутствие прерывания и циклическое ожидание)?
  • Как мне предотвратить и отследить тупики?
CoderHawk
источник
2
Большинство IME (все?) Тупиков, которые я видел, происходят из-за циклического ожидания (в основном из-за чрезмерного использования триггеров).
Сатьяджит Бхат
Циркулярность - одно из необходимых условий тупика. Вы можете избежать любой тупиковой ситуации, если все ваши сеансы получат блокировки в одном и том же порядке.
Питер Г.

Ответы:

38

Отслеживание тупиков проще всего:

По умолчанию взаимоблокировки не записываются в журнал ошибок. Вы можете заставить SQL записывать взаимоблокировки в журнал ошибок с флагами трассировки 1204 и 3605.

Записать сведения о взаимоблокировке в журнал ошибок SQL Server: DBCC TRACEON (-1, 1204, 3605)

Выключите: DBCC TRACEOFF (-1, 1204, 3605)

См. «Устранение неполадок с тупиками» для обсуждения флага трассировки 1204 и вывода, который вы получите, когда он будет включен. https://msdn.microsoft.com/en-us/library/ms178104.aspx

Профилактика является более сложной, по сути, вы должны следить за следующим:

Кодовый блок 1 блокирует ресурс A, а затем ресурс B в этом порядке.

Кодовый блок 2 блокирует ресурс B, а затем ресурс A в этом порядке.

Это классическое условие, при котором может возникнуть тупик, если блокировка обоих ресурсов не является атомарной, кодовый блок 1 может заблокировать A и получить приоритет, тогда кодовый блок 2 блокирует B, прежде чем A вернет время обработки. Теперь у вас тупик.

Чтобы предотвратить это состояние, вы можете сделать что-то вроде следующего

Кодовый блок A (псевдо-код)

Lock Shared Resource Z
    Lock Resource A
    Lock Resource B
Unlock Shared Resource Z
...

Кодовый блок B (псевдокод)

Lock Shared Resource Z
    Lock Resource B
    Lock Resource A
Unlock Shared Resource Z
...

не забывая разблокировать A и B, когда закончите с ними

это предотвратит взаимоблокировку между блоком кода A и блоком кода B

С точки зрения базы данных, я не уверен, как избежать этой ситуации, так как блокировки обрабатываются самой базой данных, то есть блокировки строк / таблиц при обновлении данных. Там, где я видел, чаще всего возникают проблемы, где вы видели свои, внутри курсора. Курсоры, как известно, неэффективны, избегайте их, если это вообще возможно.

Черный лед
источник
Вы хотели заблокировать ресурс A перед ресурсом B в кодовом блоке B? Как написано, это приведет к тупикам ... как вы сами упомянули в комментариях ранее. Насколько это возможно, вы хотите всегда блокировать ресурсы в одном и том же порядке, даже если вначале вам нужны фиктивные запросы для обеспечения этого порядка блокировки.
Джерард ONEILL
23

Мои любимые статьи для чтения и изучения взаимоблокировок: Simple Talk - отслеживание взаимоблокировок и SQL Server Central - Использование Profiler для устранения взаимоблокировок . Они дадут вам образцы и советы о том, как справиться с ситуацией.

Короче говоря, чтобы решить текущую проблему, я бы сократил вовлеченные транзакции, вынул из них ненужную часть, позаботился о порядке использования объектов, посмотрел, какой уровень изоляции действительно необходим, а не прочитал ненужные. данные...

Но лучше читайте статьи, они будут намного лучше в советах.

Мэриан
источник
16

Иногда тупиковая ситуация может быть устранена путем добавления индексации, так как она позволяет базе данных блокировать отдельные записи, а не всю таблицу, что снижает вероятность конфликтов и вероятность возникновения проблем.

Например, в InnoDB :

Если у вас нет индексов, подходящих для вашего оператора, и MySQL должен сканировать всю таблицу, чтобы обработать оператор, каждая строка таблицы блокируется, что, в свою очередь, блокирует все вставки других пользователей в таблицу. Важно создать хорошие индексы, чтобы ваши запросы не сканировали много строк без необходимости.

Другое распространенное решение - отключить согласованность транзакций, когда это не нужно, или иным образом изменить уровень изоляции , например, длительное задание для вычисления статистики ... обычно достаточно точного ответа, вам не нужны точные числа, как они меняются из-под тебя. И если это займет 30 минут, вам не нужно останавливать все другие транзакции в этих таблицах.

...

Что касается их отслеживания, это зависит от программного обеспечения базы данных, которое вы используете.

Джо
источник
Это обычная вежливость - давать комментарии при понижении голосов ... Это правильный ответ, утверждение select, переходящее на блокировку таблицы и принимающее навсегда, может, безусловно, вызвать тупик.
BlackICE
1
MS SQLServer также может дать неожиданное поведение блокировки, если индексы не кластеризованы. Он будет молчаливо игнорировать ваше направление использования блокировки на уровне строк и будет выполнять блокировку на уровне страниц. Затем вы можете получить тупики, ожидающие на странице.
Джей
7

Просто разработать на курсоре вещь. это действительно очень плохо. Он блокирует всю таблицу, а затем обрабатывает строки одну за другой.

Лучше проходить строки в виде курсора, используя цикл while

В цикле while выбор будет выполняться для каждой строки в цикле, и блокировка будет происходить только для одной строки за раз. Остальные данные в таблице бесплатны для запросов, что снижает вероятность возникновения тупиковых ситуаций.

Плюс это быстрее. Заставляет задуматься, почему в любом случае есть курсоры.

Вот пример такой структуры:

DECLARE @LastID INT = (SELECT MAX(ID) FROM Tbl)
DECLARE @ID     INT = (SELECT MIN(ID) FROM Tbl)
WHILE @ID <= @LastID
    BEGIN
    IF EXISTS (SELECT * FROM Tbl WHERE ID = @ID)
        BEGIN
        -- Do something to this row of the table
        END

    SET @ID += 1  -- Don't forget this part!
    END

Если ваше поле идентификатора невелико, вы можете выбрать отдельный список идентификаторов и повторить его:

DECLARE @IDs TABLE
    (
    Seq INT NOT NULL IDENTITY PRIMARY KEY,
    ID  INT NOT NULL
    )
INSERT INTO @IDs (ID)
    SELECT ID
    FROM Tbl
    WHERE 1=1  -- Criteria here

DECLARE @Rec     INT = 1
DECLARE @NumRecs INT = (SELECT MAX(Seq) FROM @IDs)
DECLARE @ID      INT
WHILE @Rec <= @NumRecs
    BEGIN
    SET @ID = (SELECT ID FROM @IDs WHERE Seq = @Seq)

    -- Do something to this row of the table

    SET @Seq += 1  -- Don't forget this part!
    END
Николя де Фонтене
источник
6

Отсутствие первичного ключа не является проблемой. По крайней мере, само собой. Во-первых, вам не нужен первичный, чтобы иметь индексы. Во-вторых, даже если вы выполняете сканирование таблицы (что должно произойти, если ваш конкретный запрос не использует индекс, блокировка таблицы сама по себе не вызовет взаимоблокировку. Процесс записи будет ожидать чтения, а процесс чтения - ждать записи, и, конечно, чтения не придется ждать друг друга вообще.

В дополнение к другим ответам, уровень изоляции транзакции имеет значение, потому что повторяемое чтение и сериализация - это то, что вызывает удержание блокировок «чтение» до конца транзакции. Блокировка ресурса не вызывает тупик. Держать его под замком делает. Операции записи всегда держат свой ресурс заблокированным до конца транзакции.

Моя любимая стратегия предотвращения блокировок - использовать функции «снимка». Функция Read Committed Snapshot означает, что чтение не использует блокировки! И если вам нужно больше контроля, чем «Зафиксировано чтение», есть функция «Уровень изоляции моментального снимка». Это позволяет выполнять сериализованную (с использованием здесь терминов MS) транзакцию, не блокируя других игроков.

Наконец, один класс взаимоблокировок можно предотвратить с помощью блокировки обновления. Если вы читаете и удерживаете чтение (HOLD, или с помощью Repeatable Read), и другой процесс делает то же самое, тогда оба пытаются обновить одни и те же записи, у вас будет тупик. Но если оба запрашивают блокировку обновления, второй процесс будет ожидать первого, одновременно позволяя другим процессам читать данные с помощью общих блокировок, пока данные не будут фактически записаны. Это, конечно, не будет работать, если один из процессов все еще запрашивает общую блокировку HOLD.

Жерар ОНЕЙЛ
источник
-2

В то время как курсоры работают медленно в SQL Server, вы можете избежать взаимоблокировки курсора, потянув исходные данные для курсора в таблицу Temp и запустив курсор на нем. Это удерживает курсор от блокировки таблицы фактических данных, и единственные блокировки, которые вы получаете, предназначены для обновлений или вставок, выполняемых внутри курсора, которые удерживаются только на время вставки / обновления, а не на время курсора.

Билл Джойс
источник