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

Posted

技术标签:

【中文标题】为啥在另一个快照隔离事务中插入具有引用行的外键引用行的行会导致事务挂起?【英文标题】:Why does inserting a row with a foreign key referencing a row by pk modified in another snapshot isolation transaction cause the transaction to hang?为什么在另一个快照隔离事务中插入具有引用行的外键引用行的行会导致事务挂起? 【发布时间】:2018-11-09 15:37:13 【问题描述】:

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

为了对此进行测试,我创建了一个测试数据库:

CREATE DATABASE ***
GO

USE ***

ALTER DATABASE *** SET ALLOW_SNAPSHOT_ISOLATION ON
ALTER DATABASE *** 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, '')

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

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

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

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

【问题讨论】:

【参考方案1】:

答案很简单。

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

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

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

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

Msg 3960, Level 16, State 2, Line 10 快照隔离事务 由于更新冲突而中止。您不能使用快照隔离来 在数据库中直接或间接访问表“dbo.One” '***' 更新、删除或插入已被删除的行 被另一个事务修改或删除。重试交易或 更改更新/删除语句的隔离级别。

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

这可能有点棘手,因为 SNAPSHOT 事务真的在您运行 BEGIN TRANSACTION(有点像隐式事务)相关点时开始-in-time 是事务第一次读取或更改数据库的时间。 EG

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

【讨论】:

第一种情况下为什么SQL Server需要获取key lock?在这两种情况下,被引用的键都是完全未修改的。 因为使用聚集索引修改表中的行,需要对聚集索引键进行 X 键锁定。如果 [one] 是具有非集群 PK 的堆,则它不会阻塞,因为唯一索引将位于不同的数据结构中。有点像你的第二个案例。 @DavidBrowne-Microsoft “如果是这样,那么在提交 PK 更改时必须再次验证所有 FK”正是我所期望的。我希望当它提交时,它会意识到问题,抛出更新冲突错误并回滚其更改,将事物恢复到一致状态。如果我确实接受了,就像对我来说不直观的那样,事情正如你所说的那样正在发生并且是问题的原因,那么我根本不明白如果 PK 碰巧不是集群,这个问题是如何神奇地消失的索引。 在提交时检查约束,有时称为“延迟约束检查”,只发生在带有内存表的 SQL Server 中,它根本不使用锁。至于非聚集索引,这里只是有更多的“锁定粒度”。由于行的键和行的数据行位于不同的数据结构上,由不同的锁保护,SQL Server 可以判断更新不会影响键,因此不需要阻止插入。使用聚集索引,行和 PK 键受单个锁保护,因此对 PK 或非 PK 列的任何更改都需要独占键锁。 更多信息可以在这里找到:sqlperformance.com/2014/06/sql-performance/…

以上是关于为啥在另一个快照隔离事务中插入具有引用行的外键引用行的行会导致事务挂起?的主要内容,如果未能解决你的问题,请参考以下文章

由于选定行的更新冲突,快照隔离事务中止

mysql表中,表的外键关联自身主键,为啥插入不了数据?

实体框架,插入了引用最后一个Id的外键,同时也是同一张表上之前的集合

如何将项目添加到具有Android Room中父实体的外键引用的子实体?

视图上的外键引用

引用另一个模式的外键