J'ai une table qui est utilisée par une application existante pour remplacer les IDENTITY
champs de diverses autres tables.
Chaque ligne de la table stocke le dernier ID utilisé LastID
pour le champ nommé dans IDName
.
Parfois, le proc stocké se trouve dans une impasse - je crois avoir construit un gestionnaire d'erreur approprié; Cependant, je suis intéressé de voir si cette méthodologie fonctionne comme je le pense, ou si je me trompe d'arbre ici.
Je suis à peu près certain qu'il devrait y avoir un moyen d'accéder à cette table sans aucune impasse.
La base de données elle-même est configurée avec READ_COMMITTED_SNAPSHOT = 1
.
Tout d'abord, voici le tableau:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
Et l'index non clusterisé sur le IDName
champ:
CREATE NONCLUSTERED INDEX [IX_tblIDs_IDName]
ON [dbo].[tblIDs]
(
[IDName] ASC
)
WITH (
PAD_INDEX = OFF
, STATISTICS_NORECOMPUTE = OFF
, SORT_IN_TEMPDB = OFF
, DROP_EXISTING = OFF
, ONLINE = OFF
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON
, FILLFACTOR = 80
);
GO
Quelques exemples de données:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
La procédure stockée utilisée pour mettre à jour les valeurs stockées dans la table et renvoyer l'ID suivant:
CREATE PROCEDURE [dbo].[GetNextID](
@IDName nvarchar(255)
)
AS
BEGIN
/*
Description: Increments and returns the LastID value from tblIDs
for a given IDName
Author: Max Vernon
Date: 2012-07-19
*/
DECLARE @Retry int;
DECLARE @EN int, @ES int, @ET int;
SET @Retry = 5;
DECLARE @NewID int;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET NOCOUNT ON;
WHILE @Retry > 0
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM tblIDs
WHERE IDName = @IDName),0)+1;
IF (SELECT COUNT(IDName)
FROM tblIDs
WHERE IDName = @IDName) = 0
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID)
ELSE
UPDATE tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
SET @Retry = -2; /* no need to retry since the operation completed */
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
SET @Retry = @Retry - 1;
ELSE
BEGIN
SET @Retry = -1;
SET @EN = ERROR_NUMBER();
SET @ES = ERROR_SEVERITY();
SET @ET = ERROR_STATE()
RAISERROR (@EN,@ES,@ET);
END
ROLLBACK TRANSACTION;
END CATCH
END
IF @Retry = 0 /* must have deadlock'd 5 times. */
BEGIN
SET @EN = 1205;
SET @ES = 13;
SET @ET = 1
RAISERROR (@EN,@ES,@ET);
END
ELSE
SELECT @NewID AS NewID;
END
GO
Exemples d'exécution du proc stocké:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
MODIFIER:
J'ai ajouté un nouvel index car l'index existant IX_tblIDs_Name n'est pas utilisé par le SP; Je suppose que le processeur de requêtes utilise l'index clusterisé car il a besoin de la valeur stockée dans LastID. Quoi qu'il en soit, cet index est utilisé par le plan d'exécution réel:
CREATE NONCLUSTERED INDEX IX_tblIDs_IDName_LastID
ON dbo.tblIDs
(
IDName ASC
)
INCLUDE
(
LastID
)
WITH (FILLFACTOR = 100
, ONLINE=ON
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON);
EDIT # 2:
J'ai suivi le conseil donné par @AaronBertrand et l'ai légèrement modifié. L’idée générale ici est d’affiner la déclaration pour éliminer les verrouillages inutiles et, dans l’ensemble, pour rendre le SP plus efficace.
Le code ci-dessous remplace le code ci-dessus de BEGIN TRANSACTION
à END TRANSACTION
:
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM dbo.tblIDs
WHERE IDName = @IDName), 0) + 1;
IF @NewID = 1
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID);
ELSE
UPDATE dbo.tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
Étant donné que notre code n'ajoute jamais d'enregistrement à cette table avec 0 dans, LastID
nous pouvons supposer que si @NewID est égal à 1, l'intention est d'ajouter un nouvel ID à la liste, sinon nous mettons à jour une ligne existante de la liste.
la source
SERIALIZABLE
ici.Réponses:
Premièrement, je voudrais éviter de faire un aller-retour à la base de données pour chaque valeur. Par exemple, si votre application sait qu'elle a besoin de 20 nouveaux identifiants, ne faites pas 20 allers-retours. N'effectuez qu'un seul appel de procédure stockée et incrémentez le compteur de 20. De plus, il peut être préférable de scinder votre table en plusieurs.
Il est possible d'éviter complètement les impasses. Je n'ai aucune impasse dans mon système. Il y a plusieurs façons d'accomplir cela. Je vais vous montrer comment utiliser sp_getapplock pour éliminer les blocages. Je ne sais pas si cela fonctionnera pour vous, car SQL Server est une source fermée. Je ne peux donc pas voir le code source. Par conséquent, je ne sais pas si j'ai testé tous les cas possibles.
Ce qui suit décrit ce qui fonctionne pour moi. YMMV.
Tout d’abord, commençons par un scénario dans lequel nous obtenons toujours un nombre considérable de blocages. Deuxièmement, nous utiliserons sp_getapplock pour les éliminer. Le point le plus important ici est de tester votre solution. Votre solution peut être différente, mais vous devez l'exposer à une concurrence élevée, comme je le montrerai plus tard.
Conditions préalables
Laissez-nous mettre en place une table avec des données de test:
Les deux procédures suivantes risquent fort de s’emboîter dans une impasse:
Reproduction des impasses
Les boucles suivantes doivent reproduire plus de 20 blocages chaque fois que vous les exécutez. Si vous obtenez moins de 20, augmentez le nombre d'itérations.
Dans un onglet, lancez ceci;
Dans un autre onglet, exécutez ce script.
Assurez-vous de commencer les deux en quelques secondes.
Utilisation de sp_getapplock pour éliminer les blocages
Modifiez les deux procédures, réexécutez la boucle et vérifiez que vous n'avez plus de blocages:
Utiliser une table avec une ligne pour éliminer les blocages
Au lieu d'appeler sp_getapplock, nous pouvons modifier le tableau suivant:
Une fois que cette table est créée et remplie, nous pouvons remplacer la ligne suivante
avec celui-ci, dans les deux procédures:
Vous pouvez relancer le test de résistance et constater par vous-même que nous n’avons aucune impasse.
Conclusion
Comme nous l'avons vu, sp_getapplock peut être utilisé pour sérialiser l'accès à d'autres ressources. En tant que tel, il peut être utilisé pour éliminer les blocages.
Bien entendu, cela peut considérablement ralentir les modifications. Pour résoudre ce problème, nous devons choisir la granularité appropriée pour le verrou exclusif et, autant que possible, utiliser des ensembles au lieu de lignes individuelles.
Avant d’utiliser cette approche, vous devez faire un test de stress vous-même. Tout d’abord, vous devez vous assurer d’obtenir au moins une vingtaine de blocages avec votre approche originale. Deuxièmement, vous ne devriez avoir aucune impasse lorsque vous réexécutez le même script de repro en utilisant une procédure stockée modifiée.
En général, je ne pense pas qu'il y ait un bon moyen de déterminer si votre T-SQL est à l'abri des blocages simplement en le regardant ou en regardant le plan d'exécution. Messagerie Internet uniquement, le seul moyen de déterminer si votre code est sujet à des blocages est de l'exposer à une simultanéité élevée.
Bonne chance pour éliminer les impasses! Notre système ne connaît aucune impasse, ce qui est excellent pour notre équilibre travail-vie personnelle.
la source
UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;
prévenir les blocages?L’utilisation de l’
XLOCK
indice sur votreSELECT
approche ou sur les éléments suivantsUPDATE
devrait être à l’abri de ce type de blocage:Je reviendrai avec quelques autres variantes (si ce n’est pas battu!).
la source
XLOCK
empêchera un compteur existant d’être mis à jour à partir de plusieurs connexions, n’avez-vous pas besoinTABLOCKX
d’empêcher plusieurs connexions d’ajouter le même nouveau compteur?Mike Defehr m'a montré un moyen élégant d'accomplir cela de manière très légère:
(Pour être complet, voici la table associée au proc stocké)
Voici le plan d'exécution de la dernière version:
Et voici le plan d’exécution de la version originale (susceptible de blocage):
Clairement, la nouvelle version gagne!
À des fins de comparaison, la version intermédiaire avec
(XLOCK)
etc, produit le plan suivant:Je dirais que c'est une victoire! Merci pour l'aide de tous!
la source
SERIALIZABLE
n'existe pas pour empêcher les fantômes. Il existe une sémantique d’isolation sérialisable , c’est-à-dire le même effet persistant sur la base de données que si les transactions impliquées avaient été exécutées en série dans un ordre non spécifié.Ne pas voler le tonnerre de Mark Storey-Smith, mais il est sur quelque chose avec son poste ci-dessus (qui a d'ailleurs reçu le plus de votes positifs). Le conseil que j'ai donné à Max était centré autour de la construction "UPDATE set @variable = column = column + column" que je trouve vraiment cool, mais je pense qu'elle est peut-être non documentée (elle doit être prise en charge, même si elle existe spécifiquement pour le protocole TCP repères).
Voici une variante de la réponse de Mark - puisque vous renvoyez la nouvelle valeur d'ID sous forme de jeu d'enregistrements, vous pouvez supprimer complètement la variable scalaire, aucune transaction explicite ne devrait être nécessaire non plus, et je conviendrais qu'il est inutile de modifier le niveau d'isolation ainsi que. Le résultat est très propre et assez lisse ...
la source
J'ai corrigé un blocage similaire dans un système l'année dernière en modifiant ceci:
Pour ça:
En général, choisir un
COUNT
juste pour déterminer la présence ou l'absence est très inutile. Dans ce cas, puisque c’est 0 ou 1, ce n’est pas comme si c’était beaucoup de travail, mais (a) cette habitude peut se répercuter sur d’autres cas où elle coûtera beaucoup plus cher (dans ce cas, utilisezIF NOT EXISTS
plutôt queIF COUNT() = 0
), et (b) l'analyse supplémentaire est complètement inutile. LeUPDATE
effectue essentiellement le même contrôle.En outre, cela ressemble à une odeur sérieuse de code pour moi:
Quel est le point ici? Pourquoi ne pas simplement utiliser une colonne d'identité ou dériver cette séquence en utilisant
ROW_NUMBER()
au moment de la requête?la source
IDENTITY
. Cette table prend en charge certains codes hérités écrits dans MS Access qui seraient assez impliqués pour la mise à niveau. LaSET @NewID=
ligne incrémente simplement la valeur stockée dans la table pour l'ID donné (mais vous le savez déjà). Pouvez-vous développer sur comment je pourrais utiliserROW_NUMBER()
?LastID
signifie réellement votre modèle. Quel est son objectif? Le nom n'est pas tout à fait explicite. Comment Access l'utilise-t-il?GetNextID('WhatevertheIDFieldIsCalled')
pour obtenir le prochain ID à utiliser, puis l'insère dans la nouvelle ligne avec les données nécessaires.