在 Sql Server 中,有沒有辦法檢查一組選定的行是否被鎖定?
我們正在嘗試更新/刪除數十億行表中的大量記錄。由於這是一張流行的表格,因此該表格的不同部分有很多活動。任何大型更新/刪除活動都會被長時間阻塞(因為它正在等待獲得所有行的鎖或頁面鎖或表鎖),從而導致超時或需要多天才能完成任務。
因此,我們正在改變一次刪除小批量行的方法。但是我們想檢查選定的(比如說 100 或 1000 或 2000 行)目前是否被不同的程序鎖定。
- 如果沒有,則繼續刪除/更新。
- 如果它們被鎖定,則轉到下一組記錄。
- 最後,回到開頭並嘗試更新/刪除遺漏的內容。
這是可行的嗎?
謝謝, ToC
如果我正確理解請求,目標是刪除成批的行,同時 DML 操作正在整個表的行上發生。目標是刪除一批;但是,如果包含在該批次定義的範圍內的任何底層行被鎖定,那麼我們必須跳過該批次並移動到下一個批次。然後我們必須返回到之前未刪除的任何批次,並重試我們原來的刪除邏輯。我們必須重複這個循環,直到所有需要的行批次都被刪除。
如前所述,使用 READPAST 提示和 READ COMMITTED(預設)隔離級別是合理的,以便跳過可能包含阻塞行的過去範圍。我會更進一步,建議使用 SERIALIZABLE 隔離級別和蠶食刪除。
SQL Server 使用 Key-Range 鎖來保護隱式包含在 Transact-SQL 語句讀取的記錄集中的一系列行,同時使用可序列化事務隔離級別…在此處了解更多資訊: https ://technet.microsoft.com /en-US/library/ms191272(v=SQL.105).aspx
使用蠶食刪除,我們的目標是隔離一系列行,並確保在刪除它們時不會對這些行發生任何更改,也就是說,我們不希望幻讀或插入。可序列化的隔離級別就是為了解決這個問題。
在展示我的解決方案之前,我想補充一點,我既不建議將數據庫的預設隔離級別切換為 SERIALIZABLE,也不建議我的解決方案是最好的。我只是想介紹它,看看我們可以從這裡去哪裡。
一些家政注意事項:
- 我使用的 SQL Server 版本是 Microsoft SQL Server 2012 - 11.0.5343.0 (X64)
- 我的測試數據庫正在使用 FULL 恢復模型
為了開始我的實驗,我將建立一個測試數據庫、一個範例表,並在表中填充 2,000,000 行。
USE [master]; GO SET NOCOUNT ON; IF DATABASEPROPERTYEX (N'test', N'Version') > 0 BEGIN ALTER DATABASE [test] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [test]; END GO -- Create the test database CREATE DATABASE [test]; GO -- Set the recovery model to FULL ALTER DATABASE [test] SET RECOVERY FULL; -- Create a FULL database backup -- in order to ensure we are in fact using -- the FULL recovery model -- I pipe it to dev null for simplicity BACKUP DATABASE [test] TO DISK = N'nul'; GO USE [test]; GO -- Create our table IF OBJECT_ID('dbo.tbl','U') IS NOT NULL BEGIN DROP TABLE dbo.tbl; END; CREATE TABLE dbo.tbl ( c1 BIGINT IDENTITY (1,1) NOT NULL , c2 INT NOT NULL ) ON [PRIMARY]; GO -- Insert 2,000,000 rows INSERT INTO dbo.tbl SELECT TOP 2000 number FROM master..spt_values ORDER BY number GO 1000
此時,我們將需要一個或多個索引,SERIALIZABLE 隔離級別的鎖定機制可以作用於這些索引。
-- Add a clustered index CREATE UNIQUE CLUSTERED INDEX CIX_tbl_c1 ON dbo.tbl (c1); GO -- Add a non-clustered index CREATE NONCLUSTERED INDEX IX_tbl_c2 ON dbo.tbl (c2); GO
現在,讓我們檢查一下是否創建了 2,000,000 行
SELECT COUNT(*) FROM tbl;
所以,我們有我們的數據庫、表、索引和行。因此,讓我們設置蠶食刪除的實驗。首先,我們必須決定如何最好地創建一個典型的蠶食刪除機制。
DECLARE @BatchSize INT = 100 , @LowestValue BIGINT = 20000 , @HighestValue BIGINT = 20010 , @DeletedRowsCount BIGINT = 0 , @RowCount BIGINT = 1; SET NOCOUNT ON; GO WHILE @DeletedRowsCount < ( @HighestValue - @LowestValue ) BEGIN SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION DELETE FROM dbo.tbl WHERE c1 IN ( SELECT TOP (@BatchSize) c1 FROM dbo.tbl WHERE c1 BETWEEN @LowestValue AND @HighestValue ORDER BY c1 ); SET @RowCount = ROWCOUNT_BIG(); COMMIT TRANSACTION; SET @DeletedRowsCount += @RowCount; WAITFOR DELAY '000:00:00.025'; CHECKPOINT; END;
如您所見,我將顯式事務放置在 while 循環中。如果您想限制日誌刷新,請隨意將其放在循環之外。此外,由於我們處於 FULL 恢復模式,您可能希望在執行蠶食刪除操作時更頻繁地創建事務日誌備份,以確保可以防止您的事務日誌異常增長。
所以,我對這個設置有幾個目標。首先,我想要我的鑰匙範圍鎖;所以,我盡量保持批次盡可能小。我也不想對我的“巨大”表的並發性產生負面影響;所以,我想盡快把我的鎖拿走。所以,我建議你把你的批量縮小。
現在,我想提供一個非常簡短的範例來說明這個刪除常式的實際應用。我們必須在 SSMS 中打開一個新視窗並從表中刪除一行。我將使用預設的 READ COMMITTED 隔離級別在隱式事務中執行此操作。
DELETE FROM dbo.tbl WHERE c1 = 20005;
這一行真的被刪除了嗎?
SELECT c1 FROM dbo.tbl WHERE c1 BETWEEN 20000 AND 20010;
是的,它被刪除了。
現在,為了查看我們的鎖,讓我們在 SSMS 中打開一個新視窗並添加一兩個程式碼片段。我正在使用 Adam Mechanic 的 sp_whoisactive,可以在這裡找到:sp_whoisactive
SELECT DB_NAME(resource_database_id) AS DatabaseName , resource_type , request_mode FROM sys.dm_tran_locks WHERE DB_NAME(resource_database_id) = 'test' AND resource_type = 'KEY' ORDER BY request_mode; -- Our insert sp_lock 55; -- Our deletions sp_lock 52; -- Our active sessions sp_whoisactive;
現在,我們準備開始了。在一個新的 SSMS 視窗中,讓我們開始一個顯式事務,該事務將嘗試重新插入我們刪除的一行。同時,我們將啟動我們的蠶食刪除操作。
插入程式碼:
BEGIN TRANSACTION SET IDENTITY_INSERT dbo.tbl ON; INSERT INTO dbo.tbl ( c1 , c2 ) VALUES ( 20005 , 1 ); SET IDENTITY_INSERT dbo.tbl OFF; --COMMIT TRANSACTION;
讓我們開始這兩個操作,從插入開始,然後是刪除。我們可以看到鍵範圍鎖和排他鎖。
插入生成了這些鎖:
蠶食刪除/選擇持有這些鎖:
我們的插入按預期阻止了我們的刪除:
現在,讓我們送出插入事務,看看發生了什麼。
正如預期的那樣,所有交易都完成了。現在,我們必須檢查插入是否是幻像,或者刪除操作是否也將其刪除。
SELECT c1 FROM dbo.tbl WHERE c1 BETWEEN 20000 AND 20015;
實際上,插入已被刪除;因此,不允許幻像插入。
所以,總而言之,我認為這個練習的真正目的不是嘗試跟踪每一個行、頁或表級別的鎖,並嘗試確定批處理的元素是否被鎖定,因此需要我們的刪除操作來等待。這可能是提問者的意圖;然而,這項任務是艱鉅的,即使不是不可能,也基本上是不切實際的。真正的目標是確保一旦我們用自己的鎖隔離了批次的範圍,然後再刪除批次,就不會出現不需要的現象。SERIALIZABLE 隔離級別實現了這個目標。關鍵是要保持小塊,控制交易日誌,並消除不需要的現象。
如果您想要速度,那麼不要建構無法分區的超深表,因此無法使用分區切換來獲得最快的結果。速度的關鍵是分區和並行;痛苦的關鍵是啃咬和活鎖。
請讓我知道你在想什麼。
我創建了一些實際的 SERIALIZABLE 隔離級別範例。它們應該在下面的連結中可用。
因此,我們正在改變一次刪除小批量行的方法。
這是一個非常好的小批量或小塊刪除的好主意。我會根據數據庫的恢復模型添加一個小的和 - if , then do a and if then do a以避免事務日誌膨脹 - 在批次之間。
waitfor delay '00:00:05'``FULL``log backup``SIMPLE``manual CHECKPOINT
但是我們想檢查選定的(比如說 100 或 1000 或 2000 行)目前是否被不同的程序鎖定。
您所說的內容並非完全可能開箱即用(請記住您的 3 個要點)。如果上述建議 -
small batches + waitfor delay
不起作用(前提是您進行了適當的測試),那麼您可以使用query HINT
.不要使用
NOLOCK
- 請參閱kb/308886、 Itzik Ben-Gan 的 SQL Server 讀取一致性問題、 Aaron Bertrand 的無處不在的 NOLOCK和SQL Server NOLOCK 提示以及其他糟糕的想法。
READPAST
提示將有助於您的方案。提示的要點READPAST
是 - 如果存在行級鎖,則 SQL 伺服器不會讀取它。指定數據庫引擎不讀取被其他事務鎖定的行。指定時
READPAST
,將跳過行級鎖。也就是說,數據庫引擎會跳過行而不是阻塞目前事務,直到釋放鎖為止。
DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)
在我有限的測試中,我發現在使用查詢會話隔離級別READ COMMITTED
並將SET TRANSACTION ISOLATION LEVEL READ COMMITTED
其設置為預設隔離級別時,吞吐量非常好。