首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> SQL Server 資料庫 >> 關於SqlServer

深入解析 MSSQL 高頻高併發場景下的鍵查找死鎖問題與解決方案

     編輯:關於SqlServer

死鎖對資料庫管理員(DBA)與開發人員而言並不陌生,其成因多樣。雖然開發人員在設計階段通常會考量並盡量避免死鎖,但在某些特殊應用場景——如高頻查詢、高併發存取——因資料庫設計的潛在缺陷,仍可能出現難以捕捉的死鎖,進而影響業務連續性。本文將深入剖析由設計問題引發的「鍵查找死鎖」,並提供實務解決方案。

問題重現與死鎖捕捉

為了模擬真實場景,我們建立測試環境並啟用 Trace Profiler 追蹤死鎖圖(Locks: Deadlock Graph),當然也可使用追蹤旗標或擴充事件來捕捉死鎖資訊。

建立測試資料表

create table testklup (
    clskey int not null,
    nlskey int not null,
    cont1 int not null,
    cont2 char(3000)
);
create unique clustered index inx_cls on testklup(clskey);
create unique nonclustered index inx_nlcs on testklup(nlskey) include(cont1);
insert into testklup select 1,1,100,'aaa';
insert into testklup select 2,2,200,'bbb';
insert into testklup select 3,3,300,'ccc';

模擬高頻更新操作(會話 1)

declare @i int;
set @i = 100;
while 1 = 1
begin
    update testklup set cont1 = @i where clskey = 1;
    set @i = @i + 1;
end

模擬高頻查詢操作(會話 2)

declare @cont2 char(3000);
while 1 = 1
begin
    select @cont2 = cont2 from testklup where nlskey = 1;
end

執行會話 2 片刻後,即可觀察到死鎖錯誤(圖 1-1),而在追蹤中捕捉到的死鎖圖如圖 1-2 所示。

死鎖錯誤訊息

圖 1-1 死鎖錯誤訊息

死鎖圖

圖 1-2 死鎖圖分析

死鎖根因分析

從死鎖圖可見:讀進程(SPID 108)請求寫進程(SPID 79)持有的排他鎖(X 鎖)而被阻塞,同時寫進程又申請讀進程持有的共享鎖(S 鎖),形成循環等待。關鍵在於讀操作的執行計畫(圖 1-3)涉及「鍵查找」,需回聚集索引取得 cont2 欄位,因此需要額外的 S 鎖;寫操作(圖 1-4)更新叢集索引鍵值,持有 X 鎖並需更新非叢集索引。

讀操作執行計畫

圖 1-3 讀操作執行計畫(含 Key Lookup)

寫操作執行計畫

圖 1-4 寫操作執行計畫

在預設隔離等級(Read Committed)下,讀取的 S 鎖屬瞬間獲取、讀完即釋放,不會等待交易結束。因此低併發、低頻率場景不易發生死鎖。但高頻模擬使 S 鎖獲取頻率極高,僅兩個會話(一讀一寫)即觸發死鎖。

核心原因: 讀查詢因非叢集索引未覆蓋 cont2,觸發鍵查找,產生額外的聚集索引鎖需求,與寫操作的鎖順序相反,導致死鎖。

解決方案

針對「消除額外鍵查找鎖需求」與「讀操作避免獲取鎖」兩大方向,提供三種實務做法:

方案一:建立覆蓋索引,消除鍵查找

將查詢所需欄位 cont2 包含於非叢集索引中,使查詢無需回表:

CREATE NONCLUSTERED INDEX [inx_nlskey_incont2] 
ON [dbo].[testklup] ([nlskey] ASC) 
INCLUDE ([cont2]);

方案二:分步查詢,避免鍵查找

先由非叢集索引取得叢集鍵,再由叢集索引取得目標欄位,雖為兩條語句,但各自鎖範圍明確,避免交叉:

declare @cont2 char(3000), @clskey int;
while 1 = 1
begin
    select @clskey = clskey from testklup where nlskey = 1;
    select @cont2 = cont2 from testklup where clskey = @clskey;
end

方案三:使用 NOLOCK / 快照隔離,讀不加鎖

透過 WITH (NOLOCK) 或啟用 READ_COMMITTED_SNAPSHOT,採用樂觀並發,讀操作不申請 S 鎖,從根本上避免讀寫衝突:

declare @cont2 char(3000);
while 1 = 1
begin
    select @cont2 = cont2 from testklup with (nolock) where nlskey = 1;
end

總結

解決死鎖關鍵在於「看清本質」:找出鎖競爭的根本原因(本例為鍵查找產生的額外鎖),再依環境特性選擇最適合的方案。覆蓋索引效能最佳但增加儲存;分步查詢無需改動結構但需修改程式碼;NOLOCK 最簡單但可能讀到髒資料,需評估業務容忍度。實務上建議優先考慮覆蓋索引,兼顧效能與一致性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved