两次间隔数毫秒的UPDATE操作死锁,求锁分区外的解决办法
咱们先来搞清楚为啥相同的UPDATE也会死锁——虽然语句一模一样,但如果WHERE条件Col2=@P1匹配了多行数据,数据库在扫描这些行的时候,可能因为执行计划的细微差异(比如并行扫描的顺序、索引碎片影响),导致两个事务以相反的顺序获取行锁。比如事务1先锁了Id=1,再去锁Id=2;事务2却先锁了Id=2,再去锁Id=1,这不就互相卡住形成死锁了嘛。
除了你已经了解的锁分区,还有这些实用的解决思路:
给Col2创建覆盖非聚集索引
如果当前Col2没有索引,数据库会执行全表扫描,然后对扫描到的每一行加锁,锁的范围大且容易冲突。创建包含Col1的覆盖索引,让数据库直接通过索引定位到要更新的行,锁的粒度更精准:CREATE NONCLUSTERED INDEX IX_Tbl1_Col2 ON Tbl1(Col2) INCLUDE(Col1);这样UPDATE操作不需要回表,直接通过索引就能完成更新,大幅减少锁的冲突概率。
强制事务按固定顺序获取锁
既然死锁的根源是行锁获取顺序不一致,那咱们就强制所有UPDATE事务按相同的顺序处理行。可以在UPDATE语句里加上ORDER BY子句,指定按聚集主键Id排序:UPDATE Tbl1 SET Col1=@P0 WHERE Col2 =@P1 ORDER BY Id;这样不管哪个事务执行,都会先处理Id小的行,再处理Id大的行,从根源上避免交叉锁的情况。
缩短事务持有锁的时间
如果这两个UPDATE是嵌套在更大的事务里,尽量把UPDATE操作单独拆分出来,或者移除事务中不必要的操作(比如无关的查询、IO等待),让锁的持有时间尽可能短。死锁需要两个事务互相等待对方释放锁,锁持有的时间越短,冲突的窗口就越小。应用层添加死锁重试机制
作为兜底方案,在应用代码里捕获SQL Server的死锁错误(错误号1205),然后重试UPDATE操作。死锁本身是偶发的冲突,重试2-3次通常就能成功,这也是生产环境中常用的容错手段。启用快照隔离/读提交快照隔离
开启这两个隔离级别后,数据库会使用行版本控制来避免读锁和写锁的冲突。虽然两个UPDATE的写锁冲突还是可能存在,但可以减少因为其他并发读操作引发的锁竞争,间接降低死锁概率。你可以通过以下语句开启:ALTER DATABASE YourDatabaseName SET READ_COMMITTED_SNAPSHOT ON;
内容的提问来源于stack exchange,提问作者AMMH

