SQL Server 死鎖經驗總結
作者:Terrylee
將死鎖減至最少
雖然不能完全避免死鎖,但可以使死鎖的數量減至最少。將死鎖減至最少可以增加事務的吞吐量並減少系統開銷,因爲只有很少的事務:
- 回滾,而回滾會取消事務執行的所有工作。
- 由於死鎖時回滾而由應用程序重新提交。
下列方法有助於最大限度地降低死鎖:
- 按同一順序訪問對象。
- 避免事務中的用戶交互。
- 保持事務簡短並在一個批處理中。
- 使用低隔離級別。
- 使用綁定連接。
按同一順序訪問對象
如果所有併發事務按同一順序訪問對象,則發生死鎖的可能性會降低。例如,如果兩個併發事務獲得 Supplier 表上的鎖,然後獲得 Part 表上的鎖,則在其中一個事務完成之前,另一個事務被阻塞在 Supplier 表上。第一個事務提交或回滾後,第二個事務繼續進行。不發生死鎖。將存儲過程用於所有的數據修改可以標準化訪問對象的順序。
避免事務中的用戶交互
避免編寫包含用戶交互的事務,因爲運行沒有用戶交互的批處理的速度要遠遠快於用戶手動響應查詢的速度,例如答覆應用程序請求參數的提示。例如,如果事務正在等待用戶輸入,而用戶去喫午餐了或者甚至回家過週末了,則用戶將此事務掛起使之不能完成。這樣將降低系統的吞吐量,因爲事務持有的任何鎖只有在事務提交或回滾時纔會釋放。即使不出現死鎖的情況,訪問同一資源的其它事務也會被阻塞,等待該事務完成。
保持事務簡短並在一個批處理中
在同一數據庫中併發執行多個需要長時間運行的事務時通常發生死鎖。事務運行時間越長,其持有排它鎖或更新鎖的時間也就越長,從而堵塞了其它活動並可能導致死鎖。
保持事務在一個批處理中,可以最小化事務的網絡通信往返量,減少完成事務可能的延遲並釋放鎖。
使用低隔離級別
確定事務是否能在更低的隔離級別上運行。執行提交讀允許事務讀取另一個事務已讀取(未修改)的數據,而不必等待第一個事務完成。使用較低的隔離級別(例如提交讀)而不使用較高的隔離級別(例如可串行讀)可以縮短持有共享鎖的時間,從而降低了鎖定爭奪。
使用綁定連接
使用綁定連接使同一應用程序所打開的兩個或多個連接可以相互合作。次級連接所獲得的任何鎖可以象由主連接獲得的鎖那樣持有,反之亦然,因此不會相互阻塞
檢測死鎖
如果發生死鎖了,我們怎麼去檢測具體發生死鎖的是哪條SQL 語句或存儲過程?
這時我們可以使用以下存儲過程來檢測,就可以查出引起死鎖的進程和SQL 語句。SQL Server 自帶的系統存儲過程sp_who 和sp_lock 也可以用來查找阻塞和死鎖, 但沒有這裏介紹的方法好用。
go
create procedure sp_who_lock
as
begin
declare @spid int , @bl int ,
@intTransactionCountOnEntry int ,
@intRowcount int ,
@intCountProperties int ,
@intCounter int
create table #tmp_lock_who (
id int identity ( 1 , 1 ),
spid smallint ,
bl smallint )
IF @@ERROR <> 0 RETURN @@ERROR
insert into #tmp_lock_who(spid,bl) 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
IF @@ERROR <> 0 RETURN @@ERROR
-- 找到臨時表的記錄數
select @intCountProperties = Count ( * ), @intCounter = 1
from #tmp_lock_who
IF @@ERROR <> 0 RETURN @@ERROR
if @intCountProperties = 0
select ' 現在沒有阻塞和死鎖信息 ' as message
-- 循環開始
while @intCounter <= @intCountProperties
begin
-- 取第一條記錄
select @spid = spid, @bl = bl
from #tmp_lock_who where Id = @intCounter
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 )
end
-- 循環指針下移
set @intCounter = @intCounter + 1
end
drop table #tmp_lock_who
return 0
end
殺死鎖和進程
如何去手動的殺死進程和鎖?最簡單的辦法,重新啓動服務。但是這裏要介紹一個存儲過程,通過顯式的調用,可以殺死進程和鎖。
go
if exists ( select * from dbo.sysobjects where id = object_id (N ' [dbo].[p_killspid] ' ) and OBJECTPROPERTY (id, N ' IsProcedure ' ) = 1 )
drop procedure [ dbo ] . [ p_killspid ]
GO
create proc p_killspid
@dbname varchar ( 200 ) -- 要關閉進程的數據庫名
as
declare @sql nvarchar ( 500 )
declare @spid nvarchar ( 20 )
declare #tb cursor for
select spid = cast (spid as varchar ( 20 )) from master..sysprocesses where dbid = db_id ( @dbname )
open #tb
fetch next from #tb into @spid
while @@fetch_status = 0
begin
exec ( ' kill ' + @spid )
fetch next from #tb into @spid
end
close #tb
deallocate #tb
go
-- 用法
exec p_killspid ' newdbpy '
查看鎖信息
如何查看系統中所有鎖的詳細信息?在企業管理管理器中,我們可以看到一些進程和鎖的信息,這裏介紹另外一種方法。
create table #t(req_spid int ,obj_name sysname)
declare @s nvarchar ( 4000 )
, @rid int , @dbname sysname, @id int , @objname sysname
declare tb cursor for
select distinct req_spid,dbname = db_name (rsc_dbid),rsc_objid
from master..syslockinfo where rsc_type in ( 4 , 5 )
open tb
fetch next from tb into @rid , @dbname , @id
while @@fetch_status = 0
begin
set @s = ' select @objname=name from [ ' + @dbname + ' ]..sysobjects where id=@id '
exec sp_executesql @s ,N ' @objname sysname out,@id int ' , @objname out, @id
insert into #t values ( @rid , @objname )
fetch next from tb into @rid , @dbname , @id
end
close tb
deallocate tb
select 進程id = a.req_spid
,數據庫 = db_name (rsc_dbid)
,類型 = case rsc_type when 1 then ' NULL 資源(未使用) '
when 2 then ' 數據庫 '
when 3 then ' 文件 '
when 4 then ' 索引 '
when 5 then ' 表 '
when 6 then ' 頁 '
when 7 then ' 鍵 '
when 8 then ' 擴展盤區 '
when 9 then ' RID(行 ID) '
when 10 then ' 應用程序 '
end
,對象id = rsc_objid
,對象名 = b.obj_name
,rsc_indid
from master..syslockinfo a left join #t b on a.req_spid = b.req_spid
go
drop table #t
總結
雖然不能完全避免死鎖,但我們可以將死鎖減至最少,並通過一定的方法來檢測死鎖。