В одной из наших баз данных у нас есть таблица, к которой интенсивно одновременно обращается несколько потоков. Потоки обновляют или вставляют строки через 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
все еще открыт. И если это приемлемо, можно ли что-то сделать, чтобы предотвратить блокировку объекта?