提到sql server,想必最让人头疼的当属锁机制了。在默认的read committed隔离模式下,连最基本的select操作都要申请各种粒度的锁,而且在读取数据过程中会不断有锁升级、转化。在非未提交读的隔离级别中,一个select操作会对每一条读到的记录或键值加S锁(何时释放还要视记录是否返回以及隔离级别而定),对每一条用到的Index上的键值加S锁,对读过的每个page和table上加IS锁...update、insert、delete操作申请锁的量和复杂度就更大了。
死锁和阻塞都是sql server要实现事务隔离的产物。有时候在同一个表上的事务隔离,并发度高一点会发生死锁;并发度低一点发生的是阻塞。所以死锁的问题定位和解决与阻塞有想通的地方,解决死锁最关键的就是要找到死锁双方或多方共同争抢的资源是哪个。下面分享一个最近碰到的真实生产环境上的案例,解析死锁抓取以及解决过程。
某外资物流公司
操作系统:Windows Server 2012 Enterprise x64
数据库 :SQL Server 2014 Enterprise X64
数据量 :300GB左右日常事务并发量比较高
现状 :由于一个业务sp的大量并行运行导致死锁,死锁发生一方作为牺牲资源后回滚过程很漫长导致重要业务表被锁,业务中断
解决排查过程:
首先必须找到死锁资源:
1)通过SQL Server Profiler新建一个trace,事件选择可以精简点便于我们观察死锁,选择“Locks”事件
下的Lock:Deadlock和Deadlock graph即可,trace文件大小设置为100M上限以便分析
下的Lock:Deadlock和Deadlock graph即可,trace文件大小设置为100M上限以便分析
2)一段时间后停止抓取,很直观看到死锁一直出现,且点开所有deadlock graph得到死锁图形分析,死锁都是发生在同一资源上:
3)为了查看死锁信息,数据库引擎提供了监视工具:跟踪标识(1222)。打开这个跟踪开关,所有获取的死锁信息会写到SQL Server的错误日志中供我们进一步分析。这一步打开这个开关,在SSMS中运行
DBCC TRACEON(1222,-1);
4)从trace的死锁图形看死锁发生很频繁,为了不让日志增长过大,过2至3分钟后将开关关掉。在SSMS中运行
DBCC TRACEOFF(1222,-1),这一步很重要;
5)打开SQL Server errorlog,找到死锁输出信息,这个输出内容很丰富而且比较复杂,这里只把我们所需的几个重要点挑出来
死锁信息始于 deadlock-list关键字(倒着看),deadlock victim显示死锁的牺牲方,process id显示进程id号,由于截图没那么齐全,还包含很多死锁信息,比如可以查看进程spid号,事务隔离级别,当前正进行的批处理操作,当前正在运行的语句,申请中的资源等等。
通过对错误日志的分析得到死锁批处理和死锁语句:exec usp_obal_import_so,查到死锁语句:delete from t_po_detail where po_number in(select po_number from t_so_po where so_number=@v_vchSOID and whid=@v_vchWHID) and whid=@v_vchWHID,这是usp_obal_import_so中的一段语句,锁资源:表tbl_po_detail_generic(用户脚本定时执行获得) ,但是这个sp的执行根本不会操作tbl_po_detail_generic ,是不是哪里出问题了呢?
6)我们可以在SSMS中看看这条语句的执行计划,运行语句之前在SSMS中运行
set statistics profile on或者在“查询”子菜单中选择“包括实际的执行计划”,我们用第二种更直观,如下
很明显在执行计划中可以看到tbl_po_detail_generic有个全表扫描操作,再与t_po_detail表做hash连接。全表扫描导致每次语句执行会获取该表的表锁,深究原因发现tbl_po_detail_generic的外键约束导致每次删除t_po_detail数据会操作tbl_po_detail_generic表。
很明显在执行计划中可以看到tbl_po_detail_generic有个全表扫描操作,再与t_po_detail表做hash连接。全表扫描导致每次语句执行会获取该表的表锁,深究原因发现tbl_po_detail_generic的外键约束导致每次删除t_po_detail数据会操作tbl_po_detail_generic表。
7)到这里剖析死锁工作基本结束,后面解决方法有两种:一是根据执行计划在tbl_po_detail_generic建立适当索引避免表扫描;二是如果业务逻辑许可,删掉外键约束。
总结:
要真正做到从源头上降低死锁发生几率,还是要从程式本身做好。如果不能去修改程式,可以考虑从另外几个方面消除死锁:
1 调整索引来调整执行计划,减少锁的申请数目;
2 使用'nolock'参数,让SELECT语句不要申请S锁,减少锁申请数目
3 升级锁粒度,将死锁转化成阻塞问题
4 使用快照隔离级别SNAPSHOT LEVEL