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 è
con il seguente schema di blocco
cioè IX
blocco sull'oggetto seguito da blocchi più granulari.
A volte, tuttavia, il piano di esecuzione della query è diverso
(questa forma del piano può essere forzata aggiungendo un INDEX(0)
suggerimento) e il suo modello di blocco è
notare il X
blocco posizionato sull'oggetto dopo che IX
è già stato inserito.
Dal momento che due IX
sono compatibili, ma due X
non lo sono, la cosa che succede in concorrenza è
deadlock !
E qui sorge la prima parte della domanda . Posizionare il X
blocco sull'oggetto dopo è IX
idoneo? 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 X
blocco sull'oggetto dopo mi IX
sembra 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 TABLOCK
modello di blocco sul posto diventa
e con il TABLOCKX
modello di blocco è
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 PAGLOCK
e ROWLOCK
per rendere i blocchi più granulari e ridurre la contesa. Entrambi non hanno alcun effetto ( X
sull'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 FORCESEEK
suggerimento
MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T
e ha funzionato.
E qui sorge la seconda parte della domanda . Potrebbe accadere che FORCESEEK
venga ignorato e verrà utilizzato un modello di blocco errato? (Come ho già detto, PAGLOCK
e ROWLOCK
apparentemente sono stati ignorati).
L'aggiunta UPDLOCK
non ha alcun effetto ( X
sull'oggetto ancora osservabile dopo IX
).
Rendere l' IX_Cache
indice 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 @itemKey
una dichiarazione variabile da varchar (200) a nvarchar (200) , il piano di esecuzione diventa
vedere che viene utilizzato seek, MA in questo caso il modello di X
blocco 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 X
blocco sull'oggetto dopo essere IX
ancora aperta. E se è idoneo, c'è qualcosa che si può fare per impedire il blocco degli oggetti?