关于SqlServer中的NOLOCK


在我们系统的sql查询代码中,经常可以看到select之后会加一个NOLOCK的关键字,但是至于这个关键字是什么意思,问了下大家一般也就是说“禁用读写锁,可以提升查询性能”,但是真正深层次的原因却不清楚,查询了下相关资料然后整理了下分享给大家。

nolock是什么

nolock是 SQL Server 的一个关键字,这类关键字官方将其称之为 Hints。 Hints 的设计目的是为了能够让 SQL 语句在运行时,动态修改查询优化器的行为。 在语法上,Hints 以WITH开头。除了WITH(nolock), 还有TABLOCK/INDEX/ROWLOCK等常见的 Hints。 MSDN文档上的解释是这样的:

nolock的作用等同于READUNCOMMITTED

READUNCOMMITTED这是一种 RDBMS 隔离级别。 使用nolock这个关键词,可以将当前查询语句隔离级别调整为READ UNCOMMITTED。
下面来介绍下什么是隔离级别:

隔离级别

隔离级别主要有四种,其隔离程度由高到低是:
- 可序列化(Serializable) - 可重复读(Repeatable reads) - 提交读(Read committed) - 未提交读(Read uncommitted)

隔离级别主要解决的问题是加一个针对数据资源的锁,从而保证数据操作过程中的一致性。这是最简单的实现方式,但是这种粗暴的隔离性会大大的降低性能,而设计出多种隔离级别就是为了不同的业务需求来取得两者之间的平衡。
下面来分别介绍下这四种不同的隔离级别。

Read Uncommitted

Read Uncommitted 这个隔离级别是最低粒度的隔离级别, 如同它的名字一般,它允许在操作过程中不会锁,从而让当前事务读取到其他事务的数据。 如上图所示,在 Transaction 2 查询时候,Transaction 1 未提交的数据就已经对外暴露。 如果 Transaction 1 最后 Rollback 了,那么 Transaction 读取的数据就是错误的。
因此:“读到了其他事务修改了但是未提交的数据”即是脏读 。

Read Committed

想要避免脏读,最简单的方式就是在事务更新操作上加一把写锁, 其他事务需要读取数据时候,需要等待这把写锁释放。 如上图所示,Transaction 1 在写操作时候,对数据 A 加了写锁, 那么 Transaction 2 想要读取 A,就必须等待这把锁释放。 这样就避免当前事务读取其他事务的未提交数据。 但是除了脏读,一致性的要求还需要“可重复读”,即“在一个事务内,多次读取的特定数据都必须是一致的 (即便在这过程中该数据被其他事务修改)”。 上图就是没能保证“可重复度”,Transaction 2 第一次读取到了数据 A, 然后 Transaction 1 对数据 A 更新到 A',那么当 Tranction 2 再次读取 A 时候, 它本来期望读到 A,但是却读到了 A',这和它的预期不相符了。 解决这个问题,就需要提升隔离级别到“Repeatable Read”。

Repeatable Read

这个名字非常容易理解,即保障在一个事务内重复读取时, 始终能够读取到相同的内容。来看图: 如上所示,当 Transation 2 读取 A 时候,会同时加上一把 Read Lock, 这把锁会阻止 Transaction 1 将 A 更新为 A',Transaction 1 要么选择等待, 要么就选择结束。

隔离级别到现在看起来不管是写入还是读取,我们都可以保证数据的一致性不被破坏。 但是其实还有漏洞:新增数据的一致性!

上述的三个隔离级别,都是对特定的一行数据进行加锁, 那假如将要更新的数据还没有写入数据库,如何进行加锁呢? 比如自增表的新键,或者现有数据内的空缺 Key? 如图所示,在上述操作中,Transaction 2 查询了一个范围 Range 之后,Transaction 1 在这个范围内插入了一条新的数据。此时 Transaction 2 再次进行范围查询时候, 会发现查询到的 Range 和上次已经不一样了,多了一个 newA。

这就是最高隔离级别才能解决的「幻影读」: 当两个完全相同的查询语句执行得到不同的结果集, 这常常在范围查询中出现。

Serializable

从字面意思看,该隔离级别需要将被操作的数据加锁加一把锁。 任何读写操作都需要先获得这把锁才能进行。如果操作中带 WHERE 条件, 还需要将 WHERE 条件相关的范围全部加锁。 如图所示,在 Transaction 2 操作过程中,会对 Range 进行加锁, 此时其他事务无法操作其中的数据,只能等待或者放弃。 DB 的默认隔离级别 现在我们已经理解了隔离级别,那么SQL Server 默认使用的隔离级别是什么呢?根据Customizing Transaction Isolation Level这个文档描述,SQL Server 默认隔离级别是 READ COMMITTED。 隔离级别并没有最好之说,越高隔离级别会导致性能降低。 隔离级别的设定需要考虑业务场景。

我们已经知道nolock的作用是动态调整隔离级别。 SQL Server 默认是 Read Committed, 更新操作会产生排它锁,会 block 这个资源的查询操作, 已插入但未提交的数据主键也会产生一个共享锁, 而此时则会 block 这张表的全表查询和 Insert 操作。 为了避免 Insert 被 Block,就会推荐使用nolock。