Warum würde ein Fremdschlüssel von einer Tabelle zu sich selbst einen Deadlock verursachen, wenn zwei Löschvorgänge ausgeführt werden?

7

Ich habe eine Datenbank mit einer Tabelle "SelfRef". SelfRef hat zwei Felder:

Id (guid, PK, not null)
SelfRefId (guid, nullable)

Es gibt eine Fremdschlüsseleinschränkung, die das SelfRefId-Feld wieder dem ID-Feld zuordnet.

Ich habe ein EntityFrameworkCore-Projekt, das auf die Datenbank verweist. Ich führe den folgenden Test durch:

  1. Erstellen Sie zwei Einträge in der SelfRef-Tabelle. In jedem Fall ist die SelfRefId null. Änderungen speichern.
  2. Löschen Sie beide Einträge in separaten, mehr oder weniger gleichzeitigen Aufgaben.

Ich stelle fest, dass Schritt 2 häufig einen Deadlock verursacht. Ich verstehe nicht, warum es sollte.

Ich zeige meinen Code unten, obwohl ich bezweifle, dass das Problem für diesen Code spezifisch ist:

public class TestSelfRefDeadlock
{
    private async Task CreateSelfRef_ThenDelete_Deletes() {
        var sr = new SelfRef
        {
            Id = Guid.NewGuid(),
            Name = "SR"
        };
        var factory = new SelfRefDbFactory();
        using (var db = factory.Create()) {
            db.Add(sr);
            await db.SaveChangesAsync();  // EDIT: Changing this to db.SaveChanges() appears to fix the problem, at least in this test scenario.
        }
        using (var db = factory.Create()) {
            db.SelfRef.Remove(sr);
            await db.SaveChangesAsync();
        }
    }

    private IEnumerable<Task> DeadlockTasks() {
        for (int i=0; i<2; i++) {
            yield return CreateSelfRef_ThenDelete_Deletes();
        }
    }

    [Fact]
    public async Task LotsaDeletes_DoNotDeadlock()
        => await Task.WhenAll(DeadlockTasks());
}

EDIT: Ich habe bestätigt, dass der gleiche Deadlock in EF6 auftritt.

So erstellen Sie die Tabelle in meiner Datenbank:

USE [SelfReferential]
GO

/****** Object:  Table [dbo].[SelfRef]    Script Date: 3/20/2018 3:43:50 PM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[SelfRef](
    [Id] [uniqueidentifier] NOT NULL,
    [SelfReferentialId] [uniqueidentifier] NULL,
    [Name] [nchar](10) NULL,
 CONSTRAINT [PK_SelfRef] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[SelfRef]  WITH CHECK ADD  CONSTRAINT [FK_SelfRef_SelfRef] FOREIGN KEY([SelfReferentialId])
REFERENCES [dbo].[SelfRef] ([Id])
GO

ALTER TABLE [dbo].[SelfRef] CHECK CONSTRAINT [FK_SelfRef_SelfRef]
GO

So generieren Sie die Entitäten:

Scaffold-DbContext "Server=localhost;Database=SelfReferential;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -Context SelfRefDb -OutputDir Entities -Force 

Die DbFactory-Klasse:

public class SelfRefDbFactory : IFactory<SelfRefDb>
{
    private const string str1 = @"Data Source=MyPcName;Initial Catalog=SelfReferential;Integrated Security=True;ApplicationIntent=ReadWrite;";
    private const string str2 = @"Data Source=MyPcName;Initial Catalog=SelfReferential;Integrated Security=True;ApplicationIntent=ReadWrite;MultipleActiveResultSets=True";
    public SelfRefDb Create() {
        var options = new DbContextOptionsBuilder<SelfRefDb>()
            .UseSqlServer(str1).Options;
        return new SelfRefDb(options);
    }
}

Die Fehlermeldung:

Message: System.InvalidOperationException : An exception has been raised that is likely due to a transient failure. If you are connecting to a SQL Azure database consider using SqlAzureExecutionStrategy.
---- Microsoft.EntityFrameworkCore.DbUpdateException : An error occurred while updating the entries. See the inner exception for details.
-------- System.Data.SqlClient.SqlException : Transaction (Process ID 58) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

Einige SQL-Ereignisse aus dem Profiler sind unten aufgeführt. Ich überspringe zahlreiche "Audit Login" - und "Audit Logout" -Ereignisse und die Felder werden einzeln kopiert. Es muss einen besseren Weg geben, Dinge zu extrahieren, aber ich weiß nicht, was es ist.

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [SelfRef] ([Id], [Name], [SelfReferentialId])
VALUES (@p0, @p1, @p2);
',N'@p0 uniqueidentifier,@p1 nvarchar(10),@p2 
uniqueidentifier',@p0='93671E2E-28E5-414D-A3DB-239FA433640C',@p1=N'SR',@p2=NULL

Dieser spezielle Lauf war mit zwei Threads. Nach zwei Ereignissen wie dem oben genannten sah ich zwei von:

exec sp_reset_connection 

dann zwei wie folgt:

exec sp_executesql N'SET NOCOUNT ON;
DELETE FROM [SelfRef]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;

',N'@p0 uniqueidentifier',@p0='F5B53458-08C5-485E-8364-2A2842E95158'

Zwei weitere Verbindungsrücksetzungen, dann war es geschafft.

Deadlock xml:

<deadlock-list>
    <deadlock victim="process1fe3db6b468">
        <process-list>
            <process id="process1fe3db6b468" taskpriority="0" logused="300" waitresource="KEY: 14:72057594041401344 (427c492d0b23)" waittime="147" ownerId="218910" transactionname="user_transaction" lasttranstarted="2018-03-22T14:33:57.880" XDES="0x2021f8bc408" lockMode="S" schedulerid="2" kpid="8540" status="suspended" spid="53" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2018-03-22T14:33:57.883" lastbatchcompleted="2018-03-22T14:33:57.880" lastattention="1900-01-01T00:00:00.880" clientapp=".Net SqlClient Data Provider" hostname="WILLIAMASUS" hostpid="14656" loginname="MicrosoftAccount\[email protected]" isolationlevel="read committed (2)" xactid="218910" currentdb="14" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
                <executionStack>
                    <frame procname="adhoc" line="2" stmtstart="78" stmtend="154" sqlhandle="0x0200000087849c297464e5637211740e8fde989bf9ffc37a0000000000000000000000000000000000000000">
unknown     </frame>
                    <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown     </frame>
                </executionStack>
                <inputbuf>
(@p0 uniqueidentifier)SET NOCOUNT ON;
DELETE FROM [SelfRef]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;

    </inputbuf>
            </process>
            <process id="process1fe3db684e8" taskpriority="0" logused="300" waitresource="KEY: 14:72057594041401344 (8e30f77e2707)" waittime="146" ownerId="218908" transactionname="user_transaction" lasttranstarted="2018-03-22T14:33:57.880" XDES="0x20227f6b458" lockMode="S" schedulerid="1" kpid="8300" status="suspended" spid="54" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2018-03-22T14:33:57.883" lastbatchcompleted="2018-03-22T14:33:57.880" lastattention="1900-01-01T00:00:00.880" clientapp=".Net SqlClient Data Provider" hostname="WILLIAMASUS" hostpid="14656" loginname="MicrosoftAccount\[email protected]" isolationlevel="read committed (2)" xactid="218908" currentdb="14" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
                <executionStack>
                    <frame procname="adhoc" line="2" stmtstart="78" stmtend="154" sqlhandle="0x0200000087849c297464e5637211740e8fde989bf9ffc37a0000000000000000000000000000000000000000">
unknown     </frame>
                    <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown     </frame>
                </executionStack>
                <inputbuf>
(@p0 uniqueidentifier)SET NOCOUNT ON;
DELETE FROM [SelfRef]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;

    </inputbuf>
            </process>
        </process-list>
        <resource-list>
            <keylock hobtid="72057594041401344" dbid="14" objectname="SelfReferential.dbo.SelfRef" indexname="PK_SelfRef" id="lock20229074e00" mode="X" associatedObjectId="72057594041401344">
                <owner-list>
                    <owner id="process1fe3db684e8" mode="X"/>
                </owner-list>
                <waiter-list>
                    <waiter id="process1fe3db6b468" mode="S" requestType="wait"/>
                </waiter-list>
            </keylock>
            <keylock hobtid="72057594041401344" dbid="14" objectname="SelfReferential.dbo.SelfRef" indexname="PK_SelfRef" id="lock20229073c80" mode="X" associatedObjectId="72057594041401344">
                <owner-list>
                    <owner id="process1fe3db6b468" mode="X"/>
                </owner-list>
                <waiter-list>
                    <waiter id="process1fe3db684e8" mode="S" requestType="wait"/>
                </waiter-list>
            </keylock>
        </resource-list>
    </deadlock>
</deadlock-list>
William Jockusch
quelle
Ist es sicher anzunehmen, dass die Implementierung von SelfRefDbFactory.Createvollständig threadsicher ist und immer neue Instanzen Ihres Datenbankkontexts zurückgibt?
1
Können Sie Profiler verwenden, um die tatsächlich gesendeten Abfragen zu erfassen?
HLGEM
1
@ Kekse prüfen DeadlockTasks. Es werden viele Aufrufe CreateSelfRef_ThenDelete_Deletesin separaten Aufgaben ausgeführt. Diese Methode erstellt zwei verschiedene Kontexte , dh zwei Verbindungen, um zwei verschiedene Entitäten zu ändern. Das sind zu viele Vorgänge, die dieselben Objekte betreffen, die gleichzeitig ausgeführt werden. Warum irgendetwas davon? Erstellen Sie einfach einen einzelnen Kontext, ändern Sie alle Entitäten nach Bedarf und rufen Sie SaveChangeseinmal an
Panagiotis Kanavos
2
@Panagiotis, ich denke, es ist ziemlich klar, dass das OP diesen Test geschrieben hat, um absichtlich gleichzeitig in dieselbe Tabelle einzufügen und daraus zu löschen - etwas, das für viele Anwendungen sicherlich keine ungewöhnliche Anforderung ist. Was ist los mit vielen Kontexten? Dies ist kein Argument für Codierungsstandards oder Leistung.
1
Können Sie das Deadlock Graph XML hinzufügen?
Mark Sinkinson

Antworten:

2

Das Deadlock-XML zeigt an, dass die beiden Sitzungen über zwei verschiedene Zeilen kämpfen, wobei jede Sitzung eine e (X) -Clusive-Sperre für eine hat und eine (S) -Hared-Sperre für die andere anfordert.

Gegeben:

  1. Zum Löschen einer PK muss zunächst sichergestellt werden, dass keine FK-Verweise darauf vorhanden sind
  2. Die Transaktionsisolationsstufe ist "Read Committed".
  3. Sie verwenden das Verbindungspooling
  4. Die beiden konkurrierenden Sitzungen stammen aus derselben Anwendung (gemäß dem hostpidWert).
  5. Es scheint, dass das Ausführen von Schritt 1 als asynchron das Problem berücksichtigt, während das nicht asynchrone Speichern nicht erfolgt
  6. Es gibt keinen Hinweis darauf, dass Transaktionen verwendet werden, obwohl EF dies möglicherweise hinter den Kulissen tut, zumal das Deadlock-XML trancount="2"für beide Sitzungen angezeigt wird

es ist möglich, dass entweder:

  1. Durch die Verwendung der asynchronen Option beim Speichern wird das Timing geändert und / oder ob die App-Ebene eine Transaktion startet oder nicht
  2. Durch das Verbindungspooling können die Threads die von ihnen verwendete Verbindung / Sitzung austauschen

Nun, das Verbindungspooling (Nummer 2) sollte im Allgemeinen keine Probleme verursachen, aber da es Szenarien gibt, in denen dies möglich ist (z. B. wenn verteilte Transaktionen verwendet werden), wollte ich dies nicht ausschließen. Und da ich nicht weiß, wie EF und / oder die asynchrone Option die Dinge handhaben, könnte es sich durchaus um eine Kombination aus asynchronem und Verbindungspooling handeln.

Warum versuchen Sie nicht zuerst, die asynchrone Speicherung für Schritt 1 beizubehalten, sondern deaktivieren das Verbindungspooling, indem Sie es Pooling=false;zu Ihrer Verbindungszeichenfolge hinzufügen .

Unabhängig davon, ob das Deaktivieren des Verbindungspoolings hilfreich ist oder nicht , sollten Sie beim Erstellen von Elementen in Betracht ziehen, Async nicht zu verwenden, da das Problem nicht durch asynchrone Speicherung behoben wird (oder zumindest bisher). Vielleicht nur zum Löschen und Auswählen verwenden? Selbst wenn wir die genaue Änderung des Verhaltens zwischen der Verwendung und der Nichtverwendung von Async in Schritt 1 bestimmen, kann möglicherweise nichts umgangen werden (oder zumindest nicht umgangen werden, ohne Dinge zu tun, die wahrscheinlich nicht getan werden sollten).

Solomon Rutzky
quelle
Das Deaktivieren des Poolings scheint die Wahrscheinlichkeit eines Deadlocks zu verringern, jedoch nicht auf Null.
William Jockusch