SQL Server隔离级别和表锁定

时间:2018-01-18 15:26:04

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

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

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

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

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

2 个答案:

答案 0 :(得分:1)

Serializable隔离模式不一定会锁定整个表。如果你有一个关于BatchGuid的索引你可能会没问题,但如果没有,那么SQL可能会升级到表锁。

您可能想要了解的一些事项:

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

答案 1 :(得分:0)

如果使用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

ADDENDUM

在回应评论时,我不相信有任何竞争条件的机会,但我并非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;

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

我很高兴被证明是错误的并且承认我很幸运,因为这10万次迭代中没有一次发生冲突,但我不相信这是运气。我真的相信只有一个锁,因此如果你有交易并不重要,你只需要正确的隔离级别。