web-dev-qa-db-ja.com

2つの削除を実行するときに、テーブルからそれ自体への外部キーがデッドロックを引き起こすのはなぜですか?

「SelfRef」というテーブルのあるデータベースがあります。 SelfRefには2つのフィールドがあります。

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

SelfRefIdフィールドをIdフィールドにマップする外部キー制約があります。

データベースを参照するEntityFrameworkCoreプロジェクトがあります。私は次のテストを実行しています:

  1. SelfRefテーブルに2つのエントリを作成します。いずれの場合も、SelfRefIdはnullです。変更内容を保存。
  2. 別々の、多かれ少なかれ同時のタスクで両方のエントリを削除します。

ステップ2でデッドロックが発生することがよくあります。なぜそうすべきかわかりません。

以下にコードを示しますが、問題がこのコードに固有のものであるとは思えません。

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());
}

編集:EF6でも同じデッドロックが発生することを確認しました。

データベースにテーブルを作成するには:

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

エンティティを生成するには:

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

DbFactoryクラス:

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);
    }
}

エラーメッセージ:

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.

プロファイラーからのSQLイベントの一部を以下に示します。多数の「監査ログイン」および「監査ログアウト」イベントをスキップしていますが、フィールドが1つずつコピーされます。何かを抽出するためのより良い方法があるはずですが、それが何であるかはわかりません。

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

この特定の実行は2つのスレッドで行われました。上記のような2つのイベントの後、私は次の2つを見ました。

exec sp_reset_connection 

次に、このような2つ:

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

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

さらに2つの接続がリセットされ、それが行われました。

デッドロック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>
7

デッドロックXMLは、2つのセッションが2つの異なる行をめぐって争っており、各セッションが一方にe(X)clusiveロックを持ち、もう一方に(S)haredロックを要求していることを示しています。

与えられた:

  1. PKを削除するには、まずPKへの既存のFK参照がないことを確認する必要があります
  2. トランザクション分離レベルは「コミットされた読み取り」です
  3. 接続プールを使用しています
  4. 2つの競合するセッションは同じアプリケーションからのものです(hostpidの値による)
  5. それはようです非同期としてステップ1を行うことは、非同期を保存しない間は問題を許容しません
  6. 特にデッドロックXMLが両方のセッションでtrancount="2"を示しているため、EFがバックグラウンドで実行している可能性がありますが、トランザクションが使用されていることを示すものはありません。

次のいずれかが可能です。

  1. 保存でasyncオプションを使用すると、タイミングやアプリレイヤーがトランザクションを開始しているかどうか、またはその両方が変更されます。
  2. 接続プーリングにより、スレッドは使用していた接続/セッションを交換できます

現在、接続プーリング(番号2)は一般にで問題が発生することはありませんが、それが可能なシナリオがあるため(分散トランザクションの場合など)使用されている)、私はそれを除外したくありませんでした。そして、EFおよび/または非同期オプションがどのように処理するのか私にはわからないので、それは非同期プールと接続プールの組み合わせである可能性が非常に高いです。

では、最初にステップ1の非同期保存を維持してみてください。ただし、接続文字列にPooling=false;を追加して、接続プールを無効にしてください。

もちろん、保存時に非同期を使用するnotとすると、接続プールを無効にすることが役立つかどうかにかかわらず、問題は解決されます(または、少なくともこれまでのところ表示されます)。アイテムを作成するときは、非同期を使用しないことを検討する必要があります。おそらくそれを削除と選択にのみ使用しますか?手順1でasyncを使用する場合と使用しない場合の動作の正確な変化を特定したとしても、回避できるものではない可能性があります(または、少なくとも実行すべきでないことを行わないと回避できません)。

2
Solomon Rutzky