web-dev-qa-db-ja.com

外部キーによる挿入時のデッドロックの防止(MSSQL)

開発者がデッドロックが発生するとは考えていなかったデッドロックを伴うアプリケーションの問題が最近発見されました。

これをさらに理解するために、想像できる限りの基本的なテストシナリオを作成することを目指しました。その結果、1つの親と1つの子という2つのテーブルができました。

この単純なシナリオでは、Management Studioの2つのインスタンスを起動し、両方のインスタンスでSAMEクエリを(ほぼ、ウィンドウを切り替えるのと同じくらい迅速に)同時に実行します。すぐにそのうちの1つがデッドロックで終了します。

私はさまざまなアプローチについて読み、SNAPSHOT分離レベルを有効にしてみましたが、何も解決されず、デッドロックが残っています。

挿入がどのようにしてデッドロックを引き起こす可能性があるのか​​について質問し、その理由について洞察を得て、うまくいけば、問題を解決するための方法を提供することさえ期待しています。

最初に、簡単なテーブルレイアウトを示します。

-- (Optional code to drop the tables and sequences) 
drop table ChildTable
go
drop table ParentTable
go

drop sequence Seq_ParentSequence 
drop sequence Seq_ChildSequence

create sequence Seq_ParentSequence as bigint start with 1 increment by 1 cache
create sequence Seq_ChildSequence as bigint start with 1 increment by 1 cache



-- Table creation
create table ParentTable (
    ID bigint not null,
    Name nvarchar(100),

    CONSTRAINT [PK_ParentTable] PRIMARY KEY CLUSTERED 
    (
        ID ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
)



create table ChildTable (
    ID bigint not null,
    ParentID bigint not null,
    Name nvarchar(100),
        CONSTRAINT [PK_ChildTable] PRIMARY KEY CLUSTERED 
    (
        ID ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]

)


alter table childtable add Constraint FK_ChildTable_ParentTable foreign key (ParentId) references ParentTable (ID) 

CREATE NONCLUSTERED INDEX [IDX_ChildTable_ParentID] ON [dbo].[ChildTable]
(
    [ID] ASC,
    [ParentID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
GO

次は、Management Studioの両方のインスタンスでテストデータを挿入するために使用するクエリです。

BEGIN

    BEGIN TRANSACTION
        ;with cte as (select 1 as IdNumber union all select IdNumber+1 from cte where IdNumber < 1000)
        select next value for Seq_ParentSequence as ID, 'Test' + cast(IdNumber as nvarchar(100)) as Name  into #tmpParent from cte option (MAXRECURSION 1000) 

        ;with cte as (select 1 as IdNumber union all select IdNumber+1 from cte where IdNumber < 100)
        select next value for Seq_ChildSequence  as ID, t.ID as ParentId, 'Test' + cast(IdNumber as nvarchar(100)) + '/' + t.Name as Name  into #tmpChild  from cte c, #tmpParent t

        insert into ParentTable (ID, Name) 
        select * from #tmpParent

        -- This will cause a dead lock if two users execute this same query at the same time ...
        insert into ChildTable(ID, ParentID, Name)
        select * from #tmpChild


        drop table #tmpParent
        drop table #tmpChild

    COMMIT;

END

クライアントの1つが次のエラーを受け取ります。

トランザクション(プロセスID 55)は、別のプロセスとのロックリソースでデッドロックされ、デッドロックの犠牲者として選択されました。トランザクションを再実行します。

そして、これがデッドロックに関するSQLプロファイラからのトレースです。 (これはたまたま私がスナップショットアイソレーションを使用したときですが、そこにあるかどうかは私のテストに違いはありませんでした)

<deadlock-list>
 <deadlock victim="process268532188">
  <process-list>
   <process id="process268532188" taskpriority="0" logused="354112" waitresource="OBJECT: 27:437576597:0 " waittime="3051" ownerId="37934645" transactionname="user_transaction" lasttranstarted="2017-06-02T11:10:09.640" XDES="0x147ca2040" lockMode="IX" schedulerid="1" kpid="1748" status="suspended" spid="65" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2017-06-02T11:10:09.627" lastbatchcompleted="2017-06-02T11:09:13.650" lastattention="1900-01-01T00:00:00.650" clientapp="Microsoft SQL Server Management Studio - Query" hostname="AF-6L64K32" hostpid="8056" loginname="sa" isolationlevel="snapshot (5)" xactid="37934645" currentdb="27" lockTimeout="4294967295" clientoption1="671090720" clientoption2="390200">
    <executionStack>
     <frame procname="adhoc" line="14" stmtstart="1398" stmtend="1546" sqlhandle="0x020000004b3b5d03b307699ab43002ca065170d084c4ba3b0000000000000000000000000000000000000000">
insert into ChildTable(ID, ParentID, Name)
        select * from #tmpChild     </frame>
    </executionStack>
    <inputbuf>
BEGIN
    SET TRANSACTION ISOLATION LEVEL SNAPSHOT  
    BEGIN TRANSACTION
        ;with cte as (select 1 as IdNumber union all select IdNumber+1 from cte where IdNumber &lt; 1000)
        select newid() as ID, &apos;Test&apos; + cast(IdNumber as nvarchar(100)) as Name  into #tmpParent from cte option (MAXRECURSION 1000) 

        ;with cte as (select 1 as IdNumber union all select IdNumber+1 from cte where IdNumber &lt; 100)
        select newid() as ID, t.ID as ParentId, &apos;Test&apos; + cast(IdNumber as nvarchar(100)) as Name  into #tmpChild  from cte c, #tmpParent t

        insert into ParentTable (ID, Name) 
        select * from #tmpParent

        -- This will cause a dead lock if two users execute this same query at the same time ...
        insert into ChildTable(ID, ParentID, Name)
        select * from #tmpChild

        drop table #tmpParent
        drop table #tmpChild

    COMMIT;

END
</inputbuf>
   </process>
   <process id="process2065cb868" taskpriority="0" logused="26540956" waitresource="KEY: 27:72057594039304192 (815b539c4fac)" waittime="2387" ownerId="37934179" transactionname="user_transaction" lasttranstarted="2017-06-02T11:10:09.430" XDES="0x1f3989740" lockMode="S" schedulerid="1" kpid="3732" status="suspended" spid="69" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2017-06-02T11:10:09.433" lastbatchcompleted="2017-06-02T11:09:56.257" lastattention="1900-01-01T00:00:00.257" clientapp="Microsoft SQL Server Management Studio - Query" hostname="AF-6L64K32" hostpid="29028" loginname="sa" isolationlevel="snapshot (5)" xactid="37934179" currentdb="27" lockTimeout="4294967295" clientoption1="671090720" clientoption2="390200">
    <executionStack>
     <frame procname="adhoc" line="16" stmtstart="1540" stmtend="1688" sqlhandle="0x02000000a9b4ae00517f6148e4fd39beff736b1ea63241b70000000000000000000000000000000000000000">
insert into ChildTable(ID, ParentID, Name)
        select * from #tmpChild     </frame>
    </executionStack>
    <inputbuf>
--ALTER DATABASE TestPlayground  
--SET ALLOW_SNAPSHOT_ISOLATION ON 
BEGIN
    SET TRANSACTION ISOLATION LEVEL SNAPSHOT  
    BEGIN TRANSACTION
        ;with cte as (select 1 as IdNumber union all select IdNumber+1 from cte where IdNumber &lt; 1000)
        select newid() as ID, &apos;Test&apos; + cast(IdNumber as nvarchar(100)) as Name  into #tmpParent from cte option (MAXRECURSION 1000) 

        ;with cte as (select 1 as IdNumber union all select IdNumber+1 from cte where IdNumber &lt; 100)
        select newid() as ID, t.ID as ParentId, &apos;Test&apos; + cast(IdNumber as nvarchar(100)) as Name  into #tmpChild  from cte c, #tmpParent t

        insert into ParentTable (ID, Name) 
        select * from #tmpParent

        -- This will cause a dead lock if two users execute this same query at the same time ...
        insert into ChildTable(ID, ParentID, Name)
        select * from #tmpChild

        drop table #tmpParent
        drop table #tmpChild

    COMMIT;

END    </inputbuf>
   </process>
  </process-list>
  <resource-list>
   <objectlock lockPartition="0" objid="437576597" subresource="FULL" dbid="27" objectname="TestPlayground.dbo.ChildTable" id="lock247a04200" mode="X" associatedObjectId="437576597">
    <owner-list>
     <owner id="process2065cb868" mode="X"/>
    </owner-list>
    <waiter-list>
     <waiter id="process268532188" mode="IX" requestType="wait"/>
    </waiter-list>
   </objectlock>
   <keylock hobtid="72057594039304192" dbid="27" objectname="TestPlayground.dbo.ParentTable" indexname="PK_ParentTable" id="lock237eeb580" mode="X" associatedObjectId="72057594039304192">
    <owner-list>
     <owner id="process268532188" mode="X"/>
    </owner-list>
    <waiter-list>
     <waiter id="process2065cb868" mode="S" requestType="wait"/>
    </waiter-list>
   </keylock>
  </resource-list>
 </deadlock>
</deadlock-list>

うまくいけば、基本的なものが欠けていますが、理解できません。私は、クラスター化されたものとクラスター化されていないものの両方のさまざまなインデックスを試し、デッドロックに関連して見つけた記事について読んだだけです。たとえば https://www.simple-talk.com/sql/performance/sql-server-deadlocks-by-example/ は多くの洞察を与えましたが、私は読んで解決策を思い付くことができませんでしたそれ。

したがって、この問題についての洞察と助けをいただければ幸いです。前もって感謝します!

3
Xtrophic

行のバージョン管理の分離は、リーダーがライターをブロック/デッドロックするのを回避するのに役立ちますが、ここではデッドロックを回避しません。 SNAPSHOT分離レベルまたはREAD COMMITTEDREAD_COMMITTED_SNAPSHOTデータベースオプションがオンの場合でも、従来のテーブルに対するデータ変更ではロックが必要です。

テストシステムの実行プランを見ると、多数の行が挿入されているため、(外部キーを検証するために)子テーブルの挿入中に親テーブルに対してクラスター化インデックススキャンが行われています。排他ロックは各トランザクションの終わりまで新しく挿入された行で保持されるため、子テーブルの挿入中に親テーブルを同時にスキャンすると、互いにブロックされ、デッドロックが発生します。

デッドロックを回避する1つの方法は、子テーブルの挿入に 'OPTION(LOOP JOIN)'クエリヒントを追加することです。これは、他のセッションによってロックされた新しく挿入された行に触れないようにするのに役立ちますが、パフォーマンスが低下する可能性があります。

大量挿入クエリでデッドロックを回避する別の方法は、トランザクションスコープの アプリケーションロック を取得することです。これにより、他の一括挿入クエリとの同時実行性を犠牲にして、一括挿入クエリをシリアル化することでデッドロックを回避します。

4
Dan Guzman