Dans l'une de nos bases de données, nous avons une table qui est intensivement accédée simultanément par plusieurs threads. Les threads mettent à jour ou insèrent des lignes via MERGE
. Il y a aussi des threads qui suppriment des lignes à l'occasion, donc les données de table sont très volatiles. Les threads faisant des insertions souffrent parfois d'un blocage. Le problème ressemble à celui décrit dans cette question. La différence, cependant, est que dans notre cas, chaque thread se met à jour ou insère exactement une ligne .
La configuration simplifiée suit. La table est un tas avec deux index non cluster uniques sur
CREATE TABLE [Cache]
(
[UID] uniqueidentifier NOT NULL CONSTRAINT DF_Cache_UID DEFAULT (newid()),
[ItemKey] varchar(200) NOT NULL,
[FileName] nvarchar(255) NOT NULL,
[Expires] datetime2(2) NOT NULL,
CONSTRAINT [PK_Cache] PRIMARY KEY NONCLUSTERED ([UID])
)
GO
CREATE UNIQUE INDEX IX_Cache ON [Cache] ([ItemKey]);
GO
et la requête typique est
DECLARE
@itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
@fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';
MERGE INTO [Cache] WITH (HOLDLOCK) T
USING (
VALUES (@itemKey, @fileName, dateadd(minute, 10, sysdatetime()))
) S(ItemKey, FileName, Expires)
ON T.ItemKey = S.ItemKey
WHEN MATCHED THEN
UPDATE
SET
T.FileName = S.FileName,
T.Expires = S.Expires
WHEN NOT MATCHED THEN
INSERT (ItemKey, FileName, Expires)
VALUES (S.ItemKey, S.FileName, S.Expires)
OUTPUT deleted.FileName;
c'est-à-dire que la correspondance se produit par une clé d'index unique. L'indice HOLDLOCK
est ici, en raison de la simultanéité (comme conseillé ici ).
J'ai fait une petite enquête et voici ce que j'ai trouvé.
Dans la plupart des cas, le plan d'exécution des requêtes est
avec le schéma de verrouillage suivant
c'est-à-dire IX
verrouiller l'objet suivi de verrous plus granuleux.
Parfois, cependant, le plan d'exécution des requêtes est différent
(cette forme de plan peut être forcée en ajoutant un INDEX(0)
indice) et son motif de verrouillage est
remarquez que le X
verrou placé sur l'objet après IX
est déjà placé.
Étant donné que deux IX
sont compatibles, mais deux X
ne le sont pas, la chose qui se produit sous la concurrence est
impasse !
Et ici se pose la première partie de la question . Placer un X
verrou sur un objet après avoir été IX
éligible? N'est-ce pas un bug?
La documentation indique:
Les verrous d'intention sont appelés verrous d'intention car ils sont acquis avant un verrou au niveau inférieur, et signalent donc l' intention de placer des verrous à un niveau inférieur .
et aussi
IX signifie l'intention de mettre à jour seulement certaines des lignes plutôt que toutes
donc, placer le X
verrou sur l'objet après IX
me semble TRÈS suspect.
J'ai d'abord essayé d'empêcher le blocage en essayant d'ajouter des conseils de verrouillage de table
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCK) T
et
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCKX) T
avec le TABLOCK
motif de verrouillage en place devient
et avec le TABLOCKX
motif de verrouillage est
puisque deux SIX
(ainsi que deux X
) ne sont pas compatibles, cela empêche efficacement le blocage, mais, malheureusement, empêche également la concurrence (ce qui n'est pas souhaité).
Mes prochaines tentatives consistaient à ajouter PAGLOCK
et ROWLOCK
à rendre les verrous plus granulaires et à réduire les conflits. Les deux n'ont aucun effet ( X
sur l'objet a encore été observé immédiatement après IX
).
Ma dernière tentative a consisté à forcer une "bonne" forme de plan d'exécution avec un bon verrouillage granulaire en ajoutant un FORCESEEK
indice
MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T
et ça a marché.
Et ici se pose la deuxième partie de la question . Se pourrait-il qu'il FORCESEEK
soit ignoré et qu'un mauvais schéma de verrouillage soit utilisé? (Comme je l'ai mentionné, PAGLOCK
et ROWLOCK
ont été ignorés apparemment).
L'ajout UPDLOCK
n'a aucun effet ( X
sur l'objet encore observable après IX
).
Faire IX_Cache
index ordonné en clusters, comme prévu, a travaillé. Cela a conduit à planifier avec la recherche d'index cluster et le verrouillage granulaire. De plus, j'ai essayé de forcer le scan d'index en cluster qui montrait également un verrouillage granulaire.
Toutefois. Observation supplémentaire. Dans la configuration d'origine, même avec FORCESEEK(IX_Cache(ItemKey)))
en place, si l'on change la @itemKey
déclaration de variable de varchar (200) en nvarchar (200) , le plan d'exécution devient
voir que la recherche est utilisée, MAIS le schéma de verrouillage dans ce cas montre à nouveau le X
verrou placé sur l'objet après IX
.
Ainsi, il semble que forcer la recherche ne garantisse pas nécessairement des verrous granulaires (et donc une absence de blocages). Je ne suis pas sûr que le fait d'avoir un index clusterisé garantisse un verrouillage granulaire. Ou alors?
Ma compréhension (corrigez-moi si je me trompe) est que le verrouillage est dans une large mesure situationnel, et qu'une certaine forme de plan d'exécution n'implique pas un certain schéma de verrouillage.
La question de l'éligibilité de placer le X
verrou sur l'objet après IX
toujours ouvert. Et s'il est éligible, y a-t-il quelque chose que l'on puisse faire pour empêcher le verrouillage des objets?
Réponses:
Cela semble un peu étrange, mais c'est valide. Au moment de la
IX
prise, l'intention pourrait bien être de prendre desX
serrures à un niveau inférieur. Il n'y a rien à dire que de tels verrous doivent effectivement être retirés. Après tout, il n'y a peut-être rien à verrouiller au niveau inférieur; le moteur ne peut pas le savoir à l'avance. De plus, il peut y avoir des optimisations telles que les verrous de niveau inférieur peuvent être ignorés (un exemple pourIS
et lesS
verrous peuvent être vus ici ).Plus précisément pour le scénario actuel, il est vrai que les verrous de plage de clés sérialisables ne sont pas disponibles pour un segment de mémoire, donc la seule alternative est un
X
verrou au niveau de l'objet. En ce sens, le moteur pourrait être en mesure de détecter précocement qu'unX
verrou sera inévitablement nécessaire si la méthode d'accès est une analyse en tas, et donc d'éviter de prendre leIX
verrou.D'un autre côté, le verrouillage est complexe et des verrous d'intention peuvent parfois être pris pour des raisons internes qui ne sont pas nécessairement liées à l'intention de prendre des verrous de niveau inférieur. La prise
IX
peut être le moyen le moins invasif de fournir une protection requise pour certains cas de bord obscur. Pour un type de considération similaire, voir Verrouillage partagé émis sur IsolationLevel.ReadUncommitted .Donc, la situation actuelle est malheureuse pour votre scénario de blocage, et elle peut être évitable en principe, mais ce n'est pas nécessairement la même chose qu'être un «bug». Vous pouvez signaler le problème via votre canal d'assistance normal ou sur Microsoft Connect, si vous avez besoin d'une réponse définitive à ce sujet.
Non
FORCESEEK
est moins un indice et plus une directive. Si l'optimiseur ne parvient pas à trouver un plan qui respecte le «conseil», il produira une erreur.Forcer l'index est un moyen de garantir que les verrous de plage de clés peuvent être pris. Avec les verrous de mise à jour pris naturellement lors du traitement d'une méthode d'accès pour que les lignes changent, cela offre une garantie suffisante pour éviter les problèmes de concurrence dans votre scénario.
Si le schéma de la table ne change pas (par exemple en ajoutant un nouvel index), l'indice est également suffisant pour éviter ce blocage de la requête avec lui-même. Il existe toujours une possibilité de blocage cyclique avec d'autres requêtes qui pourraient accéder au segment de mémoire avant l'index non cluster (comme une mise à jour de la clé de l'index non cluster).
Cela brise la garantie qu'une seule ligne sera affectée, donc une bobine de table désireuse est introduite pour la protection d'Halloween. Pour contourner ce problème, expliquez la garantie avec
MERGE TOP (1) INTO [Cache]...
.Il y a certainement beaucoup plus de choses qui sont visibles dans un plan d'exécution. Vous pouvez forcer une certaine forme de plan avec par exemple un guide de plan, mais le moteur peut toujours décider de prendre différents verrous lors de l'exécution. Les chances sont assez faibles si vous intégrez l'
TOP (1)
élément ci-dessus.Remarques générales
Il est quelque peu inhabituel de voir une table de tas utilisée de cette manière. Vous devriez considérer les avantages de le convertir en une table en cluster, peut-être en utilisant l'index Dan Guzman suggéré dans un commentaire:
Cela peut présenter des avantages importants en matière de réutilisation de l'espace, tout en fournissant une bonne solution de contournement pour le problème de blocage actuel.
MERGE
est également légèrement inhabituel à voir dans un environnement à forte concurrence. Un peu contre-intuitivement, il est souvent plus efficace d'exécuter des instructions séparéesINSERT
etUPDATE
, par exemple:Notez que la recherche RID n'est plus nécessaire:
Si vous pouvez garantir l'existence d'un index unique sur
ItemKey
(comme dans la question) le redondantTOP (1)
dans leUPDATE
peut être supprimé, donnant le plan le plus simple:Les deux
INSERT
et lesUPDATE
plans sont admissibles à un plan trivial dans les deux cas.MERGE
nécessite toujours une optimisation complète basée sur les coûts.Consultez le Q & A connexe SQL Server 2014 problème d'entrée simultanée pour le modèle correct à utiliser, et plus d'informations sur
MERGE
.Les blocages ne peuvent pas toujours être évités. Ils peuvent être réduits au minimum avec un codage et une conception soignés, mais l'application doit toujours être prête à gérer avec élégance l'impasse impaire (par exemple, revérifiez les conditions puis réessayez).
Si vous avez un contrôle total sur les processus qui accèdent à l'objet en question, vous pouvez également envisager d'utiliser des verrous d'application pour sérialiser l'accès à des éléments individuels, comme décrit dans SQL Server Concurrent Inserts and Deletes .
la source