Deadlock quando aggiorna diverse righe con indice non cluster


13

Sto risolvendo un problema di deadlock mentre ho notato che il comportamento del blocco è diverso quando utilizzo l'indice cluster e non cluster sul campo ID. Il problema del deadlock sembra essere risolto se l'indice cluster o la chiave primaria vengono applicati al campo ID.

Ho transazioni diverse che fanno uno o più aggiornamenti su righe diverse, ad esempio la transazione A aggiornerà la riga solo con ID = a, tx B toccherà la riga solo con ID = b ecc.

E ho capito che senza indice, l'aggiornamento acquisirà il blocco degli aggiornamenti per tutte le righe e si convertirà in blocco esclusivo quando necessario, che alla fine porterà a deadlock. Ma non riesco a scoprire perché con l'indice non cluster, il deadlock è ancora lì (anche se la percentuale di hit sembra essere diminuita)

Tabella dati:

CREATE TABLE [dbo].[user](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [userName] [nvarchar](255) NULL,
    [name] [nvarchar](255) NULL,
    [phone] [nvarchar](255) NULL,
    [password] [nvarchar](255) NULL,
    [ip] [nvarchar](30) NULL,
    [email] [nvarchar](255) NULL,
    [pubDate] [datetime] NULL,
    [todoOrder] [text] NULL
)

Traccia deadlock

deadlock-list
deadlock victim=process4152ca8
process-list
process id=process4152ca8 taskpriority=0 logused=0 waitresource=RID: 5:1:388:29 waittime=3308 ownerId=252354 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.947 XDES=0xb0bf180 lockMode=U schedulerid=3 kpid=11392 status=suspended spid=57 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.953 lastbatchcompleted=2014-04-11T00:15:30.950 lastattention=1900-01-01T00:00:00.950 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252354 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=62 sqlhandle=0x0200000062f45209ccf17a0e76c2389eb409d7d970b0f89e00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(2)<c/>@owner int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
process id=process4153468 taskpriority=0 logused=4652 waitresource=KEY: 5:72057594042187776 (3fc56173665b) waittime=3303 ownerId=252344 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.920 XDES=0x4184b78 lockMode=U schedulerid=3 kpid=7272 status=suspended spid=58 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.960 lastbatchcompleted=2014-04-11T00:15:30.960 lastattention=1900-01-01T00:00:00.960 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252344 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=60 sqlhandle=0x02000000d4616f250747930a4cd34716b610a8113cb92fbc00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(61)<c/>@uid int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
resource-list
ridlock fileid=1 pageid=388 dbid=5 objectname=SQL2012_707688_webows.dbo.user id=lock3f7af780 mode=X associatedObjectId=72057594042122240
owner-list
owner id=process4153468 mode=X
waiter-list
waiter id=process4152ca8 mode=U requestType=wait
keylock hobtid=72057594042187776 dbid=5 objectname=SQL2012_707688_webows.dbo.user indexname=10 id=lock3f7ad700 mode=U associatedObjectId=72057594042187776
owner-list
owner id=process4152ca8 mode=U
waiter-list
waiter id=process4153468 mode=U requestType=wait

Inoltre, una scoperta interessante e possibile correlata è che l'indice cluster e non cluster sembra avere comportamenti di blocco diversi

Quando si utilizza l'indice cluster, è presente un blocco esclusivo sulla chiave e un blocco esclusivo su RID durante l'aggiornamento, che è previsto; mentre ci sono due blocchi esclusivi su due RID diversi se viene utilizzato un indice non cluster, il che mi confonde.

Sarebbe utile se qualcuno potesse spiegare perché anche su questo.

Test SQL:

use SQL2012_707688_webows;
begin transaction;
update [user] with (rowlock) set todoOrder='{1}' where id = 63501
exec sp_lock;
commit;

Con ID come indice cluster:

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   1   KEY (b1a92fe5eed4)                      X   GRANT
53  5   917578307   1   PAG 1:879                               IX  GRANT
53  5   917578307   1   PAG 1:1928                              IX  GRANT
53  5   917578307   1   RID 1:879:7                             X   GRANT

Con ID come indice non cluster

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   0   PAG 1:879                               IX  GRANT
53  5   917578307   0   PAG 1:1928                              IX  GRANT
53  5   917578307   0   RID 1:879:7                             X   GRANT
53  5   917578307   0   RID 1:1928:18                           X   GRANT

EDIT1: Dettagli di deadlock senza alcun indice
Dire che ho due tx A e B, ciascuno con due istruzioni di aggiornamento, riga diversa naturalmente
tx A

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

tx B

update [user] with (rowlock) set todoOrder='{3}' where id = 63502
update [user] with (rowlock) set todoOrder='{4}' where id = 63502

{1} e {4} avrebbero una possibilità di deadlock, da allora

su {1}, è richiesto il blocco U per la riga 63502 poiché deve eseguire una scansione della tabella e il blocco X potrebbe essere stato trattenuto sulla riga 63501 poiché corrisponde alla condizione

su {4}, il blocco U è richiesto per la riga 63501 e il blocco X è già in attesa per 63502

quindi abbiamo txA con 63501 e attende 63502 mentre txB con 63502 in attesa di 63501, che è un deadlock

EDIT2: Si scopre che un bug del mio caso di test fa una situazione di differenza qui Ci scusiamo per la confusione, ma il bug fa una situazione di differenza e sembra causare eventualmente il deadlock.

Dal momento che l'analisi di Paul mi ha davvero aiutato in questo caso, quindi lo accetterò come risposta.

A causa del bug del mio caso di test, due transazioni txA e txB possono aggiornare la stessa riga, come di seguito:

tx A

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

tx B

update [user] with (rowlock) set todoOrder='{3}' where id = 63501

{2} e {3} avrebbero la possibilità di un deadlock quando:

txA richiede il blocco U sulla chiave mentre tiene il blocco X sul RID (a causa dell'aggiornamento di {1}) txB richiede il blocco U sul RID mentre tiene il blocco U sulla chiave


1
Non riesco a capire perché una transazione debba aggiornare due volte la stessa riga.
ypercubeᵀᴹ

@ypercube Un buon punto, è qualcosa che dovrei migliorare. Ma in questo caso voglio solo avere una migliore comprensione dei comportamenti di blocco
Bood,

@ypercube dopo ulteriori riflessioni Penso che sia possibile che un'applicazione con una logica complessa debba aggiornare la stessa riga due volte nella stessa tx, ad esempio potrebbero essere colonne diverse
Bood

Risposte:


16

... perché con l'indice cluster, il deadlock è ancora lì (anche se la percentuale di hit sembra essere diminuita)

La domanda non è precisamente chiara (ad es. Quanti aggiornamenti e quali idvalori sono presenti in ciascuna transazione) ma sorge uno scenario di deadlock evidente con più aggiornamenti a riga singola all'interno di una singola transazione, in cui vi è una sovrapposizione di [id]valori e gli ID sono aggiornato in un [id]ordine diverso :

[T1]: Update id 2; Update id 1;
[T2]: Update id 1; Update id 2;

Sequenza di deadlock: T1 (u2), T2 (u1), T1 (u1) in attesa , T2 (u2) in attesa .

Questa sequenza di deadlock potrebbe essere evitata aggiornando rigorosamente nell'ordine id all'interno di ciascuna transazione (acquisendo blocchi nello stesso ordine sullo stesso percorso).

Quando si utilizza l'indice cluster, è presente un blocco esclusivo sulla chiave e un blocco esclusivo su RID durante l'aggiornamento, che è previsto; mentre ci sono due blocchi esclusivi su due RID diversi se viene utilizzato un indice non cluster, il che mi confonde.

Con un indice cluster univoco attivo id, viene eseguito un blocco esclusivo sulla chiave di clustering per proteggere le scritture nei dati in riga. È RIDnecessario un blocco esclusivo separato per proteggere la scrittura nella textcolonna LOB , che è memorizzata in una pagina di dati separata per impostazione predefinita.

Quando la tabella è un heap con solo un indice non cluster attivo id, accadono due cose. Innanzitutto, un RIDblocco esclusivo si riferisce ai dati in riga dell'heap e l'altro è il blocco sui dati LOB come prima. Il secondo effetto è che è richiesto un piano di esecuzione più complesso.

Con un indice cluster e un semplice aggiornamento del predicato di uguaglianza a valore singolo, il Query Processor può applicare un'ottimizzazione che esegue l'aggiornamento (lettura e scrittura) in un singolo operatore, utilizzando un singolo percorso:

Aggiornamento per singolo operatore

La riga viene individuata e aggiornata in un'unica operazione di ricerca, richiedendo solo blocchi esclusivi (non sono necessari blocchi di aggiornamento). Una sequenza di blocco di esempio che utilizza la tabella di esempio:

acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IX lock on PAGE: 6:1:59104 -- INROW
acquiring X lock on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
acquiring IX lock on PAGE: 6:1:59091 -- LOB
acquiring X lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 -- LOB
releasing lock reference on RID: 6:1:59091:1 -- LOB
releasing lock reference on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
releasing lock reference on PAGE: 6:1:59104 -- INROW

Con solo un indice non cluster, la stessa ottimizzazione non può essere applicata perché dobbiamo leggere da una struttura b-tree e scriverne un'altra. Il piano multi-percorso prevede fasi di lettura e scrittura separate:

Aggiornamento multi-iteratore

Questo acquisisce i blocchi di aggiornamento durante la lettura, convertendoli in blocchi esclusivi se la riga si qualifica. Esempio di sequenza di blocchi con lo schema fornito:

acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IU lock on PAGE: 6:1:59105 -- NC INDEX
acquiring U lock on KEY: 6:72057594233749504 (61a06abd401c) -- NC INDEX
acquiring IU lock on PAGE: 6:1:59104 -- HEAP
acquiring U lock on RID: 6:1:59104:1 -- HEAP
acquiring IX lock on PAGE: 6:1:59104 -- HEAP convert to X
acquiring X lock on RID: 6:1:59104:1 -- HEAP convert to X
acquiring IU lock on PAGE: 6:1:59091 -- LOB
acquiring U lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 
releasing lock reference on RID: 6:1:59091:1
releasing lock reference on RID: 6:1:59104:1
releasing lock reference on PAGE: 6:1:59104 
releasing lock on KEY: 6:72057594233749504 (61a06abd401c)
releasing lock on PAGE: 6:1:59105 

Si noti che i dati LOB vengono letti e scritti nell'iteratore di aggiornamento tabella. Il piano più complesso e più percorsi di lettura e scrittura aumentano le possibilità di un deadlock.

Infine, non posso fare a meno di notare i tipi di dati utilizzati nella definizione della tabella. Non utilizzare il texttipo di dati obsoleto per le nuove attività; l'alternativa, se hai davvero bisogno della possibilità di memorizzare fino a 2 GB di dati in questa colonna, è varchar(max). Una differenza importante tra texte varchar(max)è che i textdati sono archiviati off-row per impostazione predefinita, mentre vengono varchar(max)archiviati in riga per impostazione predefinita.

Utilizzare i tipi Unicode solo se è necessaria tale flessibilità (ad esempio, è difficile capire perché un indirizzo IP avrebbe bisogno di Unicode). Inoltre, scegli i limiti di lunghezza appropriati per i tuoi attributi: 255 ovunque sembra improbabile che sia corretto.

Letture addizionali:
modelli comuni di deadlock e livelock
serie di risoluzione dei problemi di deadlock di Bart Duncan

I blocchi di tracciamento possono essere eseguiti in vari modi. SQL Server Express con servizi avanzati ( solo SP1 2014 e 2012 in poi ) contiene lo strumento Profiler , che è un modo supportato per visualizzare i dettagli di acquisizione e rilascio dei blocchi.


Risposta eccellente. In che modo stai emettendo i registri / le tracce che contengono i messaggi "acquisizione ... blocco" e "rilascio riferimento di blocco"?
Sanjiv Jivan,
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.