MERGE предотвращение блокировки

9

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

индекс выполнения плана поиска с nvarchar

Так как используется поиск, НО в этом случае снова показывает Xблокировку, установленную на объекте IX.

Таким образом, кажется, что принудительный поиск не обязательно гарантирует гранулярные блокировки (и отсутствие тупиков отсюда). Я не уверен, что кластерный индекс гарантирует гранулярную блокировку. Или это?

Мое понимание (поправьте меня, если я ошибаюсь) заключается в том, что блокировка в значительной степени ситуативна, и определенная форма плана выполнения не предполагает определенной схемы блокировки.

Вопрос о возможности установки Xблокировки на объект после того, как IXвсе еще открыт. И если это приемлемо, можно ли что-то сделать, чтобы предотвратить блокировку объекта?

я-один
источник
Соответствующий запрос на feedback.azure.com
i-one

Ответы:

9

Ставит IXзатем Xна объект Пригодные? Это ошибка или нет?

Это выглядит немного странно, но это действительно. В то время, когда IXоно взято, вполне может быть намерение взять Xзамки на более низком уровне. Там нет ничего, чтобы сказать, что такие замки на самом деле должны быть взяты. В конце концов, на нижнем уровне может быть нечего блокировать; двигатель не может знать об этом раньше времени. Кроме того, могут быть оптимизации, позволяющие пропускать блокировки более низкого уровня (пример ISи Sблокировки можно посмотреть здесь ).

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

С другой стороны, блокировка сложна, и намеренные блокировки иногда могут быть приняты по внутренним причинам, не обязательно связанным с намерением взять блокировки более низкого уровня. Взятие IXможет быть наименее инвазивным способом обеспечения требуемой защиты для некоторых непонятных крайних случаев. Для аналогичного рассмотрения см. Shared Lock, выпущенный на IsolationLevel.ReadUncommitted .

Итак, текущая ситуация неудачна для вашего тупикового сценария, и ее в принципе можно избежать, но это не обязательно то же самое, что быть «ошибкой». Вы можете сообщить о проблеме через обычный канал поддержки или через Microsoft Connect, если вам нужен точный ответ на этот вопрос.

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

Нет, FORCESEEKэто не намек, а скорее директива. Если оптимизатор не может найти план, который учитывает «подсказку», он выдаст ошибку.

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

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

... объявление переменной от varchar(200)до nvarchar(200)...

Это нарушает гарантию того, что будет затронута одна строка, поэтому для защиты Хэллоуина введена Eager Table Spool. В качестве дополнительного обходного пути для этого сделайте гарантию явной с помощью MERGE TOP (1) INTO [Cache]....

Насколько я понимаю, [...] блокировка в значительной степени ситуативна, и определенная форма плана выполнения не предполагает определенной схемы блокировки.

Конечно, в плане выполнения происходит гораздо больше. Вы можете принудительно сформировать определенную форму плана, например, с помощью направляющей плана, но двигатель все равно может решить использовать другие блокировки во время выполнения. Шансы довольно низкие, если вы включите TOP (1)элемент выше.

Основные пометки

Несколько необычно видеть таблицу кучи, используемую таким образом. Вы должны рассмотреть преимущества преобразования его в кластеризованную таблицу, возможно, используя индекс, предложенный Даном Гузманом в комментарии:

CREATE UNIQUE CLUSTERED INDEX IX_Cache ON [Cache] ([ItemKey]);

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

MERGEтакже немного необычно видеть в среде с высокой степенью параллелизма. Несколько нелогично, но зачастую более эффективно выполнять отдельные операторы INSERTи UPDATEоператоры, например:

DECLARE
    @itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
    @fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';

BEGIN TRANSACTION;

    DECLARE @expires datetime2(2) = DATEADD(MINUTE, 10, SYSDATETIME());

    UPDATE TOP (1) dbo.Cache WITH (SERIALIZABLE, UPDLOCK)
    SET [FileName] = @fileName,
        Expires = @expires
    OUTPUT Deleted.[FileName]
    WHERE
        ItemKey = @itemKey;

    IF @@ROWCOUNT = 0
        INSERT dbo.Cache
            (ItemKey, [FileName], Expires)
        VALUES
            (@itemKey, @fileName, @expires);

COMMIT TRANSACTION;

Обратите внимание, что поиск RID больше не нужен:

План выполнения

Если вы можете гарантировать существование уникального индекса ItemKey(как в вопросе), то избыточное значение TOP (1)в UPDATEможет быть удалено, что упрощает план:

Упрощенное обновление

Оба плана INSERTи UPDATEпланы могут претендовать на тривиальный план в любом случае. MERGEвсегда требует полной оптимизации на основе затрат.

Информацию о правильном шаблоне для использования и дополнительную информацию см. В соответствующей статье « Вопросы и ответы по SQL Server 2014»MERGE .

Замки не всегда могут быть предотвращены. Они могут быть сведены к минимуму при тщательном кодировании и проектировании, но приложение всегда должно быть готово к корректной обработке нечетного тупика (например, перепроверить условия и повторить попытку).

Если у вас есть полный контроль над процессами, которые обращаются к рассматриваемому объекту, вы можете также рассмотреть возможность использования блокировок приложений для сериализации доступа к отдельным элементам, как описано в SQL Server Concurrent Inserts and Deletes .

Пол Уайт 9
источник