使用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!
什么可能导致这种情况发生?
答案 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
就足够了)。
不过,我对这些提示没有太多经验。