溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

分析SIX鎖和鎖分區(qū)導(dǎo)致的死鎖

發(fā)布時間:2020-08-05 03:21:51 來源:網(wǎng)絡(luò) 閱讀:1368 作者:joe321 欄目:數(shù)據(jù)庫

什么是SIX鎖?

官方文檔鎖模式中說到:

意向排他共享 (SIX):保護針對層次結(jié)構(gòu)中某些(而并非所有)低層資源請求或獲取的共享鎖以及針對某些(而并非所有)低層資源請求或獲取的意向排他鎖。 頂級資源允許使用并發(fā) IS 鎖。 例如,獲取表上的 SIX 鎖也將獲取正在修改的頁上的意向排他鎖以及修改的行上的排他鎖。 雖然每個資源在一段時間內(nèi)只能有一個 SIX 鎖,以防止其他事務(wù)對資源進行更新,但是其他事務(wù)可以通過獲取表級的 IS 鎖來讀取層次結(jié)構(gòu)中的低層資源。

官方說明比較晦澀難懂,我嘗試用一種易懂的方式說明SIX是什么。

關(guān)于鎖有幾個概念:粒度、層次結(jié)構(gòu)和鎖之間的兼容性。鎖是用來鎖定資源,而資源是包括很多種的,而這些不同的資源代表著不同的粒度。不同的資源間存在著層次結(jié)構(gòu),如表、分區(qū)、頁、行、鍵等。鎖的類型用很多種,粗略的分類包括共享鎖(S)、更新鎖(U)、排他鎖(X)和架構(gòu)鎖(Sch)等,而不同類型的鎖,有些是互斥的,有些是兼容的。如共享鎖與其它類型的鎖相互兼容,排他鎖與其它的鎖類型互斥。

SQL Server分配鎖時,會沿著層次結(jié)構(gòu),從表級別開始分配鎖,然后到最下層的行和鍵。在分配鎖時,上級的資源會被分配意向鎖(I),用來表示這個資源的下級某個資源已經(jīng)被鎖定了。意向鎖也可以分為IS,IX,IU等類型。例如,更新表中某一行,需要在在行上分配X鎖,而在行所屬的數(shù)據(jù)頁中分配意向鎖IX,數(shù)據(jù)頁所屬的表上分配IX鎖。

如果一個會話的事務(wù)當(dāng)前持有了某個表或者數(shù)據(jù)頁的S鎖,而它接下來又要去修改表中的某一個行。這種情況下,事務(wù)需要獲取行上的X鎖和表或數(shù)據(jù)頁上的IX鎖,但是SQL Server只允許一個會話在一個資源上獲取一個鎖。也就是說沒有辦法在已經(jīng)獲得表或者頁級別的S鎖之后又分配IX給它。為了解決這個問題,于是就出現(xiàn)了兩者的結(jié)合體:S+IX=SIX。 同理,如果先持有IX,再去獲取S,也會得到SIX。

另外SQL Server中還有類似的鎖類型UIX(U+IX),SIU(S+IU),機理也是一樣的。這三種鎖被稱為轉(zhuǎn)換鎖。

 

什么是鎖分區(qū)?

首先不要把鎖分區(qū)(Lock Partitioning)和分區(qū)鎖(Partition Lock)搞混了。

官方文檔鎖分區(qū)

對于大型計算機系統(tǒng),在經(jīng)常被引用的對象上放置的鎖可能會變成性能瓶頸,因為獲取和釋放鎖對內(nèi)部鎖資源造成了爭用。鎖分區(qū)通過將單個鎖資源拆分為多個鎖資源而提高了鎖性能。此功能只適用于擁有 16 個或更多 CPU 的系統(tǒng),它是自動啟用的,而且無法禁用。只有對象鎖可以分區(qū)。

鎖任務(wù)訪問幾個共享資源,其中兩個通過鎖分區(qū)進行優(yōu)化:

  • 調(diào)節(jié)鎖(Spinlock)。它控制對鎖資源(例如行或表)的訪問。

    不進行鎖分區(qū),一個調(diào)節(jié)鎖就得管理單個鎖資源的所有鎖請求。在具有大量活動的系統(tǒng)上,在鎖請求等待釋放調(diào)節(jié)鎖時會出現(xiàn)資源爭用的現(xiàn)象。在這種情況下,獲取鎖可能變成了一個瓶頸,并且可能會對性能造成負面影響。

    為了減少對單個鎖資源的爭用,鎖分區(qū)將單個鎖資源拆分成多個鎖資源,以便將負荷分布到多個調(diào)節(jié)鎖上。

  • 內(nèi)存。它用于存儲鎖資源結(jié)構(gòu)。

    獲取調(diào)節(jié)鎖后,鎖結(jié)構(gòu)將存儲在內(nèi)存中,然后即可對其進行訪問和可能的修改。將鎖訪問分布到多個資源中有助于消除在 CPU 之間傳輸內(nèi)存塊的需要,這有助于提高性能。

獲取已分區(qū)資源的鎖時:

  • 只能獲取單個分區(qū)的 NL、SCH-S、IS、IU 和 IX 鎖模式。

  • 對于以分區(qū) ID 0 開始并且按照分區(qū) ID 順序排列的所有分區(qū),必須獲取非 NL、SCH-S、IS、IU 和 IX 模式的共享鎖 (S)、排他鎖 (X) 和其他鎖。已分區(qū)資源的這些鎖將比相同模式中未分區(qū)資源的鎖占用更多的內(nèi)存,因為每個分區(qū)都是一個有效的單獨鎖。內(nèi)存的增加由分區(qū)數(shù)決定。Windows 性能監(jiān)視器中 SQL Server 鎖計數(shù)器將顯示已分區(qū)和未分區(qū)鎖所使用內(nèi)存信息。

啟動一個事務(wù)時,它將被分配給一個分區(qū)。對于此事務(wù),可以分區(qū)的所有鎖請求都使用分配給該事務(wù)的分區(qū)。按照此方法,不同事務(wù)對相同對象的鎖資源的訪問被分布到不同的分區(qū)中。

通過一個示例觀察一下SIX和鎖分區(qū):

create  table t2 (    id int identity(1,1) ,     col1 int,     col2 int     )     
go     
insert into t2     values (floor(rand()*100),floor(rand()*100))     
go 20
set transaction isolation level serializable    
begin tran     
insert into t2     values (floor(rand()*100),floor(rand()*100))     
select id from t2     where @@ROWCOUNT>0 and id=SCOPE_IDENTITY()     
SELECT resource_type, request_mode, resource_description,resource_lock_partition     
FROM   sys.dm_tran_locks     
WHERE  resource_type <> 'database' and request_session_id=@@SPID     
rollback

分析SIX鎖和鎖分區(qū)導(dǎo)致的死鎖

這個實例有24顆CPU,所以通過resource_lock_partition看到分區(qū)編號最到23了。因為SIX模式要獲取所有鎖分區(qū),所以看到所有分區(qū)上都有SIX。

從圖中可以看出同一個事務(wù)中,不同的鎖資源可以使用不同的鎖分區(qū)。

 

實際案例分析

最近在做性能review時發(fā)現(xiàn)某些實例的Ring Buffer中記錄了一些死鎖,其中一個如下:

分析SIX鎖和鎖分區(qū)導(dǎo)致的死鎖

會話113持有了對象上的IX,需要再申請SIX。說明它修改數(shù)據(jù)后要去查詢數(shù)。

會話79持有了對象上的SIX,需要再申請SIX。這個就有點奇怪了,需要再仔細看看xml格式的死鎖信息。

<deadlock> 
    <victim-list> 
        <victimProcess 
id="process8809b88"/> 
    </victim-list> 
    <process-list> 
        <process id="process8809b88" 
taskpriority="0" logused="6844" waitresource="OBJECT: 6:1541580530:10 " 
waittime="967" ownerId="4638862771" transactionname="user_transaction" 
lasttranstarted="2016-06-06T16:45:14.617" XDES="0x8001d050" lockMode="SIX" 
schedulerid="1" kpid="41740" status="suspended" spid="113" sbid="2" ecid="0" 
priority="0" trancount="1" lastbatchstarted="2016-06-06T16:45:14.627" 
lastbatchcompleted="2016-06-06T16:45:14.627" clientapp=".Net SqlClient Data 
Provider" hostname="xxxx" hostpid="12552" loginname="xxx" 
isolationlevel="serializable (4)" xactid="4638862771" currentdb="6" 
lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056"> 
            <executionStack> 
                <frame procname="" 
line="3" stmtstart="220" 
sqlhandle="0x0200000072705a08155b23d8949f622375a2f263ba0d9099"></frame> 
            <frame procname="" line="1" 
sqlhandle="0x000000000000000000000000000000000000000000000000"></frame> 
    </executionStack> 
    <inputbuf> 
    (@0 bigint,@1 
varchar(20))insert [dbo].[CustomerVerify]([CustomerId], [VerifyRegisterCode]) 
values (@0, @1) select [VerifyID] from [dbo].[CustomerVerify] where @@ROWCOUNT 
&gt; 0 and [VerifyID] = scope_identity() 
</inputbuf> 
</process> 
<process id="process886c748" taskpriority="0" 
logused="7128" waitresource="OBJECT: 6:1541580530:0 " waittime="967" 
ownerId="4638862727" transactionname="user_transaction" 
lasttranstarted="2016-06-06T16:45:14.493" XDES="0xbe484e90" lockMode="SIX" 
schedulerid="11" kpid="35316" status="suspended" spid="79" sbid="2" ecid="0" 
priority="0" trancount="1" lastbatchstarted="2016-06-06T16:45:14.517" 
lastbatchcompleted="2016-06-06T16:45:14.517" clientapp=".Net SqlClient Data 
Provider" hostname="xxxxx" hostpid="29284" loginname="xxxxx" 
isolationlevel="serializable (4)" xactid="4638862727" currentdb="6" 
lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056"> 
<executionStack> 
    <frame procname="" line="3" 
stmtstart="220" 
sqlhandle="0x0200000072705a08155b23d8949f622375a2f263ba0d9099"></frame> 
<frame procname="" line="1" 
sqlhandle="0x000000000000000000000000000000000000000000000000"></frame> 
</executionStack> 
<inputbuf> 
(@0 bigint,@1 
varchar(20))insert [dbo].[CustomerVerify]([CustomerId], [VerifyRegisterCode]) 
values (@0, @1) select [VerifyID] from [dbo].[CustomerVerify] where @@ROWCOUNT 
&gt; 0 and [VerifyID] = scope_identity() 
</inputbuf> 
</process> 
</process-list> 
<resource-list> 
<objectlock lockPartition="10" objid="1541580530" 
subresource="FULL" dbid="6" objectname="" id="lock77e3c1b00" mode="IX" 
associatedObjectId="1541580530"> 
<owner-list> 
  <owner 
id="process886c748" mode="IX"/> 
</owner-list> 
<waiter-list> 
<waiter id="process8809b88" mode="SIX" 
requestType="wait"/> 
</waiter-list> 
</objectlock> 
<objectlock lockPartition="0" objid="1541580530" 
subresource="FULL" dbid="6" objectname="" id="lock628e080" mode="SIX" 
associatedObjectId="1541580530"> 
<owner-list> 
<owner 
id="process8809b88" mode="SIX"/> 
</owner-list> 
<waiter-list> 
<waiter id="process886c748" mode="SIX" 
requestType="wait"/> 
</waiter-list> 
</objectlock> 
</resource-list> 
</deadlock>

概括一下:

1.兩者執(zhí)行同樣的語句。插入一條數(shù)據(jù),然后把剛才插入的這條數(shù)據(jù)的自增ID取出來。堆表,無索引。

(@0 bigint,@1 varchar(20))insert [dbo].[CustomerVerify]([CustomerId], [VerifyRegisterCode]) values (@0, @1) select [VerifyID] from [dbo].[CustomerVerify] where @@ROWCOUNT &gt; 0 and [VerifyID] = scope_identity()  
 

2. SPID 79持有鎖分區(qū)10上的IX,正在等待分配鎖分區(qū)0上的SIX.

    SPID 113持有鎖分區(qū)0上的SIX,正等待待分配鎖分區(qū)10上的SIX

3.會話的事務(wù)隔離級別都是可以序列化(isolationlevel="serializable (4)")。

基于以上,可以明白死鎖是怎么發(fā)生的:

113在插入數(shù)據(jù)時持有某個鎖分區(qū)的IX,假設(shè)這個鎖分區(qū)為N,然后它要查詢剛才插入的數(shù)據(jù),所以轉(zhuǎn)換為SIX。SIX是需要分配所有鎖分區(qū)的,并且需要從第0鎖分區(qū)開始。分配到10分區(qū)時,發(fā)現(xiàn)10分區(qū)被與SIX不兼容的IX鎖給鎖定了,陷入等待。

79插入數(shù)據(jù)時被分配了IX鎖,這個鎖分區(qū)為第10分區(qū),然后查詢數(shù)據(jù)時需要將IX轉(zhuǎn)換為SIX。于是從第0鎖分區(qū)開始分配SIX,但是第0分區(qū)已經(jīng)被113的SIX鎖定,并且SIX與SIX是不兼容的,于是也陷入等待。

 

如何解決

總結(jié)前面的死鎖原因,問題就變成了:并發(fā)插入含有自增ID的堆表,并取出插入的自增ID,如何避免死鎖?

這種死鎖情況是非常非常罕見的。TF-1229可以禁用鎖分區(qū)的功能。個人覺得高并發(fā)的應(yīng)用,與其禁用鎖分區(qū)來規(guī)避這種罕見情況,還不如設(shè)計好應(yīng)用的重試機制。

有一點很奇怪,在上面的死鎖中,事務(wù)隔離級別是可序列化,而數(shù)據(jù)庫端是默認的已提交隔離級別。開發(fā)人員并沒設(shè)置連接會話的事務(wù)隔離級別,而這個會話的隔離級別卻改變了,這是什么原因呢?

我的分析是,程序中使用了Entity Framework,而EF在6.0之前的默認連接隔離級別是可序列化。開發(fā)人員直接拿來用,也沒有注意到這種問題。

參考:

What is the default transaction isolation level in Entity Framework when I issue “SaveChanges()”?

Tips to avoid deadlocks in Entity Framework applications

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI