【问题标题】:SQL Server Isolation Level And Table LockingSQL Server 隔离级别和表锁定
【发布时间】:2018-06-27 17:30:28
【问题描述】:

假设我在 SQL Server 中有一个表,用作需要处理的项目的队列。像这样的:

Id (bigint)
BatchGuid (guid)
BatchProcessed (bit)
...

...以及描述需要处理的项目的其他一些列等。因此,有许多正在运行的消费者根据需要将记录添加到此表中以指示需要处理的项目。

现在假设我有一份工作,负责从该表中获取一批项目并进行处理。假设我们想让它一次处理 10 个。现在还假设该作业可以同时运行多个实例,因此它同时访问表(以及可能正在向队列添加新记录的任何其他消费者)。

我打算做这样的事情:

using(var tx = new Transaction(Isolation.Serializable))
{
    var batchGuid = //newGuid
    executeSql("update top(10) [QUeueTable] set [BatchGuid] = batchGuid where [BatchGuid] is null");
    var itemsToProcess = executeSql("select * from [QueueTable] where [BatchGuid] = batchGuid");
    tx.Commit()
}

所以基本上我要做的是启动一个可序列化的事务,用特定的 GUID 标记 10 个项目,然后获取这 10 个项目,然后提交。

这是一个可行的策略吗?我相信可序列化的隔离级别基本上会锁定整个表以防止读/写直到事务完成 - 这是正确的吗?基本上事务会阻塞表上的所有其他读/写操作?我相信这就是我在这种情况下想要的,因为我不想读取脏数据,并且我不希望并发运行的作业在标记要处理的 10 个批次时相互踩踏。

任何关于我是否在正确的轨道上的见解将不胜感激。如果有更好的方法来实现这一点,我也欢迎其他方法。

【问题讨论】:

标签: c# sql-server transactions isolation-level


【解决方案1】:

Serializable 隔离模式不一定会锁定整个表。如果你在 BatchGuid 上有一个索引,你可能会做得很好,但如果没有,那么 SQL 可能会升级为表锁。

您可能想看的几件事:

  • 使用 OUTPUT 语句,您可以将 UPDATE 和 SELECT 组合到一个查询中
  • 如果您有多个进程运行此查询,您可能需要使用 UPDLOCK

【讨论】:

    【解决方案2】:

    如果您使用 OUTPUT 子句,您可以在单个语句中执行此操作:

    UPDATE TOP (10) [QueueTable]
    OUTPUT inserted.*
    SET [BatchGuid] = batchGuid 
    WHERE [BatchGuid] IS NULL;
    

    或者更具体地说:

    var itemsToProcess = executeSql("update top(10) [QUeueTable] output inserted.* set [BatchGuid] = batchGuid where [BatchGuid] is null");
    

    我想这是个人偏好,但我从来都不喜欢UPDATE TOP(n) 语法,因为你不能指定ORDER BY,而且在大多数情况下,当指定顶部时,你想指定一个顺序,我更喜欢使用类似的东西:

    UPDATE  q
    OUTPTUT inserted.*
    SET     [BatchGuid] = batchGuid 
    FROM    (   SELECT  TOP (10) *
                FROM    dbo.QueueTable
                WHERE   BatchGuid IS NULL
                ORDER BY ID
            ) AS q
    

    附录

    在回应评论时,我不认为有任何可能出现竞争状况,但我不是 100% 确定。我不相信这一点的原因是,尽管查询读取为 SELECT 和 UPDATE,但它是语法糖,它只是一个更新,并且使用完全相同的计划,并且锁定为顶级查询。但是,由于我不确定我决定测试:

    首先我在临时数据库中设置了一个示例表,并在日志表中记录了更新的 ID

    USE TempDB;
    GO
    CREATE TABLE dbo.T (ID BIGINT NOT NULL IDENTITY PRIMARY KEY, Col UNIQUEIDENTIFIER NULL);
    INSERT dbo.T (Col)
    SELECT TOP 1000000 NULL
    FROM sys.all_objects a, sys.all_objects b;
    
    CREATE TABLE dbo.T2 (ID BIGINT NOT NULL PRIMARY KEY);
    

    然后我在 10 个不同的 SSMS 窗口中运行了这个:

    WHILE 1 = 1
    BEGIN
        DECLARE @ID UNIQUEIDENTIFIER = NEWID();
    
        UPDATE  T
        SET     Col = @ID
        OUTPUT inserted.ID INTO dbo.T2 (ID)
        FROM    (   SELECT  TOP 10 *
                    FROM    dbo.T
                    WHERE   Col IS NULL
                    ORDER BY ID
                ) t;
    
        IF @@ROWCOUNT = 0
            RETURN;
    END
    

    在我停止所有 10 个线程之前,整个过程运行了 20 分钟,更新了大约 500,000 行。由于两次更新同一行会在插入 T2 作为主键违规时抛出错误,并且需要停止所有 10 个线程,这表明没有竞争条件,为了确认这一点,我运行了以下命令:

    SELECT Col, COUNT(*)
    FROM dbo.T
    WHERE Col IS NOT NULL
    GROUP BY Col
    HAVING COUNT(*) <> 10;
    

    正如预期的那样,没有返回任何行。

    我很高兴被证明是错误的并承认我很幸运,因为这 100,000 次迭代中没有一个发生冲突,但我不相信这是运气。我真的相信只有一个锁,因此是否有事务并不重要,您只需要正确的隔离级别即可。

    【讨论】:

    • 太棒了。感谢该提示,我以前从未见过 OUTPUT 关键字。这仍然需要具有可序列化隔离级别的事务吗?是否有可能出现死锁或类似情况?
    • 不需要事务,与在没有输出的情况下更新行相比,您不太可能发生冲突。
    • 听起来不错,GarethD。我会试一试。再次感谢您的帮助。
    • 我不同意“不需要交易”。尝试在两个不同的进程中执行那段代码,都在一个循环中执行你的代码。它行不通。如果将代码包装在事务中,则会导致死锁。您需要包装事务并使用 SELECT top(10) * from table WITH (UPDLOCK) 更改 SELECT top(10) * from table
    • 如果有人不同意,我想知道原因,而不是匿名投反对票。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-27
    相关资源
    最近更新 更多