sqlserver預設隔離級別下併發update同一張表引起的死鎖

来源:https://www.cnblogs.com/wy123/archive/2019/05/14/10865115.html
-Advertisement-
Play Games

提到死鎖,最最常規的場景之一是Session1 以排它鎖的方式鎖定A表,請求B表,session2以排它鎖的方式鎖定B表,請求A表之類的,訪問順序不一致導致死鎖的情況本文通過簡化,測試這樣一種稍顯特殊的場景:對同一張表,併發update其中的多行記錄引起的死鎖,同時簡單分析,對於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沒有太大壓力),重啟資料庫服務之後,瞬間輕鬆。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文參考-https://www.cnblogs.com/xxaxx/p/3584036.html oracle沒有像sqlserver中identity一樣的函數,需要依賴於序列、觸發器來實現自增主鍵。 創建表 Create table t_user( pk_user number(6), use ...
  • 1 資料庫基本操作? (1) 查看資料庫 show databases; (2)切換到指定的資料庫 use dbname; (3)創建資料庫 create database 庫名 charset=utf8; (4)刪除資料庫 drop database 庫名; 2 數據表基本操作? (1) 查看當前 ...
  • 在動態編程中,我們需要獲取表的結構數據,如表名,數據類型,精度等數據。你可以參考下麵幾行代碼: DECLARE @table_Name SYSNAME = 'Q' SELECT t.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, c.NUMERIC_PRECISION ...
  • 一、Linux lucene: 全文檢索引擎的架構 solr: 基於lucene的全文搜索伺服器,實現了可配置、可擴展並對查詢性能進行了優化,並且提供了一個完善的功能管理界面。 推薦一個大數據學習群 142974151每天晚上20:10都有一節【免費的】大數據直播課程,專註大數據分析方法,大數據編程 ...
  • 搜索存儲過程,視圖,函數(Table-valued Function, Scalar-valued Function)內容。 DECLARE @search_word NVARCHAR(MAX) = 'CAST' SELECT sys.objects.name AS [Object Name] FR ...
  • 本文引用自- https://www.cnblogs.com/qianyuliang/p/6649983.html https://blog.csdn.net/defonds/article/details/80455816 方法一:vm_concat() 函數 oracle wm_concat(c ...
  • 想知道某一天是星期幾,下麵Insus.NET寫了一個函數,具體是怎樣,可以細細研究。 SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- -- Author: Insus.NET -- Create date: 2019-05-15 -- Upda ...
  • 一 資料庫鏡像簡介 1.1 資料庫鏡像概述 資料庫鏡像維護一個資料庫的兩個副本,這兩個副本必須駐留在不同的 SQL Server 資料庫引擎伺服器實例上。 通常,這些伺服器實例駐留在不同位置的電腦上。 啟動資料庫上的資料庫鏡像操作時,在這些伺服器實例之間形成一種關係,稱為“資料庫鏡像會話”。 其中 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...