Disons que vous avez le code suivant (veuillez ignorer que c'est affreux):
BEGIN TRAN;
DECLARE @id int
SELECT @id = id + 1 FROM TableA;
UPDATE TableA SET id = @id; --TableA must have only one row, apparently!
COMMIT TRAN;
-- @id is returned to the client or used somewhere else
À mon avis, cela ne gère PAS correctement la concurrence. Ce n'est pas parce que vous avez une transaction que quelqu'un d'autre ne lira pas la même valeur que vous aviez avant d'arriver à votre déclaration de mise à jour.
Maintenant, en laissant le code tel quel (je me rends compte que cela est mieux géré comme une seule instruction ou encore mieux en utilisant une colonne d'auto-incrémentation/d'identité), quels sont les moyens sûrs de le faire gérer correctement la concurrence et d'empêcher les conditions de concurrence qui permettent à deux clients d'obtenir la même chose valeur id?
Je suis presque sûr que l'ajout d'une WITH (UPDLOCK, HOLDLOCK)
au SELECT fera l'affaire. Le niveau d'isolation des transactions SERIALISABLES semble fonctionner aussi puisqu'il interdit à quiconque de lire ce que vous avez fait jusqu'à la fin du transfert ( [~ # ~] mise à jour [ ~ # ~] : c'est faux. Voir la réponse de Martin). Est-ce vrai? Vont-ils tous les deux fonctionner aussi bien? Est-ce que l'un est préféré à l'autre?
Imaginez faire quelque chose de plus légitime qu'une mise à jour d'ID - un calcul basé sur une lecture que vous devez mettre à jour. Il pourrait y avoir de nombreuses tables impliquées, dont certaines que vous écrirez et d'autres que vous ne ferez pas. Quelle est la meilleure pratique ici?
Après avoir écrit cette question, je pense que les conseils de verrouillage sont meilleurs car vous ne verrouillez que les tables dont vous avez besoin, mais j'apprécierais la contribution de n'importe qui.
P.S. Et non, je ne connais pas la meilleure réponse et je veux vraiment mieux comprendre! :)
Il suffit de traiter l'aspect du niveau d'isolation SERIALIZABLE
. Oui, cela fonctionnera, mais avec un risque de blocage.
Deux transactions pourront toutes deux lire la ligne simultanément. Ils ne se bloqueront pas car ils prendront soit un objet S
un verrou ou un index RangeS-S
verrous dépendant de la structure de la table et ces verrous sont compatibles . Mais ils se bloqueront lorsqu'ils tenteront d'acquérir les verrous nécessaires à la mise à jour (objet IX
verrou ou index RangeS-U
respectivement), ce qui entraînera un blocage.
L'utilisation d'un indice explicite UPDLOCK
à la place sérialisera les lectures, évitant ainsi le risque de blocage.
Je pense que la meilleure approche pour vous serait d'exposer votre module à une concurrence élevée et de voir par vous-même. Parfois, UPDLOCK seul suffit et il n'est pas nécessaire de HOLDLOCK. Parfois, sp_getapplock fonctionne très bien. Je ne ferais aucune déclaration générale ici - parfois, l'ajout d'un index, d'un déclencheur ou d'une vue indexée supplémentaire modifie le résultat. Nous devons souligner le code de test et voir par nous-mêmes au cas par cas.
J'ai écrit plusieurs exemples de tests de résistance ici
Edit: pour une meilleure connaissance des internes, vous pouvez lire les livres de Kalen Delaney. Cependant, les livres peuvent se désynchroniser comme toute autre documentation. De plus, il y a trop de combinaisons à considérer: six niveaux d'isolement, de nombreux types de verrous, des index cluster/non cluster et qui sait quoi d'autre. C'est beaucoup de combinaisons. En plus de cela, SQL Server est une source fermée, nous ne pouvons donc pas télécharger le code source, le déboguer, etc. - ce serait la source ultime de connaissances. Tout le reste peut être incomplet ou obsolète après la prochaine version ou le prochain Service Pack.
Donc, vous ne devriez pas décider de ce qui fonctionne pour votre système sans vos propres tests de résistance. Quoi que vous ayez lu, cela peut vous aider à comprendre ce qui se passe, mais vous devez prouver que les conseils que vous avez lus fonctionnent pour vous. Je pense que personne ne peut le faire pour vous.
Dans ce cas particulier, l'ajout d'un verrou UPDLOCK
au SELECT
éviterait en effet des anomalies. L'ajout de HOLDLOCK
n'est pas nécessaire car un verrou de mise à jour est maintenu pendant la durée de la transaction, mais j'avoue l'inclure moi-même comme une (peut-être mauvaise) habitude dans le passé.
Imaginez faire quelque chose de plus légitime qu'une mise à jour d'ID, un calcul basé sur une lecture que vous devez mettre à jour. Il pourrait y avoir de nombreuses tables impliquées, dont certaines que vous écrirez et d'autres que vous ne ferez pas. Quelle est la meilleure pratique ici?
Il n'y a pas de meilleure pratique. Votre choix de contrôle d'accès simultané doit être basé sur les exigences de l'application. Certaines applications/transactions doivent s'exécuter comme si elles détenaient la propriété exclusive de la base de données, évitant à tout prix les anomalies et les inexactitudes. D'autres applications/transactions peuvent tolérer un certain degré d'interférence les unes des autres.
Edit: @ Le commentaire d'AlexKuznetsov m'a incité à relire la question et à supprimer l'erreur très évidente dans ma réponse. Remarque à vous-même sur l'affichage tard dans la nuit.