使用 WITH (NOLOCK) 和事务

Posted

技术标签:

【中文标题】使用 WITH (NOLOCK) 和事务【英文标题】:Using WITH (NOLOCK) and Transactions 【发布时间】:2013-12-09 21:03:49 【问题描述】:

假设我有一个简单的查询,例如

Select * From MyTable WITH (NOLOCK)

并且,在执行此查询的同时,另一个用户正在事务上下文中将 100 行插入此表中。

从理论上讲,Select 语句是否有可能,因为它使用 NOLOCK,可以在事务提交或回滚之前读取插入到表中的 100 行的子集?如果我正确理解 NOLOCK,这似乎是一种可能性,但想验证一下。

SQL Server 2012

【问题讨论】:

实际上,我认为您可以阅读所有 100 个,而不仅仅是提交或回滚之前的子集。 @Blam 不同意。请参阅我更新的答案,否则证明是这样。 @AaronBertrand - 我想你可能有不同的目的。我将 Blam 的评论解释为 除了读取受影响行的(正确)子集之外,您还可以在提交/回滚之前读取整组行。 @Martin 我把它读作“插入的所有 100 行,或者什么都没有” - 毕竟,这个问题涉及另一个插入 100 行的事务。我也看到了您的解释,现在再次阅读并记住您的评论,但远不清楚这就是什么意思。 是的,只有 Blam 可以确定! @Randy BTW 以及读取未提交行的子集,您甚至可以读取some circumstances中的部分更新列@ 【参考方案1】:

当然,您可以读取在事务开始和提交或回滚之间受影响的部分或全部未提交数据。这就是NOLOCK 的重点——允许您读取未提交的数据,这样您就不必等待写入者,并避免放置大多数锁,这样写入者就不必等待您。

证明 #1

这很容易证明。在一个窗口中,创建此表:

CREATE TABLE dbo.what(id INT);

在第二个窗口中,运行以下查询:

DECLARE @id INT;

WHILE 1 = 1
BEGIN
 SELECT @id = id FROM dbo.what WITH (NOLOCK) WHERE id = 2;

 IF @id = 2
 BEGIN
   PRINT @id;
   BREAK;
 END
END

现在回到第一个窗口,开始有意长时间运行的事务,但将其回滚:

BEGIN TRANSACTION;
GO

INSERT dbo.what SELECT 2;
GO 10000

ROLLBACK TRANSACTION;

一旦您在第一个窗口中启动此操作,第二个窗口中的查询将停止并吐出已读取的未提交值。

证明 #2

这主要是为了反驳@Blam 上面的评论,我不同意:

实际上,我认为您可以阅读全部 100 个,而不仅仅是提交或回滚之前的子集。

您当然可以读取受事务影响的行子集。尝试以下类似的示例,这次将 100 个集合插入表中,1000 次,并使用 (NOLOCK) 在查询中检索计数。窗口#1(如果你还没有测试过上面的证明#1):

CREATE TABLE dbo.what(id INT);

窗口 #2:

DECLARE @c INT;

WHILE 1 = 1
BEGIN
 SELECT @c = COUNT(*) FROM dbo.what WITH (NOLOCK) WHERE id = 2;

 IF @c > 0
   PRINT @c;

 IF @c > 10000
   BREAK;
END

回到窗口 #1:

BEGIN TRANSACTION;
GO

INSERT dbo.what SELECT TOP (100) 2 FROM sys.all_objects;
GO 1000

ROLLBACK TRANSACTION;

窗口#2 将旋转直到您开始交易。一旦你这样做,你就会开始看到计数滴入。但它们不会是 100 的倍数(更不用说 100,000,@Blam 似乎正在做出全有或全无的说法)。以下是我的删节结果:

1
10
12
14
17
19
23
25
29
...
85
87
91
95
98
100
100
...
9700
9700
9763
9800
9838
9900
9936
10000
10000
10000
10080

NOLOCK 查询显然不会在读取数据之前等待任何单个语句完成,更不用说整个事务了。因此,无论每条语句影响多少行,也无论整个事务中有多少条语句,您都可以获得处于任何变化状态的数据。

其他副作用

还有NOLOCK 可以skip rows, or read the same row twice 的情况,这取决于扫描类型和另一个事务何时生成页面拆分。基本上发生的事情是(NOLOCK) 查询正在读取数据,其他写入实际上可以将数据移动到不同的位置 - 因为它们可以 - 将您已经读取的行移动到扫描中更靠前的位置,或者移动在扫描的早期,您尚未阅读的行。

建议

一般来说,这是个坏消息,您应该考虑使用READ_COMMITTED_SNAPSHOT - 它具有相同的好处,即允许读者不阻止作者,反之亦然,但可以在某个时间点为您提供一致的数据视图,忽略所有后续的数据修改(虽然这对 tempdb 有影响,所以一定要测试它)。 Very thorough information here.

【讨论】:

【参考方案2】:

正如 Aaron 优雅地解释的那样,是的,您将读取脏的未提交数据。但是如果你想避免读取脏数据,并且还没有准备好开始使用乐观锁定,你可以尝试使用下面的READPAST table 提示,尽管它会跳过任何被锁定的行,因此您不会看到尚未提交的插入和更新行。

SELECT *
FROM MyTable WITH (READPAST)

请注意,此表提示要求数据库在 READ COMMITTED(默认)或 REPEATABLE READ 隔离级别下运行。

【讨论】:

以上是关于使用 WITH (NOLOCK) 和事务的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server 中WITH (NOLOCK)

在 JOIN 查询中的某些表上使用 WITH (NOLOCK)

关于sql中的with(nolock)

Laravel Eloquent 和 Query Builder “with (nolock)”

sqlserver with(nolock)

在数据仓库场景中使用 WITH(NOLOCK) 有啥缺点吗