Resolvendo deadlock de 2 tabelas relacionadas apenas através da exibição indexada

16

Eu tenho uma situação em que estou tendo impasses, e acho que reduzi os culpados, mas não tenho muita certeza do que posso fazer para consertá-lo.

Este é um ambiente de produção executando o SQL Server 2008 R2.

Para fornecer uma visão um pouco simplificada da situação:


Eu tenho 3 tabelas conforme definido abaixo:

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,
    ...
)

A member_activitytabela possui uma Chave Primária composta definida como member_id, activity_id, porque eu só preciso procurar dados nessa tabela dessa maneira.

Eu também tenho um índice não clusterizado em follow:

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

Além disso, tenho uma exibição vinculada ao esquema, network_activitydefinida da seguinte maneira:

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

Que também possui um índice clusterizado exclusivo:

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

Agora, tenho dois procedimentos armazenados em conflito. Eles passam pelo seguinte processo:

-- 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 (...)

Esses 2 procedimentos são executados no isolamento READ COMMITTED. Consegui consultar a saída de 1222 eventos estendidos e interpretei o seguinte em relação aos impasses:

O SP1 está aguardando um RangeS-Sbloqueio de chave no IX_follow_member_id_includesíndice enquanto o SP2 mantém um bloqueio (X) conflitante

O SP2 está aguardando um Sbloqueio de modo PK_member_activity enquanto o SP1 mantém um bloqueio (X) conflitante

O impasse parece estar acontecendo na última linha de cada consulta (as inserções). O que não está claro para mim é por que o SP1 está querendo um bloqueio no IX_follow-member_id_includesíndice. O único link, para mim, parece ser dessa exibição indexada e é por isso que eu o incluí.

Qual seria a melhor maneira de impedir que esses impasses aconteçam? Qualquer ajuda seria muito apreciada. Não tenho muita experiência na resolução de problemas de conflito.

Informe-me se houver mais informações que eu possa fornecer que possam ajudar!

Desde já, obrigado.


Editar 1: Adicionando mais algumas informações por solicitação.

Aqui está a saída 1222 deste conflito:

<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>

Nesse caso,

AssociatedObjectId 72057594098679808 corresponde a member_activity, PK_member_activity

AssociatedObjectId 72057594104905728 corresponde a follow, IX_follow_member_id_includes

Além disso, aqui está uma imagem mais precisa do que o SP1 e o SP2 estão fazendo

-- 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)

também 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)

Edit 2: Depois de reler os comentários, pensei em adicionar algumas informações sobre quais colunas são chaves estrangeiras ...

  • member_activity.member_idé uma chave estrangeira para uma membertabela
  • member_activity.activity_idé uma chave estrangeira para a activitytabela
  • follow.member_idé uma chave estrangeira para uma membertabela
  • follow.follower_idé uma chave estrangeira para uma membertabela

Atualização 1:

Fiz algumas alterações que achei que poderiam ajudar a evitar o impasse, sem sorte.

As alterações que fiz foram as seguintes:

-- 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)
;

e com o 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

Com essas duas mudanças, ainda pareço estar tendo impasses.

Se houver mais alguma coisa que eu possa fornecer, entre em contato. Obrigado.

Leland Richardson
fonte
A leitura confirmada não aceita bloqueios de intervalo de chave, apenas serializáveis. Se o impasse de fato mostra a leitura confirmada (2), minha opinião é que você está acessando a alteração de uma chave estrangeira que será transformada em serializável sob as cobertas (embora ainda digamos leitura confirmada). Honestamente, precisaríamos de todo o ddl e sp para ajudar ainda mais.
Sean diz Remover Sara Chipps
@SeanGallardy, obrigado. Editei para incluir a saída 1222 no caso de interpretar errado e adicionei mais detalhes sobre o que os SPs estão fazendo. Isso ajuda?
Leland Richardson
2
@SeanGallardy A parte do plano de consulta que mantém a exibição indexada é executada internamente em SERIALIZABLE(há um pouco mais do que isso, mas este é um comentário, não uma resposta :)
Paul White Reintegrar Monica
@PaulWhite Obrigado pela compreensão, eu não sabia disso! Fazendo um teste rápido, posso obter definitivamente os modos de bloqueio serializáveis ​​com a exibição indexada durante a inserção nos procedimentos armazenados (RangeI-N, RangeS-S, RangeS-U). Parece que o impasse está ocorrendo nos modos incompatíveis de bloqueio, batendo no momento certo um contra o outro durante as inserções nos procedimentos armazenados quando eles caem dentro dos limites do bloqueio (por exemplo, na área mantida pelo bloqueio de alcance). Tanto um timing quanto uma colisão de dados de entrada, eu pensaria.
Sean diz Retirar Sara Chipps
Pergunta: Se eu adicionasse uma dica HOLDLOCK nas instruções SELECT, isso impediria que o bloqueio acontecesse na inserção?
Leland Richardson

Respostas:

4

O conflito se resume a network_activityser uma exibição indexada que precisa ser mantida (internamente) nas instruções DML. É provavelmente por isso que o SP1 está querendo um bloqueio no IX_follow-member_id_includesíndice, pois provavelmente é usado pelo View (parece ser um índice de cobertura para o View).

Duas opções possíveis:

  1. Considere soltar o índice de cluster na exibição para que ele não seja mais uma exibição indexada. O benefício de tê-lo supera o custo de manutenção? Você seleciona com frequência suficiente ou o ganho de desempenho de indexá-lo vale a pena? Se você executar esses procs com bastante frequência, talvez o custo seja maior que o benefício?

  2. Se o benefício de indexar a Visualização compensar o custo, considere isolar as operações DML nas tabelas básicas dessa Visualização. Isso pode ser feito através do uso de bloqueios de aplicativos (consulte sp_getapplock e sp_releaseapplock ). Os bloqueios de aplicativos permitem criar bloqueios em torno de conceitos arbitrários. Ou seja, você pode definir o @Resource"network_activity" em ambos os procs armazenados, o que os forçará a esperar sua vez. Cada proc seguiria a mesma estrutura:

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

    Você precisa gerenciar os ROLLBACKpróprios erros (conforme declarado na documentação vinculada do MSDN), para colocar como de costume TRY...CATCH. Mas, isso permite que você gerencie a situação.
    Observe: sp_getapplock / sp_releaseapplockdeve ser usado com moderação; Definitivamente, os bloqueios de aplicativos podem ser muito úteis (como em casos como este), mas devem ser usados ​​apenas quando for absolutamente necessário.

Solomon Rutzky
fonte
Obrigado pela ajuda. Vou ler um pouco mais sobre a opção 2 e ver se isso funciona para nós. A visualização é lida um pouco, e o índice de cluster é uma grande ajuda ... então prefiro não removê-lo ainda. Voltarei uma atualização assim que der uma chance.
Leland Richardson
Eu acho que usar sp_getapplock funcionará. Ainda não pude testá-lo em nosso ambiente de produção, mas queria garantir que você recebesse a recompensa antes que ela expirasse. Vou atualizar aqui quando puder confirmar que funciona!
Leland Richardson
Obrigado. Uma coisa boa sobre os bloqueios de aplicativos é que você pode alterar o nível de granularidade da concatenação em algo como member_ido @Resourcevalor. Isso não parece se aplicar a essa situação em particular, mas já a vi usada dessa maneira e é bastante útil, especialmente em um sistema multilocatário em que você deseja limitar o processo a um único encadeamento por cliente, mas ainda tem que ser multithread nos clientes.
Solomon Rutzky
Eu queria fazer uma atualização e dizer que isso acabou funcionando em nosso ambiente de produção. :)
Leland Richardson