SQL 服务器隔离级别和 Table 锁定

SQL Server Isolation Level And Table Locking

假设我在 SQL 服务器中有一个 table 作为需要处理的项目的队列。像这样:

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

...以及其他一些描述需要处理的项目的列,等等。因此有许多 运行 消费者根据需要向此 table 添加记录以表明项目需要处理。

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

我正打算做这样的事情:

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 个项目,然后提交。

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

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

如果使用 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,而且在大多数情况下指定 top 时,你想要指定一个顺序,我更喜欢使用类似的东西:

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,但它是语法糖,它只是一个更新,并且使用完全相同的计划,并且与顶级查询一样锁定。但是,由于我不确定我决定测试:

首先,我在临时数据库中设置了一个示例 table,并设置了一个日志记录 table 来记录更新的 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 windows 我 运行 这个:

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 个线程之前,整个过程 运行 更新了 ~500,000 行 20 分钟。由于在插入到 T2 时两次更新同一行会抛出错误并作为主键违规,并且需要停止所有 10 个线程,这表明不存在竞争条件,为了确认这一点,我 运行 以下内容:

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

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

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

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

您可能想要查看的一些内容:

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