一般情况下,索引都是用于缓解死锁的。

但是,索引本身也会引发死锁。其本质原因是:索引也是一种资源,既然是资源,它就会被争抢。而死锁的本质就是多个事务之间资源的争抢和彼此等待。

在解释这一切之前,看理解键查找。

 

  • 键查找

先执行下面的代码,插入一些测试数据

 
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

 

运行之后,就发先死锁。死锁的原因就是之前分析的那样的。

 

posted on 2016-06-22 16:14  zooz  阅读(2858)  评论(0编辑  收藏  举报