只更新单个 table 的事务是否总是隔离的?

Is a transaction that only updates a single table always isolated?

根据 UPDATE documentation,一个 UPDATE 总是在整个 table 上获取排他锁。但是,我想知道排他锁是在确定要更新的行之前获取的,还是仅在实际更新之前获取的。

我的具体问题是我的 UPDATE 中有一个嵌套的 SELECT,如下所示:

UPDATE Tasks
SET Status = 'Active'
WHERE Id = (SELECT TOP 1 Id 
            FROM Tasks
            WHERE Type = 1
                AND (SELECT COUNT(*) 
                     FROM Tasks 
                     WHERE Status = 'Active') = 0
            ORDER BY Id)

现在我在想是否真的能保证恰好有一个 任务与 Status = 'Active' 之后如果可以并行执行相同的语句类型:

UPDATE Tasks
SET Status = 'Active'
WHERE Id = (SELECT TOP 1 Id 
            FROM Tasks
            WHERE Type = 2           -- <== The only difference
                AND (SELECT COUNT(*) 
                     FROM Tasks 
                     WHERE Status = 'Active') = 0
            ORDER BY Id)

如果对于这两个语句,要更改的行将在获取锁之前确定,我可能会得到两个必须阻止的活动任务。

如果是这种情况,我该如何预防呢?我可以在不将事务级别设置为 SERIALIZABLE 或弄乱锁定提示的情况下阻止它吗?

Is a single SQL Server statement atomic and consistent? 的回答中我得知问题出现在嵌套的 SELECT 访问另一个 table 时。但是,如果只关注更新的 table,我不确定是否需要关心这个问题。

这不是您问题的答案...但是您的问题让我很痛苦:)

;WITH cte AS 
(
    SELECT *, RowNum = ROW_NUMBER() OVER (PARTITION BY [type] ORDER BY id)
    FROM Tasks
)
UPDATE cte
SET [Status] = 'Active'
WHERE RowNum = 1
    AND [type] = 1
    AND NOT EXISTS(
            SELECT 1
            FROM Tasks
            WHERE [Status] = 'Active'
        )

如果您只想在一个任务中使用 static = active,则设置 table 以确保这是真的。使用过滤后的唯一索引:

create unique index unq_tasks_status_filter_active on tasks(status)
    where status = 'Active';

第二个并发 update 可能会失败,但可以确保您的唯一性。您的应用程序代码可以处理此类失败的更新,并且 re-try.

依赖更新的实际执行计划可能很危险。这就是让数据库进行此类验证更安全的原因。基础实施细节可能会有所不同,具体取决于 SQL 服务器的环境和版本。例如,在单线程、单处理器环境中工作的东西可能在并行环境中不工作。适用于一种隔离级别的方法可能不适用于另一种隔离级别。

编辑:

而且,我无法抗拒。为了提高效率,考虑将查询写成:

UPDATE Tasks
    SET Status = 'Active'
    WHERE NOT EXISTS (SELECT 1
                      FROM Tasks
                      WHERE Status = 'Active'
                     ) AND
          Id = (SELECT TOP 1 Id 
                FROM Tasks
                WHERE Type = 2           -- <== The only difference
                ORDER BY Id
               );

然后在 Tasks(Status)Tasks(Type, Id) 上放置索引。事实上,使用正确的查询,您可能会发现查询速度非常快(尽管对索引进行了更新),从而大大减轻了您对当前更新的担忧。这不会解决竞争条件,但它至少可以让它变得罕见。

如果您正在捕获错误,那么使用唯一过滤索引,您可以这样做:

UPDATE Tasks
    SET Status = 'Active'
    WHERE Id = (SELECT TOP 1 Id 
                FROM Tasks
                WHERE Type = 2           -- <== The only difference
                ORDER BY Id
               );

如果行已经处于活动状态,这将 return 出错。

注意:所有这些查询和概念都可以应用于 "one active per group"。这个答案是针对您提出的问题。如果您遇到 "one active per group" 问题,请考虑再问一个问题。

不行,至少嵌套的select语句可以在开始更新和获取锁之前处理。为确保没有其他查询干扰此更新,需要将事务隔离级别设置为 SERIALIZABLE.

这篇文章(及其所属的系列文章)很好地解释了 SQL 服务器中并发的微妙之处:

http://sqlperformance.com/2014/02/t-sql-queries/confusion-caused-by-trusting-acid