以下测试用例会在 SQL Server 中导致死锁,但不会在 Oracle 中导致死锁。
流程 1 选择主键列一条具有精确命中 where 子句的记录。使用一个索引。 进程 2 使用行级锁定和另一个索引以类似于 fifo 的方式从表中选择相同的记录,并按预期阻塞。 流程1通过主键删除记录。
对于 SQL Server,这会导致死锁。 使用 Oracle 不会发生死锁。
有没有办法避免 SQL Server 中的死锁?
您将在下面找到有关如何重现和比较该行为的更多详细信息。
SQL Server 版本 2019 和 Windows 10
create table deadlocktest (
pk int,
id1 int,
id2 int,
x int,
y int,
seq int,
primary key nonclustered (pk) );
create UNIQUE NONCLUSTERED index idx_id1 on deadlocktest(id1, x, seq);
create UNIQUE NONCLUSTERED index idx_id2 on deadlocktest(id2);
create UNIQUE NONCLUSTERED index idx_seq on deadlocktest(seq);
insert into deadlocktest values (1, 10, 100, 1000, 10000, 1);
insert into deadlocktest values (2, 20, 200, 2000, 20000, 2);
选项 READ_COMMITTED_SNAPSHOT 和 ALLOW_SNAPSHOT_ISOLATION 已启用。
自动提交已关闭,事务隔离级别 = READCOMMITTED。
T1: BEGIN TRANSACTION;
T2: BEGIN TRANSACTION;
T1: select top 1 pk, x from deadlocktest WITH (UPDLOCK ROWLOCK) where id1 = 10 and x = 1000 and seq = 1;
T2: select top 1 pk, x from deadlocktest WITH (UPDLOCK ROWLOCK) where id1 = 10 and x = 1000 order by seq;
BLOCKED
T1: delete from deadlocktest where pk = 1;
T2: DEADLOCK ERROR
如果T1中的select不使用UPDLOCK ROWLOCK,也会发生同样的情况。它仅在此测试用例中用于控制执行顺序。
左侧(受害者)是T2 select语句,右侧是T1 insert语句。
查询计划是(您将看到正在使用不同的索引):
select top 1 pk, x from deadlocktest WITH (UPDLOCK ROWLOCK) where id1 = 10 and x = 1000 order by seq
|--Top(TOP EXPRESSION:((1)))
|--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000]))
|--Index Seek(OBJECT:([XGEN].[dbo].[deadlocktest].[idx_id1]), SEEK:([XGEN].[dbo].[deadlocktest].[id1]=(10) AND [XGEN].[dbo].[deadlocktest].[x]=(1000)) ORDERED FORWARD)
|--RID Lookup(OBJECT:([XGEN].[dbo].[deadlocktest]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)
select top 1 pk, x from deadlocktest WITH (UPDLOCK ROWLOCK) where id1 = 10 and x = 1000 and seq = 1
|--Top(TOP EXPRESSION:((1)))
|--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000]))
|--Index Seek(OBJECT:([XGEN].[dbo].[deadlocktest].[idx_seq]), SEEK:([XGEN].[dbo].[deadlocktest].[seq]=(1)) ORDERED FORWARD)
|--RID Lookup(OBJECT:([XGEN].[dbo].[deadlocktest]), SEEK:([Bmk1000]=[Bmk1000]), WHERE:([XGEN].[dbo].[deadlocktest].[id1]=(10) AND [XGEN].[dbo].[deadlocktest].[x]=(1000)) LOOKUP ORDERED FORWARD)
delete from deadlocktest where pk = 1
|--Table Delete(OBJECT:([XGEN].[dbo].[deadlocktest]), OBJECT:([XGEN].[dbo].[deadlocktest].[PK__deadlock__321403CE902EA0FE]), OBJECT:([XGEN].[dbo].[deadlocktest].[idx_id1]), OBJECT:([XGEN].[dbo].[deadlocktest].[idx_id2]), OBJECT:([XGEN].[dbo].[deadlocktest].[idx_seq]))
|--Index Seek(OBJECT:([XGEN].[dbo].[deadlocktest].[PK__deadlock__321403CE902EA0FE]), SEEK:([XGEN].[dbo].[deadlocktest].[pk]=CONVERT_IMPLICIT(int,[@1],0)) ORDERED FORWARD)
Windows 10 上的 Oracle XE 18
create table deadlocktest (
pk int,
id1 int,
id2 int,
x int,
y int,
seq int,
primary key (pk) );
insert into deadlocktest values (1, 10, 100, 1000, 10000, 1);
insert into deadlocktest values (2, 20, 200, 2000, 20000, 2);
create UNIQUE index idx_id1 on deadlocktest(id1, x, seq);
create UNIQUE index idx_id2 on deadlocktest(id2);
create UNIQUE index idx_seq on deadlocktest(seq);
自动提交已关闭,事务隔离级别 = READCOMMITTED。
T1: SET TRANSACTION READ WRITE;
T2: SET TRANSACTION READ WRITE;
T1: select pk, x from deadlocktest where id1 = 10 and x = 1000 and seq = 1 for update;
T2: select pk, x from deadlocktest where id1 = 10 and x = 1000 order by seq for update;
BLOCKED
T1: delete from deadlocktest where pk = 1;
无死锁,T1完成删除语句,T2一直阻塞直到T1结束。
T1 中带锁的 select 用于此测试场景,以控制并发事务。在实际应用程序上下文中,此选择丢失,并且死锁如上面的死锁图中所示。
这种僵局取决于计划。 如果优化器选择该计划,Oracle 上也可能发生这种情况。 所以这是您代码中的错误。
正如@Martin Smith所说
“对于T1,您可以使用WITH (UPDLOCK ROWLOCK, INDEX = idx_id1 )来确保它使用与T2相同的访问路径。否则两者都可以获得不同索引中的键的U锁。”
至于“在T1的选择中添加WITH(...,INDEX = idx_id1)似乎有效。但这也会影响优化器”
当然可以。 这就是重点。 您引入了一种表设计,如果优化器选择“错误”索引,该设计将导致死锁。 优化器不会以避免死锁的方式选择索引,除非您指示它这样做。