防止使用外键插入时出现死锁 (MSSQL)

Xtr*_*hic 3 foreign-key sql-server deadlock sql-server-2012

我们最近发现我们的应用程序中存在我们开发人员认为不会发生的死锁问题。

为了进一步理解这一点,我着手创建一个我能想象到的基本测试场景。结果是两张桌子,一张家长和一张孩子。

在这个简单的场景中,我启动了 Management Studio 的两个实例,并在两个实例中同时(几乎,以我可以切换窗口的速度)同时执行了 SAME 查询。很快,其中一个以僵局告终。

我已经阅读了不同的方法并尝试启用 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
Run Code Online (Sandbox Code Playgroud)

接下来是我在 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
Run Code Online (Sandbox Code Playgroud)

其中一个客户端收到以下错误:

事务(进程 ID 55)在锁定资源上与另一个进程发生死锁,并已被选为死锁牺牲品。重新运行事务。

这是来自 SQL Profiler 的关于死锁的跟踪。(这个碰巧是在我使用快照隔离时,但是否在那里并没有对我的测试产生影响)

<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>
Run Code Online (Sandbox Code Playgroud)

希望我错过了一些基本的东西,但我无法弄清楚。我尝试了不同的索引,包括聚集索引和非聚集索引,并阅读了我发现的所有与死锁相关的文章。例如https://www.simple-talk.com/sql/performance/sql-server-deadlocks-by-example/提供了很多见解,但我无法通过阅读来提出解决方案。

因此,再次对此事的任何见解和帮助将不胜感激!提前致谢!

Dan*_*man 5

尽管行版本隔离将有助于避免读取器阻塞/死锁写入器,反之亦然,但它不会在这里避免死锁。锁定需要具有对即使是在传统的表中的数据修改SNAPSHOT隔离级别,或READ COMMITTEDREAD_COMMITTED_SNAPSHOT数据库选项。

查看我的测试系统上的执行计划,由于插入了大量行,我看到在子表插入期间(为了验证外键)对父表进行了聚集索引扫描。因为独占锁一直保持在新插入的行上直到每个事务结束,所以在子表插入期间对父表的并发扫描会相互阻塞并导致死锁。

避免死锁的一种方法是向子表插入添加“OPTION (LOOP JOIN)”查询提示。这将有助于避免触及被其他会话锁定的新插入行,但可能会降低性能。

另一种避免大量插入查询死锁的方法是获取事务范围的应用程序锁。这将通过以与其他大量插入查询的并发为代价序列化大量插入查询来避免死锁。