You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

SQL Server级联删除死锁:解决方案及事务锁定可行性问询

解决级联删除引发的死锁问题

先来看你提供的死锁详细信息,咱们一步步拆解:

<deadlock> 
  <victim-list> 
    <victimProcess id="process1f5b50928" /> 
  </victim-list> 
  <process-list> 
    <process id="process1f5b50928" taskpriority="0" logused="288" waitresource="KEY: 81:72057594039107584 (8194443284a0)" waittime="5575" ownerId="1131789084" transactionname="user_transaction" lasttranstarted="2019-03-18T10:24:30.773" XDES="0x1b36356a8" lockMode="RangeS-U" schedulerid="2" kpid="3604" status="suspended" spid="134" sbid="2" ecid="0" priority="0" trancount="3" lastbatchstarted="2019-03-18T10:24:30.790" lastbatchcompleted="2019-03-18T10:24:30.790" lastattention="1900-01-01T00:00:00.790" clientapp=".Net SqlClient Data Provider" hostpid="21004" isolationlevel="read uncommitted (1)" xactid="1131789084" currentdb="81" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056"> 
      <executionStack> 
        <frame procname="adhoc" line="1" sqlhandle="0x0200000064c34002e8fa5b7dac17b29471b98d1653a1e03a0000000000000000000000000000000000000000"> (@1 int)DELETE [dbo].[Users] WITH(rowlock) WHERE [Id]=@1 </frame> 
        <frame procname="adhoc" line="1" sqlhandle="0x020000001485b7340562f882a8e2556c22adf5f74806757d0000000000000000000000000000000000000000"> DELETE FROM [dbo].[Users] WITH (ROWLOCK) WHERE Id IN (723540); </frame> 
      </executionStack> 
      <inputbuf> DELETE FROM [dbo].[Users] WITH (ROWLOCK) WHERE Id IN (723540); </inputbuf> 
    </process> 
    <process id="process252569c38" taskpriority="0" logused="288" waitresource="PAGE: 81:1:564854 " waittime="4416" ownerId="1131789067" transactionname="user_transaction" lasttranstarted="2019-03-18T10:24:30.703" XDES="0x1addc76a8" lockMode="S" schedulerid="1" kpid="5568" status="suspended" spid="76" sbid="2" ecid="0" priority="0" trancount="3" lastbatchstarted="2019-03-18T10:24:30.757" lastbatchcompleted="2019-03-18T10:24:30.730" lastattention="1900-01-01T00:00:00.730" clientapp=".Net SqlClient Data Provider" hostpid="21004" isolationlevel="read uncommitted (1)" xactid="1131789067" currentdb="81" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056"> 
      <executionStack> 
        <frame procname="adhoc" line="1" sqlhandle="0x0200000064c34002e8fa5b7dac17b29471b98d1653a1e03a0000000000000000000000000000000000000000"> (@1 int)DELETE [dbo].[Users] WITH(rowlock) WHERE [Id]=@1 </frame> 
        <frame procname="adhoc" line="1" sqlhandle="0x020000004cb6d313b5f743b8f2df7ba8dfe667de5ac86d29000000000000000000000000000000000000000"> DELETE FROM [dbo].[Users] WITH (ROWLOCK) WHERE Id IN (723537); </frame> 
      </executionStack> 
      <inputbuf> DELETE FROM [dbo].[Users] WITH (ROWLOCK) WHERE Id IN (723537); </inputbuf> 
    </process> 
  </process-list> 
  <resource-list> 
    <keylock hobtid="72057594039107584" dbid="81" objectname=".dbo.ABSENTSTATUSES" indexname="PK__ABSENTST__3214EC07EB7F77EE" id="lock1df18ec00" mode="RangeS-U" associatedObjectId="72057594039107584"> 
      <owner-list> 
        <owner id="process252569c38" mode="RangeS-U" /> 
      </owner-list> 
      <waiter-list> 
        <waiter id="process1f5b50928" mode="RangeS-U" requestType="wait" /> 
      </waiter-list> 
    </keylock> 
    <pagelock fileid="1" pageid="564854" dbid="81" subresource="FULL" objectname=".dbo.Users" id="lock2c6111980" mode="IX" associatedObjectId="72057594039042048"> 
      <owner-list> 
        <owner id="process1f5b50928" mode="IX" /> 
      </owner-list> 
      <waiter-list> 
        <waiter id="process252569c38" mode="S" requestType="convert" /> 
      </waiter-list> 
    </pagelock> 
  </resource-list> 
</deadlock>

从这份报告能看出来,死锁的核心是两个并发删除Users行的事务,因为级联删除触发的锁获取顺序相反,形成了循环等待

  • 进程process1f5b50928(删Id=723540)先拿到了Users表的IX页锁,现在等着拿ABSENTSTATUSES表的RangeS-U键锁
  • 进程process252569c38(删Id=723537)先拿到了ABSENTSTATUSES表的RangeS-U键锁,现在等着把Users表的锁转换成S

两边都握着对方需要的资源,自然就死锁了。


先回答你的核心问题:能不能在事务中锁定关联表的行?

当然可以!而且这正是解决这类死锁的关键——只要你让所有事务都按照统一的顺序获取锁,就能打破循环等待的局面。


具体的解决方案,按优先级排序:

1. 统一锁顺序,显式预锁子表行

在删除Users行之前,先显式锁定ABSENTSTATUSES里的关联行,确保所有事务都遵循「子表→主表」的锁顺序。这样就不会出现你锁主表我锁子表的反向情况了。

示例代码(单条删除):

BEGIN TRANSACTION;
-- 用UPDLOCK+HOLDLOCK锁定子表关联行,确保锁在整个事务期间保持
SELECT 1 FROM [dbo].[ABSENTSTATUSES] WITH (UPDLOCK, HOLDLOCK) WHERE UserId = @UserId;
-- 再删除主表的用户行
DELETE FROM [dbo].[Users] WHERE Id = @UserId;
COMMIT TRANSACTION;

如果是批量删除,把UserId换成IN集合就行:

BEGIN TRANSACTION;
SELECT 1 FROM [dbo].[ABSENTSTATUSES] WITH (UPDLOCK, HOLDLOCK) WHERE UserId IN (@Id1, @Id2);
DELETE FROM [dbo].[Users] WHERE Id IN (@Id1, @Id2);
COMMIT TRANSACTION;

2. 给子表的外键列加索引

你看死锁里涉及到ABSENTSTATUSES的主键索引,但如果关联Users的外键列(比如UserId)没有单独的非聚集索引,级联删除时SQL Server会扫描整个ABSENTSTATUSES表,加大量的锁,这会大幅增加死锁的概率。

赶紧给外键列建个索引:

CREATE NONCLUSTERED INDEX IX_ABSENTSTATUSES_UserId ON [dbo].[ABSENTSTATUSES](UserId);

这个操作能减少级联删除时的锁范围,从根源上降低锁冲突的可能。

3. 替换级联删除为手动顺序删除

如果级联删除的逻辑比较复杂,干脆把外键的ON DELETE CASCADE属性去掉,改为在事务里先删子表,再删主表:

BEGIN TRANSACTION;
-- 先删子表的关联记录
DELETE FROM [dbo].[ABSENTSTATUSES] WHERE UserId IN (@Id1, @Id2);
-- 再删主表的用户记录
DELETE FROM [dbo].[Users] WHERE Id IN (@Id1, @Id2);
COMMIT TRANSACTION;

这种方式完全由你控制锁的顺序,彻底避免级联删除带来的锁顺序不可控问题。

4. 调整事务隔离级别(可选)

你当前用的是READ UNCOMMITTED隔离级别,虽然它能减少锁,但在写操作场景下,可能会因为锁的粒度问题加剧死锁。可以尝试开启数据库的快照隔离,改用READ COMMITTED SNAPSHOT

-- 先开启数据库的快照隔离
ALTER DATABASE YourDatabaseName SET ALLOW_SNAPSHOT_ISOLATION ON;
ALTER DATABASE YourDatabaseName SET READ_COMMITTED_SNAPSHOT ON;

这样SQL Server会用行版本控制替代部分锁,减少锁冲突的概率。


内容的提问来源于stack exchange,提问作者AlexeyS

火山引擎 最新活动