共計 3588 個字符,預計需要花費 9 分鐘才能閱讀完成。
這期內容當中丸趣 TV 小編將會給大家帶來有關 SQL Server 中怎么實現一個自旋鎖,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
為什么我們需要自旋鎖?
用閂鎖同步多個線程間數據結構訪問,在每個共享數據結構前都放置一個閂鎖沒有意義的。閂鎖與此緊密關聯:當你不能獲得閂鎖(因為其他人已經有一個不兼容的閂鎖拿到),查詢就會強制等待,并進入掛起(SUSPENDED)狀態。查詢在掛起狀態等待直到可以拿到閂鎖,然后就會進入可執行(RUNNABLE)狀態。對于查詢執行只要沒有可用的 CPU,查詢就一直在可執行(RUNNABLE)狀態。一旦 CPU 有空閑,查詢會進入運行(RUNNING)狀態,最后成功獲取到閂鎖,用它來保護訪問的共享數據結構。下圖展示了 SQLOS 對協調線程調度實現的狀態機。
因為太多關聯的閂鎖,對“忙碌”數據結構使用閂鎖保護沒有意義。因此 SQL Server 實現所謂
自旋鎖(Spinlocks)。自旋鎖就像一個閂鎖,存儲引擎使用的一個輕量級同步對象,用來同步對共享數據結構線程訪問。和閂鎖的主要區別是你積極等待自旋鎖——不離開 CPU。在自旋鎖上的“等待”總會發生在運行(RUNNING)狀態的 CPU。在你閉合循環里旋轉直到獲得自旋鎖。這就是所謂的忙碌等待(busy wait)。自旋鎖的最大優點是當查詢在自旋鎖上等待時,不會涉及到上下文切換。另一方面忙碌等待浪費 CPU 周期,其他查詢也許能對它們更有效的使用。
為了避免太多的 CPU 周期浪費,SQL Server 2008 R2 及后續版本實現所謂的指數補償機制(exponential backoff mechanism),那里在 CPU 上一些時間的休眠后,線程停止旋轉。在線程進入休眠期間,增加了嘗試獲得自旋鎖的超時。這個行為可以降低對 CPU 性能的影響。
(補充說明:Spinlock 中文可以稱為自旋鎖。它是一個輕量級的,用戶態的同步對象,和 critical section 類似,但是粒度比前者小多了。它主要用來保護某些特定的內存對象的多線程并發訪問。Spinlock 是排他性的。一次只能一個線程擁有。
Spinlock 的設計目標是非常快和高效率。Spinlock 內部如何工作呢?它首先試圖獲得某個對象的鎖,如果目標被其它線程占有,就在那里輪詢(spin)一定時間。如果還得不到鎖,就 sleep 一小會,然后繼續 spin。反復這個過程直到得到對象的占有權。)
自旋鎖與故障排除對自旋鎖故障排除的主要 DMV 是 sys.dm_os_spinlock_stats。這個 DMV 里返回的每一行都代表 SQL Server 里的一個自旋鎖。SQL Server 2014 實現了 262 個不同自旋鎖。我們來詳細看下這個 DMV 里的各個列:
name:自旋鎖名稱 collision:當嘗試訪問保護的數據結構時,被自旋鎖阻塞的線程次數 spins:在循環里嘗試獲得自旋鎖的自旋鎖線程次數 spins_per_collision:旋轉和碰撞之間的比率 sleep_time:因為退避線程休眠時間 backoffs:為了其他線程在 CPU 上繼續,線程退避次數在這個 DMV 里最重要的列是 backoffs,對于特定的自旋鎖類型,這列告訴你退避發生頻率。高頻率的退避會屈服于 CPU 消耗引起 SQL Server 里的自旋鎖競爭(Spinlock Contention)。我就見過一個 32 核的 SQL Server 服務器,CPU 運行在 100% 而不進行任何工作——典型的自旋鎖競爭癥狀。
對自旋鎖問題進行故障排除你可以使用擴展事件提供的 sqlos.spinlock_backoff。當退避(backoff)發生時,就會觸發這個擴展事件。如果你捕獲了這個事件,你還要保證你使用非常好的選擇性謂語,因為在 SQL Server 里退避會經常發生。一個好的謂語可以是特定的自旋鎖類型,通過剛才提到的 DMV 你已經看到。下列代碼給你展示了如何創建這樣的擴展事件會話。
-- Retrieve the type value for the LOCK_HASH spinlock. -- That value is used by the next XEvent session SELECT * FROM sys.dm_xe_map_values WHERE name = spinlock_types AND map_value = LOCK_HASH GO -- Tracks the spinlock_backoff event CREATE EVENT SESSION SpinlockContention ON SERVER ADD EVENT sqlos.spinlock_backoff( ACTION ( package0.callstack ) WHERE ( [type] = 129 -- Value from the previous query )) ADD TARGET package0.histogram ( SET source = package0.callstack , source_type = 1 ) GO
從代碼里可以看到,這里我在調用堆棧(callstack)上使用了直方圖(histogram)目標來 bucktize。因此對于特定的自旋鎖,你可以可能到 SQL Serve 里生成的最高退避(backoffs)代碼路徑。你甚至可以通過啟用 3656 跟蹤標記(trace flag)來標識調用堆棧。這里你可以看到來自這個擴展會話的輸出:
sqldk.dll!XeSosPkg::spinlock_backoff::Publish+0x138sqldk.dll!SpinlockBase::Sleep+0xc5sqlmin.dll!Spinlock 129,7,1 ::SpinToAcquireWithExponentialBackoff+0x169sqlmin.dll!lck_lockInternal+0x841sqlmin.dll!XactWorkspaceImp::GetSharedDBLockFromLockManager+0x18dsqlmin.dll!XactWorkspaceImp::GetDBLockLocal+0x15bsqlmin.dll!XactWorkspaceImp::GetDBLock+0x5asqlmin.dll!lockdb+0x4a sqlmin.dll!DBMgr::OpenDB+0x1ecsqlmin.dll!sqlusedb+0xebsqllang.dll!usedb+0xb3sqllang.dll!LoginUseDbHelper::UseByMDDatabaseId+0x93sqllang.dll!LoginUseDbHelper::FDetermineSessionDb+0x3e1sqllang.dll!FRedoLoginImpl+0xa1bsqllang.dll!FRedoLogin+0x1c1sqllang.dll!process_request+0x3ecsqllang.dll!process_commands+0x4a3sqldk.dll!SOS_Task::Param::Execute+0x21esqldk.dll!SOS_Scheduler::RunTask+0xa8sqldk.dll!SOS_Scheduler::ProcessTasks+0x279sqldk.dll!SchedulerManager::WorkerEntryPoint+0x24csqldk.dll!SystemThread::RunWorker+0x8fsqldk.dll!SystemThreadDispatcher::ProcessWorker+0x3absqldk.dll!SchedulerManager::ThreadEntryPoint+0x226
使用提供調用堆棧,不難找出自旋鎖競爭發生的地方。在那個指定的笤俑堆棧里競爭發生在 LOCK_HASH 自旋鎖類型里,它是保護鎖管理器的哈希表。每次在鎖管理器里加鎖或解鎖被執行時,自旋鎖必須在對應的哈希桶里獲得。如你所見,在調用堆棧里,當從 XactWorkspacelmp 類調用 GetSharedDBLockFromLockManager 函數時,自旋鎖被獲得。這表示當競爭到數據庫時,共享數據庫鎖被嘗試獲取。最后在用很高的退避(backoffs)的 LOCK_HASH 自旋鎖里,這屈服于自旋鎖競爭。
上述就是丸趣 TV 小編為大家分享的 SQL Server 中怎么實現一個自旋鎖了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注丸趣 TV 行業資訊頻道。