1. 死锁原理
根据操作系统中的定义:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
死锁的四个必要条件:
互斥条件(mutual exclusion):资源不能被共享,只能由一个进程使用。
请求与保持条件(hold and wait):已经得到资源的进程可以再次申请新的资源。
非剥夺条件(no pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。
循环等待条件(circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。
对应到sql server中,当在两个或多个任务中,如果每个任务锁定了其他任务试图锁定的资源,此时会造成这些任务永久阻塞,从而出现死锁;这些资源可能是:单行(rid,堆中的单行)、索引中的键(key,行锁)、页(pag,8kb)、区结构(ext,连续的8页)、堆或b树(hobt) 、表(tab,包括数据和索引)、文件(file,数据库文件)、应用程序专用资源(app)、元数据(metadata)、分配单元(allocation_unit)、整个数据库(db)。一个死锁示例如下图所示:
说明:t1、t2表示两个任务;r1和r2表示两个资源;由资源指向任务的箭头(如r1->t1,r2->t2)表示该资源被改任务所持有;由任务指向资源的箭头(如t1->s2,t2->s1)表示该任务正在请求对应目标资源;
其满足上面死锁的四个必要条件:
(1).互斥:资源s1和s2不能被共享,同一时间只能由一个任务使用;
(2).请求与保持条件:t1持有s1的同时,请求s2;t2持有s2的同时请求s1;
(3).非剥夺条件:t1无法从t2上剥夺s2,t2也无法从t1上剥夺s1;
(4).循环等待条件:上图中的箭头构成环路,存在循环等待。
死锁监控一直都很麻烦,我没有找到很好的方法
如果大家有好的方法,我也很想学习一下
我的方法比较简单:
1.sp_altermessage 1205 修改1205 错误让他能够写入日志 这样 代理中的警告才能使用
2.当然是启动 代理中的警告。开数据库邮件,会把死锁错误发送到操作员邮箱里面。缺点就是没有详细的死锁信息。
3.使用sql server 2008 r2 自带的 扩展事件中system_health默认是开启的,里面会抓取比较多的值
你可以使用 sys.dm_xe_session_events 联合 sys.dm_xe_sessions 查看 抓取了那些值 当然其中一个是死锁信息。
当产生死锁的时候你何以使用:
代码如下 复制代码
select
replace(
replace(xeventdata.xevent.value('(data/value)[1]', 'varchar(max)'),
'', ''),
'','')
from
(select cast(target_data as xml) as targetdata
from sys.dm_xe_session_targets st
join sys.dm_xe_sessions s on s.address = st.event_session_address
where name = 'system_health' ) as data
cross apply targetdata.nodes ('//ringbuffertarget/event') as xeventdata (xevent)
where xeventdata.xevent.value('@name', 'varchar(4000)') = 'xml_deadlock_report'
查询所有的死锁信息,当然如果出现内存瓶颈的时候能保存多久我不确定,如果死锁太多你无法顺利的找到,你想把结果减少一点,可以在每次查询死锁后使用:
alter event session system_health on server
state = stop
go
alter event session system_health on server
state = start
关闭并打开这个扩展事件的session,那么保存在内存的记录就会被清空。下次的死锁信息就是最新的
监控实例二
下面的sql语句运行之后,便可以查找出sqlserver死锁和阻塞的源头。
查找出sqlserver的死锁和阻塞的源头 --查找出sqlserver死锁和阻塞的源头
代码如下 复制代码
use master
go
declare @spid int,@bl int
declare s_cur cursor for
select 0 ,blocked
from (select * from sysprocesses where blocked>0 ) a
where not exists(select * from (select * from sysprocesses where blocked>0 ) b
where a.blocked=spid)
union select spid,blocked from sysprocesses where blocked>0
open s_cur
fetch next from s_cur into @spid,@bl
while @@fetch_status = 0
begin
if @spid =0
select '引起数据库死锁的是:
'+ cast(@bl as varchar(10)) + '进程号,其执行的sql语法如下'
else
select '进程号spid:'+ cast(@spid as varchar(10))+ '被' + '
进程号spid:'+ cast(@bl as varchar(10)) +'阻塞,其当前进程执行的sql语法如下'
dbcc inputbuffer (@bl )
fetch next from s_cur into @spid,@bl
end
close s_cur
deallocate s_cur
查看当前进程,或死锁进程,并能自动杀掉死进程 --查看当前进程,或死锁进程,并能自动杀掉死进程
--因为是针对死的,所以如果有死锁进程,只能查看死锁进程。当然,你可以通过参数控制,不管有没有死锁,都只查看死锁进程。
代码如下 复制代码
create proc p_lockinfo
@kill_lock_spid bit=1, --是否杀掉死锁的进程,1 杀掉, 0 仅显示
@show_spid_if_nolock bit=1 --如果没有死锁的进程,是否显示正常进程信息,1 显示,0 不显示
as
declare @count int,@s nvarchar(1000),@i int
select id=identity(int,1,1),标志,
进程id=spid,线程id=kpid,块进程id=blocked,数据库id=dbid,
数据库名=db_name(dbid),用户id=uid,用户名=loginame,累计cpu时间=cpu,
登陆时间=login_time,打开事务数=open_tran, 进程状态=status,
工作站名=hostname,应用程序名=program_name,工作站进程id=hostprocess,
域名=nt_domain,网卡地址=net_address
into #t from(
select 标志='死锁的进程',
spid,kpid,a.blocked,dbid,uid,loginame,cpu,login_time,open_tran,
status,hostname,program_name,hostprocess,nt_domain,net_address,
s1=a.spid,s2=0
from master..sysprocesses a join (
select blocked from master..sysprocesses group by blocked
)b on a.spid=b.blocked where a.blocked=0
union all
select '|_牺牲品_>',
spid,kpid,blocked,dbid,uid,loginame,cpu,login_time,open_tran,
status,hostname,program_name,hostprocess,nt_domain,net_address,
s1=blocked,s2=1
from master..sysprocesses a where blocked0
)a order by s1,s2
select @count=@@rowcount,@i=1
if @count=0 and @show_spid_if_nolock=1
begin
insert #t
select 标志='正常的进程',
spid,kpid,blocked,dbid,db_name(dbid),uid,loginame,cpu,login_time,
open_tran,status,hostname,program_name,hostprocess,nt_domain,net_address
from master..sysprocesses
set @count=@@rowcount
end
if @count>0
begin
create table #t1(id int identity(1,1),a nvarchar(30),
b int,eventinfo nvarchar(255))
if @kill_lock_spid=1
begin
declare @spid varchar(10),@标志 varchar(10)
while @ibegin
select @spid=进程id,@标志=标志 from #t where id=@i
insert #t1 exec('dbcc inputbuffer('+@spid+')')
if @标志='死锁的进程' exec('kill '+@spid)
set @i=@i+1
end
end
else
while @ibegin
select @s='dbcc inputbuffer('+cast(进程id as varchar)+')'
from #t where id=@i
insert #t1 exec(@s)
set @i=@i+1
end
select a.*,进程的sql语句=b.eventinfo
from #t a join #t1 b on a.id=b.id
end
go
exec p_lockinfo
避免死锁
上面1中列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件,就可以避免死锁发生,一般有以下几种方法(from sql server 2005联机丛书):
(1).按同一顺序访问对象。(注:避免出现循环)
(2).避免事务中的用户交互。(注:减少持有资源的时间,较少锁竞争)
(3).保持事务简短并处于一个批处理中。(注:同(2),减少持有资源的时间)
(4).使用较低的隔离级别。(注:使用较低的隔离级别(例如已提交读)比使用较高的隔离级别(例如可序列化)持有共享锁的时间更短,减少锁竞争)
(5).使用基于行版本控制的隔离级别:2005中支持快照事务隔离和指定read_committed隔离级别的事务使用行版本控制,可以将读与写操作之间发生的死锁几率降至最低:
set allow_snapshot_isolation on --事务可以指定 snapshot 事务隔离级别;
set read_committed_snapshot on --指定 read_committed 隔离级别的事务将使用行版本控制而不是锁定。默认情况下(没有开启此选项,没有加with nolock提示),select语句会对请求的资源加s锁(共享锁);而开启了此选项后,select不会对请求的资源加s锁。
注意:设置 read_committed_snapshot 选项时,数据库中只允许存在执行 alter database 命令的连接。在 alter database 完成之前,数据库中决不能有其他打开的连接。数据库不必一定要处于单用户模式中。
(6).使用绑定连接。(注:绑定会话有利于在同一台服务器上的多个会话之间协调操作。绑定会话允许一个或多个会话共享相同的事务和锁(但每个回话保留其自己的事务隔离级别),并可以使用同一数据,而不会有锁冲突。可以从同一个应用程序内的多个会话中创建绑定会话,也可以从包含不同会话的多个应用程序中创建绑定会话。在一个会话中开启事务(begin tran)后,调用exec sp_getbindtoken @token out;来取得token,然后传入另一个会话并执行exec sp_bindsession @token来进行绑定(最后的示例中演示了绑定连接)。
两个死锁示例及解决方法
5.1 sql死锁
(1). 测试用的基础数据:
代码如下 复制代码
create table lock1(c1 int default(0));
create table lock2(c1 int default(0));
insert into lock1 values(1);
insert into lock2 values(1);
(2). 开两个查询窗口,分别执行下面两段sql
--query 1
begin tran
update lock1 set c1=c1+1;
waitfor delay '00:01:00';
select * from lock2
rollback tran;
--query 2
begin tran
update lock2 set c1=c1+1;
waitfor delay '00:01:00';
select * from lock1
rollback tran;
上面的sql中有一句waitfor delay '00:01:00',用于等待1分钟,以方便查看锁的情况。
(3). 查看锁情况
在执行上面的waitfor语句期间,执行第二节中提供的语句来查看锁信息:
query1中,持有lock1中第一行(表中只有一行数据)的行排他锁(rid:x),并持有该行所在页的意向更新锁(pag:ix)、该表的意向更新锁(tab:ix);query2中,持有lock2中第一行(表中只有一行数据)的行排他锁(rid:x),并持有该行所在页的意向更新锁(pag:ix)、该表的意向更新锁(tab:ix);
执行完waitfor,query1查询lock2,请求在资源上加s锁,但该行已经被query2加上了x锁;query2查询lock1,请求在资源上加s锁,但该行已经被query1加上了x锁;于是两个查询持有资源并互不相让,构成死锁。
(4). 解决办法
a). sql server自动选择一条sql作死锁牺牲品:运行完上面的两个查询后,我们会发现有一条sql能正常执行完毕,而另一个sql则报如下错误:
服务器: 消息 1205,级别 13,状态 50,行 1
事务(进程 id xx)与另一个进程已被死锁在 lock 资源上,且该事务已被选作死锁牺牲品。请重新运行该事务。
这就是上面第四节中介绍的锁监视器干活了。
b). 按同一顺序访问对象:颠倒任意一条sql中的update与select语句的顺序。例如修改第二条sql成如下:
代码如下 复制代码
--query2
begin tran
select * from lock1--在lock1上申请s锁
waitfor delay '00:01:00';
update lock2 set c1=c1+1;--lock2:rid:x
rollback tran;
当然这样修改也是有代价的,这会导致第一条sql执行完毕之前,第二条sql一直处于阻塞状态。单独执行query1或query2需要约1分钟,但如果开始执行query1时,马上同时执行query2,则query2需要2分钟才能执行完;这种按顺序请求资源从一定程度上降低了并发性。
c). select语句加with(nolock)提示:默认情况下select语句会对查询到的资源加s锁(共享锁),s锁与x锁(排他锁)不兼容;但加上with(nolock)后,select不对查询到的资源加锁(或者加sch-s锁,sch-s锁可以与任何锁兼容);从而可以是这两条sql可以并发地访问同一资源。当然,此方法适合解决读与写并发死锁的情况,但加with(nolock)可能会导致脏读。
代码如下 复制代码
select * from lock2 with(nolock)
select * from lock1 with(nolock)
d). 使用较低的隔离级别。sql server 2000支持四种事务处理隔离级别(til),分别为:read uncommitted、read committed、repeatable read、serializable;sql server 2005中增加了snapshot til。默认情况下,sql server使用read committed til,我们可以在上面的两条sql前都加上一句set transaction isolation level read uncommitted,来降低til以避免死锁;事实上,运行在read uncommitted til的事务,其中的select语句不对结果资源加锁或加sch-s锁,而不会加s锁;但还有一点需要注意的是:read uncommitted til允许脏读,虽然加上了降低til的语句后,上面两条sql在执行过程中不会报错,但执行结果是一个返回1,一个返回2,即读到了脏数据,也许这并不是我们所期望的。
e). 在sql前加set lock_timeout timeout_period,当请求锁超过设定的timeout_period时间后,就会终止当前sql的执行,牺牲自己,成全别人。
f). 使用基于行版本控制的隔离级别(sql server 2005支持):开启下面的选项后,select不会对请求的资源加s锁,不加锁或者加sch-s锁,从而将读与写操作之间发生的死锁几率降至最低;而且不会发生脏读。啊
set allow_snapshot_isolation on
set read_committed_snapshot on
g). 使用绑定连接(使用方法见下一个示例。)
5.2 程序死锁(sql阻塞)
看一个例子:一个典型的数据库操作事务死锁分析,按照我自己的理解,我觉得这应该算是c#程序中出现死锁,而不是数据库中的死锁;下面的代码模拟了该文中对数据库的操作过程:
代码如下 复制代码
//略去的无关的code
sqlconnection conn = new sqlconnection(connectionstring);
conn.open();
sqltransaction tran = conn.begintransaction();
string sql1 = update lock1 set c1=c1+1;
string sql2 = select * from lock1;
executenonquery(tran, sql1); //使用事务:事务中lock了table
executenonquery(null, sql2); //新开一个connection来读取table
public static void executenonquery(sqltransaction tran, string sql)
{
sqlcommand cmd = new sqlcommand(sql);
if (tran != null)
{
cmd.connection = tran.connection;
cmd.transaction = tran;
cmd.executenonquery();
}
else
{
using (sqlconnection conn = new sqlconnection(connectionstring))
{
conn.open();
cmd.connection = conn;
cmd.executenonquery();
}
}
}
执行到executenonquery(null, sql2)时抛出sql执行超时的异常,下图从数据库的角度来看该问题:
代码从上往下执行,会话1持有了表lock1的x锁,且事务没有结束,回话1就一直持有x锁不释放;而会话2执行select操作,请求在表lock1上加s锁,但s锁与x锁是不兼容的,所以回话2的被阻塞等待,不在等待中,就在等待中获得资源,就在等待中超时。。。从中我们可以看到,里面并没有出现死锁,而只是select操作被阻塞了。也正因为不是数据库死锁,所以sql server的锁监视器无法检测到死锁。
我们再从c#程序的角度来看该问题:
c#程序持有了表lock1上的x锁,同时开了另一个sqlconnection还想在该表上请求一把s锁,图中已经构成了环路;太贪心了,结果自己把自己给锁死了。。。
虽然这不是一个数据库死锁,但却是因为数据库资源而导致的死锁,上例中提到的解决死锁的方法在这里也基本适用,主要是避免读操作被阻塞,解决方法如下:
a). 把select放在update语句前:select不在事务中,且执行完毕会释放s锁;
b). 把select也放加入到事务中:executenonquery(tran, sql2);
c). select加with(nolock)提示:可能产生脏读;
d). 降低事务隔离级别:select语句前加set transaction isolation level read uncommitted;同上,可能产生脏读;
e). 使用基于行版本控制的隔离级别(同上例)。
g). 使用绑定连接:取得事务所在会话的token,然后传入新开的connection中;执行exec sp_bindsession @token后绑定了连接,最后执行exec sp_bindsession null;来取消绑定;最后需要注意的四点是:
(1). 使用了绑定连接的多个connection共享同一个事务和相同的锁,但各自保留自己的事务隔离级别;
(2). 如果在sql3字符串的“exec sp_bindsession null”换成“commit tran”或者“rollback tran”,则会提交整个事务,最后一行c#代码tran.commit()就可以不用执行了(执行会报错,因为事务已经结束了-,-)。
(3). 开启事务(begin tran)后,才可以调用exec sp_getbindtoken @token out来取得token;如果不想再新开的connection中结束掉原有的事务,则在这个connection close之前,必须执行“exec sp_bindsession null”来取消绑定连接,或者在新开的connectoin close之前先结束掉事务(commit/tran)。
(4). (sql server 2005 联机丛书)后续版本的 microsoft sql server 将删除该功能。请避免在新的开发工作中使用该功能,并着手修改当前还在使用该功能的应用程序。 请改用多个活动结果集 (mars) 或分布式事务。
代码如下 复制代码
tran = connection.begintransaction();
string sql1 = update lock1 set c1=c1+1;
executenonquery(tran, sql1); //使用事务:事务中lock了测试表lock1
string sql2 = @declare @token varchar(255);
exec sp_getbindtoken @token out;
select @token;;
string token = executescalar(tran, sql2).tostring();
string sql3 = exec sp_bindsession @token;update lock1 set c1=c1+1;exec sp_bindsession null;;
sqlparameter parameter = new sqlparameter(@token, sqldbtype.varchar);
parameter.value = token;
executenonquery(null, sql3, parameter); //新开一个connection来操作测试表lock1
tran.commit();
附:锁兼容性(from sql server 2005 联机丛书)
锁兼容性控制多个事务能否同时获取同一资源上的锁。如果资源已被另一事务锁定,则仅当请求锁的模式与现有锁的模式相兼容时,才会授予新的锁请求。如果请求锁的模式与现有锁的模式不兼容,则请求新锁的事务将等待释放现有锁或等待锁超时间隔过期。
