De: problème d'entrée simultanée SQL Server 2014
Question de suivi:
Comment supprimer et réinsérer des lignes dans un environnement parallèle et multithread tout en évitant les conditions de course, les blocages, etc.? Utilisons-nous encore (UPDLOCK, SERIALIZABLE
) ou une autre serrure?
Nous avons deux tables: Order
et OrderLineDetail
. La table Order
est le parent qui stocke les informations générales; et OrderLineDetail
est la table enfant, car les commandes ont plusieurs éléments de ligne de détail.
Les commandes peuvent être mises à jour, nous vérifions donc d'abord si OrderId
est présent dans le tableau, puis nous insérons ou mettons à jour en conséquence.
En raison de modifications du client, d'un problème de ligne, d'un serveur occupé, etc., les fichiers de commandes peuvent être envoyés plusieurs fois. Nous avons une colonne de temps Lastfiledatetime
. Si l'horodatage entrant est plus ancien, il n'a aucun effet sur la table.
Pour la table OrderLineDetail
, parfois nous n'avons pas de clés naturelles ou de clés de substitution dans nos fichiers, donc nous supprimons tous les éléments enfants OrderLineDetail
et repeuplons (il s'agit d'un ancien système de fichiers XML ).
Create Table Orders
(OrderId bigint primary key, -- this is in xml files
Lastfiledatetime datetime null
)
Create Table OrderLineDetail
(OrderLineDetailid bigint primary key identity(1,1), -- this is Not in the xml files
OrderLineDescription varchar(50),
OrderLineQuantity int,
OrderId bigint foreign key references Orders(OrderId),
Lastfiledatetime datetime
)
Nous travaillons dans un environnement de traitement parallèle multithreading, en utilisant le niveau d'isolement de capture instantanée de lecture de SQL Server 2016.
Pour mettre à jour le parent Order
, nous procédons comme suit:
BEGIN TRANSACTION;
IF NOT EXISTS
(
SELECT *
FROM Order WITH (UPDLOCK, SERIALIZABLE)
WHERE Order ID=@OrderID
)
INSERT INTO Order () VALUES ()
ELSE
UPDATE Order
SET ...
WHERE OrderID=@OrderID
AND LastFileDatetime<@CreatedTime;
COMMIT TRANSACTION;
Comment supprimer et réinsérer la table enfant OrderLineDetail
dans un environnement parallèle et multithreading tout en évitant les conditions de course, les blocages, etc.? Utilisons-nous encore (UPDLOCK, SERIALIZABLE
) ou une autre serrure?
BEGIN TRANSACTION;
IF EXISTS
(
SELECT *
FROM OrderLineDetail WITH (UPDLOCK, SERIALIZABLE)
WHERE Order ID=@OrderID
)
DELETE OrderLineDetail
WHERE OrderId=@OrderId
AND LastFileDatetime<@CreatedTime;
INSERT INTO OrderLineDetail () VALUES ()
COMMIT TRANSACTION;
Le UPDLOCK, SERIALIZABLE
le modèle consiste à éviter les résultats incorrects (y compris les erreurs de violation de fausse clé) en raison de conditions de concurrence , lors de l'exécution d'une opération particulièrement courante appelée UPSERT
- met à jour une ligne existante si elle existe; sinon, insérez une nouvelle ligne.
... tout en évitant les conditions de course, les blocages, etc.?
Vous semblez rechercher une combinaison magique d'indices qui permettra à un système de base de données d'effectuer des opérations hautement simultanées sans conflit . En général, il n'y a rien de tel. La seule façon d'éviter complètement les blocages est de toujours modifier les objets dans le même ordre.
Cela peut être difficile, voire impossible , à réaliser dans une relation de clé étrangère. Considérez que lors de l'insertion d'un nouvel objet, vous devez ajouter la ligne parent avant la ou les lignes enfants. Lors de la suppression d'un objet, vous devez supprimer le ou les enfants avant le parent.
Cela ne veut pas dire qu'un verrouillage soigneux n'empêchera pas encore les conditions de concurrence , mais il ne peut garantir d'éviter les blocages dans toutes les situations.
En ce qui concerne la question, oui , en utilisant le UPDLOCK, SERIALIZABLE
hint protègera des conditions de concurrence de UPSERT
, mais non , il n'empêchera pas les blocages. Cela peut même contribuer à augmenter leur fréquence. C'est difficile à évaluer à l'avance, et même les concepteurs de bases de données SQL Server expérimentés peuvent se tromper.
Pour les exigences plus complexes impliquant plusieurs objets, une solution courante consiste à toujours verrouiller exclusivement et exclusivement la ligne parent avant de modifier les enfants, même lorsque l'ordre naturel consiste à traiter l'enfant en premier.
Par exemple, lors de la suppression d'une commande:
DECLARE @OrderID bigint = 12345;
BEGIN TRANSACTION;
-- EXTRA STEP
-- Dummy update with XLOCK to exclusively lock the parent row
UPDATE TOP (1) dbo.Orders WITH (XLOCK)
SET Lastfiledatetime = NULL
WHERE OrderId = @OrderID;
-- Remove children
DELETE dbo.OrderLineDetail
WHERE OrderId = @OrderID;
-- Remove parent
DELETE dbo.Orders
WHERE OrderId = @OrderID;
COMMIT TRANSACTION;
L'étape supplémentaire de verrouillage exclusif de la ligne parent aidera à éliminer les interblocages, si toutes les modifications suivent le même ordre de modification: parent d'abord, puis enfant (ren). Si un processus accède à des objets dans l'ordre inverse, la possibilité d'un blocage dû à des verrous incompatibles se produit.
Notez qu'il peut être important de modifier réellement quelque chose dans la ligne parent, car SQL Server peut ne pas honorer le verrou exclusif (XLOCK
) dans certaines situations , s'il peut dire qu'il ne l'est pas nécessaire à l'opération en cours.
Une deuxième implémentation à peu près de la même idée consiste à utiliser des verrous d'application, comme mentionné dans les questions et réponses Implémentation de verrous d'application dans SQL Server (modèle de verrouillage distribué) (voir les liens de documentation ici).
C'est plus facile à certains égards, mais encore une fois, vous devez vous assurer que tout le code qui modifie les objets protégés utilise le même schéma. Dès que quelque chose peut accéder aux objets sous-jacents sans prendre le ou les verrous d'application requis, l'ensemble du schéma tombe en panne.
Un exemple ci-dessous montre comment nous pourrions prendre un verrou d'application exclusif sur un numéro de commande particulier avant de traiter les éléments de la commande:
BEGIN TRANSACTION;
-- The order number we want exclusive access to
DECLARE @OrderID integer = 12345;
-- Compute the locking resource string
DECLARE @Resource nvarchar(255) = N'dbo.Order' + CONVERT(nvarchar(11), @OrderID);
-- Return code from sys.sp_getapplock
DECLARE @RC integer;
-- Build dynamic SQL
DECLARE @SQL nvarchar(max) =
N'
EXECUTE @RC = sys.sp_getapplock
@Resource = ' + QUOTENAME(@Resource) + N',
@LockMode = Exclusive,
@LockTimeout = -1;'
-- Try to acquire the lock
EXECUTE sys.sp_executesql
@SQL,
N'@RC integer OUTPUT',
@RC = @RC OUTPUT;
-- Do something with the return code value if necessary
SELECT @RC;
--- Sensitive operations go here
-- Release the application lock early if you can
-- using sys.sp_releaseapplock
ROLLBACK TRANSACTION;
La concurrence est difficile; Désolé pour ça.
Et en utilisant la réponse de Paul White, heureusement, j'aurai moins de chances de blocage. Je supprimerai les détails de la commande enfant, mais je ne supprimerai jamais la commande parent. Je vais toujours insérer/modifier l'ordre parent, puis supprimer et insérer les détails de la commande enfant. Je devrai exécuter toutes les transactions dans cet ordre.
Donc ma dernière solution ci-dessous est de tout garder en une seule transaction,
Le VERROUILLAGE (UPDLOCK, SERIALIZABLE) dégénérera en un verrou exclusif pour l'insertion et la modification (et les verrous peuvent escalader mais ne se désamorcent jamais en transaction) - Je peux donc insérer/modifier la table de commande avec un nouveau verrou exclusif
Ensuite, je peux supprimer et insérer les détails de la ligne de commande enfant (qui obtiendront également des verrous exclusifs)
Je garderai tout dans cet ordre. Toutes les autres transactions seront interrompues jusqu'à ce que cela se termine, car il a un verrouillage exclusif dans la transaction.
BEGIN TRANSACTION;
IF NOT EXISTS
(
SELECT *
FROM Order WITH (UPDLOCK, SERIALIZABLE)
WHERE Order ID=@OrderID
)
INSERT INTO Order () VALUES ()
ELSE
BEGIN
UPDATE Order
SET ...
WHERE OrderID=@OrderID
AND LastFileDatetime<@CreatedTime
END
DELETE OrderLineDetail
WHERE OrderId=@OrderId
AND LastFileDatetime<@CreatedTime;
INSERT INTO OrderLineDetail () VALUES ()
COMMIT TRANSACTION;