В одной из наших баз данных у нас есть таблица, к которой интенсивно одновременно обращается несколько потоков. Потоки обновляют или вставляют строки через MERGE
. Есть также потоки, которые время от времени удаляют строки, поэтому данные таблицы очень изменчивы. Потоки, выполняющие upserts, иногда страдают от взаимоблокировки. Проблема выглядит аналогично описанной в этом вопросе. Разница, однако, в том, что в нашем случае каждый поток обновляет или вставляет ровно одну строку .
Упрощенная настройка следующая. Таблица является кучей с двумя уникальными некластеризованными индексами
CREATE TABLE [Cache]
(
[UID] uniqueidentifier NOT NULL CONSTRAINT DF_Cache_UID DEFAULT (newid()),
[ItemKey] varchar(200) NOT NULL,
[FileName] nvarchar(255) NOT NULL,
[Expires] datetime2(2) NOT NULL,
CONSTRAINT [PK_Cache] PRIMARY KEY NONCLUSTERED ([UID])
)
GO
CREATE UNIQUE INDEX IX_Cache ON [Cache] ([ItemKey]);
GO
и типичный запрос
DECLARE
@itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
@fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';
MERGE INTO [Cache] WITH (HOLDLOCK) T
USING (
VALUES (@itemKey, @fileName, dateadd(minute, 10, sysdatetime()))
) S(ItemKey, FileName, Expires)
ON T.ItemKey = S.ItemKey
WHEN MATCHED THEN
UPDATE
SET
T.FileName = S.FileName,
T.Expires = S.Expires
WHEN NOT MATCHED THEN
INSERT (ItemKey, FileName, Expires)
VALUES (S.ItemKey, S.FileName, S.Expires)
OUTPUT deleted.FileName;
то есть сопоставление происходит по уникальному ключу индекса. Намек HOLDLOCK
здесь, из-за параллелизма (как рекомендовано здесь ).
Я провел небольшое расследование, и вот что я нашел.
В большинстве случаев план выполнения запроса
со следующей схемой блокировки
т.е. IX
замок на объекте, сопровождаемый более детальными замками.
Иногда, однако, план выполнения запроса отличается
(эту форму плана можно форсировать, добавив INDEX(0)
подсказку), и ее схема блокировки
X
блокировка уведомления на объекте IX
уже установлена
Поскольку два IX
совместимы, а два X
нет, то при параллельности
тупик !
И тут возникает первая часть вопроса . Устанавливает ли X
замок на объект после того, как IX
имеет право? Разве это не ошибка?
Документация гласит:
Преднамеренные блокировки называются намеренными блокировками, потому что они получены перед блокировкой на более низком уровне, и, следовательно, сигнализируют о намерении установить блокировки на более низком уровне .
а также
IX означает намерение обновить только некоторые строки, а не все
Итак, установка X
блокировки на объекте после IX
выглядит ОЧЕНЬ подозрительно для меня.
Сначала я попытался предотвратить взаимоблокировку, пытаясь добавить подсказки блокировки таблицы
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCK) T
а также
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCKX) T
с TABLOCK
узором блокировки на месте становится
и с TABLOCKX
шаблоном блокировки
поскольку два SIX
(а также два X
) несовместимы, это эффективно предотвращает взаимоблокировку, но, к сожалению, также предотвращает параллелизм (что нежелательно).
Моими следующими попытками было добавить PAGLOCK
и ROWLOCK
сделать блокировки более детальными и уменьшить конфликт. И то, и другое не оказывает влияния ( X
на объекте все еще наблюдалось сразу после IX
).
Моя последняя попытка была заставить "хорошую" форму плана выполнения с хорошей гранулярной блокировкой, добавив FORCESEEK
подсказку
MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T
и это сработало.
И тут возникает вторая часть вопроса . Может ли это случиться так, что FORCESEEK
будет проигнорировано и будет использоваться неправильная схема блокировки? (Как я уже упоминал, PAGLOCK
и, ROWLOCK
казалось бы , были проигнорированы).
Добавление не UPDLOCK
имеет никакого эффекта ( X
на объекте, все еще наблюдаемом после IX
).
Создание IX_Cache
кластерного индекса, как и ожидалось, сработало. Это привело к планированию с Clustered Index Seek и гранулярной блокировкой. Кроме того, я попытался заставить Clustered Index Scan, которая также показала гранулярную блокировку.
Однако. Дополнительное наблюдение. В исходной настройке даже при FORCESEEK(IX_Cache(ItemKey)))
наличии одного изменения @itemKey
объявления переменной с varchar (200) на nvarchar (200) план выполнения становится
Так как используется поиск, НО в этом случае снова показывает X
блокировку, установленную на объекте IX
.
Таким образом, кажется, что принудительный поиск не обязательно гарантирует гранулярные блокировки (и отсутствие тупиков отсюда). Я не уверен, что кластерный индекс гарантирует гранулярную блокировку. Или это?
Мое понимание (поправьте меня, если я ошибаюсь) заключается в том, что блокировка в значительной степени ситуативна, и определенная форма плана выполнения не предполагает определенной схемы блокировки.
Вопрос о возможности установки X
блокировки на объект после того, как IX
все еще открыт. И если это приемлемо, можно ли что-то сделать, чтобы предотвратить блокировку объекта?
Ответы:
Это выглядит немного странно, но это действительно. В то время, когда
IX
оно взято, вполне может быть намерение взятьX
замки на более низком уровне. Там нет ничего, чтобы сказать, что такие замки на самом деле должны быть взяты. В конце концов, на нижнем уровне может быть нечего блокировать; двигатель не может знать об этом раньше времени. Кроме того, могут быть оптимизации, позволяющие пропускать блокировки более низкого уровня (примерIS
иS
блокировки можно посмотреть здесь ).Более конкретно, для текущего сценария верно, что сериализуемые блокировки диапазона ключей недоступны для кучи, поэтому единственной альтернативой является
X
блокировка на уровне объекта. В этом смысле механизм может быть в состоянии обнаружить на раннем этапе, чтоX
блокировка неизбежно потребуется, если метод доступа представляет собой сканирование кучи, и, таким образом, избегайте взятияIX
блокировки.С другой стороны, блокировка сложна, и намеренные блокировки иногда могут быть приняты по внутренним причинам, не обязательно связанным с намерением взять блокировки более низкого уровня. Взятие
IX
может быть наименее инвазивным способом обеспечения требуемой защиты для некоторых непонятных крайних случаев. Для аналогичного рассмотрения см. Shared Lock, выпущенный на IsolationLevel.ReadUncommitted .Итак, текущая ситуация неудачна для вашего тупикового сценария, и ее в принципе можно избежать, но это не обязательно то же самое, что быть «ошибкой». Вы можете сообщить о проблеме через обычный канал поддержки или через Microsoft Connect, если вам нужен точный ответ на этот вопрос.
Нет,
FORCESEEK
это не намек, а скорее директива. Если оптимизатор не может найти план, который учитывает «подсказку», он выдаст ошибку.Форсирование индекса - это способ обеспечить блокировку диапазона ключей. Вместе с блокировками обновления, которые обычно используются при обработке метода доступа для изменения строк, это обеспечивает достаточную гарантию во избежание проблем параллелизма в вашем сценарии.
Если схема таблицы не изменяется (например, добавляется новый индекс), подсказки также достаточно, чтобы избежать взаимоблокировки этого запроса с самим собой. Существует возможность циклического тупика с другими запросами, которые могут получить доступ к куче перед некластеризованным индексом (например, обновление ключа некластеризованного индекса).
Это нарушает гарантию того, что будет затронута одна строка, поэтому для защиты Хэллоуина введена Eager Table Spool. В качестве дополнительного обходного пути для этого сделайте гарантию явной с помощью
MERGE TOP (1) INTO [Cache]...
.Конечно, в плане выполнения происходит гораздо больше. Вы можете принудительно сформировать определенную форму плана, например, с помощью направляющей плана, но двигатель все равно может решить использовать другие блокировки во время выполнения. Шансы довольно низкие, если вы включите
TOP (1)
элемент выше.Основные пометки
Несколько необычно видеть таблицу кучи, используемую таким образом. Вы должны рассмотреть преимущества преобразования его в кластеризованную таблицу, возможно, используя индекс, предложенный Даном Гузманом в комментарии:
Это может иметь важные преимущества повторного использования пространства, а также обеспечивает хороший обходной путь для текущей проблемы взаимоблокировки.
MERGE
также немного необычно видеть в среде с высокой степенью параллелизма. Несколько нелогично, но зачастую более эффективно выполнять отдельные операторыINSERT
иUPDATE
операторы, например:Обратите внимание, что поиск RID больше не нужен:
Если вы можете гарантировать существование уникального индекса
ItemKey
(как в вопросе), то избыточное значениеTOP (1)
вUPDATE
может быть удалено, что упрощает план:Оба плана
INSERT
иUPDATE
планы могут претендовать на тривиальный план в любом случае.MERGE
всегда требует полной оптимизации на основе затрат.Информацию о правильном шаблоне для использования и дополнительную информацию см. В соответствующей статье « Вопросы и ответы по SQL Server 2014»
MERGE
.Замки не всегда могут быть предотвращены. Они могут быть сведены к минимуму при тщательном кодировании и проектировании, но приложение всегда должно быть готово к корректной обработке нечетного тупика (например, перепроверить условия и повторить попытку).
Если у вас есть полный контроль над процессами, которые обращаются к рассматриваемому объекту, вы можете также рассмотреть возможность использования блокировок приложений для сериализации доступа к отдельным элементам, как описано в SQL Server Concurrent Inserts and Deletes .
источник