2012-03-06 92 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