本帖提供兩種做法,可避免在 SQL Server 事務鎖定時產(chǎn)生的不正?;蜷L時間阻塞,讓用戶和程序也無限期等待,甚至引起 connection pooling 連接數(shù)超過容量。 所謂的「阻塞」,是指當一個數(shù)據(jù)庫會話中的事務,正在鎖定其他會話事務想要讀取或修改的資源,造成這些會話發(fā)出的請求進入等待的狀態(tài)。SQL Server 默認會讓被阻塞的請求無限期地一直等待,直到原來的事務釋放相關的鎖,或直到它超時 (根據(jù) SET LOCK_TIMEOUT,本文后續(xù)會提到)、服務器關閉、進程被殺死。一般的系統(tǒng)中,偶爾有短時間的阻塞是正常且合理的;但若設計不良的程序,就可能導致長時間的阻塞,這樣就不必要地鎖定了資源,而且阻塞了其他會話欲讀取或更新的需求。遇到這種情況,可能就需要手工排除阻塞的狀態(tài),而本文接下來要介紹兩種排除阻塞的做法。
日前公司 server-side 有組件,疑似因撰寫時 exception-handling 做得不周全,導致罕見的特殊例外發(fā)生時,讓 SQL Server 的事務未執(zhí)行到 cmmmit 或 rollback,造成某些表或記錄被「鎖定 (lock)」。后來又有大量的 request,要透過代碼訪問這些被鎖定的記錄,結(jié)果造成了嚴重的長時間「阻塞」,最后有大量 process (進程) 在 SQL Server 呈現(xiàn)「等待中 (WAIT)」的狀態(tài)。 由于 SQL Server 的「事務隔離級別」默認是 READ COMMITTED (事務期間別人無法讀取),加上 SQL Server 的鎖定造成阻塞時,默認是別的進程必須無限期等待 (LOCK_TIMEOUT = -1)。結(jié)果這些大量的客戶端 request 無限期等待永遠不會提交或回滾的事務,并一直占用著 connection pool 中的資源,最后造成 connection pooling 連接數(shù)目超載。
SELECT @@LOCK_TIMEOUT
SET LOCK_TIMEOUT 3000 后面的 3000,其單位為毫秒,亦即會先等待被鎖定的對象 3 秒鐘。若事務仍未釋放鎖,則會拋回如下代號為 1222 的錯誤信息,可供程序員編程時做相關的逾時處理: 消息 1222,級別 16,狀態(tài) 51,第 3 行
-------------------------------------------------------------------------------------------
--會話 A BEGIN TRAN; UPDATE Orders SET EmployeeID=7 WHERE OrderID=10248 --rollback; --故意不提交或回滾
--會話 B SELECT * FROM Orders WHERE OrderID=10248
分別執(zhí)行后,因為欲訪問的記錄是同一條,按照 SQL Server 「事務隔離級別」和「鎖」的默認值,會話 B 將無法讀取該條數(shù)據(jù),而且會永遠一直等下去 (若在現(xiàn)實項目里寫出這種代碼,就準備被客戶和老板臭罵)。 -------------------------------------------------------------------------------------------
--會話 B SET LOCK_TIMEOUT 3000 SELECT * FROM Orders WHERE OrderID=10248 --SET LOCK_TIMEOUT -1
執(zhí)行結(jié)果: 消息 1222,級別 16,狀態(tài) 51,第 3 行 -------------------------------------------------------------------------------------------
撰寫不當?shù)?SQL 語句,會讓數(shù)據(jù)庫的索引無法使用,造成全表掃描或全聚集索引掃描。例如不當?shù)模篘OT、OR 算符使用,或是直接用 + 號做來串接兩個字段當作 WHERE 條件,都可能造成索引失效,變成全表掃描,除了性能變差之外,此時若這句不良的 SQL 語句,是本帖前述會話 B 的語句,由于會造成全表掃描或聚集索引掃描,因此就一定會被會話 A 的事務阻塞 (因為掃描全表時,一定也會讀到 OrderID=10248 這一條會話 A 正在鎖定的記錄)。
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 能正常執(zhí)行 SELECT 語句。
SELECT * FROM Orders WHERE OrderID=10250 OR ShipCountry='Brazil' ![]() 圖 2 未正確使用索引的 SQL 語句,以水平的方向使用索引。用 OR 算符時,必須「所有」用到的字段都有加上索引,才能有效使用索引、避免全表掃描 -------------------------------------------------------------------------------------------
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 53 -------------------------------------------------------------------------------------------
SELECT * FROM Orders WITH (NOLOCK) WHERE OrderID=10248
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
------------------------------------------------------------------------------------------- 本帖尚未提到死鎖和其他更進階的議題,等下次有空再繼續(xù)泡茶聊天。 |