简单的SQL Update语句不具有事务性

时间:2019-03-05 23:33:07

标签: sql sql-server tsql sql-update transactional

使用SQL 2016。

我有一个Orders表:

 OrderID int identity
 NumberOfItems int

还有一个Items表:

ItemId int identity
OrderId int
DateUpdated datetime

创建一个订单,并通过身份分配一个OrderId。然后,我必须从“项目”表中为其分配“最新鲜的”“ NumberOfItems”项目。最新鲜的含义是,它们已根据DateUpdated日期进行了最近的更新。通过将商品的OrderId更新为相关的OrderId,可以“分配”商品。

我有此SQL以事务方式分配项目(@OrderID和@NumberOfItems是输入参数):

    UPDATE Items
        SET OrderId = @OrderId
        WHERE ItemId IN
               (SELECT TOP(@NumberOfItems) ItemId FROM Items
                WHERE OrderId IS NULL -- not already assigned
                ORDER BY DateStatusUpdated DESC -- freshest first
               )

应该很好吧?这应该以事务方式将项目分配给订单,无论对服务器运行此语句的频率是多高或并发,一旦已分配相同的项目,就永远不应将其重新分配给另一个订单。由于使用的单个UPDATE语句具有必要的事务性质,因此几乎任何关系数据库都应保证这一点。

好吧,它已经以这种方式工作了几千万个订单。然后,昨晚,有两个订单相隔约50毫秒(对于该应用而言,这并不是特别接近),并且将一个商品分配给OrderN,然后将同一商品重新分配给OrderN + 1!

什么可能导致这种情况发生?

2 个答案:

答案 0 :(得分:0)

从技术上讲,有两个对Item表的调用。 试试这个重构查询,它与单次调用的功能相同:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
;WITH LimitedUpdate AS (
    SELECT TOP(@NumberOfItems) i.OrderId
    FROM Items i WITH (UPDLOCK)
    WHERE i.OrderId IS NULL
    ORDER BY i.DateStatusUpdated DESC
)
UPDATE LimitedUpdate SET OrderId = @OrderId
;

保留原始查询:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE Items WITH (UPDLOCK)
SET OrderId = @OrderId
WHERE ItemId IN
        (SELECT TOP(@NumberOfItems) ItemId FROM Items WITH (UPDLOCK)
        WHERE OrderId IS NULL -- not already assigned
        ORDER BY DateStatusUpdated DESC -- freshest first
        )
;

答案 1 :(得分:0)

READ COMMITTED隔离级别无法提供您想要的保证,因为您已经很难找到自己了。

简而言之,引擎首先执行SELECT部分,而不锁定表/行。 仅在执行UPDATE阶段时才锁定表/行,但是此步骤稍后发生。

因此,同时运行的两个查询可能会读取相同的行,然后尝试UPDATE

无论您如何尝试重写查询,总会有两个单独的阶段-SELECT,然后是UPDATE。您可以在执行计划中看到它们。

“简单的解决方法”是对整个查询使用SERIALIZABLE隔离级别,但这可能会过高并且有些提示(例如UPDLOCK就足够了)。

不过,我对这些提示没有太多经验。