Prévention des blocages MERGE

9

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 HOLDLOCKest 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

index recherche plan d'exécution

avec le schéma de verrouillage suivant

recherche d'index modèle de verrouillage

c'est-à-dire IXverrouiller l'objet suivi de verrous plus granuleux.

Parfois, cependant, le plan d'exécution des requêtes est différent

plan d'exécution de l'analyse de table

(cette forme de plan peut être forcée en ajoutant un INDEX(0)indice) et son motif de verrouillage est

modèle de verrouillage de numérisation de table

remarquez que le Xverrou placé sur l'objet après IXest déjà placé.

Étant donné que deux IXsont compatibles, mais deux Xne le sont pas, la chose qui se produit sous la concurrence est

impasse

graphique de blocage

impasse !

Et ici se pose la première partie de la question . Placer un Xverrou 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 Xverrou sur l'objet après IXme 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 TABLOCKmotif de verrouillage en place devient

fusionner le modèle de verrouillage du verrou

et avec le TABLOCKXmotif de verrouillage est

fusionner le modèle de verrouillage holdlock tablockx

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 PAGLOCKet ROWLOCKà rendre les verrous plus granulaires et à réduire les conflits. Les deux n'ont aucun effet ( Xsur 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 FORCESEEKindice

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 FORCESEEKsoit ignoré et qu'un mauvais schéma de verrouillage soit utilisé? (Comme je l'ai mentionné, PAGLOCKet ROWLOCKont été ignorés apparemment).


L'ajout UPDLOCKn'a aucun effet ( Xsur l'objet encore observable après IX).

Faire IX_Cacheindex 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 @itemKeydéclaration de variable de varchar (200) en nvarchar (200) , le plan d'exécution devient

index recherche un plan d'exécution avec nvarchar

voir que la recherche est utilisée, MAIS le schéma de verrouillage dans ce cas montre à nouveau le Xverrou 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 Xverrou sur l'objet après IXtoujours ouvert. Et s'il est éligible, y a-t-il quelque chose que l'on puisse faire pour empêcher le verrouillage des objets?

i-one
la source
Demande connexe sur feedback.azure.com
i-one

Réponses:

9

Le placement IXsuivi de Xl'objet est-il éligible? Est-ce un bug ou non?

Cela semble un peu étrange, mais c'est valide. Au moment de la IXprise, l'intention pourrait bien être de prendre des Xserrures à 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 pour ISet les Sverrous 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 Xverrou au niveau de l'objet. En ce sens, le moteur pourrait être en mesure de détecter précocement qu'un Xverrou sera inévitablement nécessaire si la méthode d'accès est une analyse en tas, et donc d'éviter de prendre le IXverrou.

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 IXpeut ê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.

Se pourrait-il qu'il FORCESEEKsoit ignoré et qu'un mauvais schéma de verrouillage soit utilisé?

Non FORCESEEKest 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).

... déclaration de variable de varchar(200)à nvarchar(200)...

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]....

D'après ce que je comprends, [...] le verrouillage est dans une large mesure situationnel et la forme de certains plans d'exécution n'implique pas un certain schéma de verrouillage.

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:

CREATE UNIQUE CLUSTERED INDEX IX_Cache ON [Cache] ([ItemKey]);

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.

MERGEest é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ées INSERTet UPDATE, par exemple:

DECLARE
    @itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
    @fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';

BEGIN TRANSACTION;

    DECLARE @expires datetime2(2) = DATEADD(MINUTE, 10, SYSDATETIME());

    UPDATE TOP (1) dbo.Cache WITH (SERIALIZABLE, UPDLOCK)
    SET [FileName] = @fileName,
        Expires = @expires
    OUTPUT Deleted.[FileName]
    WHERE
        ItemKey = @itemKey;

    IF @@ROWCOUNT = 0
        INSERT dbo.Cache
            (ItemKey, [FileName], Expires)
        VALUES
            (@itemKey, @fileName, @expires);

COMMIT TRANSACTION;

Notez que la recherche RID n'est plus nécessaire:

Plan d'exécution

Si vous pouvez garantir l'existence d'un index unique sur ItemKey(comme dans la question) le redondant TOP (1)dans le UPDATEpeut être supprimé, donnant le plan le plus simple:

Mise à jour simplifiée

Les deux INSERTet les UPDATEplans sont admissibles à un plan trivial dans les deux cas. MERGEné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 .

Paul White 9
la source