Deadlock beim Aktualisieren verschiedener Zeilen mit nicht gruppiertem Index

13

Ich löse ein Deadlocking-Problem, während ich bemerkte, dass sich das Sperrverhalten unterscheidet, wenn ich einen gruppierten und einen nicht gruppierten Index für das ID-Feld verwende. Das Deadlock-Problem scheint gelöst zu sein, wenn auf das ID-Feld ein verkrusteter Index oder ein Primärschlüssel angewendet wird.

Ich habe verschiedene Transaktionen, die eine oder mehrere Aktualisierungen für verschiedene Zeilen durchführen, z. B. aktualisiert die Transaktion A nur die Zeile mit der ID = a, tx B berührt nur die Zeile mit der ID = b usw.

Und ich habe verstanden, dass das Update ohne Index eine Aktualisierungssperre für alle Zeilen erhält und bei Bedarf eine exklusive Sperre umwandelt, was schließlich zu einem Deadlock führt. Ich kann jedoch nicht herausfinden, warum der Deadlock bei nicht geclustertem Index immer noch vorhanden ist (obwohl die Trefferquote offenbar gesunken ist).

Datentabelle:

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
)

Deadlock-Trace

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

Eine weitere interessante und mögliche verwandte Erkenntnis ist, dass Clustered- und Non-Clustered-Index unterschiedliche Sperrverhalten zu haben scheinen

Bei Verwendung des Clustered-Index gibt es eine exklusive Sperre für den Schlüssel sowie eine exklusive Sperre für RID, wenn eine Aktualisierung durchgeführt wird, was erwartet wird. Es gibt jedoch zwei exklusive Sperren für zwei verschiedene RIDs, wenn ein nicht gruppierter Index verwendet wird, was mich verwirrt.

Wäre hilfreich, wenn jemand erklären könnte, warum dies so ist.

SQL testen:

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

Mit id als Clustered Index:

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

Mit id als Non-Clustered Index

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: Angaben zu Deadlocks ohne Index Angenommen,
ich habe zwei tx A und B mit jeweils zwei Update-Anweisungen, unterschiedliche Zeilen natürlich
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} und {4} hätten seitdem die Chance auf einen Deadlock

Bei {1} wird eine U-Sperre für Zeile 63502 angefordert, da ein Tabellenscan durchgeführt werden muss, und die X-Sperre könnte in Zeile 63501 angehalten worden sein, da sie der Bedingung entspricht

Bei {4} wird eine U-Sperre für Zeile 63501 angefordert, und die X-Sperre gilt bereits für 63502

also hat txA 63501 und wartet 63502, während txB 63502 auf 63501 wartet, was ein Deadlock ist

EDIT2: Es stellt sich heraus, dass ein Fehler in meinem Testfall hier einen Unterschied ausmacht. Entschuldigen Sie die Verwirrung, aber der Fehler macht einen Unterschied aus und scheint letztendlich den Deadlock zu verursachen.

Da Pauls Analyse mir in diesem Fall wirklich geholfen hat, akzeptiere ich das als Antwort.

Aufgrund des Fehlers in meinem Testfall können zwei Transaktionen txA und txB dieselbe Zeile wie folgt aktualisieren:

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} und {3} hätten die Möglichkeit eines Deadlocks, wenn:

txA fordert eine U-Sperre für den Schlüssel an, während X die RID sperrt (aufgrund der Aktualisierung von {1}). txB fordert eine U-Sperre für die RID an, während U die RID sperrt

Bood
quelle
1
Ich kann mir nicht vorstellen, warum eine Transaktion dieselbe Zeile zweimal aktualisieren muss.
Ypercubeᵀᴹ
@ypercube Guter Punkt, das sollte ich verbessern. Aber in diesem Fall möchte ich nur das Verhalten von Sperren besser verstehen
Bood,
@ypercube nach mehr Gedanken Ich denke, es ist möglich, dass eine Anwendung mit komplexer Logik die gleiche Zeile zweimal im selben Text aktualisieren muss, zum Beispiel könnte es sich um verschiedene Spalten handeln
Bood

Antworten:

16

... warum mit Clustered Index der Deadlock immer noch da ist (obwohl die Trefferquote gesunken zu sein scheint)

Die Frage ist nicht genau klar (z. B. wie viele Aktualisierungen und welche idWerte in jeder Transaktion enthalten sind), aber ein offensichtliches Deadlock-Szenario ergibt sich bei mehreren einzeiligen Aktualisierungen innerhalb einer einzelnen Transaktion, bei denen sich [id]Werte und IDs überschneiden in einer anderen [id]Reihenfolge aktualisiert :

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

Deadlock-Sequenz: T1 (u2), T2 (u1), T1 (u1) warten , T2 (u2) warten .

Diese Deadlock-Sequenz kann vermieden werden, indem die Aktualisierung innerhalb jeder Transaktion streng in der ID-Reihenfolge erfolgt (Erfassung von Sperren in derselben Reihenfolge auf demselben Pfad).

Bei Verwendung des Clustered-Index gibt es eine exklusive Sperre für den Schlüssel sowie eine exklusive Sperre für RID, wenn eine Aktualisierung durchgeführt wird, was erwartet wird. Es gibt jedoch zwei exklusive Sperren für zwei verschiedene RIDs, wenn ein nicht gruppierter Index verwendet wird, was mich verwirrt.

Wenn ein eindeutiger Clustered-Index idaktiviert ist, wird der Clustering-Schlüssel exklusiv gesperrt, um Schreibvorgänge in die In-Row-Daten zu schützen. RIDZum Schutz des Schreibvorgangs in die LOB- textSpalte, die standardmäßig auf einer separaten Datenseite gespeichert ist, ist eine separate exklusive Sperre erforderlich .

Wenn die Tabelle ein Heap mit nur einem nicht gruppierten Index ist id, passieren zwei Dinge. Erstens RIDbezieht sich eine exklusive Sperre auf die Heap-In-Row-Daten und die andere auf die LOB-Daten wie zuvor. Der zweite Effekt ist, dass ein komplexerer Ausführungsplan erforderlich ist.

Mit einem Clustered-Index und einer einfachen Aktualisierung von Gleichheitsprädikaten mit einem Wert kann der Abfrageprozessor eine Optimierung anwenden, die die Aktualisierung (Lesen und Schreiben) in einem einzelnen Operator unter Verwendung eines einzelnen Pfads ausführt:

Single-Operator-Update

Die Zeile wird in einer einzelnen Suchoperation gefunden und aktualisiert, wobei nur exklusive Sperren erforderlich sind (es sind keine Aktualisierungssperren erforderlich). Beispiel für eine Sperrsequenz anhand Ihrer Beispieltabelle:

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

Mit nur einem nicht gruppierten Index kann nicht dieselbe Optimierung angewendet werden, da von einer B-Baumstruktur gelesen und eine andere geschrieben werden muss. Der Mehrpfadplan verfügt über separate Lese- und Schreibphasen:

Multi-Iterator-Update

Dadurch werden beim Lesen Aktualisierungssperren aktiviert und beim Qualifizieren der Zeile in exklusive Sperren konvertiert. Beispiel für eine Sperrsequenz mit dem angegebenen Schema:

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 

Beachten Sie, dass die LOB-Daten im Table Update-Iterator gelesen und geschrieben werden. Der komplexere Plan und mehrere Lese- und Schreibpfade erhöhen die Wahrscheinlichkeit eines Deadlocks.

Schließlich kann ich nicht umhin, die in der Tabellendefinition verwendeten Datentypen zu bemerken. Sie sollten den veralteten textDatentyp nicht für neue Arbeiten verwenden. Die Alternative, wenn Sie wirklich die Möglichkeit benötigen, bis zu 2 GB Daten in dieser Spalte zu speichern, ist varchar(max). Ein wichtiger Unterschied zwischen textund varchar(max)besteht darin, dass textDaten standardmäßig varchar(max)offline gespeichert werden, während sie standardmäßig in Reihe gespeichert werden .

Verwenden Sie Unicode-Typen nur, wenn Sie diese Flexibilität benötigen (z. B. ist schwer einzusehen, warum eine IP-Adresse Unicode benötigen würde). Wählen Sie außerdem geeignete Längenbeschränkungen für Ihre Attribute aus - 255 ist wahrscheinlich nicht überall korrekt.

Zusätzliche Lektüre:
Häufige Deadlock- und Livelock-Muster
Bart Duncans Deadlock-Fehlerbehebungsserie

Das Nachverfolgen von Sperren kann auf verschiedene Arten erfolgen. SQL Server Express mit erweiterten Diensten (nur ab 2014 und 2012 SP1 ) enthält das Profiler- Tool, mit dem Sie die Details der Erfassung und Freigabe von Sperren anzeigen können.

Paul White 9
quelle
Hervorragende Antwort. Wie geben Sie die Protokolle / Trace aus, die die Meldungen "Erfassung ... Sperre" und "Freigabe der Sperrreferenz" enthalten?
Sanjiv Jivan