Устранение тупика из 2 таблиц, связанных только через индексированное представление

17

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

Это в производственной среде под управлением SQL Server 2008 R2.

Чтобы дать вам несколько упрощенное представление о ситуации:


У меня есть 3 таблицы, как указано ниже:

TABLE activity (
    id, -- PK
    ...
)

TABLE member_activity (
    member_id, -- PK col 1
    activity_id, -- PK col 2
    ...
)

TABLE follow (
    id, -- PK
    follower_id,
    member_id,
    ...
)

member_activityТаблица имеет соединение Первичный ключ определяется как member_id, activity_id, потому что я только когда - либо нужно искать данные в этой таблице таким образом.

У меня также есть некластеризованный индекс follow:

CREATE NONCLUSTERED INDEX [IX_follow_member_id_includes] 
ON follow ( member_id ASC ) INCLUDE ( follower_id )

Кроме того, у меня есть привязанное к схеме представление, network_activityкоторое определяется следующим образом:

CREATE VIEW network_activity
WITH SCHEMABINDING
AS

SELECT
    follow.follower_id as member_id,
    member_activity.activity_id as activity_id,
    COUNT_BIG(*) AS cb
FROM member_activity
INNER JOIN follow ON follow.member_id = member_activity.member_id
INNER JOIN activity ON activity.id = member_activity.activity_id
GROUP BY follow.follower_id, member_activity.activity_id

Который также имеет уникальный кластерный индекс:

CREATE UNIQUE CLUSTERED INDEX [IX_network_activity_unique_member_id_activity_id] 
ON network_activity
(
    member_id ASC,
    activity_id ASC
)

Теперь у меня есть две заблокированные хранимые процедуры. Они проходят следующий процесс:

-- SP1: insert activity
-----------------------
INSERT INTO activity (...)
SELECT ... FROM member_activity WHERE member_id = @a AND activity_id = @b
INSERT INTO member_activity (...)


-- SP2: insert follow
---------------------
SELECT follow WHERE member_id = @x AND follower_id = @y
INSERT INTO follow (...)

Эти 2 процедуры выполняются в режиме READ COMMITTED. Мне удалось запросить выходные данные расширенных событий 1222 и интерпретировать следующее в отношении взаимоблокировок:

SP1 ожидает RangeS-Sблокировки ключа для IX_follow_member_id_includesиндекса, в то время как SP2 содержит конфликтующую (X) блокировку

SP2 ожидает Sблокировки режима, PK_member_activity а SP1 держит конфликтующую (X) блокировку

Кажется, что тупик возникает в последней строке каждого запроса (вставки). Что мне неясно, так это то, почему SP1 хочет блокировки IX_follow-member_id_includesиндекса. Мне кажется, что единственная ссылка - это индексированное представление, поэтому я включил его.

Что было бы для меня лучшим способом предотвратить возникновение этих тупиков? Любая помощь приветствуется. У меня нет большого опыта в решении тупиковых ситуаций.

Пожалуйста, дайте мне знать, если есть какая-то дополнительная информация, которая может вам помочь!

Заранее спасибо.


Редактировать 1: Добавление дополнительной информации для каждого запроса.

Вот выход 1222 из этого тупика:

<deadlock>
    <victim-list>
        <victimProcess id="process4c6672748" />
    </victim-list>
    <process-list>
        <process id="process4c6672748" taskpriority="0" logused="332" waitresource="KEY: 8:72057594104905728 (25014f77eaba)" waittime="581" ownerId="474698706" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.287" XDES="0x298487970" lockMode="RangeS-S" schedulerid="1" kpid="972" status="suspended" spid="79" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T10:25:00.283" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698706" currentdb="8" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
            <executionStack>
                <frame procname="" line="7" stmtstart="1194" stmtend="1434" sqlhandle="0x02000000a26bb72a2b220406876cad09c22242e5265c82e6" />
                <frame procname="" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 1 --> </inputbuf>
        </process>
        <process id="process6cddc5b88" taskpriority="0" logused="456" waitresource="KEY: 8:72057594098679808 (89013169fc76)" waittime="567" ownerId="474698698" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.283" XDES="0x30c459970" lockMode="S" schedulerid="4" kpid="4204" status="suspended" spid="70" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T15:04:55.870" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698698" currentdb="8" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
            <executionStack>
                <frame procname="" line="18" stmtstart="942" stmtend="1250" sqlhandle="0x03000800ca458d315ee9130100a300000100000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 2 --> </inputbuf>
        </process>
    </process-list>
    <resource-list>
        <keylock hobtid="72057594104905728" dbid="8" objectname="" indexname="" id="lock33299fc00" mode="X" associatedObjectId="72057594104905728">
            <owner-list>
                <owner id="process6cddc5b88" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process4c6672748" mode="RangeS-S" requestType="wait" />
            </waiter-list>
        </keylock>
        <keylock hobtid="72057594098679808" dbid="8" objectname="" indexname="" id="lockb7e2ba80" mode="X" associatedObjectId="72057594098679808">
            <owner-list>
                <owner id="process4c6672748" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process6cddc5b88" mode="S" requestType="wait" />
            </waiter-list>
        </keylock>
    </resource-list>
</deadlock>

В таком случае,

relatedObjectId 72057594098679808 соответствует member_activity, PK_member_activity

relatedObjectId 72057594104905728 соответствует follow, IX_follow_member_id_includes

Кроме того, вот более точная картина того, что делают SP1 и SP2

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m1 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m1, @activityId, @field1)

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m2 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m2, @activityId, @field1)

также SP2:

-- SP2: insert follow
---------------------

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

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

  • member_activity.member_idэто внешний ключ к memberтаблице
  • member_activity.activity_idэто внешний ключ к activityтаблице
  • follow.member_idэто внешний ключ к memberтаблице
  • follow.follower_idэто внешний ключ к memberтаблице

Обновление 1:

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

Я сделал следующие изменения:

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m1 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m2 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

и с SP2:

-- SP2: insert follow
---------------------

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow WITH ( UPDLOCK )
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

COMMIT

С этими двумя изменениями у меня все еще есть тупики.

Если есть что-то еще, что я могу предоставить, пожалуйста, дайте мне знать. Благодарю.

Леланд Ричардсон
источник
Операция read commit не принимает блокировки диапазона ключей, а только сериализуемые. Если тупик действительно показывает read commit (2), то я предполагаю, что вы получаете доступ к изменению внешнего ключа, который будет преобразован в сериализуемый под прикрытием (хотя все еще будет сказано, что фиксация выполнена). Нам, честно говоря, нужны все ddl и sp, чтобы помочь в дальнейшем.
Шон говорит Удалить Сара Чиппс
@SeanGallardy, спасибо. Я отредактировал, чтобы включить вывод 1222 на случай, если я неправильно интерпретировал, и я добавил больше деталей о том, что делают SP. Это помогает?
Леланд Ричардсон,
2
@SeanGallardy Часть плана запроса, которая поддерживает индексированное представление, выполняется внутри SERIALIZABLE(есть немного больше, чем это, но это комментарий, а не ответ :)
Пол Уайт, восстановите Монику
@PaulWhite Спасибо за понимание, я этого не знал! Выполняя быстрый тест, я определенно могу получить сериализуемые режимы блокировки с индексированным представлением во время вставки в ваши хранимые процедуры (RangeI-N, RangeS-S, RangeS-U). Кажется, будто тупик возникает из-за несовместимых режимов блокировки, которые в нужный момент сталкиваются друг с другом во время вставок в хранимых процедурах, когда они попадают в границы блокировки (например, в области, удерживаемой блокировкой диапазона). Я бы подумал, что и синхронизация, и столкновение входных данных.
Шон говорит Удалить Сара Чиппс
Вопрос: Если бы я добавил подсказку HOLDLOCK к операторам SELECT, это предотвратило бы блокировку при вставке?
Леланд Ричардсон,

Ответы:

5

Конфликт сводится к network_activityтому, чтобы быть индексированным представлением, которое необходимо поддерживать (внутренне) в выражениях DML. Вероятнее всего, именно поэтому SP1 хочет заблокировать IX_follow-member_id_includesиндекс, поскольку он, вероятно, используется представлением (он выглядит как закрывающий индекс для представления).

Два возможных варианта:

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

  2. Если преимущество индексирования представления перевешивает затраты, то рассмотрите возможность изоляции операций DML и базовых таблиц этого представления. Это можно сделать с помощью блокировки приложений (см. Sp_getapplock и sp_releaseapplock ). Блокировки приложений позволяют создавать блокировки вокруг произвольных концепций. Это означает, что вы можете определить @Resourceкак «network_activity» в обоих ваших сохраненных процессах, что заставит их ждать своей очереди. Каждый процесс будет следовать той же структуре:

    BEGIN TRANSACTION;
    EXEC sp_getapplock @Resource = 'network_activity', @LockMode = 'Exclusive';
    ...current proc code...
    EXEC sp_releaseapplock @Resource = 'network_activity';
    COMMIT TRANSACTION;

    Вам нужно ROLLBACKсамим управлять ошибками (как указано в связанной документации MSDN), так что вставляйте в обычное TRY...CATCH. Но это позволяет вам управлять ситуацией.
    Обратите внимание: sp_getapplock / sp_releaseapplockследует использовать с осторожностью; Блокировки приложений определенно могут быть очень удобными (например, в подобных случаях), но их следует использовать только в случае крайней необходимости.

Соломон Руцкий
источник
Спасибо за помощь. Я собираюсь прочитать немного больше о варианте # 2 и посмотреть, работает ли это для нас. Представление читается из довольно небольшого количества, и кластерный индекс - довольно большая помощь ... поэтому я бы предпочел пока не удалять его. Я вернусь обновление, как только я сделаю это.
Леланд Ричардсон,
Я думаю, что использование sp_getapplock будет работать. Я еще не смог опробовать его в нашей производственной среде, но я хотел убедиться, что вы получили награду до истечения срока ее действия. Я обновлю здесь, когда смогу подтвердить, что это работает!
Леланд Ричардсон,
Благодарю. Одна хорошая вещь о применении замков является то , что вы можете изменить уровень детализации конкатенации в чем - то вроде member_idв @Resourceстоимость. Похоже, что это не относится к данной конкретной ситуации, но я видел, что она используется таким образом, и это довольно удобно, особенно в мультитенантной системе, где вы хотите ограничить процесс одним потоком для каждого клиента, но все еще иметь многопоточность среди клиентов.
Соломон Руцкий,
Я хотел дать обновление и сказать , что это было в конечном итоге работает в нашей производственной среде. :)
Леланд Ричардсон