【问题标题】:Is a transaction that only updates a single table always isolated?只更新单个表的事务是否总是隔离的?
【发布时间】:2016-01-08 14:29:32
【问题描述】:

根据UPDATE documentationUPDATE 总是在整个表上获得排他锁。但是,我想知道是在确定要更新的行之前获得独占锁还是仅在实际更新之前获得。

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

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 访问另一个表时会出现问题。但是,如果只关注更新的表,我不确定是否需要关心这个问题。

【问题讨论】:

  • 该文档无论如何都是错误的。更新通常不会锁定整个表。
  • 嗯,好的,谢谢。但是我在哪里可以找到正确的文档呢?
  • 文档实际上并没有说UPDATE 锁定了整个表。它说它获得了一个排他锁,但排他锁不必在整个表上。
  • 如果您锁定整个数据库,您的并发性就会受到限制,因此现代数据库会尝试仅锁定正在修改的记录。至少在可能的情况下。当然,这取决于数据库、查询和版本。无论如何,这似乎不是最强大的锁定机制。这可能会导致难以重现或调试的错误。
  • 我只是想锁定表,而不是整个数据库。我正在做的事情很少发生,所以阻碍并发不是问题。我只需要确定一条记录将被设置为活动状态。

标签: sql sql-server transaction-isolation


【解决方案1】:

如果您只想要一个静态 = 活动的任务,请设置表格以确保这是真的。使用过滤的唯一索引:

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

第二个并发update 可能会失败,但可以确保您的唯一性。您的应用程序代码可以处理此类失败的更新,然后重试。

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

编辑:

而且,我无法抗拒。出于效率目的,请考虑将查询编写为:

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
               );

如果一行已经处于活动状态,这将返回错误。

注意:所有这些查询和概念都可以应用于“每组一个活动”。这个答案正在解决您提出的问题。如果您有“每组一个活动”的问题,请考虑再问一个问题。

【讨论】:

  • 感谢您指出这一点!不幸的是,这在我工作的环境中是不可能的。我的陈述无论如何都被简化了,所以我不确定如果我可以设置这样的索引是否会有所帮助。我想了解 UPDATE 是如何处理的。
  • @lex82 - 如果简化是例如如果每个(一个或多个其他列的组合)只能有一个活动,那么您只需将这些列放在索引中而不是status
【解决方案2】:

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

;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'
        )

【讨论】:

  • 有可能OP的原始查询比这个版本效率更高。注意:OP 的解决方案应该使用NOT EXISTS 而不是(SELECT COUNT(*) . . .)
  • 我喜欢 RowNum,但我发现 IsActive 令人困惑,因为它与特定行无关。我会使用条件语句,但我很确定这会产生我想要避免的问题,因为表不会被锁定。
  • @Devart 。 . .看看我的答案的编辑部分。您的 CTE 版本仍然需要读取整个表格来处理窗口函数。在这种情况下,我们希望在不必扫描表或相关索引的情况下找到合适的行。 (我应该从严格的逻辑角度补充说,我确实喜欢你的回答。)
【解决方案3】:

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

这篇文章(以及它所属的系列文章)很好地解释了 SQL Server 中并发的微妙之处:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多