五维思考

学习要加,骄傲要减,机会要乘,懒惰要除。 http://www.5dthink.cn

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

一、锁(LOCK)知识及应用

1.1 锁的基础知识

在任何多用户的数据库中,必须有一套用于数据修改的一致的规则。对于真正的事务处理型数据库,当两个不同的进程试图同时修改同一份数据时,数据库管理系统(DBMS)负责解决它们之间潜在的冲突。

任何关系数据库必须支持事务的ACID属性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、永久性(Durability)。ACID属性确保数据库中的数据更改被正确地收集到一起,并且数据将保持在与所采取动作相一致的状态。

锁的主要作用是提供事务所需的隔离。隔离确保事务之间不会相互干扰,即,一个给定的事务不会读取或修改正在被另一个事务修改的数据。此外,锁提供的隔离性有助于保证事务间的一致性。没有锁,一致的事务处理是不可能的。

在并发环境中如果没有锁和隔离,可能发生以下4种情况:

  • 丢失更新 : 在这种情况下,事务与事务之间没有隔离。多个事务能够读取同一份数据并且修改它。最后对数据集做出修改的事务将胜出,而其他所有事务所作的修改都丢失了。
  • 脏读 : 在这种情况下,一个事务能够读取正被其他事务修改的数据。被第一个事务读取的数据是不一致的,因为另一个事务可能会回滚所作的修改。
  • 不可重复读 : 这种情况有点类似于没有任何隔离,一个事务两次读取数据,但是在第二次读取发生前,另一个事务修改了该数据;因此,两次读取所得到的结果是不同的。因为读操作不能保证每次都是课重复进行的,这种情况被称作“不可重复读”。
  • 幻读 : 这种情况类似于不可重复读。然而,不是先前被读取的实际行在事务完成前发生了改变,而是额外的行被添加到了表中,导致第二次读取返回了不同的行集合。

1.2 锁的分类

锁的类别有两种分法:

1、从数据库系统的角度来看

分为独占锁(即排它锁)、共享锁和更新锁

MS-SQL Server 使用以下资源锁模式。

锁模式 描述
共享锁 (S) 用于不更改或不更新数据的操作(只读操作),如SELECT语句
更新锁 (U) 用于可更新的资源中。防止当多个会话在读取、锁定以及随后可能进行的资源更新时发生常见形式的死锁
排它锁(X) 用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新
意向锁 用于建立锁的层次结构。意向锁的类型为:意向共享 (IS)、意向排它 (IX) 以及与意向排它共享 (SIX)
架构锁 在执行依赖于表架构的操作时使用。架构锁的类型为:架构修改 (Sch-M) 和架构稳定性 (Sch-S)
大容量更新锁(BU) 向表中大容量复制数据并指定了TABLOCK提示时使用
(1)共享锁

缺省地,SQL Server 为所有读操作应用共享锁。共享 (S) 锁允许并发事务读取 (SELECT) 一个资源。资源上存在共享 (S) 锁时,任何其它事务都不能修改数据。一旦已经读取数据,便立即释放资源上的共享 (S) 锁,除非将事务隔离级别设置为可重复读或更高级别,或者在事务生存周期内用锁定提示保留共享 (S) 锁。

(2)更新锁

更新 (U) 锁可以防止通常形式的死锁。一般更新模式由一个事务组成,此事务读取记录,获取资源(页或行)的共享 (S) 锁,然后修改行,此操作要求锁转换为排它 (X) 锁。如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则一个事务尝试将锁转换为排它 (X) 锁。共享模式到排它锁的转换必须等待一段时间,因为一个事务的排它锁与其它事务的共享模式锁不兼容;发生锁等待。第二个事务试图获取排它 (X) 锁以进行更新。由于两个事务都要转换为排它 (X) 锁,并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。

若要避免这种潜在的死锁问题,请使用更新 (U) 锁。一次只有一个事务可以获得资源的更新 (U) 锁。如果事务修改资源,则更新 (U) 锁转换为排它 (X) 锁。否则,锁转换为共享锁。

(3)排它锁

排它 (X) 锁可以防止并发事务对资源进行访问。其它事务不能读取或修改排它 (X) 锁锁定的数据

(4)意向锁

意向锁表示SQL Server需要在层次结构中的某些底层资源上获取共享 (S) 锁或排它 (X) 锁。例如,放置在表级的共享意向锁表示事务打算在表中的页或行上放置共享 (S) 锁。在表级设置意向锁可防止另一个事务随后在包含那一页的表上获取排它 (X) 锁。意向锁可以提高性能,因为SQL Server仅在表级检查意向锁来确定事务是否可以安全地获取该表上的锁。而无须检查表中的每行或每页上的锁以确定事务是否可以锁定整个表。
意向锁包括意向共享 (IS)、意向排它 (IX) 以及与意向排它共享 (SIX)。

  • 意向共享 (IS):通过在各资源上放置S锁,表明事务的意向是读取层次结构中的部分(而不是全部)底层资源。

  • 意向排它 (IX):通过在各资源上放置X锁,表明事务的意向是修改层次结构中的部分(而不是全部)底层资源。IX IS的超集。

  • 与意向排它共享 (SIX):通过在各资源上放置IX锁,表明事务的意向是读取层次结构中的全部底层资源并修改部分(而不是全部)底层资源。允许顶层资源上的并发IS锁。例如,表的SIX锁在表上放置一个SIX锁(允许并发IS锁),在当前所修改页上放置IX锁(在已修改行上放置X锁)。虽然每个资源在一段时间内只能有一个SIX锁,以防止其它事务对资源进行更新,但是其它事务可以通过获取表级的IS锁来读取层次结构中的底层资源。

(5)独占锁

只允许进行锁定操作的程序使用,其他任何对他的操作均不会被接受。执行数据更新命令时,SQL Server会自动使用独占锁。当对象上有其他锁存在时,无法对其加独占锁。

共享锁:共享锁锁定的资源可以被其他用户读取,但其他用户无法修改它,在执行Select时,SQL Server会对对象加共享锁。

(6)更新锁

SQL Server准备更新数据时,它首先对数据对象作更新锁锁定,这样数据将不能被修改,但可以读取。等到SQL Server确定要进行更新数据操作时,他会自动将更新锁换为独占锁,当对象上有其他锁存在时,无法对其加更新锁。

2、从程序员的角度看:

  • 乐观锁:完全依靠数据库来管理锁的工作。

  • 悲观锁:程序员自己管理数据或对象上的锁处理。

1.3 锁的粒度

锁粒度是被封锁目标的大小,封锁粒度小则并发性高,但开销大,封锁粒度大则并发性低但开销小。锁会给数据库带来阻塞,因此越大粒度的锁造成更多的阻塞,但由于大粒度的锁需要更少的锁,因此会提升性能。而小粒度的锁由于锁定更少资源,会减少阻塞,因此提高了并发,但同时大量的锁也会造成性能的下降。SQL Server支持的锁粒度可以分为为行、页、键、键范围、索引、表或数据库等:

  • DATABASE : 无论何时当一个SQL Server 进程正在使用除master以外的数据库时,Lock Manager为该进程授予数据库级的锁。数据库级的锁总是共享锁,用于跟踪何时数据库在使用中,以防其他进程删除该数据库,将数据库置为脱机,或者恢复数据库。注意,由于mastertempdb数据库不能被删除或置为脱机,所以不需要在它们之上加锁。
  • FILE : 文件级的锁用于锁定数据库文件。
  • EXTENT : Extent锁用于锁定extents,通常仅在空间分配和重新分配的时候使用。一个extent由8个连续的数据页或索引页组成。Extent锁可以是共享锁也可以是独占锁。
  • ALLOCATION_UNIT : 使用在数据库分配单元上。
  • TABLE : 这种级别的锁将锁定整个表,包括数据和索引。何时将获得表级锁的例子包括在Serializable隔离级别下从包含大量数据的表中选取所有的行,以及在表上执行不带过滤条件的updatedelete
  • Heap or B-Tree (HOBT) : 用于堆数据页,或者索引的二叉树结构。
  • PAGE : 使用页级锁,由8KB数据或者索引信息组成的整个页被锁定。当需要读取一页的所有行或者需要执行页级别的维护如页拆分后更新页指针时,将会获取页级锁。
  • Row ID (RID) : 使用RID锁,页内的单一行被锁定。无论何时当提供最大化的资源并发性访问是有效并且可能时,将获得RID锁。
  • KEY : SQL Server使用两种类型的Key锁。其中一个的使用取决于当前会话的锁隔离级别。对于运行于Read Committed 或者Repeatable Read隔离模式下的事务,SQL Server 锁定与被访问的行相关联的的实际索引key。(如果是表的聚集索引,数据行位于索引的叶级。行上在这些你看到的是Key锁而不是行级锁。)若在Serializable隔离模式下,通过锁定一定范围的key值从而不允许新的行插入到该范围内,SQL Server防止了“幻读”。这些锁因而被称作“key-range lock”。
  • METADATA : 用于锁定系统目录信息(元数据)。
  • APPLICATION : 允许用户定义他们自己的锁,指定资源名称、锁模式、所有者、timeout间隔。

1.4 锁类型(与粒度相对应)

  1. HOLDLOCK: 在该表上保持共享锁,直到整个事务结束,而不是在语句执行完立即释放所添加的锁。  
  2. NOLOCK:不添加共享锁和排它锁,当这个选项生效后,可能读到未提交读的数据或“脏数据”,这个选项仅仅应用于SELECT语句。  
  3. PAGLOCK:指定添加页锁(否则通常可能添加表锁)。 
  4. READCOMMITTED用与运行在提交读隔离级别的事务相同的锁语义执行扫描。默认情况下,SQL Server 2000 在此隔离级别上操作。
  5. READPAST: 跳过已经加锁的数据行,这个选项将使事务读取数据时跳过那些已经被其他事务锁定的数据行,而不是阻塞直到其他事务释放锁,READPAST仅仅应用于READ COMMITTED隔离性级别下事务操作中的SELECT语句操作。  
  6. READUNCOMMITTED:等同于NOLOCK。  
  7. REPEATABLEREAD:设置事务为可重复读隔离性级别。 
  8. ROWLOCK:使用行级锁,而不使用粒度更粗的页级锁和表级锁。  
  9. SERIALIZABLE:用与运行在可串行读隔离级别的事务相同的锁语义执行扫描。等同于 HOLDLOCK。 
  10. TABLOCK:指定使用表级锁,而不是使用行级或页面级的锁,SQL Server在该语句执行完后释放这个锁,而如果同时指定了HOLDLOCK,该锁一直保持到这个事务结束。  
  11. TABLOCKX:指定在表上使用排它锁,这个锁可以阻止其他事务读或更新这个表的数据,直到这个语句或整个事务结束。 
  12. UPDLOCK :指定在读表中数据时设置更新锁(update lock)而不是设置共享锁,该锁一直保持到这个语句或整个事务结束,使用UPDLOCK的作用是允许用户先读取数据(而且不阻塞其他用户读数据),并且保证在后来再更新数据时,这一段时间内这些数据没有被其他用户修改。

1.5 锁定时间的长短

锁保持的时间长度为保护所请求级别上的资源所需的时间长度。
用于保护读取操作的共享锁的保持时间取决于事务隔离级别。采用READ COMMITTED的默认事务隔离级别时,只在读取页的期间内控制共享锁。在扫描中,直到在扫描内的下一页上获取锁时才释放锁。如果指定HOLDLOCK提示或者将事务隔离级别设置为REPEATABLE READ SERIALIZABLE,则直到事务结束才释放锁。
根据为游标设置的并发选项,游标可以获取共享模式的滚动锁以保护提取。当需要滚动锁时,直到下一次提取或关闭游标(以先发生者为准)时才释放滚动锁。但是,如果指定 HOLDLOCK,则直到事务结束才释放滚动锁。
用于保护更新的排它锁将直到事务结束才释放。

如果一个连接试图获取一个锁,而该锁与另一个连接所控制的锁冲突,则试图获取锁的连接将一直阻塞到:

将冲突锁释放而且连接获取了所请求的锁。

连接的超时间隔已到期。默认情况下没有超时间隔,但是一些应用程序设置超时间隔以防止无限期等待

1.6 锁的自定义

  1. 处理死锁和设置死锁优先级
    死锁就是多个用户申请不同封锁,由于申请者均拥有一部分封锁权而又等待其他用户拥有的部分封锁而引起的无休止的等待
    可以使用SET DEADLOCK_PRIORITY控制在发生死锁情况时会话的反应方式。如果两个进程都锁定数据,并且直到其它进程释放自己的锁时,每个进程才能释放自己的锁,即发生死锁情况。

  2. 处理超时和设置锁超时持续时间
    @@LOCK_TIMEOUT 返回当前会话的当前锁超时设置,单位为毫秒
    SET LOCK_TIMEOUT 设置允许应用程序设置语句等待阻塞资源的最长时间。当语句等待的时间大于LOCK_TIMEOUT设置时,系统将自动取消阻塞的语句,并给应用程序返回"已超过了锁请求超时时段"的 1222 号错误信息

1.7 查看锁的信息

  1. 执行 EXEC SP_LOCK 报告有关锁的信息

  2. 查询分析器中按Ctrl+2可以看到锁的信息

1.8 使用注意事项

如何避免死锁,最小化锁竞争

  1. 使用事务时,尽量缩短事务的逻辑处理过程,及早提交或回滚事务,事务持有锁的时间越短,锁竞争发生的机会就越少;将不是事务所管理的工作单元锁必需的命令移出事务。;

  2. 设置死锁超时参数为合理范围,如:3分钟-10分种;超过时间,自动放弃本次操作,避免进程悬挂;

  3. 优化程序,检查并避免死锁现象出现;

  4. .对所有的脚本和SP都要仔细测试,在正是版本之前。

  5. 所有的SP都要有错误处理(通过@error)

  6. 一般不要修改SQL SERVER事务的默认级别。不推荐强行加锁

  7. 将组成事务的语句作为一个的单独的批命令处理,以消除BEGIN TRANCOMMIT TRAN语句之间的网络延迟造成的不必要的延迟。

  8. 考虑完全地使用存储过程编写事务代码。典型地,存储过程比批命令运行更快。

  9. 在游标中尽可早地Commit更新。因为游标处理比面向集合的处理慢得多,因此导致锁被持有的时间更久。

  10. 使用每个进程所需的最低级别的锁隔离。比如说,如果脏读是可接受的并且不要求结果必须精确,那么可以考虑使用事务隔离级别0(Read Uncommitted),仅在绝对必要时才使用Repeatable Read or Serializable隔离级别。

  11. BEGIN TRANCOMMIT TRAN语句之间,绝不允许用户交互,因为这样做可能锁被持有无限期的时间。

1.9 几个有关锁的问题

◆行锁

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM table ROWLOCK WHERE id = 1

◆表锁

SELECT * FROM table WITH (HOLDLOCK)

加锁语句:

-- sybase:
update 表 set col1=col1 where 1=0 ;
-- MsSQL:
select col1 from 表 with(tablockx) where 1=0 ;
-- oracle:
LOCK TABLE 表 IN EXCLUSIVE MODE ;

加锁后其它人不可操作,直到加锁用户解锁,用commitrollback解锁

◆排它锁

--新建两个连接,在第一个连接中执行以下语句
begin tran 
update table1 set A='aa' where B='b2' 
waitfor delay '00:00:30' --等待30秒 
commit tran 
--在第二个连接中执行以下语句 
begin tran 
select * from table1 where B='b2' 
commit tran

若同时执行上述两个语句,则select查询必须等待update执行完毕才能执行即要等待30秒

◆共享锁

--在第一个连接中执行以下语句
begin tran 
select * from table1 holdlock  --holdlock人为加锁 
where B='b2' 
waitfor delay '00:00:30' --等待30秒 
commit tran

◆共享锁

--在第一个连接中执行以下语句
begin tran 
select * from table1 holdlock --holdlock人为加锁 
where B='b2' 
waitfor delay '00:00:30' --等待30秒 
commit tran
--在第二个连接中执行以下语句
begin tran 
select A,C from table1 where B='b2' 
update table1 set A='aa' where B='b2' 
commit tran

若同时执行上述两个语句,则第二个连接中的select查询可以执行,而update必须等待第一个事务释放共享锁转为排它锁后才能执行,即要等待30秒

◆死锁

--在第一个连接中执行以下语句 
begin tran 
update table1  set A='aa' where B='b2' 
waitfor delay '00:00:30' 
update table2  set D='d5'  where E='e1' 
commit tran
--在第二个连接中执行以下语句
begin tran 
update table2  set D='d5'  where E='e1' 
waitfor delay '00:00:10' 
update table1  set A='aa'  where B='b2' 
commit tran

同时执行,系统会检测出死锁,并中止进程

1.10 应用程序锁

应用程序锁就是客户端代码生成的锁,而不是sql server本身生成的锁

处理应用程序锁的两个过程

sp_getapplock --锁定应用程序资源
sp_releaseapplock --为应用程序资源解锁

注意: 锁定数据库的一个表的区别

SELECT * FROM table WITH (HOLDLOCK) --其他事务可以读取表,但不能更新删除
SELECT * FROM table WITH (TABLOCKX) --其他事务不能读取表,更新和删除

二、锁的分析及应用系列

2.1 用Profile查看锁信息

这个工具我想大家都明白,它的监视能力真的是无所不能。。。锁的痉挛状态也全在它的掌握之中。

  1. 首先我做一个Person表,Name字段设定4000字节,这样一个数据页可以容纳2条数据,如下图:
DROP TABLE dbo.Person
CREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'aaaaa')
--插入6条,生成3个数据页
INSERT INTO dbo.Person DEFAULT VALUES
go 6
  1. 下面我们看看数据在数据页的分布情况。

img

  1. 然后我们开启Profile,在“事件选择”的Events中选择Lock:AcquiredLock:Released ,然后运行如下图:

img

使用测试数据

  1. 首先我执行一个简单的 SELECT * FROM dbo.Person,看看表/数据页/记录的加锁情况。

img

从图中可以看到,select执行的大概步骤如下:

第一步:给表(Object)加上IS(意向共享锁)。

第二步:先给1:78号数据页加IS锁,扫描78号数据页,然后释放IS锁。

第三步:同样的道理扫描之后的数据页。

第四步:最后释放表的IS锁,结束整个锁流程。

看完上面的一系列的Lock:AcquiredLock:Released的话,你有没有发现一个问题,不是说好给记录(RID)加上S锁么???这里没加,是因为引擎进入78号数据页的时候,未发现它存在IU锁或者IX锁。。。所以。。。这个属于锁的组合,后续会说。

  1. 接下来用UPDATE dbo.Person SET NAME='bbbbb' WHERE ID=3来看看update的整个过程,乍一看,Profile捕获到的记录还是比较多的,下面具体看图:

img

第一步: 给表(Object)加上IX锁,

第二步: 给数据页(1:78)数据页分配IU锁。然后开始逐一扫描78号数据页的RID记录,进入前就Acquired,退出后就Released,当扫描完78号数据页的所有RID后,再释放78 号数据页的IU锁,进入下一个数据页。。。

第三步: 我们发现ID=3是在89号数据页上,当引擎扫到该RID之后,我们观察到89号的数据页由IU锁变成了IX锁,并且把1:89:0(slot为0的记录)由U锁变成X锁,变成X 后,就排斥了其他所有的锁,这时候就可以进行Update操作了。

第四步: 后面就继续90号数据页,步骤类似,第二步和第三步。

不知道细心的你有没有发现,在Released Object之前我们才释放1:89:0的X锁,然后释放89号数据页的IX锁,这说明什么???说明这个Update是贯穿于这个事务的,不像Select操作中,扫完一个数据页就释放一个数据页。

  1. 最后再看一个DELETE FROM dbo.Person WHERE ID=3 的操作。

img

  大概扫了一下上面的图,或许你感觉和Update操作大差不差,会扫描数据页中的每个记录并加上U锁。当在1:89:0槽位中找到了目标记录后,然后将U锁转化为X锁,具体可以参考Update

2.2 深入探讨锁机制

  以上只是做了一个堆表让大家初步的认识到锁的痉挛状态,但是在现实世界上并没有这么简单的事情,起码我的表不会没有索引对吧,还有就是我的表一定会有很多的连接过来,10:1的读写,很多码农可能都会遇到类似神乎其神的死锁,卡住,读不出来,插不进入等等神仙的事情导致性能低下,这篇我们一起来探讨下。

2.2.1 当select遇到性能低下的update会怎么样?

  1. 还是使用原始的person表,插入6条数据,由于是4000字节,所以两条数据就是一个数据页,如下图:
DROP TABLE dbo.Person
CREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'aaaaa')
--插入6条数据,刚好3个数据页
INSERT INTO dbo.Person DEFAULT VALUES
go 6
  1. 为了模拟性能低下的Update操作,我们开个显式事务来更新ID=4的记录,并且用profile看一下,如下图:
BEGIN TRAN
UPDATE dbo.Person SET NAME='bbbbb' WHERE id=4

img

  1. 然后我们开下另一个会话连接,读取ID=6的记录会是怎样?好奇吗?
SELECT * FROM Person WHERE ID=6

img

从上面流程你是否看到,当扫描到89号数据页的slot1槽位的时候卡住了。。。我想你应该知道update正好已经给这条记录加上了X锁。。。如果你够细心,你还会发现,给S锁附加记录的条件是在当引擎发现记录所在的数据页已经附加上了IX锁的情况下,才给该号数据页下的每条记录附加S锁,对吧。。。好了,既然在Profile上面看不到了,我还是有其他办法来判断到底select语句现在处于什么状态。

  1. 使用sys.dm_tran_locks来看当前各个连接持有锁的状态。
SELECT  l.request_session_id,
        DB_NAME(l.resource_database_id),OBJECT_NAME(p.object_id),
        l.resource_description,l.request_type,
        l.request_status,request_mode 
FROM sys.dm_tran_locks AS l
LEFT JOIN sys.partitions AS p
ON l.resource_associated_entity_id=p.hobt_id

img

仔细观察上图可以看到,当前有51和52号会话,51号在1:89:1槽位上使用了X锁并且没有释放,52号此时也进入了1:89:1中,并且想给该RowID附加S锁,但是你也知道SX锁是排斥的,所以很无奈的一直保持等待状态。

2.2.2 使用索引或许可以帮你逃过一劫

  当你看完上面的讲叙,是不是有点害怕???要是在生产环境下出现了这种情况,那我们是不是死的很惨???那接下来使用索引是不是真的可以帮我们躲过一劫呢?下面跟我一起看一看。

  1. 新建索引index
-- 在`ID`列上建一个index
CREATE INDEX idx_person ON dbo.Person(ID)
  1. 然后我们看下数据页的分布情况,可以看到下图中78,89,90是表数据页,93号为索引数据页。
DBCC TRACEON(2588,3604)
DBCC IND(Ctrip,Person,-1)

img

  1. 麻蛋的,继续执行上面的那个慢update
BEGIN TRAN
UPDATE dbo.Person SET NAME='bbbbb' WHERE id=4
  1. 激动人心的时刻来了,由于数据太少,所以我这里强制让引擎执行我创建的索引,看看结果怎样?

img

居然没卡住???现在是不是有一股强烈的好奇心来了,狗狗狗。。。马上开启profile,看看到底都发生了什么?

img

仔细看完这个图,是不是觉得很有意思呢???具体步骤如下:

第一步:给表(Object)加上IS锁。

第二步:因为要走索引,给93号索引数据页加上IS锁。

第三步:找到93号索引数据页的目标key,给这个key加上S锁,有人可能就会问了。。。这个key不就是6嘛,为什么这个key=(61005a25560e),你要是太好奇我可以告 诉你,年轻人说话不要太屌,每行索引记录都有一个散列值,这个值就是根据索引的几个字段散列出来的,好处就是防止你的索引长度过大,导致锁这个记录的 时候太耗费锁空间了。。。。如果你还是不太相信的话,我用DBCC给你看一看。      

img

第四步:根据这个key直接跳到存放记录的90号数据页中,万幸的是update的记录刚好不在90号数据页中。。。。就这样躲过一劫了。。。然后select顺利的读取到了该 读的记录,最后释放相关的IS锁。

2.3 到底可以指定多少个锁

  这个问题有意思,我们不需要记,只要你装一个SQL Prompt,有了这个神器,你就知道到底有多少个?如下图:

1 DROP TABLE dbo.Person
2 CREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'xxxxx')
3 INSERT INTO dbo.Person DEFAULT VALUES
4 go 6

img

一眼扫下去,还是蛮多的,不过你要注意了,那些所谓的XXXLock才是我们需要关注的,根据上面的图,我们大概把锁分个类。。。

  • 粒度锁:PAGLOCK、TABLOCK、TABLOCKX、ROWLOCK、NOLOCK
  • 模式锁:HOLDLOCK、UPDLOCK、XLOCK

接下来我从粒度锁说起:

  1. NOLOCK

  都说nolock是无锁模式的,那到底是怎样的无锁呢???到这篇为止,你应该知道,如果不加nolock,我们的表,数据页是附加IS锁的,那接下来我用profile看下两者有什么区别。 

img 

从上图中,你会看到加上nolock之后,object上面附加了Sch-S锁,这个锁叫做“架构稳定锁”,很简单就是sql编译时附加的一把锁,目的就是防止在编译时,有其他连接修改表结构,而这个锁只与Sch-M锁冲突,与其他锁都兼容,这说明什么?说明其他连接锁住了记录也没关系,我的nolock不跟他们打交道,这样的话,就可能会读到脏数据,不过没关系,携程的很多业务是容许脏数据的,毕竟比锁等待,死锁要强得多,再说nolock读到了其他连接未修改或者未提交的数据,这个概率也比较低,就算遇到了也没关系,一般不会招来客诉的,客人或许再刷下页面,数据或许就正确了,对不对。。。

  1. TABLOCK

  这个还是比较见名识义的,就是附加在table上的锁,也就是表锁了,很恐怖的。。。下面我举个Update的例子,看看前后对比。

img

img

在上面你有没有看到,X锁已经附加到OBJECT上面去了。。。这样的话,其他连接就动不了这个Object了,只能等待。。。

  1. PAGLOCK

  看了名字你应该也知道,就是附加到页面这个级别的锁,我也举一个Update的例子。

BEGIN TRAN
UPDATE dbo.Person SET NAME='aaaaa' WHERE ID=6

BEGIN TRAN
UPDATE dbo.Person WITH(PAGLOCK) SET NAME='bbbbb' WHERE ID=4

img

img

从上面两个图中,你应该可以看到,原来附加到RID上面的U锁,由于PagLock的提升,现在要附加到Page上面了,这个就是所谓的数据页锁。

  1. TABLOCKX、 ROWLOCK

这两个我就不细说了,TABLOCKX就是直接附加在table上的X锁,你可以通过select看一下。

img

ROWLOCK的话,默认情况下就是ROWLOCK,比如默认的Update,你会发现RID上被附加的U锁,这个就是行锁。

  1. UPDLOCK

 这个锁还是蛮有意思的,它就是update锁,如果你select下,它会呈现update的锁痉挛效果。

img

  

  1. XLOCK

  知道了UPDLOCK锁,我想XLOCK你也应该明白了。。。它就是delete锁,即排他锁,我可以让select带上排他锁。

img

  1. HOLDLOCK

  最后一个我也没闹明白,据说是让语句在整个事务中持有锁,然后我就用selectupdate调试一下。

SELECT * FROM dbo.Person(HOLDLOCK)
UPDATE dbo.Person WITH(HOLDLOCK) SET NAME='bbbbb' WHERE ID=4

img

从图中可以看到,HOLDLOCK不管是在select还是Update中,都是对表持有锁,没心情研究了,明天可以回家了。。。留给大家观察吧。

三、锁机制(悲观锁/乐观锁)实测解析

在使用SQL时,大都会遇到这样的问题,你Update一条记录时,需要通过Select来检索出其值或条件,然后在通过这个值来执行修改操作。但当以上操作放到多线程中并发处理时会出现问题:某线程select了一条记录但还没来得及update时,另一个线程仍然可能会进来select到同一条记录。一般解决办法就是使用锁和事务的联合机制:

  1. select放在事务中, 否则select完成, 锁就释放了
  2. 要阻止另一个select , 则要手工加锁, select 默认是共享锁, select之间的共享锁是不冲突的, 所以, 如果只是共享锁, 即使锁没有释放, 另一个select一样可以下共享锁, 从而select出数据
BEGIN TRAN
SELECT * FROM Table WITH(UPDLOCK) 
--或者 SELECT * FROM Table WITH(TABLOCKX,READPAST) 具体情况而定
UPDATE ....
COMMIT TRAN

所有SelectWith (NoLock)解决阻塞死锁,在查询语句中使用NOLOCKREADPAST 处理一个数据库死锁的异常时候,其中一个建议就是使用NOLOCK或者READPAST

有关NOLOCK READPAST的一些技术知识点:

  • 对于非银行等严格要求事务的行业,搜索记录中出现或者不出现某条记录,都是在可容忍范围内,所以碰到死锁,应该首先考虑,我们业务逻辑是否能容忍出现或者不出现某些记录,而不是寻求对双方都加锁条件下如何解锁的问题。
  • NOLOCKREADPAST都是处理查询、插入、删除等操作时候,如何应对锁住的数据记录。但是这时候一定要注意NOLOCK READPAST的局限性,确认你的业务逻辑可以容忍这些记录的出现或者不出现。

简单来说:

  1. NOLOCK 可能把没有提交事务的数据也显示出来
  2. READPAST 会把被锁住的行不显示出来

不使用NOLOCKREADPAST,在Select操作时候则有可能报错误:事务(进程 ID **)与另一个进程被死锁在锁资源上,并且已被选作死锁牺牲品。

SELECT * FROM Table WITH(NOLOCK)
SELECT * FROM Table WITH(READPAST)

实际开始动手用代码说话吧!

SQL Server 在查询分析器里面开两个连接

3.1 插入锁

结论:“表锁”锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select,所以Select如果不想等待就要在Select后加With(Nolock),但这样会产生脏数据,就是其他事务已更新但并没有提交的数据,如果该事务进行了RollBack则取出的数据就是错误的,所以好自己权衡利弊,一般情况下90%以上的Select都允许脏读,只有账户金额相关的不允许。

------------------A连接 Insert Lock------------------
BEGIN TRAN
INSERT INTO dbo.UserInfo( Name, Age, Mobile, AddTime, Type )
VALUES( 'eee',2,'555',GETDATE(),0 )
          
SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode
--ROLLBACK TRAN
 
------------------B连接 Insert Lock------------------
INSERT INTO dbo.UserInfo( Name, Age, Mobile, AddTime, Type )
VALUES( 'fff', 2, '123', GETDATE(), 1 ) --可以执行插入
          
SELECT * FROM dbo.UserInfo --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Age=1 --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Id=3 --可以执行查询(根据主键可以)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Age=1 --可以执行查询
UPDATE dbo.UserInfo SET Type=5 WHERE Name='fff'  --需要等待解锁
DELETE FROM dbo.UserInfo WHERE Name='fff' --需要等待解锁

3.2 更新锁

结论:“表锁”锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作,也不影响以主键Id为条件的Select

-----------------------A连接 Update Lock-----------------------
BEGIN TRAN
UPDATE dbo.UserInfo SET Name = 'eee' WHERE Age = 2 

SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode 
--ROLLBACK TRAN
 
-----------------------B连接 Update Lock-----------------------
INSERT INTO dbo.UserInfo(Name, Age, Mobile, AddTime, Type) 
VALUES('ppp', 15, '666', GETDATE(), 9) --可以执行插入
SELECT * FROM dbo.UserInfo --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Name='ppp' --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Id=3 --可以执行查询(根据主键可以)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'ppp' --可以执行查询
UPDATE dbo.UserInfo SET Age=8 WHERE Name='ccc' --需要等待解锁
DELETE dbo.UserInfo WHERE Age = 5 --需要等待解锁

3.3 主键锁

结论:“行锁+表锁” 锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作,也不影响以主键Id为条件的Select、Update、Delete

------------------------A连接 Key Lock--------------------
BEGIN TRAN
UPDATE dbo.UserInfo SET Name='hhh' WHERE Id=3 --以主键为条件
 
SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode 
--ROLLBACK TRAN
 
------------------------B连接 Key Lock----------------------
INSERT INTO dbo.UserInfo( Name, Age, Mobile, AddTime, Type )
VALUES('kkk', 18, '234', GETDATE(), 7) --可以执行插入
SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以执行查询
 
-----//全表查询及操作正在处理的行
SELECT * FROM dbo.UserInfo --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
DELETE dbo.UserInfo WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
-----//使用非主键为条件的操作
SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --需要等待解锁(非主键不可)
DELETE dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
-----//使用主键为条件的操作
SELECT * FROM dbo.UserInfo WHERE id=1 --可以执行查询(根据主键可以)
UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以执行更新(根据主键可以)
DELETE dbo.UserInfo WHERE Id=1 --可以执行删除(根据主键可以)

3.4 索引锁

结论:“行锁+表锁” 锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作,也不影响以主键Id为条件的Select、Update、Delete,也不影响以索引列Name为条件的Update、Delete但不可以Select

------------------------A连接 Index Lock------------------------
DROP INDEX dbo.UserInfo.Index_UserInfo_Name
CREATE INDEX Index_UserInfo_Name ON dbo.UserInfo(Name)
 
BEGIN TRAN
UPDATE dbo.UserInfo SET age=66 WHERE Name='ddd' --使用name索引列为条件
 
SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode 
--ROLLBACK TRAN
 
------------------------B连接 Index Lock------------------------
INSERT INTO dbo.UserInfo( Name, Age, Mobile, AddTime, Type )
VALUES('iii', 20, '235235235', GETDATE(), 12) --可以执行插入
SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以执行查询
 
-----//全表查询及操作正在处理的行
SELECT * FROM dbo.UserInfo --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
DELETE dbo.UserInfo WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
-----//使用非主键非索引为条件的操作
SELECT * FROM dbo.UserInfo WHERE Age=5 --需要等待解锁(非主键不可)
UPDATE dbo.UserInfo SET Name='ooo' WHERE Age=5 --需要等待解锁(非主键不可)
DELETE dbo.UserInfo WHERE Age=5 --需要等待解锁(非主键不可)
-----//使用主键为条件的操作
SELECT * FROM dbo.UserInfo WHERE Id=1 --可以执行更新(根据主键可以)
UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以执行更新(根据主键可以)
DELETE dbo.UserInfo WHERE Id=1 --可以执行删除(根据主键可以)
-----//使用索引为条件的操作
SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --可以执行更新(根据索引可以)
DELETE dbo.UserInfo WHERE Name='aaa' --可以执行删除(根据索引可以)

3.5 悲观锁

结论:可以理解为在使用版本控制软件的时候A迁出了一个文件,并且将这个文件锁定,B就无法再迁出该文件了,直到A迁入解锁后才能被其他人迁出。

------------------------A连接 Update Lock(悲观锁)------------------------
BEGIN TRAN
SELECT * FROM dbo.UserInfo WITH(UPDLOCK) WHERE Id=2
SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode
--COMMIT TRAN
--ROLLBACK TRAN

------------------------B连接 Update Lock(悲观锁)------------------------
SELECT * FROM dbo.UserInfo --可以执行查询
SELECT * FROM dbo.UserInfo WHERE id=2 --可以执行查询
SELECT * FROM dbo.UserInfo WHERE Name='ooo' --可以执行查询

UPDATE dbo.UserInfo SET Age=3 WHERE id=1 --可以执行更新(根据主键可以)
UPDATE dbo.UserInfo SET Age=3 WHERE Name='ccc' --需要等待解锁(非主键不可)

DELETE dbo.UserInfo WHERE id=1 --可以执行删除(根据主键可以)
DELETE dbo.UserInfo WHERE name='ccc' --需要等待解锁(非主键不可)

3.6 乐观锁

结论:可以理解为同样在使用版本控制软件的时候A迁出了一个文件,B也可以迁出该文件,两个人都可以对此文件进行修改,其中一个人先进行提交的时候,版本并没有变化所以可以正常提交,另一个后提交的时候,发现版本增加不对称了,就提示冲突由用户来选择如何进行合并再重新进行提交。

--------------------------A客户端连接 Lock(乐观锁)------------------------
--DROP TABLE Coupon
-----------------创建优惠券表-----------------
CREATE TABLE Coupon
(
  Id INT PRIMARY KEY IDENTITY(1,1),
  Number VARCHAR(50) NOT NULL,
  [User] VARCHAR(50),
  UseTime DATETIME,
  IsFlag BIT DEFAULT(0) NOT NULL,
  CreateTime DATETIME DEFAULT(GETDATE()) NOT NULL
)
INSERT INTO dbo.Coupon(Number) VALUES ('10000001')
INSERT INTO dbo.Coupon(Number) VALUES ('10000002')
INSERT INTO dbo.Coupon(Number) VALUES ('10000003')
INSERT INTO dbo.Coupon(Number) VALUES ('10000004')
INSERT INTO dbo.Coupon(Number) VALUES ('10000005')
INSERT INTO dbo.Coupon(Number) VALUES ('10000006')
 
--SELECT * FROM dbo.Coupon WITH(NOLOCK) --查询数据
--UPDATE Coupon SET [User]=NULL, UseTime=NULL, IsFlag=0 --还原数据
 
-----------------1、模拟高并发普通更新-----------------
DECLARE @User VARCHAR(50)    --模拟要使用优惠券的用户
DECLARE @TempId INT          --模拟抽选出来的要使用的优惠券
SET @User='a'
BEGIN TRAN
SELECT @TempId=Id FROM dbo.Coupon WHERE IsFlag=0    --高并发时此语句有可能另外一个该事务已取出的Id
--WAITFOR DELAY '00:00:05'    --改用此方式要开两个SQL Management客户端
UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
COMMIT TRAN
--ROLLBACK TRAN
 
-----------------2、悲观锁解决方案-----------------
DECLARE @User VARCHAR(50)    --模拟要使用优惠券的用户
DECLARE @TempId INT          --模拟抽选出来的要使用的优惠券
SET @User='a'
BEGIN TRAN
SELECT @TempId=Id FROM dbo.Coupon WITH(UPDLOCK) WHERE IsFlag=0    --高并发时此语句会锁定取出的Id数据行
--WAITFOR DELAY '00:00:05'    --改用此方式要开两个SQL Management客户端
UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
COMMIT TRAN
--ROLLBACK TRAN
 
-----------------3、乐观锁解决方案-----------------
--增加数据行版本戳类型字段(微软新推荐数据字段,该字段每张表只能有一个,会在创建行或更新行时自动进行修改无需人为干涉,该字段不能建立索引及主键因为会频繁修改)
ALTER TABLE dbo.Coupon ADD RowVer ROWVERSION NOT NULL 
DECLARE @User VARCHAR(50)    --模拟要使用优惠券的用户
DECLARE @TempId INT          --模拟抽选出来的要使用的优惠券
DECLARE @RowVer BINARY(8)    --抽选出来的优惠券的版本(ROWVERSION数据类型存储大小为8字节)
SET @User='a'
BEGIN TRY
    BEGIN TRAN
    SELECT @TempId=Id, @RowVer=RowVer FROM dbo.Coupon WHERE IsFlag=0    --取出可用的Id及对应的版本戳
    --WAITFOR DELAY '00:00:05'    --改用此方式要开两个SQL Management客户端
    UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId AND RowVer=@RowVer
    IF(@@ROWCOUNT > 0)
        BEGIN
            PRINT('修改成功')
            COMMIT TRAN
        END
    ELSE
        BEGIN
            PRINT('该数据已被其他用户修改')
            ROLLBACK TRAN
        END
END TRY
BEGIN CATCH
    ROLLBACK TRAN
END CATCH
 
--------------------------B客户端连接 Lock(乐观锁)------------------------
--此测试需要开两个SQL Management Studio客户端,在A客户端使用WAITFOR DELAY来模拟并发占用,在B客户端执行与A客户端相同的SQL脚本即可(注释掉WAITFOR),所以在此不放相同代码了。

在乐观锁和悲观锁之间进行选择的标准是:冲突的频率与严重性。如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。但是,如果冲突的结果对于用户来说痛苦的,那么就需要使用悲观策略。

我认为如果同一张表的并发很高,但并发处理同一条数据的冲突几率很低,那就应该使用乐观锁,反之,如果同一张表的并发不高,但同时处理同一条数据的几率很高,就应该使用悲观锁。

四、WITH (NOLOCK)浅析

4.1 概念介绍

开发人员喜欢在SQL脚本中使用WITH(NOLOCK)WITH(NOLOCK)其实是表提示(table_hint)中的一种。它等同于READUNCOMMITTED。具体的功能作用如下所示:

  1. 指定允许脏读。不发布共享锁来阻止其他事务修改当前事务读取的数据,其他事务设置的排他锁不会阻碍当前事务读取锁定数据。允许脏读可能产生较多的并发操作,但其代价是读取以后会被其他事务回滚的数据修改。这可能会使您的事务出错,向用户显示从未提交过的数据,或者导致用户两次看到记录(或根本看不到记录)。有关脏读、不可重复读和幻读的详细信息,请参阅并发影响。

  2. READUNCOMMITTEDNOLOCK提示仅适用于数据锁。所有查询(包括那些带有READUNCOMMITTEDNOLOCK提示的查询)都会在编译和执行过程中获取 Sch-S(架构稳定性)锁。因此,当并发事务持有表的 Sch-M(架构修改)锁时,将阻塞查询。例如,数据定义语言 (DDL) 操作在修改表的架构信息之前获取Sch-M锁。所有并发查询(包括那些使用READUNCOMMITTEDNOLOCK提示运行的查询)都会在尝试获取Sch-S锁时被阻塞。相反,持有Sch-S锁的查询将阻塞尝试获取Sch-M锁的并发事务。有关锁行为的详细信息,请参阅锁兼容性(数据库引擎)。

  3. 不能为通过插入、更新或删除操作修改过的表指定READUNCOMMITTED NOLOCKSQL Server 查询优化器忽略FROM子句中应用于UPDATEDELETE语句的目标表的READUNCOMMITTEDNOLOCK提示。

4.2 功能与缺陷

使用WIHT(NOLOCK)有利也有弊,所以在决定使用之前,你一定需要了解清楚WITH(NOLOCK)的功能和缺陷,看其是否适合你的业务需求,不要觉得它能提升性能,稀里糊涂的就使用它。

1、使用WITH(NOLOCK)时查询不受其它排他锁阻塞

打开会话窗口1,执行下面脚本,不提交也不回滚事务,模拟事务真在执行过程当中

BEGIN TRAN
UPDATE TEST SET NAME='Timmy' WHERE OBJECT_ID =1;
--ROLLBACK

打开会话窗口2,执行下面脚本,你会发现执行结果一直查询不出来(其实才两条记录)。当前会话被阻塞了

SELECT * FROM TEST;

打开会话窗口3,执行下面脚本,查看阻塞情况,你会发现在会话2被会话1给阻塞了,会话2的等待类型为LCK_M_S:“当某任务正在等待获取共享锁时出现”

SELECT wt.blocking_session_id                     AS BlockingSessesionId
      ,sp.program_name                           AS ProgramName
      ,COALESCE(sp.LOGINAME, sp.nt_username)    AS HostName    
      ,ec1.client_net_address                    AS ClientIpAddress
      ,db.name                                   AS DatabaseName        
      ,wt.wait_type                              AS WaitType                    
      ,ec1.connect_time                          AS BlockingStartTime
      ,wt.WAIT_DURATION_MS/1000                  AS WaitDuration
      ,ec1.session_id                            AS BlockedSessionId
      ,h1.TEXT                                   AS BlockedSQLText
      ,h2.TEXT                                   AS BlockingSQLText
FROM sys.dm_tran_locks AS tl
INNER JOIN sys.databases db ON db.database_id = tl.resource_database_id
INNER JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2 ON ec2.session_id = wt.blocking_session_id
LEFT OUTER JOIN master.dbo.sysprocesses sp ON SP.spid = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

img

此时查看会话1(会话1的会话ID为53,执行脚本1前,可以用SELECT @@spid查看会话ID)的锁信息情况,你会发现表TEST(ObjId=1893581784)持有的锁信息如下所示

img

打开会话窗口4,执行下面脚本.你会发现查询结果很快就出来,会话4并不会被会话1阻塞。

SELECT * FROM TEST WITH(NOLOCK)

从上面模拟的这个小例子可以看出,正是由于加上WITH(NOLOCK)提示后,会话1中事务设置的排他锁不会阻碍当前事务读取锁定数据,所以会话4不会被阻塞,从而提升并发时查询性能。

2、WITH(NOLOCK) 不发布共享锁来阻止其他事务修改当前事务读取的数据,这个就不举例子了。

本质上WITH(NOLOCK)是通过减少锁和不受排它锁影响来减少阻塞,从而提高并发时的性能。所谓凡事有利也有弊,WITH(NOLOCK)在提升性能的同时,也会产生脏读现象。如下所示,表TEST有两条记录,准备更新OBJECT_ID=1的记录,此时事务既没有提交也没有回滚

img

BEGIN TRAN  
UPDATE TEST SET NAME='Timmy' WHERE OBJECT_ID =1;  
--ROLLBACK  

此时另外一个会话使用WITH(NOLOCK)查到的记录为未提交的记录值

img

假如由于某种原因,该事务回滚了,那么我们读取到的OBJECT_ID=1的记录就是一条脏数据。

脏读又称无效数据的读出,是指在数据库访问中,事务T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2所读取到的数据是无效的。

4.3 WITH(NOLOCK)使用场景

什么时候使用要视你系统业务情况,综合考虑性能情况与业务要求来决定是否使用WITH(NOLOCK), 例如:涉及到金融或会计成本之类的系统,出现脏读那是要产生严重问题的,关键业务系统也要慎重考虑。大体来说一般有下面一些场景可以使用WITH(NOLOCK)

  1. 基础数据表,这些表的数据很少变更。
  2. 历史数据表,这些表的数据很少变更。
  3. 业务允许脏读情况出现涉及的表。
  4. 数据量超大的表,出于性能考虑,而允许脏读。

另外一点就是不要滥用WITH(NOLOCK)

4.4 WITH(NOLOCK)与NOLOCK区别

为了搞清楚WITH(NOLOCK)NOLOCK的区别,我查了大量的资料,我们先看看下面三个SQL语句有啥区别

  SELECT * FROM TEST NOLOCK
  SELECT * FROM TEST (NOLOCK);
  SELECT * FROM TEST WITH(NOLOCK);

上面的问题概括起来也就是说NOLOCK、(NOLOCK)、 WITH(NOLOCK)的区别:

  1. 其实NOLOCK只是别名的作用,而没有任何实质作用。

  2. (NOLOCK)WITH(NOLOCK)功能一样,在SQL Server 2008及以后版本不推荐使用(NOLOCK)了。

  3. 语法上有些许出入,如下所示

--这种语法会报错
SELECT  * FROM   sys.indexes  WITH(NOLOCK) AS i
--Msg 156, Level 15, State 1, Line 1
--Incorrect syntax near the keyword 'AS'.
 
--这种语法正常
SELECT  * FROM   sys.indexes  (NOLOCK) AS i
 
--可以全部改写为下面语法 
SELECT  * FROM   sys.indexes   i WITH(NOLOCK)  
SELECT  * FROM   sys.indexes   i (NOLOCK) 

4.5 WITH(NOLOCK)会不会产生锁

很多人误以为使用WITH(NOLOCK)后数据库不会产生任何锁,实质上,数据库依然对该表对象生成Sch-S(架构稳定性)锁以及DB类型的共享锁,如下所示,可以在一个会话中查询一个大表,然后在另外一个会话中查看锁信息(也可以使用SQL Profile查看会话锁信息)

不使用WTIH(NOLOCK)

img

使用WITH(NOLOCK)

img

另外,使用WITH(NOLOCK)并不是说就不会被其它会话阻塞,依然可能会产生Schema Change Blocking

会话1:执行下面SQL语句,暂时不提交,模拟事务正在执行

BEGIN TRAN 
ALTER TABLE TEST ADD Grade VARCHAR(10) ;  

会话2:执行下面语句,你会发现会话被阻塞,截图如下所示。

SELECT * FROM TEST WITH(NOLOCK)

img

posted on 2022-05-03 21:12  五维思考  阅读(1203)  评论(1编辑  收藏  举报

QQ群:1. 全栈码农【346906288】2. VBA/VSTO【2660245】