2012-03-06 4 views
3

J'ai beaucoup lu sur la prévention des conditions de course, mais généralement avec un enregistrement dans un scénario plus élevé. Par exemple: Atomic UPSERT in SQL Server 2005Prévenir les conditions de course sur plusieurs rangées

J'ai une exigence différente, et il est d'empêcher les conditions de course sur plusieurs rangées. Par exemple, dire que j'ai la structure de tableau suivant:

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 

Il pourrait y avoir plusieurs processus insérer dans GiftCardTransactions et je dois éviter d'insérer si SUM(GiftCardTransactions.Amount) + insertingAmount irait plus GiftCards.OriginalAmount.

Je sais que je pourrais utiliser TABLOCKX sur GiftCardTransactions, mais évidemment ce ne serait pas faisable pour beaucoup de transactions. Une autre façon serait d'ajouter une colonne GiftCards.RemainingAmount et ensuite j'ai seulement besoin de verrouiller une ligne (mais avec possibilité d'escalade de verrou), mais malheureusement ce n'est pas une option pour moi en ce moment (cela aurait-il été la meilleure option?) . Au lieu d'essayer d'empêcher l'insertion en premier lieu, peut-être la réponse est d'insérer simplement, puis sélectionnez SUM(GiftCardTransactions.Amount), et l'annulation si nécessaire. C'est un cas limite, donc je ne m'inquiète pas d'utiliser inutilement des valeurs PK, etc.

Donc la question est, sans modifier la structure de la table et en utilisant n'importe quelle combinaison de transactions, niveaux d'isolement et astuces, comment puis-je atteindre ceci avec une quantité minimale de verrouillage?

Répondre

8

J'ai déjà rencontré cette situation dans le passé et j'ai fini par utiliser SP_GetAppLock pour créer un sémaphore sur une clé afin d'éviter une condition de concurrence. J'ai écrit un article il y a plusieurs années sur différentes méthodes. L'article est ici:

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

L'idée de base est que vous acquérez un verrou sur une clé construite qui est séparée de la table. De cette façon, vous pouvez être très précis et seulement blocs spids qui pourraient créer une condition de concurrence et ne pas bloquer les autres consommateurs de la table.

J'ai laissé la viande de l'article ci-dessous mais je voudrais appliquer cette technique par l'acquisition d'un verrou sur une clé construite telle que

@Key = 'GiftCardTransaction' + GiftCardId 

un verrou sur cette touche (et vous assurer d'appliquer systématiquement cette approche) empêcherait toute condition de concurrence potentielle comme le premier à acquérir le verrou ferait son travail avec toutes les autres demandes attendues pour le verrouillage d'être libéré (ou expirer, selon comment vous voulez que votre application fonctionne.)

La viande de l'article est ici:

SP_getapplock est un wrapper pour la procédure étendue XP_USERLOCK. Il vous permet d'utiliser le mécanisme de verrouillage SQL SERVERs pour gérer la concurrence en dehors de la portée des tables et des lignes. Il peut être utilisé pour mars PROC appels de la même manière les solutions ci-dessus avec quelques fonctionnalités supplémentaires. Ajoute des verrous directement à la mémoire du serveur, ce qui réduit vos frais généraux.

Sp_getapplock Deuxièmement, vous pouvez spécifier un délai de verrouillage sans avoir besoin de modifier les paramètres de session.Dans les cas où vous ne souhaitez qu'un seul appel pour une clé particulière, un délai d'attente rapide garantit que le processus ne retarde pas l'exécution de l'application.

Troisièmement, sp_getapplock renvoie un état qui peut être utile pour déterminer si le code doit s'exécuter du tout. Encore une fois, dans les cas où vous ne voulez qu'un appel pour une clé particulière, un code retour de 1 vous dira que le verrou a été accordé avec succès après avoir attendu que d'autres verrous incompatibles soient libérés, ainsi vous pouvez quitter sans plus de code (comme un contrôle d'existence, par exemple). Le SYNAX est la suivante:

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

Un exemple en utilisant 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 

Je sais que cela va sans dire, mais puisque la portée des serrures de sp_getapplock est une transaction explicite, être sûr de SET XACT_ABORT ON , ou inclure des vérifications dans le code pour s'assurer qu'un ROLLBACK se produit si nécessaire.

1

Mon T-SQL est un peu rouillé, mais voici ma solution à une solution. L'astuce consiste à prendre un verrou de mise à jour sur toutes les transactions pour cette carte cadeau au début de la transaction, de sorte que tant que toutes les procédures ne lisent pas les données non validées (comportement par défaut), cela verrouillera les transactions de la carte-cadeau ciblée seulement.

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 

Bien que ce soit très explicite, je pense qu'il serait plus efficace et plus T-SQL convivial pour utiliser une instruction rollback comme:

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 
Questions connexes