Простой оператор SQL Update не является транзакционным - PullRequest
1 голос
/ 06 марта 2019

Использование SQL 2016.

У меня есть таблица Orders:

 OrderID int identity
 NumberOfItems int

и таблица Items:

ItemId int identity
OrderId int
DateUpdated datetime

Заказ создан, а идентификатор заказа:назначается через личность.Затем я должен назначить ему элементы «Freshest» «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 голосов
/ 06 марта 2019

READ COMMITTED уровень изоляции не гарантирует, что вы хотите, так как вы уже открыли для себя трудный путь.

Короче говоря, двигатель сначала выполняет SELECT часть, без блокировки таблицы /строк.Он блокирует таблицу / строки только при выполнении фазы UPDATE, но этот шаг происходит позже.

Таким образом, два запущенных одновременно запроса могут прочитать одни и те же строки и затем попытаться UPDATE.

Независимо от того, как вы пытаетесь переписать запрос, всегда будет две отдельные фазы - SELECT, затем UPDATE.Вы можете увидеть их в плане выполнения.

«Простое исправление» заключается в использовании уровня изоляции SERIALIZABLE для всего запроса, но это может быть излишним, а некоторые подсказки (например, UPDLOCK могут бытьдостаточно).

У меня не очень много опыта работы с этими подсказками.

0 голосов
/ 06 марта 2019

Технически есть два вызова таблицы 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
        )
;
...