防止多行的竞争条件

时间:2012-03-06 18:26:34

标签: sql-server-2008 locking race-condition

我已经阅读了很多关于防止竞争条件的内容,但通常在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,我需要阻止插入。

我知道我可以在TABLOCKX上使用GiftCardTransactions,但很明显这对于大量交易来说是不可行的。另一种方法是添加一个GiftCards.RemainingAmount列,然后我只需要锁定一行(虽然可能会锁定升级),但不幸的是,这对我来说不是一个选项(这是不是最佳选择?)。

可能答案只是插入,然后选择SUM(GiftCardTransactions.Amount),并在必要时回滚,而不是试图阻止插入。这是一个边缘情况,所以我不担心不必要地使用PK值等等。

所以问题是,如果不修改表结构并使用任何事务,隔离级别和提示的组合,我如何通过最小量的锁定来实现这一目标?

2 个答案:

答案 0 :(得分: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 :(得分: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:

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