1 用SqlServer 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
2. 下面我们看看数据在数据页的分布情况。
3. 然后我们开启Profile,在“事件选择”的Events中选择”Lock:Acquired“和”Lock:Released“ ,然后运行,如下图:
使用测试数据
1. 首先我执行一个简单的 SELECT * FROM dbo.Person,看看表/数据页/记录的加锁情况。
从图中可以看到,select执行的大概步骤如下:
第一步:给表(Object)加上IS(意向共享锁)。
第二步:先给1:78号数据页加IS锁,扫描78号数据页,然后释放IS锁。
第三步:同样的道理扫描之后的数据页。
第四步:最后释放表的IS锁,结束整个锁流程。
看完上面的一系列的Lock:Acquired 和 Lock:Released的话,你有没有发现一个问题,不是说好给记录(RID)加上S锁么???这里没加,是因为引擎进入78号数据页的时候,未发现它存在IU锁或者IX锁。。。所以。。。这个属于锁的组合,后续会说。
2. 接下来用UPDATE dbo.Person SET NAME='bbbbb' WHERE ID=3来看看update的整个过程,乍一看,Profile捕获到的记录还是比较多的,下面具体看图:
第一步: 给表(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操作中,扫完一个数据页就释放一个数据页。
3. 最后再看一个DELETE FROM dbo.Person WHERE ID=3 的操作。
大概扫了一下上面的图,或许你感觉和Update操作大差不差,会扫描数据页中的每个记录并加上U锁。当在1:89:0槽位中找到了目标记录后,然后将U锁转化为X锁,具体可以参考Update。
2 深入的探讨锁机制
上一篇我只是做了一个堆表让大家初步的认识到锁的痉挛状态,但是在现实世界上并没有这么简单的事情,起码我的表不会没有索引对吧,,,还有就是我的表一定会有很多的连接过来,10:1的读写,很多码农可能都会遇到类似神乎其神的死锁,卡住,读不出来,插不进入等等神仙的事情导致性能低下,这篇我们一起来探讨下。
一: 当select遇到性能低下的update会怎么样?
1. 还是使用原始的person表,插入6条数据,由于是4000字节,所以两条数据就是一个数据页,如下图:
1 DROP TABLE dbo.Person 2 CREATE TABLE Person(ID INT IDENTITY,NAME CHAR(4000) DEFAULT 'aaaaa') 3 --插入6条数据,刚好3个数据页 4 INSERT INTO dbo.Person DEFAULT VALUES 5 go 6
2. 为了模拟性能低下的Update操作,我们开个显式事务来更新ID=4的记录,并且用profile看一下,如下图:
1 BEGIN TRAN 2 UPDATE dbo.Person SET NAME='bbbbb' WHERE id=4
3. 然后我们开下另一个会话连接,读取ID=6的记录会是怎样?好奇吗?
1 SELECT * FROM Person WHERE ID=6
从上面流程你是否看到,当扫描到89号数据页的slot1槽位的时候卡住了。。。我想你应该知道update正好已经给这条记录加上了X锁。。。如果你够细心,你还会发现,给S锁附加记录的条件是在当引擎发现记录所在的数据页已经附加上了IX锁的情况下,才给该号数据页下的每条记录附加S锁,对吧。。。好了,既然在Profile上面看不到了,我还是有其他办法来判断到底select语句现在处于什么状态。
4. 使用sys.dm_tran_locks来看当前各个连接持有锁的状态。
1 SELECT l.request_session_id, 2 DB_NAME(l.resource_database_id),OBJECT_NAME(p.object_id), 3 l.resource_description,l.request_type, 4 l.request_status,request_mode 5 FROM sys.dm_tran_locks AS l 6 LEFT JOIN sys.partitions AS p 7 ON l.resource_associated_entity_id=p.hobt_id
仔细观察上图可以看到,当前有51和52号会话,51号在1:89:1槽位上使用了X锁并且没有释放,52号此时也进入了1:89:1中,并且想给该RowID附加S锁,但是你也知道S和X锁是排斥的,所以很无奈的一直保持等待状态。
二:使用索引或许可以帮你逃过一劫
当你看完上面的讲叙,是不是有点害怕???要是在生产环境下出现了这种情况,那我们是不是死的很惨???那接下来使用索引是不是真的可以帮我们躲过一劫呢?下面跟我一起看一看。
1. 新建索引index
1 -- 在ID列上建一个index 2 CREATE INDEX idx_person ON dbo.Person(ID)
2. 然后我们看下数据页的分布情况,可以看到下图中78,89,90是表数据页,93号为索引数据页。
1 DBCC TRACEON(2588,3604) 2 DBCC IND(Ctrip,Person,-1)
3. 麻蛋的,继续执行上面的那个慢update
BEGIN TRAN UPDATE dbo.Person SET NAME='bbbbb' WHERE id=4
4. 激动人心的时刻来了,由于数据太少,所以我这里强制让引擎执行我创建的索引,看看结果怎样?
居然没卡住???现在是不是有一股强烈的好奇心来了,狗狗狗。。。马上开启profile,看看到底都发生了什么?
仔细看完这个图,是不是觉得很有意思呢???具体步骤如下:
第一步:给表(Object)加上IS锁。
第二步:因为要走索引,给93号索引数据页加上IS锁。
第三步:找到93号索引数据页的目标key,给这个key加上S锁,有人可能就会问了。。。这个key不就是6嘛,为什么这个key=(61005a25560e),你要是太好奇我可以告 诉你,年轻人说话不要太屌,每行索引记录都有一个散列值,这个值就是根据索引的几个字段散列出来的,好处就是防止你的索引长度过大,导致锁这个记录的 时候太耗费锁空间了。。。。如果你还是不太相信的话,我用DBCC给你看一看。
第四步:根据这个key直接跳到存放记录的90号数据页中,万幸的是update的记录刚好不在90号数据页中。。。。就这样躲过一劫了。。。然后select顺利的读取到了该 读的记录,最后释放相关的IS锁。
3 nolock引发的三级事件的一些思考
曾今有件事情让我记忆犹新,那年刚来携程不久,马上就被安排写一个接口,供企鹅公司调用他们员工的差旅信息,然后我就三下五除二的给写好了,上线之后,大概过了一个月。。。DBA那边报告数据库出现大量锁超时,并且及时根据sql的来源将email发到了我们部门,指出sql读取时间过长,并且缺少nolock,影响了大量机票订单入库,然后我就拿着sql去生产环境跑了下,22s。。。花擦。。。项目上线时间太久,版本已经不存在了,无法回滚。。。原本准备撤下接口。。。看了下撤下接口跟加上nolock时间相差不多,最后决定先加上nolock,发布紧急单。。。然后再优化,DBA那边暂时做手工解锁,发上去后,最后就是损失XXXX订单。。。定级为三级事件。然后就是追责,当然这个责任只能有老大们去承担了,出了这次由我引发的事件,我得思考了,出了事情对我不见得全是坏事,起码这次会让我铭记如心,想想也搓,来携程之前根本就不会关注要不要给select指定nolock,这其中也包括自己没遇到过大数据吧,也包括自己的能力有限,只知道有锁这个玩意,细说的话就啥也不知道了,后来才知道携程有个规则,就是很多业务产线所写的select都必须指定nolock,懂一点的人可能会说nolock可以提升性能,如果你这样说,确实是这样,因为数据库的锁是有96字节开销的,没了锁,也就没有你在profile中看到accquired和released痉挛了,当你看完我的事件之后,你可能会意识到,性能提升不是最关心的,最关心就是不要出现死锁,锁等待。。。好了,言归正传,下面我们看看到底在数据库中可以指定多少个锁???
一:到底可以指定多少个锁
这个问题有意思,我们不需要记,只要你装一个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
一眼扫下去,还是蛮多的,不过你要注意了,那些所谓的XXXLock才是我们需要关注的,根据上面的图,我们大概把锁分个类。。。
粒度锁:PAGLOCK, TABLOCK, TABLOCKX, ROWLOCK, NOLOCK
模式锁:HOLDLOCK, UPDLOCK, XLOCK
接下来我从粒度锁说起:
1. NOLOCK
都说nolock是无锁模式的,那到底是怎样的无锁呢???到这篇为止,你应该知道,如果不加nolock,我们的表,数据页是附加IS锁的,那接下来我用profile看下两者有什么区别。
从上图中,你会看到加上nolock之后,object上面附加了Sch-S锁,这个锁叫做“架构稳定锁”,很简单就是sql编译时附加的一把锁,目的就是防止在编译时,有其他连接修改表结构,而这个锁只与Sch-M锁冲突,与其他锁都兼容,这说明什么?说明其他连接锁住了记录也没关系,我的nolock不跟他们打交道,这样的话,就可能会读到脏数据,不过没关系,携程的很多业务是容许脏数据的,毕竟比锁等待,死锁要强得多,再说nolock读到了其他连接未修改或者未提交的数据,这个概率也比较低,就算遇到了也没关系,一般不会招来客诉的,客人或许再刷下页面,数据或许就正确了,对不对。。。
2.TABLOCK
这个还是比较见名识义的,就是附加在table上的锁,也就是表锁了,很恐怖的。。。下面我举个Update的例子,看看前后对比。
在上面你有没有看到,X锁已经附加到OBJECT上面去了。。。这样的话,其他连接就动不了这个Object了,只能等待。。。
3. PAGLOCK
看了名字你应该也知道,就是附加到页面这个级别的锁,我也举一个Update的例子。
1 BEGIN TRAN 2 UPDATE dbo.Person SET NAME='aaaaa' WHERE ID=6 3 4 BEGIN TRAN 5 UPDATE dbo.Person WITH(PAGLOCK) SET NAME='bbbbb' WHERE ID=4
从上面两个图中,你应该可以看到,原来附加到RID上面的U锁,由于PagLock的提升,现在要附加到Page上面了,这个就是所谓的数据页锁。
4.TABLOCKX, ROWLOCK
这两个我就不细说了,TABLOCKX就是直接附加在table上的X锁,你可以通过select看一下。
ROWLOCK的话,默认情况下就是ROWLOCK,比如默认的Update,你会发现RID上被附加的U锁,这个就是行锁。
5.UPDLOCK
这个锁还是蛮有意思的,它就是update锁,如果你select下,它会呈现update的锁痉挛效果。
6. XLOCK
知道了UPDLOCK锁,我想XLOCK你也应该明白了。。。它就是delete锁,即排他锁,我可以让select带上排他锁。
7.HOLDLOCK
最后一个我也没闹明白,据说是让语句在整个事务中持有锁,然后我就用select和update调试一下。
1 SELECT * FROM dbo.Person(HOLDLOCK) 2 UPDATE dbo.Person WITH(HOLDLOCK) SET NAME='bbbbb' WHERE ID=4