Ich arbeite jetzt schon seit einigen Tagen an diesem Deadlock-Problem und egal was ich tue, es bleibt auf die eine oder andere Weise bestehen.
Zunächst die allgemeine Prämisse: Wir haben Besuche mit VisitItems in einer Eins-zu-Viele-Beziehung.
Relevante Informationen zu VisitItems:
CREATE TABLE [BAR].[VisitItems] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[VisitType] INT NOT NULL,
[FeeRateType] INT NOT NULL,
[Amount] DECIMAL (18, 2) NOT NULL,
[GST] DECIMAL (18, 2) NOT NULL,
[Quantity] INT NOT NULL,
[Total] DECIMAL (18, 2) NOT NULL,
[ServiceFeeType] INT NOT NULL,
[ServiceText] NVARCHAR (200) NULL,
[InvoicingProviderId] INT NULL,
[FeeItemId] INT NOT NULL,
[VisitId] INT NULL,
[IsDefault] BIT NOT NULL DEFAULT 0,
[SourceVisitItemId] INT NULL,
[OverrideCode] INT NOT NULL DEFAULT 0,
[InvoiceToCentre] BIT NOT NULL DEFAULT 0,
[IsSurchargeItem] BIT NOT NULL DEFAULT 0,
CONSTRAINT [PK_BAR.VisitItems] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_BAR.VisitItems_BAR.FeeItems_FeeItem_Id] FOREIGN KEY ([FeeItemId]) REFERENCES [BAR].[FeeItems] ([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.Visits_Visit_Id] FOREIGN KEY ([VisitId]) REFERENCES [BAR].[Visits] ([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.VisitTypes] FOREIGN KEY ([VisitType]) REFERENCES [BAR].[VisitTypes]([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.FeeRateTypes] FOREIGN KEY ([FeeRateType]) REFERENCES [BAR].[FeeRateTypes]([Id]),
CONSTRAINT [FK_BAR.VisitItems_CMN.Users_Id] FOREIGN KEY (InvoicingProviderId) REFERENCES [CMN].[Users] ([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.VisitItems_SourceVisitItem_Id] FOREIGN KEY ([SourceVisitItemId]) REFERENCES [BAR].[VisitItems]([Id]),
CONSTRAINT [CK_SourceVisitItemId_Not_Equal_Id] CHECK ([SourceVisitItemId] <> [Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.OverrideCodes] FOREIGN KEY ([OverrideCode]) REFERENCES [BAR].[OverrideCodes]([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.ServiceFeeTypes] FOREIGN KEY ([ServiceFeeType]) REFERENCES [BAR].[ServiceFeeTypes]([Id])
)
CREATE NONCLUSTERED INDEX [IX_FeeItem_Id]
ON [BAR].[VisitItems]([FeeItemId] ASC)
CREATE NONCLUSTERED INDEX [IX_Visit_Id]
ON [BAR].[VisitItems]([VisitId] ASC)
Besuchsinfo:
CREATE TABLE [BAR].[Visits] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[VisitType] INT NOT NULL,
[DateOfService] DATETIMEOFFSET NOT NULL,
[InvoiceAnnotation] NVARCHAR(255) NULL ,
[PatientId] INT NOT NULL,
[UserId] INT NULL,
[WorkAreaId] INT NOT NULL,
[DefaultItemOverride] BIT NOT NULL DEFAULT 0,
[DidNotWaitAdjustmentId] INT NULL,
[AppointmentId] INT NULL,
CONSTRAINT [PK_BAR.Visits] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_BAR.Visits_CMN.Patients] FOREIGN KEY ([PatientId]) REFERENCES [CMN].[Patients] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_BAR.Visits_CMN.Users] FOREIGN KEY ([UserId]) REFERENCES [CMN].[Users] ([Id]),
CONSTRAINT [FK_BAR.Visits_CMN.WorkAreas_WorkAreaId] FOREIGN KEY ([WorkAreaId]) REFERENCES [CMN].[WorkAreas] ([Id]),
CONSTRAINT [FK_BAR.Visits_BAR.VisitTypes] FOREIGN KEY ([VisitType]) REFERENCES [BAR].[VisitTypes]([Id]),
CONSTRAINT [FK_BAR.Visits_BAR.Adjustments] FOREIGN KEY ([DidNotWaitAdjustmentId]) REFERENCES [BAR].[Adjustments]([Id]),
);
CREATE NONCLUSTERED INDEX [IX_Visits_PatientId]
ON [BAR].[Visits]([PatientId] ASC);
CREATE NONCLUSTERED INDEX [IX_Visits_UserId]
ON [BAR].[Visits]([UserId] ASC);
CREATE NONCLUSTERED INDEX [IX_Visits_WorkAreaId]
ON [BAR].[Visits]([WorkAreaId]);
Mehrere Benutzer möchten die VisitItems-Tabelle auf folgende Weise gleichzeitig aktualisieren:
Eine separate Webanforderung erstellt einen Besuch mit VisitItems (normalerweise 1). Dann (die Problemanfrage):
- Die Webanforderung geht ein, öffnet die NHibernate-Sitzung und startet die NHibernate-Transaktion (mit Repeatable Read und READ_COMMITTED_SNAPSHOT).
- Lesen Sie alle Besuchselemente für einen bestimmten Besuch von VisitId .
- Der Code prüft, ob die Elemente noch relevant sind oder ob wir neue Elemente mit komplexen Regeln benötigen (so etwas lange, z. B. 40 ms).
- Der Code findet, dass 1 Element hinzugefügt werden muss, und fügt es mit NHibernate Visit.VisitItems.Add (..) hinzu.
- Der Code gibt an, dass ein Element gelöscht werden muss (nicht das gerade hinzugefügte), und entfernt es mithilfe von NHibernate Visit.VisitItems.Remove (Element).
- Code schreibt die Transaktion fest
Mit einem Tool simuliere ich 12 gleichzeitige Anforderungen, was in einer zukünftigen Produktionsumgebung sehr wahrscheinlich ist.
[BEARBEITEN] Auf Anfrage wurden viele der Untersuchungsdetails entfernt, die ich hier hinzugefügt hatte, um sie kurz zu halten.
Nach vielen Recherchen war der nächste Schritt zu überlegen, wie ich einen Hinweis auf einen anderen Index als den in der where-Klausel verwendeten (dh den Primärschlüssel, da dieser zum Löschen verwendet wird) sperren kann, sodass ich meine lock-Anweisung auf änderte :
var items = (List<VisitItem>)_session.CreateSQLQuery(@"SELECT * FROM BAR.VisitItems WITH (XLOCK, INDEX([PK_BAR.VisitItems]))
WHERE VisitId = :visitId")
.AddEntity(typeof(VisitItem))
.SetParameter("visitId", qi.Visit.Id)
.List<VisitItem>();
Dies reduzierte die Deadlocks in der Frequenz leicht, aber sie passierten immer noch. Und hier fange ich an, mich zu verlaufen:
<deadlock-list>
<deadlock victim="process3f71e64e8">
<process-list>
<process id="process3f71e64e8" taskpriority="0" logused="0" waitresource="KEY: 5:72057594071744512 (a5e1814e40ba)" waittime="3812" ownerId="8004520" transactionname="user_transaction" lasttranstarted="2015-12-14T10:24:58.010" XDES="0x3f7cb43b0" lockMode="X" schedulerid="1" kpid="15788" status="suspended" spid="63" sbid="0" ecid="0" priority="0" trancount="1" lastbatchstarted="2015-12-14T10:24:58.013" lastbatchcompleted="2015-12-14T10:24:58.013" lastattention="1900-01-01T00:00:00.013" clientapp=".Net SqlClient Data Provider" hostname="ABC" hostpid="10016" loginname="bsapp" isolationlevel="repeatable read (3)" xactid="8004520" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="adhoc" line="1" stmtstart="18" stmtend="254" sqlhandle="0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000">
unknown
</frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown
</frame>
</executionStack>
<inputbuf>
(@p0 int)SELECT * FROM BAR.VisitItems WITH (XLOCK, INDEX([PK_BAR.VisitItems]))
WHERE VisitId = @p0
</inputbuf>
</process>
<process id="process4105af468" taskpriority="0" logused="1824" waitresource="KEY: 5:72057594071744512 (8194443284a0)" waittime="3792" ownerId="8004519" transactionname="user_transaction" lasttranstarted="2015-12-14T10:24:58.010" XDES="0x3f02ea3b0" lockMode="S" schedulerid="8" kpid="15116" status="suspended" spid="65" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2015-12-14T10:24:58.033" lastbatchcompleted="2015-12-14T10:24:58.033" lastattention="1900-01-01T00:00:00.033" clientapp=".Net SqlClient Data Provider" hostname="ABC" hostpid="10016" loginname="bsapp" isolationlevel="repeatable read (3)" xactid="8004519" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="adhoc" line="1" stmtstart="18" stmtend="98" sqlhandle="0x0200000075abb0074bade5aa57b8357410941428df4d54130000000000000000000000000000000000000000">
unknown
</frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown
</frame>
</executionStack>
<inputbuf>
(@p0 int)DELETE FROM BAR.VisitItems WHERE Id = @p0
</inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057594071744512" dbid="5" objectname="BAR.VisitItems" indexname="PK_BAR.VisitItems" id="lock449e27500" mode="X" associatedObjectId="72057594071744512">
<owner-list>
<owner id="process4105af468" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process3f71e64e8" mode="X" requestType="wait"/>
</waiter-list>
</keylock>
<keylock hobtid="72057594071744512" dbid="5" objectname="BAR.VisitItems" indexname="PK_BAR.VisitItems" id="lock46a525080" mode="X" associatedObjectId="72057594071744512">
<owner-list>
<owner id="process3f71e64e8" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process4105af468" mode="S" requestType="wait"/>
</waiter-list>
</keylock>
</resource-list>
</deadlock>
</deadlock-list>
Ein Trace der resultierenden Anzahl von Abfragen sieht folgendermaßen aus.
[BEARBEITEN] Whoa. Was für eine Woche. Ich habe jetzt die Ablaufverfolgung mit der unreduzierten Ablaufverfolgung der relevanten Anweisung aktualisiert, von der ich denke, dass sie zum Deadlock führt.
exec sp_executesql N'SELECT * FROM BAR.VisitItems WITH (XLOCK, INDEX([PK_BAR.VisitItems]))
WHERE VisitId = @p0',N'@p0 int',@p0=3826
go
exec sp_executesql N'SELECT visititems0_.VisitId as VisitId1_, visititems0_.Id as Id1_, visititems0_.Id as Id37_0_, visititems0_.VisitType as VisitType37_0_, visititems0_.FeeItemId as FeeItemId37_0_, visititems0_.FeeRateType as FeeRateT4_37_0_, visititems0_.Amount as Amount37_0_, visititems0_.GST as GST37_0_, visititems0_.Quantity as Quantity37_0_, visititems0_.Total as Total37_0_, visititems0_.ServiceFeeType as ServiceF9_37_0_, visititems0_.ServiceText as Service10_37_0_, visititems0_.InvoiceToCentre as Invoice11_37_0_, visititems0_.IsDefault as IsDefault37_0_, visititems0_.OverrideCode as Overrid13_37_0_, visititems0_.IsSurchargeItem as IsSurch14_37_0_, visititems0_.VisitId as VisitId37_0_, visititems0_.InvoicingProviderId as Invoici16_37_0_, visititems0_.SourceVisitItemId as SourceV17_37_0_ FROM BAR.VisitItems visititems0_ WHERE visititems0_.VisitId=@p0',N'@p0 int',@p0=3826
go
exec sp_executesql N'INSERT INTO BAR.VisitItems (VisitType, FeeItemId, FeeRateType, Amount, GST, Quantity, Total, ServiceFeeType, ServiceText, InvoiceToCentre, IsDefault, OverrideCode, IsSurchargeItem, VisitId, InvoicingProviderId, SourceVisitItemId) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15); select SCOPE_IDENTITY()',N'@p0 int,@p1 int,@p2 int,@p3 decimal(28,5),@p4 decimal(28,5),@p5 int,@p6 decimal(28,5),@p7 int,@p8 nvarchar(4000),@p9 bit,@p10 bit,@p11 int,@p12 bit,@p13 int,@p14 int,@p15 int',@p0=1,@p1=452,@p2=1,@p3=0,@p4=0,@p5=1,@p6=0,@p7=1,@p8=NULL,@p9=0,@p10=1,@p11=0,@p12=0,@p13=3826,@p14=3535,@p15=NULL
go
exec sp_executesql N'UPDATE BAR.Visits SET VisitType = @p0, DateOfService = @p1, InvoiceAnnotation = @p2, DefaultItemOverride = @p3, AppointmentId = @p4, ReferralRequired = @p5, ReferralCarePlan = @p6, UserId = @p7, PatientId = @p8, WorkAreaId = @p9, DidNotWaitAdjustmentId = @p10, ReferralId = @p11 WHERE Id = @p12',N'@p0 int,@p1 datetimeoffset(7),@p2 nvarchar(4000),@p3 bit,@p4 int,@p5 bit,@p6 nvarchar(4000),@p7 int,@p8 int,@p9 int,@p10 int,@p11 int,@p12 int',@p0=1,@p1='2016-01-22 12:37:06.8915296 +08:00',@p2=NULL,@p3=0,@p4=NULL,@p5=0,@p6=NULL,@p7=3535,@p8=4246,@p9=2741,@p10=NULL,@p11=NULL,@p12=3826
go
exec sp_executesql N'DELETE FROM BAR.VisitItems WHERE Id = @p0',N'@p0 int',@p0=7919
go
Jetzt scheint meine Sperre einen Effekt zu haben, da sie im Deadlock-Diagramm angezeigt wird. Aber was? Drei exklusive Schlösser und ein gemeinsames Schloss? Wie funktioniert das mit demselben Objekt / Schlüssel? Ich dachte, solange Sie eine exklusive Sperre haben, können Sie keine gemeinsame Sperre von jemand anderem erhalten? Und umgekehrt. Wenn Sie eine gemeinsame Sperre haben, kann niemand eine exklusive Sperre erhalten, er muss warten.
Ich glaube, hier fehlt mir ein tieferes Verständnis dafür, wie die Schlösser funktionieren, wenn sie mit mehreren Schlüsseln am selben Tisch belegt sind.
Hier sind einige der Dinge, die ich ausprobiert habe und deren Auswirkungen:
- Der lock-Anweisung wurde ein weiterer Indexhinweis für IX_Visit_Id hinzugefügt. Keine Änderung
- Eine zweite Spalte zur IX_Visit_Id hinzugefügt (die ID der VisitItem-Spalte). weit hergeholt, aber trotzdem versucht. Keine Änderung
- Die Isolationsstufe wurde wieder in "Festgeschrieben" geändert (Standardeinstellung in unserem Projekt), Deadlocks treten weiterhin auf
- Die Isolationsstufe wurde in serialisierbar geändert. Deadlocks passieren immer noch, aber schlimmer (verschiedene Grafiken). Das will ich sowieso nicht wirklich.
- Wenn sie ein Tischschloss nehmen, verschwinden sie (offensichtlich), aber wer würde das wollen?
- Es funktioniert, eine pessimistische Anwendungssperre (mit sp_getapplock) zu verwenden, aber das ist so ziemlich das Gleiche wie die Tabellensperre. Ich möchte das nicht tun.
- Das Hinzufügen des READPAST-Hinweises zum XLOCK-Hinweis machte keinen Unterschied
- Ich habe PageLock für den Index und PK deaktiviert, kein Unterschied
- Ich habe dem XLOCK-Hinweis einen ROWLOCK-Hinweis hinzugefügt, der keinen Unterschied macht
Einige Randnotizen zu NHibernate: Die Art und Weise, wie es verwendet wird und wie es funktioniert, ist, dass es die SQL-Anweisungen zwischenspeichert, bis es wirklich notwendig ist, sie auszuführen, es sei denn, Sie rufen Flush auf, was wir versuchen, nicht zu tun. So werden die meisten Anweisungen (zB die träge geladene Aggregatliste von VisitItems => Visit.VisitItems) nur bei Bedarf ausgeführt. Die meisten Aktualisierungs- und Löschanweisungen aus meiner Transaktion werden am Ende der Transaktion ausgeführt (wie aus dem obigen SQL-Trace hervorgeht). Ich habe wirklich keine Kontrolle über die Ausführungsreihenfolge. NHibernate entscheidet, wann was zu tun ist. Meine anfängliche Sperranweisung ist eigentlich nur ein Workaround.
Außerdem lese ich mit der lock-Anweisung die Elemente nur in eine nicht verwendete Liste ein (ich versuche nicht, die VisitItems-Liste im Visit-Objekt zu überschreiben, da NHibernate so weit wie möglich nicht funktionieren soll). Obwohl ich die Liste zuerst mit der benutzerdefinierten Anweisung gelesen habe, lädt NHibernate die Liste dennoch erneut in die Proxyobjektsammlung Visit.VisitItems. Dabei wird ein separater SQL-Aufruf verwendet, den ich im Trace sehen kann, wenn es Zeit ist, sie träge irgendwo zu laden.
Aber das sollte doch egal sein, oder? Ich habe bereits das Schloss für den besagten Schlüssel? Wird dies nicht durch erneutes Laden geändert?
Als letzte Anmerkung, vielleicht zur Verdeutlichung: Jeder Prozess fügt zuerst seinen eigenen Besuch mit VisitItems hinzu, geht dann hinein und ändert ihn (was das Löschen und Einfügen und den Deadlock auslöst). In meinen Tests gibt es nie einen Prozess, der genau denselben Visit oder dieselben VisitItems ändert.
Hat jemand eine Idee, wie er das weiter angehen kann? Kann ich irgendetwas versuchen, um dies auf intelligente Weise zu umgehen (keine Tischsperren usw.)? Außerdem möchte ich erfahren, warum diese Tripple-X-Sperre sogar für dasselbe Objekt möglich ist. Ich verstehe nicht
Bitte lassen Sie mich wissen, wenn Sie weitere Informationen benötigen, um das Rätsel zu lösen.
[EDIT] Ich habe die Frage mit der DDL für die beiden betroffenen Tabellen aktualisiert.
Außerdem wurde ich um Klärung der Erwartung gebeten: Ja, ein paar Deadlocks hier und da sind in Ordnung, wir werden es einfach wiederholen oder den Benutzer erneut einreichen lassen (im Allgemeinen). Aber bei der gegenwärtigen Frequenz mit 12 gleichzeitigen Benutzern würde ich erwarten, dass es höchstens alle paar Stunden eine gibt. Derzeit werden sie mehrmals pro Minute angezeigt.
Darüber hinaus habe ich weitere Informationen zu trancount = 2 erhalten, die möglicherweise auf ein Problem mit verschachtelten Transaktionen hinweisen, die wir nicht wirklich verwenden. Ich werde das auch untersuchen und die Ergebnisse hier dokumentieren.
SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)
für jeden executionStack-Frame den Befehl sqlhandle aus, um zu bestimmen, was tatsächlich ausgeführt wird.