最近在处理一个锁的问题时,发现一个比较郁闷的事,使用X锁竟然没法锁住查询,模拟这个问题,可使用以下T-SQL脚原本创建测试环境。数据库
USE master; GO IF @@TRANCOUNT > 0 ROLLBACK TRAN; GO -- ======================================= -- 创建测试数据库 -- a. 删除测试库, 若是已经存在的话 IF DB_ID(N'db_xlock_test') IS NOT NULL BEGIN; ALTER DATABASE db_xlock_test SET SINGLE_USER WITH ROLLBACK AFTER 0; DROP DATABASE db_xlock_test; END; -- b. 创建测试数据库 CREATE DATABASE db_xlock_test; -- c. 关闭READ_COMMITTED_SNAPSHOT 以保持SELECT 的默认加锁模式 ALTER DATABASE db_xlock_test SET READ_COMMITTED_SNAPSHOT OFF; GO -- ======================================= -- 创建测试表 USE db_xlock_test; GO CREATE TABLE dbo.tb( id int IDENTITY PRIMARY KEY, name sysname ); INSERT dbo.tb SELECT TOP(50000) O1.name + N'.' + O2.name + N'.' + O3.name FROM sys.objects O1 WITH(NOLOCK), sys.objects O2 WITH(NOLOCK), sys.objects O3 WITH(NOLOCK); GO
而后,创建一个链接,执行下面的脚原本实现加锁。并发
-- ======================================= -- 测试链接1 - 加锁 BEGIN TRAN --测试的初衷是经过SELECT加锁,结果发现UPDATE也锁不住 UPDATE dbo.tb SET name = name --SELECT COUNT(*) FROM dbo.tb WITH(XLOCK) WHERE id <= 2; SELECT spid = @@SPID, tran_count = @@TRANCOUNT, database_name = DB_NAME(), object_id = OBJECT_ID(N'dbo.tb', N'Table'); -- 显示锁 EXEC sp_lock @@SPID;
经过执行结果,能够看到对象被加锁的状况:表级和页级上是IX锁,记录上是X锁。测试
而后新建一个链接,执行下面的T-SQL查询,看看会否被链接1锁住优化
-- ======================================= -- 测试链接2 - 被阻塞(在测试链接1 执行后执行) SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT * FROM dbo.tb WHERE id <= 2;
上述查询会很快返回结果,并不会被查询1阻塞住。ui
按照咱们的了解(联机帮助上也有说明),在READ COMMITTED事务隔离级别下,查询使用共享锁(S),而根据锁的兼容级别,S锁是与X锁冲突的,因此正常状况下,链接2的查询须要等待链接1执行完成。但是测试的结果去违反了这一原则。spa
为了了解为何链接2不会被阻塞,对链接2作了一个Trace,发现一个更郁闷的问题,Trace的结果以下:.net
EventClass | TextData | ObjectID | Type | Mode |
Lock:Acquired | 21575115 | 5 - OBJECT | 6 - IS | |
Lock:Acquired | 1:77 | 0 | 6 - PAGE | 6 - IS |
Lock:Acquired | [PLANGUIDE] | 0 | 2 - DATABASE | 3 - S |
Lock:Acquired | 21575115 | 5 - OBJECT | 6 - IS | |
Lock:Acquired | 1:77 | 0 | 6 - PAGE | 6 - IS |
Lock:Acquired | 1:80 | 0 | 6 - PAGE | 6 - IS |
Lock:Acquired | 1:89 | 0 | 6 - PAGE | 6 - IS |
Trace的前面两行是链接2的Trace结果,从结果看,链接2仅使用了意向共享锁(IS),并且只是表级和页级,按照锁的兼容性原则,IS和IX(链接1在表级和页级仅使用了IX锁)是不冲突的,因此链接2的查询不会被阻塞。在增长了查询的数据量后,Trace结果代表查仍是只在表级和页级使用了IS锁(Trace结果的最后4行)。code
对于这个问题,解决的办法固然就是提高链接1锁的粒度,使用PAGLOCK表提示将锁的粒度提高到页级,这样IS与X是冲突的,就能够成功阻塞链接2。对象
但疑问就是,为何查询只在表级和页级下意向共享锁(IS),而不在行级下共享锁(S),这个彷佛与联机帮助上的说明不同(仍是一直以来理解上的误差呢)。blog
这是由于SQL Server优化器故意为之, 认为在链接1上的XLOCK并无更改数据,因此没有在链接2的行级加共享锁(S),避免链接2被阻塞:
参考连接33楼回复:https://bbs.csdn.net/topics/310262977
附:联机帮助上关于锁模式的说明
共享锁
共享锁(S 锁)容许并发事务在封闭式并发控制下读取 (SELECT) 资源。
更新锁
更新锁(U 锁)能够防止常见的死锁。 在可重复读或可序列化事务中,此事务读取数据 [获取资源(页或行)的共享锁(S 锁)],而后修改数据 [此操做要求锁转换为排他锁(X 锁)]。 若是两个事务得到了资源上的共享模式锁,而后试图同时更新数据,则一个事务尝试将锁转换为排他锁(X 锁)。 共享模式到排他锁的转换必须等待一段时间,由于一个事务的排他锁与其余事务的共享模式锁不兼容;发生锁等待。 第二个事务试图获取排他锁(X 锁)以进行更新。 因为两个事务都要转换为排他锁(X 锁),而且每一个事务都等待另外一个事务释放共享模式锁,所以发生死锁。
若要避免这种潜在的死锁问题,请使用更新锁(U 锁)。 一次只有一个事务能够得到资源的更新锁(U 锁)。 若是事务修改资源,则更新锁(U 锁)转换为排他锁(X 锁)。
排他锁
排他锁(X 锁)能够防止并发事务对资源进行访问。 使用排他锁(X 锁)时,任何其余事务都没法修改数据;仅在使用 NOLOCK 提示或未提交读隔离级别时才会进行读取操做。
数据修改语句(如 INSERT、UPDATE 和 DELETE)合并了修改和读取操做。 语句在执行所需的修改操做以前首先执行读取操做以获取数据。 所以,数据修改语句一般请求共享锁和排他锁。 例如,UPDATE 语句可能根据与一个表的联接修改另外一个表中的行。 在此状况下,除了请求更新行上的排他锁以外,UPDATE 语句还将请求在联接表中读取的行上的共享锁。
意向锁
数据库引擎使用意向锁来保护共享锁(S 锁)或排他锁(X 锁)放置在锁层次结构的底层资源上。 意向锁之因此命名为意向锁,是由于在较低级别锁前可获取它们,所以会通知意向将锁放置在较低级别上。