在冗长的 UPDATE 期间进行 SELECT - 不同事务隔离级别的 SELECT 和 SELECT WITH (NOLOCK) 会发生什么?

Dan*_*iel 5 sql sql-server sql-server-2012

给定一个需要 5 分钟左右的 UPDATE 执行,当 SELECT 尝试从同一个表中检索数据时会发生什么?对于不同的事务隔离级别和SELECT WITH (NOLOCK),SELECT 是否等待 UPDATE?如果没有,SELECT 是否返回旧数据(UPDATE 之前的数据)或当前插入记录的一部分(例如当前插入记录的 50%)?

如果发现以下问题,但它仅描述在长 SELECT 期间执行和 UPDATE 时会发生什么。

SQL Server - [SELECT] 锁定 [UPDATE] 吗?

我正在使用 MS SQL Server 2012。希望这种行为对于不同的实现是一致的。

Jod*_*dyT 6

Gavin Draper 的这篇文章很好地解释了它,并包含一些示例查询。

SQL Server 隔离级别示例

SQL Server 中的隔离级别控制锁定在事务之间的工作方式。

SQL Server 2008 支持以下隔离级别

  • 读未提交
  • 已提交读(默认)
  • 可重复读取
  • 可序列化
  • 快照

在我详细介绍每一个之前,您可能想要创建一个新数据库来运行示例,在新数据库上运行以下脚本以创建示例数据。注意:您还需要删除 IsolationTests 表并在每个示例之前重新运行此脚本以重置数据。

CREATE TABLE IsolationTests  
(
    Id INT IDENTITY,
    Col1 INT,
    Col2 INT,
    Col3 INTupdate te
)

INSERT INTO IsolationTests(Col1,Col2,Col3)  
SELECT 1,2,3  
UNION ALL SELECT 1,2,3  
UNION ALL SELECT 1,2,3  
UNION ALL SELECT 1,2,3  
UNION ALL SELECT 1,2,3  
UNION ALL SELECT 1,2,3  
UNION ALL SELECT 1,2,3
Run Code Online (Sandbox Code Playgroud)

同样,在我们进一步讨论之前,理解这两个术语很重要……

  1. 脏读——这是当你读取未提交的数据时,当这样做时,不能保证读取的数据会被提交,这意味着数据很可能是坏的。
  2. Phantom Reads——这是你正在处理的数据自你第一次读入以来被另一个事务更改的时候。这意味着在同一事务中对该数据的后续读取可能会有所不同。

读未提交

这是最低的隔离级别。Read uncommitted 导致不请求共享锁,这允许您读取当前正在其他事务中修改的数据。它还允许其他事务修改您正在读取的数据。

正如您可能想象的那样,这可能会以各种不同的方式导致一些意想不到的结果。例如,如果更新正在另一个事务中运行,则选择返回的数据可能处于中途状态,导致您的某些行返回更新后的值,而有些则没有。

要查看未提交的读取操作,让我们在 Management Studio 的一个选项卡中运行 Query1,然后在 Query1 完成之前在另一个选项卡中快速运行 Query2。

查询1

BEGIN TRAN  
UPDATE IsolationTests SET Col1 = 2  
--Simulate having some intensive processing here with a wait
WAITFOR DELAY '00:00:10'  
ROLLBACK
Run Code Online (Sandbox Code Playgroud)

查询2

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED  
SELECT * FROM IsolationTests
Run Code Online (Sandbox Code Playgroud)

请注意,Query2 不会等待 Query1 完成,更重要的是 Query2 返回脏数据。记住 Query1 回滚它的所有更改,但是 Query2 无论如何都返回了数据,这是因为它没有等待所有其他对该数据具有排他锁的事务,它只是返回当时存在的内容。

通过使用NOLOCK表提示,使用未提交读隔离级别查询数据有一个语法快捷方式。您可以将上面的 Query2 更改为如下所示,它会做完全相同的事情。

SELECT * FROM IsolationTests WITH(NOLOCK)
Run Code Online (Sandbox Code Playgroud)

读已提交

这是默认的隔离级别,意味着选择将只返回提交的数据。Select 语句将针对您正在查询的数据发出共享锁请求,这会导致您等待另一个事务是否已经对该数据具有排他锁。一旦您拥有共享锁,任何其他尝试修改该数据的事务将请求排他锁并等待您的已提交读事务完成。

通过在单独的选项卡中运行以下查询,您可以看到读取事务等待修改事务完成后再返回数据的示例,就像读取未提交时一样。

查询1

BEGIN TRAN  
UPDATE Tests SET Col1 = 2  
--Simulate having some intensive processing here with a wait
WAITFOR DELAY '00:00:10'  
ROLLBACK
Run Code Online (Sandbox Code Playgroud)

查询2

SELECT * FROM IsolationTests
Run Code Online (Sandbox Code Playgroud)

请注意 Query2 如何在返回之前等待第一个事务完成,以及返回的数据如何是我们在 Query1 执行回滚时开始的数据。未指定隔离级别的原因是“已提交读”是 SQL Server 的默认隔离级别。如果要检查正在运行的隔离级别,可以运行DBCC useroptions. 请记住隔离级别是特定于连接/事务的,因此同一数据库上的不同查询通常在不同的隔离级别下运行。

可重复读取

这与 Read Committed 类似,但有额外的保证,即如果您在事务中两次发出相同的选择,则两次都将获得相同的结果。它通过保持它在它读取的记录上获得的共享锁直到事务结束来做到这一点,这意味着任何试图修改这些记录的事务都被迫等待读取事务完成。

和以前一样运行 Query1 然后在它运行时运行 Query2

查询1

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ  
BEGIN TRAN  
SELECT * FROM IsolationTests  
WAITFOR DELAY '00:00:10'  
SELECT * FROM IsolationTests  
ROLLBACK
Run Code Online (Sandbox Code Playgroud)

查询2

UPDATE IsolationTests SET Col1 = -1
Run Code Online (Sandbox Code Playgroud)

请注意,即使您在第二个选择运行之前运行查询来修改数据,Query1 也会为两个选择返回相同的数据。这是因为由于在您指定可重复读取时打开的排他锁,更新查询被迫等待 Query1 完成。

如果您重新运行上述查询,但将 Q​​uery1 更改为 Read Committed,您会注意到两个选择返回不同的数据,并且 Query2 不会等待 Query1 完成。

关于可重复读取的最后一件事是,如果添加更多记录,数据可以在 2 个查询之间更改。可重复读保证了先前选择查询的记录不会被更改或删除,它不会停止插入新记录,因此在此隔离级别仍然很有可能获得幻读。

可序列化

此隔离级别采用可重复读取,并保证不会添加任何新数据,从而消除获得幻读的机会。它通过在查询数据上放置范围锁来实现这一点。这会导致任何其他事务尝试修改或插入此事务涉及的数据,直到它完成。

你现在知道这个练习并排运行这些查询......

查询1

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE  
BEGIN TRAN  
SELECT * FROM IsolationTests  
WAITFOR DELAY '00:00:10'  
SELECT * FROM IsolationTests  
ROLLBACK
Run Code Online (Sandbox Code Playgroud)

查询2

INSERT INTO IsolationTests(Col1,Col2,Col3)  
VALUES (100,100,100)
Run Code Online (Sandbox Code Playgroud)

您将看到 Query2 中的插入在运行之前等待 Query1 完成,从而消除了幻读的机会。如果您将 Query1 中的隔离级别更改为可重复读取,您将看到插入不再被阻止并且 Query1 中的两个选择语句返回不同数量的行。

快照

这提供了与可序列化相同的保证。那么有什么区别呢?嗯,它的工作方式更多,使用快照不会阻止其他查询插入或更新快照事务触及的数据。而是使用行版本控制,因此当数据更改时,旧版本保留在 tempdb 中,因此现有事务将看到没有更改的版本。完成更改之前启动的所有事务后,将从 tempdb 中删除前一行版本。这意味着即使另一个事务进行了更改,您也将始终获得与第一次在该事务中所做的相同的结果。

因此,从好的方面来说,您在运行交易时不会阻止其他人修改数据,但是...... 您正在使用 SQL Server 上的额外资源来保存更改的多个版本。

要使用快照隔离级别,您需要通过运行以下命令在数据库上启用它

ALTER DATABASE IsolationTests  
SET ALLOW_SNAPSHOT_ISOLATION ON
Run Code Online (Sandbox Code Playgroud)

如果您从可序列化重新运行示例,但将隔离级别更改为快照,您会注意到您仍然返回相同的数据,但 Query2 不再等待 Query1 完成。

概括

您现在应该很清楚每个不同的隔离级别是如何工作的。你可以看到你使用的级别越高,你提供的并发性就越少,你带来的阻塞就越多。您应该始终尝试使用最低的隔离级别,通常是读提交。