MERGE prevenzione del deadlock


9

In uno dei nostri database abbiamo una tabella a cui si accede in modo intensivo simultaneamente da più thread. I thread aggiornano o inseriscono le righe tramite MERGE. Esistono anche thread che a volte eliminano le righe, quindi i dati della tabella sono molto volatili. Le discussioni che eseguono upsert a volte soffrono di deadlock. Il problema è simile a quello descritto in questa domanda. La differenza, tuttavia, è che nel nostro caso ogni thread aggiorna o inserisce esattamente una riga .

Segue una configurazione semplificata. La tabella è heap con due indici non cluster unici

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

e la query tipica è

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;

vale a dire, la corrispondenza avviene tramite chiave indice univoca. Il suggerimento HOLDLOCKè qui, a causa della concorrenza (come consigliato qui ).

Ho fatto piccole indagini e quanto segue è quello che ho trovato.

Nella maggior parte dei casi il piano di esecuzione delle query è

indice cerca piano di esecuzione

con il seguente schema di blocco

indice cerca schema di blocco

cioè IXblocco sull'oggetto seguito da blocchi più granulari.

A volte, tuttavia, il piano di esecuzione della query è diverso

piano di esecuzione della scansione della tabella

(questa forma del piano può essere forzata aggiungendo un INDEX(0)suggerimento) e il suo modello di blocco è

schema di blocco scansione tabella

notare il Xblocco posizionato sull'oggetto dopo che IXè già stato inserito.

Dal momento che due IXsono compatibili, ma due Xnon lo sono, la cosa che succede in concorrenza è

punto morto

grafico di deadlock

deadlock !

E qui sorge la prima parte della domanda . Posizionare il Xblocco sull'oggetto dopo è IXidoneo? Non è un bug?

La documentazione afferma:

I blocchi di intenti sono denominati blocchi di intenti perché vengono acquisiti prima di un blocco al livello inferiore e pertanto segnalano l'intenzione di posizionare i blocchi a un livello inferiore .

e anche

IX indica l'intenzione di aggiornare solo alcune delle righe anziché tutte

quindi, posizionare il Xblocco sull'oggetto dopo mi IXsembra MOLTO sospetto.

Innanzitutto ho tentato di impedire il deadlock provando ad aggiungere suggerimenti per il blocco delle tabelle

MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCK) T

e

MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCKX) T

con il TABLOCKmodello di blocco sul posto diventa

unire la sequenza di blocco tablock holdlock

e con il TABLOCKXmodello di blocco è

unire il blocco di blocco tablockx

poiché due SIX(e due X) non sono compatibili, ciò impedisce efficacemente lo stallo, ma, sfortunatamente, impedisce anche la concorrenza (il che non è desiderato).

I miei successivi tentativi sono stati l'aggiunta PAGLOCKe ROWLOCKper rendere i blocchi più granulari e ridurre la contesa. Entrambi non hanno alcun effetto ( Xsull'oggetto è stato ancora osservato immediatamente dopo IX).

Il mio ultimo tentativo è stato quello di forzare la "buona" forma del piano di esecuzione con un buon blocco granulare aggiungendo un FORCESEEKsuggerimento

MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T

e ha funzionato.

E qui sorge la seconda parte della domanda . Potrebbe accadere che FORCESEEKvenga ignorato e verrà utilizzato un modello di blocco errato? (Come ho già detto, PAGLOCKe ROWLOCKapparentemente sono stati ignorati).


L'aggiunta UPDLOCKnon ha alcun effetto ( Xsull'oggetto ancora osservabile dopo IX).

Rendere l' IX_Cacheindice cluster, come anticipato, ha funzionato. Ha portato alla pianificazione con Clustered Index Seek e blocco granulare. Inoltre ho provato a forzare la scansione dell'indice cluster che mostrava anche il blocco granulare.

Però. Osservazione aggiuntiva. Nell'impostazione originale anche con il FORCESEEK(IX_Cache(ItemKey)))posto, se si cambia @itemKeyuna dichiarazione variabile da varchar (200) a nvarchar (200) , il piano di esecuzione diventa

indice cerca piano di esecuzione con nvarchar

vedere che viene utilizzato seek, MA in questo caso il modello di Xblocco mostra nuovamente il blocco posizionato sull'oggetto IX.

Quindi, sembra che forzare la ricerca non garantisca necessariamente blocchi granulari (e l'assenza di deadlock da qui). Non sono sicuro che avere un indice cluster garantisca un blocco granulare. O lo fa?

La mia comprensione (correggimi se sbaglio) è che il bloccaggio è situazionale in grande misura e che una certa forma del piano di esecuzione non implica un certo schema di bloccaggio.

La domanda sull'ammissibilità di posizionare il Xblocco sull'oggetto dopo essere IXancora aperta. E se è idoneo, c'è qualcosa che si può fare per impedire il blocco degli oggetti?


Richiesta relativa a feedback.azure.com
i-one,

Risposte:


9

Il posizionamento IXseguito Xdall'oggetto è ammissibile? È un bug o no?

Sembra un po 'strano, ma è valido. Al momento della IXpresa, l'intenzione potrebbe essere quella di prendere le Xserrature a un livello inferiore. Non c'è nulla da dire che tali blocchi devono essere effettivamente adottati. Dopotutto, potrebbe non esserci nulla da bloccare al livello inferiore; il motore non può saperlo in anticipo. In aggiunta, ci possono essere ottimizzazioni tale che i blocchi di livello inferiore possono essere saltati (un esempio per ISe Sserrature può essere visto qui ).

Più specificamente per lo scenario attuale, è vero che i blocchi dell'intervallo di chiavi serializzabili non sono disponibili per un heap, quindi l'unica alternativa è un Xblocco a livello di oggetto. In tal senso, il motore potrebbe essere in grado di rilevare in anticipo che Xsarà inevitabilmente richiesto un blocco se il metodo di accesso è una scansione heap, evitando quindi di IXbloccarlo.

D'altra parte, il blocco è complesso e talvolta i blocchi di intenti possono essere presi per motivi interni non necessariamente correlati all'intenzione di prendere blocchi di livello inferiore. La presa IXpuò essere il modo meno invasivo di fornire una protezione richiesta per alcuni casi oscuri. Per un simile tipo di considerazione, vedere Blocco condiviso emesso su IsolationLevel.ReadUncommitted .

Quindi, la situazione attuale è sfortunata per il tuo scenario di deadlock, e potrebbe essere evitabile in linea di principio, ma non è necessariamente la stessa cosa di un "bug". Puoi segnalare il problema tramite il tuo normale canale di supporto o su Microsoft Connect, se hai bisogno di una risposta definitiva su questo.

Potrebbe accadere che FORCESEEKvenga ignorato e verrà utilizzato un modello di blocco errato?

No. FORCESEEKè meno un suggerimento e più una direttiva. Se l'ottimizzatore non riesce a trovare un piano che onori il 'suggerimento', produrrà un errore.

Forzare l'indice è un modo per garantire che si possano eseguire blocchi dell'intervallo di chiavi. Insieme ai blocchi di aggiornamento adottati naturalmente durante l'elaborazione di un metodo di accesso per la modifica delle righe, ciò fornisce una garanzia sufficiente per evitare problemi di concorrenza nello scenario.

Se lo schema della tabella non cambia (ad esempio aggiungendo un nuovo indice), anche il suggerimento è sufficiente per evitare il deadlock di questa query con se stesso. Esiste ancora la possibilità di un deadlock ciclico con altre query che potrebbero accedere all'heap prima dell'indice non cluster (come un aggiornamento della chiave dell'indice non cluster).

... dichiarazione variabile da varchar(200)a nvarchar(200)...

Ciò rompe la garanzia che una singola riga sarà interessata, quindi viene introdotta una bobina da tavolo Eager per la protezione di Halloween. Come ulteriore soluzione per questo, rendere esplicita la garanzia con MERGE TOP (1) INTO [Cache]....

La mia comprensione [...] è che il bloccaggio è situazionale in grande misura e che una certa forma del piano di esecuzione non implica un certo schema di bloccaggio.

C'è sicuramente molto altro da fare che è visibile in un piano di esecuzione. È possibile forzare una determinata forma del piano con, ad esempio, una guida del piano, ma il motore può comunque decidere di eseguire diversi blocchi in fase di esecuzione. Le possibilità sono abbastanza basse se si incorpora l' TOP (1)elemento sopra.

Revisione generale

È in qualche modo insolito vedere una tabella heap utilizzata in questo modo. Dovresti considerare i meriti di convertirlo in una tabella raggruppata, forse usando l'indice suggerito da Dan Guzman in un commento:

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

Ciò può avere importanti vantaggi in termini di riutilizzo dello spazio, oltre a fornire una buona soluzione per l'attuale problema di deadlock.

MERGEè anche leggermente insolito da vedere in un ambiente ad alta concorrenza. In qualche modo controintuitivamente, è spesso più efficiente eseguire istruzioni separate INSERTe UPDATE, ad esempio:

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;

Nota come la ricerca RID non è più necessaria:

Progetto esecutivo

Se puoi garantire l'esistenza di un indice univoco su ItemKey(come nella domanda) il ridondante TOP (1)nel UPDATEpuò essere rimosso, dando il piano più semplice:

Aggiornamento semplificato

Sia INSERTe UPDATEpiani di qualificarsi per un piano banale in entrambi i casi. MERGErichiede sempre un'ottimizzazione basata sui costi.

Vedere la relativa domanda e risposta SQL Server 2014 Problema di input simultaneo per il modello corretto da utilizzare e ulteriori informazioni MERGE.

I deadlock non possono sempre essere evitati. Possono essere ridotti al minimo con un'attenta codifica e progettazione, ma l'applicazione deve essere sempre pronta a gestire con garbo lo stallo dispari (ad esempio ricontrollare le condizioni, quindi riprovare).

Se si ha il controllo completo sui processi che accedono all'oggetto in questione, si potrebbe anche considerare l'utilizzo dei blocchi dell'applicazione per serializzare l'accesso ai singoli elementi, come descritto in Inserimenti ed eliminazioni simultanee di SQL Server .

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.