我在一个系统中遇到了一个有趣的问题,由于架构更改,单个线程中的第一个数据库事务阻止了第二个数据库事务完成,直到发生超时。
为了测试这一点,我创建了一个测试数据库:
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, '')
在第一个测试中,修改表 One 中的行的事务已启动,但尚未提交。另一个事务正在插入到表 2 中,引用同一行的列在表 1 的第一个事务中被修改。第二个事务将永远挂起,直到第一个事务被提交。
事务等待的原因是由于第一个事务持有的 LCK_M_S 键锁。
在我的第二个测试中,一个修改表 3 中一行的事务已启动,但尚未提交,就像在第一个测试中一样。另一个事务正在插入到表 4 中,引用同一行的列在表 3 的第一个事务中被修改。除了这一次,表四引用表三中的代理键而不是主键。交易立即完成,不受第一笔交易的影响。
我需要帮助理解为什么在引用在第一个事务中修改的表的单独表中插入一行时,后一个事务总是被前一个事务阻止。我认为明显无益的答案是由于外键约束。但为什么?尤其是因为这是快照隔离,为什么后者的事务完全关心前者呢?它引用的行已经存在,并且外键可以很容易地验证,正如第二个测试所证明的那样,其中引用代理键的外键可以顺利完成。