在 Sql Server 中,有没有办法检查选定的一组行是否被锁定?

ToC*_*ToC 21 sql-server locking blocking

我们正在尝试更新/删除数十亿行表中的大量记录。由于这是一张受欢迎的桌子,因此这张桌子的不同部分有很多活动。任何大型更新/删除活动都被长时间阻塞(因为它正在等待获得所有行的锁或页锁或表锁),从而导致超时或需要多天才能完成任务。

因此,我们正在更改一次删除小批量行的方法。但是我们想检查选定的(比如 100、1000 或 2000 行)当前是否被不同的进程锁定。

  • 如果没有,则继续删除/更新。
  • 如果它们被锁定,则移至下一组记录。
  • 最后,回到开头并尝试更新/删除遗漏的。

这是可行的吗?

谢谢,ToC

oou*_*ire 11

如果我正确理解请求,目标是删除成批的行,同时对整个表的行进行 DML 操作。目标是删除一个批次;然而,如果包含在由所述批次定义的范围内的任何底层行被锁定,那么我们必须跳过该批次并移动到下一个批次。然后,我们必须返回到之前未删除的任何批次,并重试我们原来的删除逻辑。我们必须重复这个循环,直到所有需要的行的批次都被删除。

如前所述,使用 READPAST 提示和 READ COMMITTED(默认)隔离级别是合理的,以便跳过可能包含阻塞行的范围。我将更进一步,建议使用 SERIALIZABLE 隔离级别和蚕食删除。

SQL Server 使用 Key-Range 锁来保护隐式包含在 Transact-SQL 语句读取的记录集中的一系列行,同时使用可序列化事务隔离级别...在此处找到更多信息:https : //technet.microsoft.com /en-US/library/ms191272(v=SQL.105).aspx

通过蚕食删除,我们的目标是隔离一系列行,并确保在删除这些行时不会发生任何更改,也就是说,我们不希望出现幻读或插入。可序列化隔离级别就是为了解决这个问题。

在演示我的解决方案之前,我想补充一点,我既不建议将数据库的默认隔离级别切换为 SERIALIZABLE,也不建议我的解决方案是最好的。我只是想把它呈现出来,看看我们可以从哪里开始。

一些家政注意事项:

  1. 我使用的 SQL Server 版本是 Microsoft SQL Server 2012 - 11.0.5343.0 (X64)
  2. 我的测试数据库使用 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
Run Code Online (Sandbox Code Playgroud)

此时,我们将需要一个或多个索引,SE​​RIALIZABLE 隔离级别的锁定机制可以在这些索引上起作用。


-- 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
Run Code Online (Sandbox Code Playgroud)

现在,让我们检查一下是否创建了 2,000,000 行


SELECT
    COUNT(*)
FROM
    tbl;
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

所以,我们有我们的数据库、表、索引和行。所以,让我们建立一个实验来蚕食删除。首先,我们必须决定如何最好地创建一个典型的蚕食删除机制。


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;
Run Code Online (Sandbox Code Playgroud)

如您所见,我将显式事务放置在 while 循环中。如果您想限制日志刷新,请随意将其放置在循环之外。此外,由于我们处于 FULL 恢复模式,您可能希望在运行蚕食删除操作时更频繁地创建事务日志备份,以确保可以防止您的事务日志异常增长。

所以,我对这个设置有几个目标。首先,我想要我的钥匙范围锁;所以,我尽量保持批次尽可能小。我也不想对我的“巨大”表上的并发产生负面影响;所以,我想尽快把我的锁拿走。因此,我建议您减小批量大小。

现在,我想提供一个非常简短的示例,说明此删除例程的实际操作。我们必须在 SSMS 中打开一个新窗口并从我们的表中删除一行。我将使用默认的 READ COMMITTED 隔离级别在隐式事务中执行此操作。


DELETE FROM
    dbo.tbl
WHERE
    c1 = 20005;
Run Code Online (Sandbox Code Playgroud)

这一行真的被删除了吗?


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20010;
Run Code Online (Sandbox Code Playgroud)

是的,它被删除了。

删除行的证明

现在,为了查看我们的锁,让我们在 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;

Run Code Online (Sandbox Code Playgroud)

现在,我们准备开始了。在一个新的 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;
Run Code Online (Sandbox Code Playgroud)

让我们开始两个操作,从插入开始,然后是删除。我们可以看到键范围锁和排他锁。

范围和排他锁

插入生成了这些锁:

插入的锁

啃删除/选择持有这些锁:

在此处输入图片说明

我们的插入按预期阻止了我们的删除:

插入块删除

现在,让我们提交插入事务,看看发生了什么。

提交删除

正如预期的那样,所有交易都完成了。现在,我们必须检查插入是否是幻像或删除操作是否也将其删除。


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20015;
Run Code Online (Sandbox Code Playgroud)

实际上,插入被删除了;所以,不允许幻像插入。

无幻影插入

所以,总而言之,我认为这个练习的真正意图不是尝试跟踪每一行、页面或表级锁,并尝试确定批处理的元素是否被锁定,因此需要我们的删除操作等待。这可能是提问者的意图;然而,这项任务是艰巨的,如果不是不可能的话,基本上是不切实际的。真正的目标是确保一旦我们用我们自己的锁隔离了我们的批次范围,然后在删除批次之前,不会出现不需要的现象。SERIALIZABLE 隔离级别实现了这个目标。关键是要保持您的小点,控制您的事务日志,并消除不需要的现象。

如果您想要速度,那么不要构建无法分区的超深表,因此无法使用分区切换来获得最快的结果。速度的关键是分区和并行;痛苦的关键是啃咬和活锁。

请让我知道你的想法。

我创建了一些正在运行的 SERIALIZABLE 隔离级别的进一步示例。它们应该可以在下面的链接中找到。

删除操作

插入操作

相等操作 - 下一个键值的键范围锁定

相等操作 - 单例获取现有数据

相等操作 - 不存在数据的单例提取

不等式操作 - 键范围锁定范围和下一个键值


Kin*_*hah 9

因此,我们正在更改一次删除小批量行的方法。

这是一个非常好的删除小批量块的主意。我会根据数据库的恢复模型添加一个小的waitfor delay '00:00:05'并且取决于数据库的恢复模型 - 如果FULL,则执行 a log backupSIMPLE然后执行 amanual CHECKPOINT以避免事务日志膨胀 - 在批次之间。

但是我们想检查选定的(比如 100、1000 或 2000 行)当前是否被不同的进程锁定。

您所说的并非完全可以开箱即用(请记住您的 3 个要点)。如果上述建议 -small batches + waitfor delay不起作用(前提是您进行了适当的测试),那么您可以使用query HINT.

不要使用NOLOCK- 请参阅kb/308886Itzik Ben-Gan 的 SQL Server 读取一致性问题将 NOLOCK 无处不在 - Aaron BertrandSQL Server NOLOCK 提示和其他糟糕的想法

READPAST提示将有助于您的场景。READPAST提示的要点是 - 如果存在行级锁,则 SQL Server 不会读取它。

指定数据库引擎不读取被其他事务锁定的行。当READPAST指定,行级锁被跳过。也就是说,数据库引擎跳过行而不是阻塞当前事务,直到释放锁。

在我有限的测试中,我发现在使用DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)查询会话隔离级别并将其设置为READ COMMITTED使用SET TRANSACTION ISOLATION LEVEL READ COMMITTED默认隔离级别时,吞吐量非常好。


mou*_*iin -11

您可以在删除时使用NoLOCK,如果行被锁定,它们将不会被删除。它并不理想,但可能对你有用。

DELETE TA FROM dbo.TableA TA WITH (NOLOCK) WHERE Condition = True
Run Code Online (Sandbox Code Playgroud)

  • 如果我在本地计算机上尝试这样做,我会收到 `Msg 1065, Level 15, State 1, Line 15 The NOLOCK and READUNCOMMITTED lockhints are not allowed for target table of INSERT, UPDATE, DELETE or MERGE statements.`,[自 2005 年起已弃用](https://technet.microsoft.com/en-us/library/ms143729%28v=sql.90%29.aspx) (7认同)