本帖提供兩種作法,可避免在 SQL Server 事務鎖定時產生的不正常或長時間阻塞,讓用戶和程序也無限期等待,甚至引發 connection pooling 鏈接數超過容量。html
所謂的「阻塞」,是指當一個數據庫會話中的事務,正在鎖定其餘會話事務想要讀取或修改的資源,形成這些會話發出的請求進入等待的狀態。SQL Server 默認會讓被阻塞的請求無限期地一直等待,直到原來的事務釋放相關的鎖,或直到它超時 (根據 SET LOCK_TIMEOUT,本文後續會提到)、服務器關閉、進程被殺死。通常的系統中,偶爾有短期的阻塞是正常且合理的;但若設計不良的程序,就可能致使長時間的阻塞,這樣就沒必要要地鎖定了資源,並且阻塞了其餘會話欲讀取或更新的需求。遇到這種狀況,可能就須要手工排除阻塞的狀態,而本文接下來要介紹兩種排除阻塞的作法。程序員
日前公司 server-side 有組件,疑似因撰寫時 exception-handling 作得不周全,致使罕見的特殊例外發生時,讓 SQL Server 的事務未執行到 cmmmit 或 rollback,形成某些表或記錄被「鎖定 (lock)」。後來又有大量的 request,要透過代碼訪問這些被鎖定的記錄,結果形成了嚴重的長時間「阻塞」,最後有大量 process (進程) 在 SQL Server 呈現「等待中 (WAIT)」的狀態。算法
因爲 SQL Server 的「事務隔離級別」默認是 READ COMMITTED (事務期間別人沒法讀取),加上 SQL Server 的鎖定形成阻塞時,默認是別的進程必須無限期等待 (LOCK_TIMEOUT = -1)。結果這些大量的客戶端 request 無限期等待永遠不會提交或回滾的事務,並一直佔用着 connection pool 中的資源,最後形成 connection pooling 鏈接數目超載。數據庫
查了一些書,若咱們要查詢 SQL Server 目前會話中的 lock 超時時間,可用如下的命令:編程
SELECT @@LOCK_TIMEOUT服務器
執行結果默認爲 -1,意即欲訪問的對象或記錄被鎖定時,會無限期等待。若欲更改當前會話的此值,可用下列命令:網絡
SET LOCK_TIMEOUT 3000session
後面的 3000,其單位爲毫秒,亦即會先等待被鎖定的對象 3 秒鐘。若事務仍未釋放鎖,則會拋回以下代號爲 1222 的錯誤信息,可供程序員編程時作相關的逾時處理:併發
消息 1222,級別 16,狀態 51,第 3 行
已超過了鎖請求超時時段。 ide
若將 LOCK_TIMEOUT 設置爲 0,亦即當欲訪問對象被鎖定時,徹底不等待就拋回代號 1222 的錯誤信息。此外,此一 SET LOCK_TIMEOUT 命令,影響範例只限當前會話 (進程),而非對某個表作永久的設置。
-------------------------------------------------------------------------------------------
接下來咱們在 SSMS 中,開兩個會話 (查詢窗口) 作測試,會話 A 建立會形成阻塞的事務進程,會話 B 去訪問被鎖定的記錄。
分別執行後,由於欲訪問的記錄是同一條,按照 SQL Server 「事務隔離級別」和「鎖」的默認值,會話 B 將沒法讀取該條數據,並且會永遠一直等下去 (若在現實項目裏寫出這種代碼,就準備被客戶和老闆臭罵)。
-------------------------------------------------------------------------------------------
若將會話 B 先加上 SET LOCK_TIMEOUT 3000 的設置,以下,則會話 B 會先等待 3 秒鐘,才拋出代號 1222 的「鎖請求已超時」錯誤信息:
執行結果:
消息 1222,級別 16,狀態 51,第 3 行
已超過了鎖請求超時時段。
語句已終止。
-------------------------------------------------------------------------------------------
另根據我以前寫的文章「30 分鐘快快樂樂學 SQL Performance Tuning」所述:
http://www.cnblogs.com/WizardWu/archive/2008/10/27/1320055.html
撰寫不當的 SQL 語句,會讓數據庫的索引沒法使用,形成全表掃描或全彙集索引掃描。例如不當的:NOT、OR 算符使用,或是直接用 + 號作來串接兩個字段看成 WHERE 條件,均可能形成索引失效,變成全表掃描,除了性能變差以外,此時若這句不良的 SQL 語句,是本帖前述會話 B 的語句,因爲會形成全表掃描或彙集索引掃描,所以就必定會被會話 A 的事務阻塞 (由於掃描全表時,必定也會讀到 OrderID=10248 這一條會話 A 正在鎖定的記錄)。
下方的 SQL 語句,因爲 OrderID 字段有設索引,所以下圖 1 的「執行計劃」,會以算法中的「二分查找法」在索引中快速查找 OrderID=10250 的記錄。
SELECT * FROM Orders WHERE OrderID=10250
SELECT * FROM Orders WHERE OrderID=10250 AND ShipCountry='Brazil'
圖 1 有正確使用到索引的 SQL 語句,以垂直的方向使用索引。用 AND 算符時,只要有任一個字段有加上索引,就能受惠於索引的好處,並避免全表掃描
此時若咱們將這句 SQL 語句,看成前述會話 B 的語句,因爲它和會話 A 所 UPDATE 的 OrderID=10248 不是同一條記錄,所以不會受會話 A 事務未回滾的影響,會話 B 能正常執行 SELECT 語句。
但若咱們將會話 B 的 SQL 語句,改用以下的 OR 算符,因爲 ShipCountry 字段沒有加上索引,此時會形成彙集索引掃描 (和全表掃描同樣,會對整個表作逐條記錄的 scan)。如此一來,除了性能低落之外,還會由於在逐條掃描時,讀到會話 A 中鎖定的 OrderID=10248 那一條記錄,形成阻塞,讓會話 B 永遠呈現「等待中」的狀態。
SELECT * FROM Orders WHERE OrderID=10250 OR ShipCountry='Brazil'
圖 2 未正確使用索引的 SQL 語句,以水平的方向使用索引。用 OR 算符時,必須「全部」用到的字段都有加上索引,纔能有效使用索引、避免全表掃描
-------------------------------------------------------------------------------------------
發生阻塞時,透過如下命令,可看出是哪一個進程 session id,阻塞了哪幾個進程 session id,且期間通過了多少「毫秒 (ms)」。以下圖 3 裏 session id = 53 阻塞了 session id = 52 的進程。另透過 SQL Server Profiler 工具,也能看到相同的內容。
SELECT blocking_session_id, wait_duration_ms, session_id FROM sys.dm_os_waiting_tasks
圖 3 本帖前述會話 A 的 UPDATE 語句 (53),阻塞了會話 B 的 SELECT 語句 (52)
透過如下兩個命令,咱們還能看到整個數據庫的鎖定和阻塞詳細信息:
SELECT * FROM sys.dm_tran_locks
EXEC sp_lock
圖 4 session id = 52 的 process 因阻塞而一直處於等待中 (WAIT)
另透過 KILL 命令,可直接殺掉形成阻塞的 process,以下:
KILL 53
-------------------------------------------------------------------------------------------
欲解決無限期等待的問題,除了前述的 SET LOCK_TIMEOUT 命令外,還有更省事的作法,以下,在會話 B 的 SQL 語句中,在表名稱後面加上 WITH (NOLOCK) 關鍵字,表示要求 SQL Server,沒必要去考慮這個表的鎖定狀態爲什麼,所以也可減小「死鎖 (dead lock)」發生的機率。但 WITH (NOLOCK) 不適用 INSERT、UPDATE、DELETE。
SELECT * FROM Orders WITH (NOLOCK) WHERE OrderID=10248
相似的功能,也可以下,在 SQL 語句前,先設置「事務隔離級別」爲可「髒讀 (dirty read)」。
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM Orders WHERE OrderID=10248
兩種作法的效果相似,讓會話 B 即便讀到被鎖阻塞的記錄,也永遠沒必要等待,但可能讀到別人未提交的數據。雖說這種作法讓會話 B 不用請求共享鎖,亦即永遠不會和其餘事務發生衝突,但應考慮項目開發實際的需求,若會話 B 要查詢的是原物料的庫存量,或銀行系統的關鍵數據,就不適合用這種作法,而應改用第一種作法的 SET LOCK_TIMEOUT 命令,明確讓數據庫拋回等候逾時的錯誤代號 1222,再本身寫代碼作處理。
-------------------------------------------------------------------------------------------
歸根究柢,咱們在編程時,就應該避免寫出會形成長時間阻塞的 SQL 語句,亦即應最小化鎖定爭用的可能性,如下爲一些建議: