假设我们有一笔交易 A:
BEGIN TRAN A;
//--------------------< time a
SELECT productid, unitprice
FROM Production.Products
WHERE productid = 2;
//--------------------< time b
UPDATE Production.Products
SET unitprice += 1.00
WHERE productid = 3;
COMMIT TRAN A;
我们知道,对于事务 A(在默认的 Read Committed 隔离级别下),需要一个共享锁(针对 productid 2)和一个排他锁(针对 productid 3)。
我的问题是,对于 productid 3 的独占锁,锁是什么时候发生的?在事务开始时,事务真正开始更新的时间是在时间 a 还是时间 b?
您可以使用 DMV 亲自查看。在 SSMS 中打开两个查询选项卡。将其粘贴到一个中:
将您的陈述粘贴到另一个中。
运行我的查询。这将显示服务器上可以忽略的后台活动。
只运行您的
BEGIN TRAN A;
语句并重新运行 DMV 查询。您会看到有一个新事务,name=A
但没有更多锁。现在突出显示您的选择语句并再次运行它以及 DMV 查询。什么也没有变!S锁怎么了?好吧,在默认的 READ COMMITTED 隔离级别下运行,一旦数据库引擎完成对特定行的处理,锁就会被释放。请参阅此处的“SQL Server Locking Read Committed”部分。因此,S 锁在执行查询时被占用,但在再次检查 DMV 时已被释放。(您可以看到使用扩展事件发生的锁定和释放。此答案中有一些查询,您可以修改这些查询以自己查看。)
我们可以使用HOLDLOCK表提示使锁保持到事务结束。将您的 SELECT 更改为
..FROM Production.Products WITH(HOLDLOCK)..
并重新运行 SELECT 和 DMV 查询。您将看到表(和其他对象)上的共享锁。只运行 UPDATE 语句。共享 (S) 锁已变为排他 (X) 锁。因为这些锁正在保护更改的数据,所以它们始终保持到事务结束 - 不需要额外的提示。
运行 COMMIT 将释放锁并删除事务。
对于您最初的问题 - 何时对 productid 3 的行进行排他锁 - 答案是,当查询处理器在执行为该更新语句生成的查询计划时到达该特定行时。
发生时会采用排他锁
UPDATE
。这是文档中的内容:
这很模糊,并不能真正回答问题,所以让我们进一步阅读:
事务日志是事实。在更新之前,日志中没有记录任何内容。因此,只有在更新发生时才需要排他锁。
如果您一次运行每条语句并查看相关表上的锁,您自己会看到这种行为。
在事务访问该行的那一刻,每个行锁都会被获取或转换。行上的共享锁定
WHERE productid = 2
将在“时间 a”之后但在“时间 b”之前的某个时间进行。行上的更新锁WHERE productid = 3
将在“时间 b”之后的某个时间被占用,然后在不久之后转换为排他锁。这就是为什么会出现死锁的原因——如果所有的锁都被提前获取,死锁就不会发生。
SQL Server(或任何其他 DBMS)不能做这样的事情,因为在事务开始时,甚至不知道可能匹配语句谓词的行是否存在。您需要先去尝试找到它们,一次一个,这是在它们上放置必要锁的自然位置。