2012-03-06 35 views
3

我已經閱讀了很多關於防止競爭狀態防止競爭條件,但通常在UPSERT情況下一個記錄。例如: Atomic UPSERT in 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

我知道我可以在GiftCardTransactions上使用TABLOCKX,但顯然這對大量交易是不可行的。另一種方法是增加一個GiftCards.RemainingAmount列,然後我只需要鎖定一行(雖然鎖升級的可能性),但不幸的是,這不是在這個時候我的選擇(這將一直是最好的選擇嗎?) 。

,而不是試圖阻止在第一位置插入,也許答案是剛剛插入,然後選擇SUM(GiftCardTransactions.Amount),並在必要時回滾。這是一個邊緣情況,所以我不擔心不必要地使用PK值,等等。

所以問題是,沒有修改表結構和使用任何事務組合,隔離級別和提示,我怎麼能用最少量的鎖定實現這一目標?

回答

8

我遇到了在過去的這個確切的情況和最終使用SP_GetAppLock到一個鍵,以防止競爭條件創建信號。幾年前我寫了一篇文章,討論各種方法。這篇文章是在這裏:

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

的基本想法是,你獲取關於構建關鍵是與該表獨立的鎖。通過這種方式,你可以非常精確地和只有塊spid,可能會創建競爭條件並且不阻止其他消費者的表。

我已經離開了下面的文章的肉,但我會通過收購上構建的關鍵,如

@Key = 'GiftCardTransaction' + GiftCardId 

獲取這個鑰匙的鎖鎖(並確保您始終將此應用此技術方法)可以防止任何潛在的競爭狀況,因爲第一個獲得鎖的人會在所有其他請求等待鎖被釋放(或超時,取決於你希望你的應用如何工作)的情況下工作。

文章的肉在這裏:

SP_getapplock是擴展過程XP_USERLOCK的包裝。它允許您使用SQL SERVER鎖定機制來管理表和行範圍之外的併發。它可以用來以與上述解決方案相同的方式編組PROC調用,並具有一些附加功能。

Sp_getapplock直接增加了鎖定在服務器內存這使你的開銷低。

其次,你可以無需更改會話設置指定鎖定超時。在你只需要一個特定鍵的調用運行的情況下,快速超時將確保proc不會長時間保持應用程序的執行。

第三,sp_getapplock返回一個狀態,該狀態可用於確定代碼是否應該運行。同樣,如果您只需要一次調用某個特定的鍵,則返回碼1會告訴您在等待其他不兼容的鎖被釋放後,該鎖已被成功授予,因此您可以退出而不運行任何更多的代碼(如例如存在檢查)。 的synax如下:

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

我的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友好的使用ROLLBACK語句,如:

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