在数据库中打开死锁监测可以收集到数据库发生的死锁情况。打开的方式有2种:
1 打开1222监控
执行SQL语句:
Dbcc traceon(1222,-1);
然后在系统日志里查看死锁的信息。
2 启动SQL Profiler(建议使用):
下面就是一个发生死锁的实例图:
下面提供对这个死锁分析思路,如有不当之处,还望大家批评指正。
一共3个问题,下面逐个回答。
第一个问题:被锁定的资源是什么?
上面写的很清楚,是一个Page 锁, 那么Page 锁是什么呢?
通常死锁是你操作A表,然后又要操作B表,而另外一个进程先操作的B表,然后等待你释放A表的锁而导致的死锁,这时看的图上面就会明确说Table Lock,而不是Page Lock。
要解释Page Lock 得先解释下什么是Page。
在SQL Server 中,数据存放的时候是放在一个8K的数据单元里,这个数据单元称为 Page.
读取数据时,不是一笔一笔的读取,而是一个Page 一个Page 的读取,所以SQL Server 的文档中都会说,可能读取了一些不需要的数据。
知道了Page 之后,怎么会有Page 锁呢? Table Lock ,Row Lock 比较容易理解,但是Page Lock 就不太容易理解,实际上,这时SQL Server 对于大表采取的一种它认为性价比最好的策略。如果采取表锁,那么一旦Update 数据,则别人就不能Select 了,否则会出现数据不一致的情况(就是所谓的脏读,幻读),但是你有可能会说,你修改你的,我读取的并不是你要改的那一笔,你不要锁定我,应该采取Row Lock,但是想象一下,对于一个有100万笔数据的表来说,采取行锁,成本得有多高?所以SQL Server采取了一个中庸的方式,使用8K的数据页Page作为锁的单位,这样就平衡了并发与性能的问题,性价比最高。
定位Page
一个表里有很多Page,那么本例中锁住的Page 到底是什么呢?
执行SQL语句查询DB的名字
Select db_name(5)
打开这个数据库,执行如下SQL语句,可以查询这个Page 属于谁.
select object_name(i.object_id), i.name from sys.partitions as p inner join sys.indexes as i on i.object_id = p.object_id and i.index_id = p.index_id where p.partition_id = ‘72057594048544768‘
是一个非聚集的索引。
Page里放了什么?
目前我们已经了解了,它锁定的是一个索引的一个数据页(如果您对索引的Page 组织结构感兴趣,请参考 索引内部结构实例)。 那么锁住的2个Page里面存放了什么信息?
执行如下SQL:
DBCC TRACEON(3604) DBCC Page (DBName,1,46574417,1)
结果如下:
执行如下SQL:
DBCC Page (DBName,1,93865146,1)
结果如下:
第二个问题:是谁在锁住这些页?
查看之后发现:
1个SQL语句是Select
2个SQL 语句是Update
Select 语句为什么要锁定这2页?
本例中的Select 语句使用了本索引,使用它来快速定位,那么它根据索引的分支节点寻找,然后到指定的Page 中去读取寻找,因为是Top 500,所以,可能找到一批数据,但是还不够500,所以接着往其他页里找,直到找满500笔为止
Update 语句为什么要锁定这2页?
找到这笔数据直接修改不就完了吗?并不是的,修改一个索引的值,至少要经过如下3个步骤:
第三个问题: Exchange Event是啥意思?
本例分析总结:
某个Index 里有多个数据页,而 Update 语句正在更新这个Index 里的一笔数据,锁定了2个页,与Select 中使用的2页发生互抢,形成死锁。
解决方案:
调整语句,调整Select 吗?
让它不使用这个索引?索引建立就是为了给Select 用的,这个Select 不用,其他Select 也会用到。
调整 Update 吗?
因为更新的栏位是包含在索引中的,无论怎么调整,更新时总是会同步修改索引的。
结语:
死锁是不可避免的,我们要做的是降低发生的频率,这是我们常听到的,但是为什么呢? 如果是更新表的顺序不一致,那么应该可以通过修改提交更新SQL可以消除,但是本例呢?你打算通过什么方式让这种情况永不存在?
降低发生的频率是对我们的要求,比如本例,怎么降低?
降低SQL 执行的频率:
发出Select 语句和Update 语句太频繁,业务上是否真的这么频繁?是否都定相同的时间来执行?比如Update和Select 都是5秒轮一次?
提高SQL执行的效率:
Select 语句返回的数据有多少笔?能否再减少一些?返回有多少栏位?能否减少一些?
Select 语句的查询计划是否应该优化?最多3-5页面读取就应该返回结果,这个Select有多少logic read ?是否需要添加其他准确定位的索引?(难道一个Select 我们就添加一个Index吗?当然不是,这是在频繁出现这种死锁的情况使用的,因为既然频繁出现,说明这个 Select执行更频繁,很容易和Update 撞在一起。)
能不能Select 不要锁?
业务上是否可以允许小范围的脏读?