为什么在另一个快照隔离事务中插入带有引用行的外键的行会导致事务挂起?

Sam*_*eby 6 sql-server transactions snapshot-isolation

我在一个系统中遇到了一个有趣的问题,由于架构更改,单个线程中的第一个数据库事务阻止第二个数据库事务完成,直到发生超时。

为了测试这个,我创建了一个测试数据库:

CREATE DATABASE StackOverflow
GO

USE StackOverflow

ALTER DATABASE StackOverflow SET ALLOW_SNAPSHOT_ISOLATION ON
ALTER DATABASE StackOverflow SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE
GO

CREATE TABLE One (
    Id int CONSTRAINT pkOne PRIMARY KEY,
    A varchar(10) NOT NULL
)

CREATE TABLE Two (
    Id int CONSTRAINT pkTwo PRIMARY KEY,
    B varchar(10) NOT NULL,
    OneId int NOT NULL CONSTRAINT fkTwoToOne REFERENCES One
)
GO

-----------------------------------------------

CREATE TABLE Three (
    Id int CONSTRAINT pkThree PRIMARY KEY,
    SurrogateId int NOT NULL CONSTRAINT ThreeSurrUnique UNIQUE,
    C varchar(10) NOT NULL
)
GO

CREATE TABLE Four (
    Id int CONSTRAINT pkFour PRIMARY KEY,
    D varchar(10) NOT NULL,
    ThreeSurrogateId int NOT NULL CONSTRAINT fkFourToThree REFERENCES Three(SurrogateId)
)
GO

--Seed data
INSERT INTO One (Id, A) VALUES (1, '')
INSERT INTO Three (Id, SurrogateId, C) VALUES (3, 50, '')
Run Code Online (Sandbox Code Playgroud)

在第一个测试中,修改表 One 中一行的事务已启动,但尚未提交。另一个事务插入到表二中,其中一列引用同一行在表一中的第一个事务中被修改。第二个事务将永远挂起,直到提交第一个事务。

SQL Management Studio 测试事务挂起

事务等待的原因是由于第一个事务持有 LCK_M_S 密钥锁。

SQL Management Studio 活动监视器 LCK_M_S 键锁

在我的第二个测试中,修改表三中一行的事务已启动,但尚未提交,就像在第一个测试中一样。另一个事务正在插入表四,其中引用同一行的列在表三的第一个事务中被修改。除了这一次,表四引用了表三中的代理键而不是主键。事务立即完成并且不受第一笔事务的影响。

SQL Management Studio 测试事务挂起没有问题

我需要帮助理解为什么在将行插入到引用在第一个事务中修改的表的单独表中时,后一个事务总是被前一个事务阻塞。我认为明显无用的答案是因为外键约束。但为什么?特别是因为这是快照隔离,为什么后者的事务完全关心前者呢?它所引用的行已经存在,外键可以很容易地验证,正如第二个测试所证明的那样,其中引用代理键的外键无障碍地完成。

Dav*_*oft 5

答案相当简单。

当查询读取以验证外键约束时,它们总是使用锁,而不是行版本控制。想象一下,如果一个事务正在更改 PK 值,并且并发会话插入了一行引用旧的PK 值。不允许根据版本存储中行的一致版本来验证 FK 约束。如果是,那么在提交 PK 更改时必须再次验证所有 FK 。

在第一种情况下,更新事务在 FK 的目标索引上有一个键锁,因此并发会话无法读取 PK 值。

第二,更新不影响 FK 中涉及的唯一键。更新能够在目标键值上放置共享锁,因为更新会话对不同唯一索引中的键具有独占键锁。

在第一个事务提交后的第一个示例中,第二个事务因快照隔离更新冲突而失败:

消息 3960,级别 16,状态 2,第 10 行快照隔离事务因更新冲突而中止。您不能使用快照隔离直接或间接访问数据库 'StackOverflow' 中的表 'dbo.One' 以更新、删除或插入已被另一个事务修改或删除的行。重试事务或更改更新/删除语句的隔离级别。

这是因为在 SNAPSHOT 隔离中,您无法读取自事务开始以来已更改的行。并且由于 FK 验证不能使用行版本,因此它需要从事务开始更新的行中读取 PK 。这违反了 SNAPSHOT 隔离,因为该 PK 值可能在 SNAPSHOT 事务开始时不存在。

这可能有点棘手,因为当您运行 BEGIN TRANSACTION(有点像 IMPLICIT TRANSACTIONS)时,SNAPSHOT 事务并没有真正在时间点开始,相关的时间点是事务发生的时间点首先读取或更改数据库。例如

if @@trancount > 0 rollback
go
set transaction isolation level snapshot
begin transaction

drop table if exists t
create table t(id int)

--in another session run
--update one set a = a+'b' where id = 1

waitfor delay '0:0:10'

insert into two(id,b,oneid) values (2,'',1) -- fails
Run Code Online (Sandbox Code Playgroud)

  • 因为修改具有聚集索引的表中的行,需要在聚集索引键上使用 X 键锁。如果 [one] 是具有非聚集 PK 的堆,则它不会阻塞,因为唯一索引将位于不同的数据结构中。有点像你的第二种情况。 (2认同)
  • 检查提交时的约束,有时称为“延迟约束检查”,仅发生在具有内存表的 SQL Server 中,它根本不使用锁。至于非聚集索引,这里有更多的“锁粒度”。由于行的键和行的数据行位于单独的数据结构上,受单独的锁保护,SQL Server 可以判断更新不会影响键,因此不需要阻止插入。使用聚集索引,行和 PK 键由单个锁保护,因此对 PK 或非 PK 列的任何更改都需要一个独占键锁。 (2认同)
  • 更多信息可以在这里找到:https://sqlperformance.com/2014/06/sql-performance/the-snapshot-isolation-level (2认同)