是仅仅更新单个表的事务是否始终隔离?

时间:2016-01-08 14:29:32

标签: sql sql-server transaction-isolation

根据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访问另一个表时会出现问题。但是,如果只考虑更新的表格,我不确定是否必须关心这个问题。

3 个答案:

答案 0 :(得分:6)

如果您只想要一个static = active的任务,那么设置表以确保这是真的。使用过滤的唯一索引:

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

如果某行已处于活动状态,则会返回错误。

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

答案 1 :(得分:1)

这不是你问题的答案......但是你的疑问对我来说很痛苦:)。

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

答案 2 :(得分:0)

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

本文(以及它所属的系列文章)非常好地解释了SQL服务器中并发性的细微之处:

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