Заявление о слиянии само блокируется

22

У меня есть следующая процедура (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey формируют составной ключ для целевой таблицы. CompanyId - это внешний ключ родительской таблицы. Кроме того, существует некластеризованный индекс CompanyId asc, UserId asc.

Он вызывается из разных потоков, и я постоянно получаю взаимные блокировки между разными процессами, вызывающими одно и то же утверждение. Насколько я понимаю, «with (holdlock)» было необходимо для предотвращения ошибок состояния вставки / обновления гонки.

Я предполагаю, что два разных потока блокируют строки (или страницы) в разных порядках, когда они проверяют ограничения, и, таким образом, блокируются.

Это правильное предположение?

Как лучше всего разрешить эту ситуацию (то есть отсутствие взаимоблокировок, минимальное влияние на многопоточную производительность)?

Изображение плана запроса (Если вы просматриваете изображение на новой вкладке, оно доступно для чтения. Извините за небольшой размер.)

  • В @datatable не более 28 строк.
  • Я проследил весь код и нигде не вижу, чтобы мы начали транзакцию здесь.
  • Внешний ключ настроен на каскад только при удалении, и из родительской таблицы не было удалений.
Sako73
источник

Ответы:

12

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

  1. Часть MATCH в MERGE проверяет индекс на совпадения, блокируя чтение этих строк / страниц.

  2. Когда у него есть строка без совпадения, он сначала попытается вставить новую строку индекса, чтобы запросить блокировку записи строки / страницы ...

Но если другой пользователь также попал на шаг 1 в той же строке / странице, то первый пользователь будет заблокирован из обновления, и ...

Если второму пользователю также необходимо вставить на той же странице, то они в тупике.

AFAIK, есть только один (простой) способ быть на 100% уверенным, что вы не можете получить тупик с этой процедурой, и это будет добавить подсказку TABLOCKX к MERGE, но это, вероятно, очень плохо скажется на производительности.

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

Наконец, вы также можете попробовать добавить PAGLOCK, XLOCK или PAGLOCK и XLOCK. Опять же, это может сработать, а производительность может оказаться не такой уж ужасной. Вы должны попробовать это, чтобы увидеть.

RBarryYoung
источник
Как вы думаете, уровень изоляции снимка (версия строки) может быть полезным здесь?
Микаэль Эрикссон
Может быть. Или это может превратить исключения тупиковой ситуации в исключения параллелизма.
RBarryYoung
2
Указание подсказки TABLOCK для таблицы, которая является целью инструкции INSERT, имеет тот же эффект, что и указание подсказки TABLOCKX. (Источник: msdn.microsoft.com/en-us/library/bb510625.aspx )
tuespetre
31

Не было бы проблемы, если бы табличная переменная содержала только одно значение. С несколькими рядами появилась новая возможность тупика. Предположим, что два параллельных процесса (A & B) выполняются с табличными переменными, содержащими (1, 2) и (2, 1) для одной и той же компании.

Процесс A считывает место назначения, не находит строки и вставляет значение «1». Он содержит исключительную блокировку строки со значением '1'. Процесс B читает пункт назначения, не находит строки и вставляет значение '2'. Он содержит эксклюзивную блокировку строки со значением '2'.

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

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

Существующий план

Отсутствие согласованного порядка обработки строк приводит к возможности тупиковой ситуации. Второе соображение заключается в том, что отсутствие ключевой гарантии уникальности означает, что для обеспечения правильной защиты к Хэллоуину необходим Table Spool. Спул - это нетерпеливая шпуля, то есть все строки записываются на рабочий стол tempdb, а затем читаются и воспроизводятся для оператора Insert.

Переопределение TYPEтабличной переменной для включения в кластер PRIMARY KEY:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

План выполнения теперь показывает сканирование кластеризованного индекса, а гарантия уникальности означает, что оптимизатор может безопасно удалить таблицу Spool:

С первичным ключом

В тестах с 5000 итерациями MERGEоператора в 128 потоках не возникало взаимных блокировок с переменной кластеризованной таблицы. Я должен подчеркнуть, что это только на основе наблюдения; переменная кластеризованной таблицы также ( технически ) может производить свои строки в различных порядках, но шансы на согласованный порядок значительно повышаются. Разумеется, наблюдаемое поведение необходимо будет повторно тестировать для каждого нового накопительного обновления, пакета обновления или новой версии SQL Server.

В случае, если определение табличной переменной не может быть изменено, есть другая альтернатива:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Это также обеспечивает исключение буфера (и согласованности порядка строк) за счет введения явной сортировки:

План сортировки

Этот план также не приводил к тупикам при использовании того же теста. Сценарий воспроизведения ниже:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;
Пол Уайт говорит, что GoFundMonica
источник
8

Я думаю, что SQL_Kiwi предоставил очень хороший анализ. Если вам нужно решить проблему в базе данных, вы должны следовать его предложению. Конечно, вам нужно повторно протестировать, что он по-прежнему работает для вас каждый раз, когда вы обновляете, устанавливаете пакет обновления или добавляете / изменяете индекс или индексированное представление.

Есть три других варианта:

  1. Вы можете сериализовать ваши вставки так, чтобы они не сталкивались: вы можете вызвать sp_getapplock в начале вашей транзакции и получить эксклюзивную блокировку перед выполнением вашего MERGE. Конечно, вам все еще нужно провести стресс-тестирование.

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

  3. Вы можете автоматически повторить попытку после взаимоблокировок - это может быть самый медленный подход, если уровень параллелизма высок.

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

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

Мы в основном используем подход 1 в нашей системе. Это работает очень хорошо для нас.

Аляска
источник
-1

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

В неясном и далеком прошлом SQL Server имел опцию «Блокировка уровня вставки на уровне строки» - но это, похоже, умерло смертью, однако кластеризованный PK с идентификатором должен обеспечить чистоту вставок.

Эд Грин
источник