Предотвратить гоночные условия в нескольких рядах - PullRequest
3 голосов
/ 06 марта 2012

Я много читал о предотвращении состояния гонки, но обычно с одной записью в сценарии упадка. Например: Атомный UPSERT в SQL Server 2005

У меня другое требование, и оно заключается в том, чтобы не допустить гонки по нескольким рядам. Например, скажем, у меня есть следующая структура таблицы:

GiftCards:
  GiftCardId int primary key not null,
  OriginalAmount money not null

GiftCardTransactions:
  TransactionId int primary key not null,
  GiftCardId int (foreign key to GiftCards.GiftCardId),
  Amount money not null

Может быть несколько процессов, вставляемых в GiftCardTransactions, и мне нужно предотвратить вставку, если SUM(GiftCardTransactions.Amount) + insertingAmount превысит GiftCards.OriginalAmount.

Я знаю, что мог бы использовать TABLOCKX на GiftCardTransactions, но, очевидно, это было бы неосуществимо для большого количества транзакций. Другим способом было бы добавить столбец GiftCards.RemainingAmount, и тогда мне нужно заблокировать только одну строку (хотя с возможностью повышения блокировки), но, к сожалению, это не вариант для меня в настоящее время (это был бы лучший вариант ?).

Вместо того, чтобы пытаться предотвратить вставку в первую очередь, возможно, ответ заключается в том, чтобы просто вставить, затем выбрать SUM(GiftCardTransactions.Amount) и выполнить откат при необходимости. Это крайний случай, поэтому я не беспокоюсь о ненужном использовании значений PK и т. Д.

Таким образом, вопрос заключается в том, что без изменения структуры таблицы и использования какой-либо комбинации транзакций, уровней изоляции и подсказок, как я могу добиться этого с минимальной блокировкой?

Ответы [ 2 ]

8 голосов
/ 06 марта 2012

Я сталкивался с этой ситуацией в прошлом и в конечном итоге использовал SP_GetAppLock для создания семафора на ключе, чтобы предотвратить состояние гонки. Несколько лет назад я написал статью, в которой обсуждались различные методы. Статья здесь:

http://www.sqlservercentral.com/articles/Miscellaneous/2649/

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

Я оставил суть статьи ниже, но я бы применил эту технику, получив блокировку на сконструированном ключе, таком как

@Key = 'GiftCardTransaction' + GiftCardId 

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

Мясо статьи здесь:

SP_getapplock - оболочка для расширенной процедуры XP_USERLOCK. Это позволяет использовать механизм блокировки SQL SERVER для управления параллелизмом вне области таблиц и строк. Вы можете использовать его для маршалинга вызовов PROC таким же образом, как и вышеупомянутые решения, с некоторыми дополнительными функциями.

Sp_getapplock добавляет блокировки непосредственно в память сервера, что снижает ваши накладные расходы.

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

В-третьих, sp_getapplock возвращает статус, который может быть полезен при определении того, должен ли код вообще выполняться. Опять же, в случаях, когда вам нужен только один вызов для определенного ключа, код возврата 1 скажет вам, что блокировка была успешно предоставлена ​​после ожидания снятия других несовместимых блокировок, таким образом, вы можете выйти без выполнения дополнительного кода (например, проверка существования, например). Синакс выглядит следующим образом:

   sp_getapplock [ @Resource = ] 'resource_name',
      [ @LockMode = ] 'lock_mode'
      [ , [ @LockOwner = ] 'lock_owner' ]
      [ , [ @LockTimeout = ] 'value' ]

Пример использования sp_getapplock

/************** Proc Code **************/
CREATE PROC dbo.GetAppLockTest
AS

BEGIN TRAN
    EXEC sp_getapplock @Resource = @key, @Lockmode = 'Exclusive'

    /*Code goes here*/

    EXEC sp_releaseapplock @Resource = @key
COMMIT

Я знаю, что это само собой разумеется, но поскольку объем блокировок sp_getapplock является явной транзакцией, обязательно SET XACT_ABORT ON или включите проверки в коде, чтобы гарантировать, что ROLLBACK происходит там, где это необходимо.

1 голос
/ 06 марта 2012

Мой T-SQL немного ржавый, но вот мой взгляд на решение.Хитрость заключается в том, чтобы установить блокировку обновления всех транзакций для этой подарочной карты в начале транзакции, чтобы до тех пор, пока все процедуры не читают незафиксированные данные (что является поведением по умолчанию), это эффективно блокирует транзакциитолько целевая подарочная карта.

CREATE PROC dbo.AddGiftCardTransaction
    (@GiftCardID int,
    @TransactionAmount float,
    @id int out)
AS
BEGIN
    BEGIN TRANS
    DECLARE @TotalPriorTransAmount float;
    SET @TotalPriorTransAmount = SELECT SUM(Amount) 
    FROM dbo.GiftCardTransactions WTIH UPDLOCK 
    WHERE GiftCardId = @GiftCardID;

    IF @TotalPriorTransAmount + @TransactionAmount > SELECT TOP 1 OriginalAmout 
    FROM GiftCards WHERE GiftCardID = @GiftCardID;
    BEGIN
        PRINT 'Transaction would exceed GiftCard Value'
        set @id = null
        RETURN
    END
    ELSE
    BEGIN
        INSERT INTO dbo.GiftCardTransactions (GiftCardId, Amount) 
        VALUES (@GiftCardID, @TransactionAmount);
        set @id = @@identity
        RETURN
    END
    COMMIT TRANS
END

Хотя это очень явно, я думаю, что было бы более эффективным и более удобным для T-SQL использовать оператор отката, например:

BEGIN
    BEGIN TRANS
    INSERT INTO dbo.GiftCardTransactions (GiftCardId, Amount) 
    VALUES (@GiftCardID, @TransactionAmount);
    IF (SELECT SUM(Amount) 
        FROM dbo.GiftCardTransactions WTIH UPDLOCK 
        WHERE GiftCardId = @GiftCardID) 
        > 
        (SELECT TOP 1 OriginalAmout FROM GiftCards 
        WHERE GiftCardID = @GiftCardID)
    BEGIN
        PRINT 'Transaction would exceed GiftCard Value'
        set @id = null
        ROLLBACK TRANS
    END
    ELSE
    BEGIN
        set @id = @@identity
        COMMIT TRANS
    END
END
...