Risoluzione di deadlock da 2 tabelle correlate solo tramite vista indicizzata


17

Ho una situazione in cui sto ottenendo blocchi, e penso di aver ristretto i colpevoli, ma non sono sicuro di cosa posso fare per risolverlo.

Questo si trova in un ambiente di produzione che esegue SQL Server 2008 R2.

Per darti una visione leggermente semplificata della situazione:


Ho 3 tabelle come definito di seguito:

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

Il member_activity tabella ha una chiave primaria composta definita come member_id, activity_id, perché ho sempre e solo bisogno di cercare i dati su quella tabella in quel modo.

Ho anche un indice non cluster su follow:

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

Inoltre, ho una vista associata allo schema network_activitydefinita come segue:

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

Che ha anche un indice cluster univoco:

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

Ora, ho due stored procedure bloccate. Passano attraverso il seguente 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 (...)

Queste 2 procedure vengono eseguite entrambe in isolamento READ COMMITTED. Sono riuscito a interrogare l'output degli eventi estesi 1222 e ho interpretato quanto segue in relazione ai deadlock:

SP1 è in attesa di un RangeS-Sblocco tasti IX_follow_member_id_includessull'indice mentre SP2 contiene un blocco in conflitto (X)

SP2 è in attesa di un Sblocco della modalità PK_member_activity mentre SP1 ha un blocco in conflitto (X)

Il deadlock sembra verificarsi nell'ultima riga di ogni query (gli inserti). Ciò che non mi è chiaro è perché SP1 desidera un blocco IX_follow-member_id_includessull'indice. L'unico link, per me, sembra provenire da questa vista indicizzata ed è per questo che l'ho incluso.

Quale sarebbe il modo migliore per me per evitare che si verifichino questi deadlock? Qualsiasi aiuto sarebbe molto apprezzato. Non ho molta esperienza nella risoluzione dei problemi di deadlock.

Per favore fatemi sapere se ci sono ulteriori informazioni che posso fornire che potrebbero aiutare!

Grazie in anticipo.


Modifica 1: aggiunta di ulteriori informazioni per richiesta.

Ecco l'output 1222 da questo deadlock:

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

In questo caso,

associatedObjectId 72057594098679808 corrisponde a member_activity, PK_member_activity

associatedObjectId 72057594104905728 corrisponde a follow, IX_follow_member_id_includes

Inoltre, ecco un'immagine più precisa di ciò che stanno facendo SP1 e 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)

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

Modifica 2: Dopo aver riletto i commenti, ho pensato di aggiungere alcune informazioni su quali colonne sono anche le chiavi esterne ...

  • member_activity.member_idè una chiave esterna per una membertabella
  • member_activity.activity_idè una chiave esterna per la activitytabella
  • follow.member_idè una chiave esterna per una membertabella
  • follow.follower_idè una chiave esterna per una membertabella

Aggiornamento 1:

Ho apportato un paio di modifiche che pensavo potessero aiutare a prevenire lo stallo, senza fortuna.

Le modifiche che ho apportato sono state le seguenti:

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

Con questi due cambiamenti, sembra che stia ancora ottenendo deadlock.

Se c'è qualcos'altro che posso fornire, per favore fatemelo sapere. Grazie.


read commit non accetta blocchi dell'intervallo di chiavi, ma solo serializzabili. Se il deadlock in effetti mostra read commit (2), allora la mia ipotesi è che tu acceda a cambiare una chiave esterna che verrà trasformata in serializzabile sotto le copertine (anche se continua a leggere read commit). Onestamente avremmo bisogno dell'intero ddl e di sp per aiutare ulteriormente.
Sean dice Rimuovi Sara Chipps il

@SeanGallardy, grazie. Ho modificato per includere l'output 1222 nel caso in cui stavo interpretando in modo errato e ho aggiunto ulteriori dettagli su cosa stanno facendo gli SP. questo aiuta?
Leland Richardson,

2
@SeanGallardy La parte del piano di query che mantiene la vista indicizzata viene eseguita internamente SERIALIZABLE(c'è un po 'di più rispetto a quello, ma questo è un commento non una risposta :)
Paul White Reinstate Monica

@PaulWhite Grazie per la comprensione, non lo sapevo! Facendo un test rapido, posso sicuramente ottenere le modalità di blocco serializzabili con la vista indicizzata durante l'inserimento nelle procedure memorizzate (RangeI-N, RangeS-S, RangeS-U). Sembra che il deadlock stia accadendo dalle modalità di blocco incompatibili che colpiscono al momento giusto l'una contro l'altra durante gli inserimenti nelle procedure memorizzate quando cadono all'interno dei limiti del blocco (ad esempio nell'area trattenuta dal blocco dell'intervallo). Penserei sia una tempistica che una collisione dei dati di input.
Sean dice Rimuovi Sara Chipps il

Domanda: Se aggiungessi un suggerimento HOLDLOCK alle istruzioni SELECT, ciò impedirebbe che si verifichi il blocco all'inserimento?
Leland Richardson,

Risposte:


5

Il conflitto si riduce a network_activityuna vista indicizzata che deve essere mantenuta (internamente) attraverso le istruzioni DML. Questo è probabilmente il motivo per cui SP1 desidera un blocco IX_follow-member_id_includessull'indice in quanto è probabilmente utilizzato dalla vista (sembra essere un indice di copertura per la vista).

Due possibili opzioni:

  1. Valuta di eliminare l'indice cluster nella vista in modo che non sia più una vista indicizzata. Il vantaggio di averlo supera i costi di manutenzione? Ne selezioni abbastanza frequentemente o ne vale la pena il guadagno in termini di prestazioni dell'indicizzazione? Se esegui questi proc piuttosto frequentemente, forse il costo è superiore al vantaggio?

  2. Se il vantaggio derivante dall'indicizzazione della vista supera i costi, prendere in considerazione l'isolamento delle operazioni DML dalle tabelle di base di quella vista. Questo può essere fatto tramite l'uso di Application Locks (vedi sp_getapplock e sp_releaseapplock ). I blocchi applicazione consentono di creare blocchi attorno a concetti arbitrari. In altre parole, puoi definire @Resource"network_activity" in entrambi i tuoi Proc memorizzati che li costringeranno ad aspettare il loro turno. Ogni proc seguirà la stessa struttura:

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

    Devi gestire gli errori / ROLLBACKte stesso (come indicato nella documentazione MSDN collegata), quindi inserisci il solito TRY...CATCH. Ma questo ti consente di gestire la situazione.
    Nota: sp_getapplock / sp_releaseapplockdovrebbe essere usato con parsimonia; I blocchi applicazione possono sicuramente essere molto utili (come in casi come questo) ma dovrebbero essere usati solo quando assolutamente necessario.


Grazie per l'aiuto. Leggerò un po 'di più sull'opzione n. 2 e vedrò se funziona per noi. La vista viene letta da un bel po 'e l'indice cluster è di grande aiuto ... quindi preferirei non rimuoverlo ancora. Tornerò un aggiornamento dopo averlo provato.
Leland Richardson,

Penso che usando sp_getapplock funzionerà. Non sono stato ancora in grado di provarlo nel nostro ambiente di produzione, ma volevo assicurarmi che ti fosse assegnato il premio prima che scadesse. Aggiornerò qui quando posso confermare che funziona!
Leland Richardson,

Grazie. Una cosa bella di Application Locks è che puoi cambiare il livello di granularità della concatenazione in qualcosa di simile member_idal @Resourcevalore. Ciò non sembra applicarsi a questa particolare situazione, ma l'ho visto usato così ed è abbastanza utile, specialmente in un sistema multi-tenant in cui si desidera limitare il processo a un singolo thread in base al cliente, ma deve ancora essere multithread tra i clienti.
Solomon Rutzky,

Volevo dare un aggiornamento e dire che questo ha funzionato nel nostro ambiente di produzione. :)
Leland Richardson,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.