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è IXblocco 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 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 è
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
e con il TABLOCKXmodello 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 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
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?










