sqlserver默認隔離級別下並發批量update同一張表引起的死鎖


 

提到死鎖,最最常規的場景之一是Session1 以排它鎖的方式鎖定A表,請求B表,session2以排它鎖的方式鎖定B表,請求A表之類的,訪問順序不一致導致死鎖的情況
本文通過簡化,測試這樣一種稍顯特殊的場景:對同一張表,並發update其中的多行記錄引起的死鎖,同時簡單分析,對於update操作的加鎖步驟
這種場景引起的死鎖比較少見,但是並不代表不存在,在某些並發場景下,可能會引起死鎖的,應該需要引起重視。

 

測試環境搭建

sqlserver 數據庫版本:

Microsoft SQL Server 2014 - 12.0.2269.0 (X64)
Jun 10 2015 03:35:45
Copyright (c) Microsoft Corporation
Enterprise Edition (64-bit) on Windows NT 6.1 <X64> (Build 7601: Service Pack 1)

創建測試表並寫入測試數據

create table test_deadlock
(
    id int identity(1,1),
    col2 varchar(10),
    col3 varchar(5000),
    createdate datetime
)

alter table test_deadlock
add constraint pk_test_deadlock primary key(id)


create index idx_col2 on test_deadlock(col2)
go


insert into test_deadlock
select concat('A',cast(rand()*1000000 as int)),replicate('B',5000),getdate()
go 1000000

 

死鎖重現

兩個session,假如是session和session2,分別造sqlserver manager studio查詢窗口中執行如下語句,模擬兩個session的並發操作

create table #t(col2 varchar(10))

insert into #t
select top 1000 col2 from test_deadlock
order by newid()
go



while 1=1
begin
    update a
    set createdate=getdate() 
    from test_deadlock a inner join #t b on a.col2 = b.col2
end

邏輯就是隨機寫里臨時表中1000行記錄,然后依據臨時表中的數據更新測試表test_deadlock
分別執行兩個session的sql之后,因為#t是隨機寫入1000行數據,兩個session中#t的數據肯定是不完全一樣的,然后執行兩個session的語句
等待執行一段時間之后(並不一定每次都會出現,或者很快出現,需要多次測試),就會發現其中一個session的執行遇到了死鎖,如下

通過sqlserver自帶的擴展事件[system_health]查看死鎖的詳細信息

查看死鎖xml的詳細信息,非常清楚地顯示,兩個session在占用了某個key值的U鎖之后,相互請求對方占用的U鎖的,結果就是死鎖

對於兩個session wait的key分別是:KEY: 5:72057594045595648 (d813f2171ef9)和KEY: 5:72057594045595648 (872f5c19a82c)
關於key值格式的含義,簡單說一下,以KEY: 5:72057594045595648 (872f5c19a82c)為例,5代表數據Id,72057594045595648 代表index的id,(872f5c19a82c)代表key值的內部Id
關於前兩者,相對比較簡單,通過系統表可以查詢出來,如下是當前測試庫執行的查詢結果,很直觀地顯示了key值中的信息含義

關於(872f5c19a82c)代表key值的內部Id,可以通過一個未公布的系統函數 %%lockres%% 查看得到,如下,也相對比較清楚。關於 %%lockres%% 不做過多的表述,偏避免偏離主題。

有興趣了解%%lockres%% 的參考:https://dba.stackexchange.com/questions/106762/how-can-i-convert-a-key-in-a-sql-server-deadlock-report-to-the-value

 

現在可以清楚地得到:
session1 以U鎖的方式鎖定了一個idx_col2上的索引值 (d813f2171ef9),請求索引值 (872f5c19a82c)上的U鎖
session2 以U鎖的方式鎖定了一個idx_col2上的索引值 (872f5c19a82c),請求索引值(d813f2171ef9)上的U鎖
U鎖與U鎖不兼容,然后發生了死鎖。

這里簡單分析一下,這兩個key值內部Id對應的具體的key值以及兩個session的鎖定情況。
兩個內部Id對應的col2字段分別是A229853和A76383,
也就是說session1 對col2=A229853 key值加U鎖,請求col2 = A76383的key值的U鎖
session2 對col2=A76383 key值加U鎖,請求col2 = A229853 的key值的U鎖

那么去具體的session(查詢窗口)中的臨時表驗證一下這兩個Id是不是同時存在於兩個session的臨時表中,是不是這樣的。

沒有問題,session1和session2中都包含了這兩個id,這里默認查詢出來的順序,也剛好相反,更加可以支持上述推斷,在執行update的過程中,造成了上述的死鎖。

 

死鎖原因分析

update的加鎖,這里是對1000個col2的值執行更新,內部可以看做是一個過程,即便是使用了索引查找(index seek)
批量update,雖然是一個事物,但是執行的過程,對於目標數據的加鎖是一個過程(逐行),這個過程不是隔離的或者說排他性執行的(除非是表級別的排它鎖)

粗略地講,推斷其大概過程如下:
依次遍歷符合條件的目標數據(#t中的col2與test_deadlock)進行查找,如果找到,加U鎖(尚不考慮鎖升級為表鎖)

偽代碼如下

foreach(key in #t)--直接以#t中的col2字段的值,通過索引查找的方式驅動test_deadlock,依次加(U)鎖
{
  if(key==test_deadlock.col2)
  {
    updlock test_deadlock.col2
  }
}

加鎖,這里是逐行對test_deadlock中的col2 key加U鎖是一個過程,而不是一瞬間。一旦兩個session加鎖目標存在交集,並且對加鎖的key值加鎖順序不一致,就潛在發生死鎖的可能性。
比如session1 先對key = A229853 的值加U鎖,再視圖key = A229853 加U鎖
session2先對key = A229853 的值加U鎖,再試圖key = A229853 加U鎖,死鎖就因此而產生
這里的update語句,內部是一個事物沒錯,但是需要個事物區分來看,當前update加的鎖是一個key鎖,而不是表鎖,既然是key鎖,需要逐個查找然后加鎖,而不是一次性加鎖
這就是當前這種場景產生死鎖的原因。

該死鎖產生的條件
1,存在比較大的並發量,或者是做並發壓力測試
2,並發session update的目標數據存在交集
3,服務器資源使用率比較高或者負載比較重的時候更容易出現

 

如何解決

上述分析原因是一次對不同的key值加U鎖,不同session加鎖目標存在交集,且加鎖順序不一致引起的,在read committed或者可重復度隔離級別,都無法解決上述家鎖沖突問題

1,從邏輯key值加鎖順序入手:

問題的本質在於,並發session對目標數據加鎖目標存在交集,且對加鎖目標的加鎖(key值)順序不一致,如果使並發session對加鎖目標(key值)加鎖順序一致,也就不會出現死鎖的情況了。
如果按照對key值順序的方式加鎖(通過在key值上創建cluster索引),
可以將上面key值的隨機訪問變為順序訪問(table scan變為cluster index scan),只會出現相互阻塞,而不是死鎖,想一想為什么……
這里通過對#t的目標值字段,創建一個索引,再經並發測試,不會出現死鎖.
但是這種方式,並上不是最佳的,因為執行計划不會永遠只有一種,
這里主觀上要求一定是#t表驅動目標表test_deadlock,面對更加復雜的實際情形,如何保證?難道需要再次加強制索引+驅動順序提示?(事實證明,這種方式極其不可靠)

insert into #t
select top 1000 col2 from test_deadlock
order by newid()

create cluster index idx_col2 on #t(col2)
go

while 1=1
begin
    update a
    set createdate=getdate() 
    from test_deadlock a   inner join #t b on a.col2 = b.col2
end

2,從鎖定方式入手:

可序列化隔離級別,或者直接對update的目標表加表級別鎖解決
事實上經過測試,出乎意料的是,可序列化隔離級別依舊解決不了上述加鎖沖突,如下,當然,序列化隔離級別下,死鎖類型跟默認隔離級別下並不完全相同,是RangeS-U之間的沖突
序列化可能造成的死鎖,也有大把的例子,這里不繼續扯了,參考https://stackoverflow.com/questions/27347730/serializable-transaction-deadlockhttps://stackoverflow.com/questions/39029573/why-does-a-serializable-isolation-level-lead-to-deadlock-and-concurrency-issues

經測試,直接加表級別鎖,可以解決上述死鎖問題,參考如下

while 1=1
begin
    update a
    set createdate=getdate() 
    from test_deadlock a with(updlock,tablock) inner join #t b on a.col2 = b.col2
end

另外,如果其他辦法,也未嘗不可,從應用程序的角度,可以使用類似於單例模式,從發起端開始排隊,或者將目標數據寫入隊列的方式,依次排隊打入數據的方式執行,都是可以避免死鎖的。

 

最后
這種死鎖,並不會輕易生成,但它是確確實實存在的,或者說是潛在的,筆者根據實際場景在本地反復做測試,中間也遇到一些問題
1,基於當前測試方式和場景,測試數據量要足夠大,設計case的時候,要避免自動的鎖升級造成測試干擾,避免某些寫法讓#t中生成的數據有序,都會重現這種場景
2,如果想從默認擴展事件system_health的ring_buffer中查看死鎖信息,類似本文,需要對[system_health]默認的擴展事件加大ring_buffer的max_events_limit和max_memory。
  至於為什么,參考這個:https://www.cnblogs.com/wy123/p/9055731.html
3,對於高性能的服務器,比較難以重現這種場景,因為每個session都執行的相對較快
4,測試庫如果是完整恢復模式,小心撐爆事物日志或者磁盤空間

 

該死鎖產生場景的擴展

對於類似的死鎖產生場景,與並發批量update的邏輯一樣,在並發批量做delete的時候也會出現死鎖,並且已經在生產環境發現過。
並發批量insert,會不會產生類似的死鎖,如果產生了,又如何解決,繼續測試。
可能會死鎖產生的場景,還是超出了預料……


並發insert的造成的死鎖場景,也可以間接模擬出來,通過將批量insert拆分成多句單個的insert,來模擬key值加鎖順序沖突造成的死鎖死鎖
如下代碼創建一個測試表

create table test_deadlock2
(
    id int identity(1,1),
    col2 int,
    createdate datetime
)

alter table test_deadlock2
add constraint pk_test_deadlock2 primary key(id)

--注意,這里是的col2是一個唯一索引
create unique index idx_col2 on test_deadlock2(col2)

--預先寫入測試數據
insert into test_deadlock2 values (0,getdate()),(100,getdate()),(200,getdate()),(300,getdate())

這里模式的死鎖方式並不是偶然的,是必然的,需要了解insert的加鎖機制

第一步,session1中,開事物,執行第一句insert語句

第二步,session2中,開事物,執行第一句insert語句

第三步,session1中執行第二句insert,被阻塞

第四部,session2中執行第二句insert語句,session2作為死鎖的犧牲品,session1順利完成

最后,session2死鎖應運而生,session1 正常執行完成,至於問題的原因,跟上面批量update造成的死鎖,基本上完全一樣

只不過這里把批量insert(類似於insert into table select col from #t)轉換為單行的insert,使得問題更容易出現。

 

僅測試本文的場景,筆者個人的機器是12GB內存,8核心I7 CPU,測試過程,機器沒有開其他應用程序,已經巨卡無比,內存幾乎完全占滿(當然CPU沒有太大壓力),重啟數據庫服務之后,瞬間輕松。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM