一般情况下,索引都是用于缓解死锁的。
但是,索引本身也会引发死锁。其本质原因是:索引也是一种资源,既然是资源,它就会被争抢。而死锁的本质就是多个事务之间资源的争抢和彼此等待。
在解释这一切之前,看理解键查找。
- 键查找
先执行下面的代码,插入一些测试数据
CREATE TABLE Person ( id int identity, name varchar(32), regdate varchar(12) --注册日期 primary key(id) ) go create index ix_regdate on Person ( regdate ) go declare @bgdate date set @bgdate = dateadd(DAY,-1000,GETDATE()) while @bgdate < GETDATE() begin declare @strbgdate varchar(10) set @strbgdate = CONVERT(varchar,@bgdate,112) insert into Person(name,regdate) values ('a',@strbgdate), ('b',@strbgdate), ('c',@strbgdate), ('d',@strbgdate), ('e',@strbgdate), ('f',@strbgdate), ('i',@strbgdate), ('j',@strbgdate), ('k',@strbgdate) set @bgdate = dateadd(DAY,1,@bgdate) end go
我们查看上述查找 20160101 的name的记录时,执行计划中有一个键查找。
这个键查找是为了输出 name 字段。
他的过程是这样的。
where 后面 的查询条件是 regdate,查询优化器检查有没有索引可以用,发现有 Ix_regdate索引可以用。于是就用到了 ix_regdate索引
接着,要输出name字段,优化器检查ix_regdate 中是否有包含name? 结果没有。
于是优化器,通过ix_regdate 路由到 PK_person 聚焦索引。然后通过PK_Person聚焦索引,查找到name字段。 (用黄色文字标记的部分就是键查找)
另一种触发键查找的行为
在where 条件中指定 name='a' 因为 ix_regdate索引没有包含name,所以,又要路由到PK 聚焦索引,去查找name字段,所以也触发了键查找。
下面这种查询,不会触发键查找。因为 ix_regdate 会包含聚焦索引的值。(非聚焦索引都会和聚焦索引建立对应关系)
这种写法也不会触发键查找。因为,聚焦索引会包含所有的字段
通常情况下,键查找是不可避免的,也不是特别必须避免。除非它严重影响了效率。
解决键查找的办法是,在 索引中包含字段。下面是SQL 的语法示例
CREATE NONCLUSTERED INDEX [ix_regdate] ON [dbo].[Person] ( [regdate] ASC ) INCLUDE ( [name])
当包含了这个字段之后,由于ix_regdate包含了name字段,所以不需要路由到聚焦索引,直接就取出值了。因此避免了键查找,加速查询速度。
但是带来的代价是
1、insert 和 update 会变慢。因为不仅要修改表中的值,还需要更新索引中的name。(没有深入去研究,我认为,ix_regdate中的name 和 表中的name 是2个不同的内存,但维持值保持一致,而且为了维持值一致,数据库在执行 insert 和 update的时候,会采用事务级别控制)
2、ix_regdate 索引会变大,需要占据更多的磁盘空间。
所以include 操作需要非常谨慎,仔细权衡include 的必要性。
- 锁
上面看了键查找之后,我们注意到,我们的查询,引用2个索引
ix_regdate 和 pk 聚焦。
那么查询是,是否会对 ix_regdate 和 pk 发出锁呢?
答案是:是
一个查询会引用资源,索引是一种资源。一般称之为 键。
所以在索引上产生的锁,被称为键锁。
- 死锁
既然一个事务会引用到多个资源,那么就会发生死锁。
以上述查询为例,
select name from person where regdate = '20160101'
这样的查询,引用了ix_regdate 和 pk 聚焦索引
那么,在这个事务提交之前,这个事务会对 ix_regdate 和 pk 加入 共享锁。
那么,我们能保证,这个事务,会同时拿到 ix_regdate 和 pk聚焦的共享锁吗?
答案是:不能。
也就是说,这个查找一定是 先拿到 ix_regdate的共享锁。接着发现要输出name字段,语句再去找pk聚焦产生共享锁的。中间有一个很微妙的时间差。
如果,在这个时间差的档口,有一另一个事务先拿到了pk的 X锁(排它锁),然后他需要 ix_regdate 的排它锁。会发生什么事情?
那么这2个事务会彼此死锁。
另一个事务只需要执行这样语句即可
update person set regdate = '20160102' where ID = 7444
当这2个事务同时发生时。
update事务,因为是ID查找,所以先对PK 聚焦发出U锁,找到记录之后,对PK 发出X锁。
然后更新的是regdate字段,前面说了。 regdate字段,是一个索引字段。所以同时需要更新 ix_regdate 索引,所以又需要对ix_regdate 发出X锁。
而此时 ix_regdate 已经有S锁了。所以 update事务等待 S锁放弃。
而此时 pk聚焦上已经有X锁了。select事务等待PK 上的S锁,因此等待pk上的x锁放弃。
由此产生了死锁。
只不过这样产生死锁的概率非常小罢了。
那么我们放大 这种出错的概率
事务1
BEGIN TRAN /* *当数据量比较大的时候,数据库可能会选择PAGLOCK,因为测试数据比较少,只会发出ROWLOCK,为了放大效果,将锁的范围扩大到PAGLOCK */ SELECT * FROM person WITH(xlock,PAGLOCK) WHERE ID = 7446 WAITFOR delay '00:00:10' UPDATE person SET regdate = '20160102' WHERE ID = 7446 COMMIT
事务2
BEGIN TRAN select id,name from person where regdate = '20160101' COMMIT
运行之后,就发先死锁。死锁的原因就是之前分析的那样的。