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




