web-dev-qa-db-fra.com

Prévention des blocages MERGE

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 existe également des threads qui suppriment des lignes à l'occasion, de sorte que 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 la question this . La différence, cependant, est que dans notre cas, chaque thread 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. Le conseil HOLDLOCK est ici, à cause de la concurrence (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 seek execution plan

avec le schéma de verrouillage suivant

index seek locking pattern

c'est-à-dire IX verrouiller l'objet suivi de verrous plus granulaires.

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

table scan execution plan

(cette forme de plan peut être forcée en ajoutant INDEX(0) hint) et son schéma de verrouillage est

table scan locking pattern

remarque X verrou placé sur l'objet après que IX est déjà placé.

Puisque deux IX sont compatibles, mais deux X ne le sont pas, ce qui se passe sous la concurrence est

deadlock

deadlock graph

blocage !

Et ici se pose la première partie de la question . Le verrouillage de X sur l'objet après IX est-il éligible? N'est-ce pas un bug?

Documentation déclare:

Verrous d'intention sont nommés verrous d'intention car ils sont acquis avant un verrou au niveau inférieur, et donc signalent l'intention de placer les verrous à un plus bas niveau .

et aussi

IX signifie l'intention de mettre à jour seulement certaines des lignes plutôt que toutes

donc, placer 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 schéma de verrouillage TABLOCK en place devient

merge holdlock tablock locking pattern

et avec le schéma de verrouillage TABLOCKX est

merge holdlock tablockx locking pattern

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 pour rendre les verrous plus précis et réduire les conflits. Les deux n'ont aucun effet (X sur l'objet était toujours observé immédiatement après IX).

Ma dernière tentative consistait à forcer une "bonne" forme de plan d'exécution avec un bon verrouillage granulaire en ajoutant FORCESEEK hint

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 que FORCESEEK soit ignoré et qu'un mauvais schéma de verrouillage soit utilisé? (Comme je l'ai mentionné, PAGLOCK et ROWLOCK étaient apparemment ignorés).


L'ajout de UPDLOCK n'a aucun effet (X sur un objet encore observable après IX).

La mise en cluster de IX_Cache, Comme prévu, a fonctionné. Cela a conduit à planifier avec Clustered Index Seek et un verrouillage granulaire. De plus, j'ai essayé de forcer Analyse 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 déclaration de variable @itemKey De varchar (200) à nvarchar (200) , le plan d'exécution devient

index seek execution plan with nvarchar

voir que la recherche est utilisée, MAIS le schéma de verrouillage dans ce cas montre à nouveau X verrou placé sur l'objet après IX.

Il semble donc que le forçage de la recherche ne garantisse pas nécessairement les verrous granulaires (et donc l'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 que la forme de certains plans d'exécution n'implique pas un certain schéma de verrouillage.

La question de l'éligibilité de placer X verrou sur l'objet après IX est toujours ouverte. Et s'il est éligible, y a-t-il quelque chose que l'on puisse faire pour empêcher le verrouillage des objets?

8
i-one

Le placement de IX suivi de X sur l'objet est-il éligible? Est-ce un bug ou non?

Cela semble un peu étrange, mais c'est valide. Au moment où le IX est pris, l'intention pourrait bien être de prendre les verrous X à 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 les verrous IS et S peut être vu 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, donc la seule alternative est un verrou X au niveau de l'objet. En ce sens, le moteur pourrait être en mesure de détecter précocement qu'un verrou X sera inévitablement requis si la méthode d'accès est une analyse en tas, et ainsi éviter de prendre le verrou IX.

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. Prendre IX peut être le moyen le moins invasif de fournir une protection requise pour certains cas Edge obscurs. Pour une considération similaire, voir Verrouillage partagé émis sur IsolationLevel.ReadUncommitted .

Ainsi, la situation actuelle est regrettable 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 que FORCESEEK soit ignoré et qu'un mauvais schéma de verrouillage soit utilisé?

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

... 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 incorporez l'élément TOP (1) 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 avoir des avantages importants de réutilisation de l'espace, ainsi que fournir 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 INSERT et UPDATE distinctes, 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:

Execution plan

Si vous pouvez garantir l'existence d'un index unique sur ItemKey (comme dans la question), la TOP (1) redondante dans UPDATE peut être supprimée, ce qui donne le plan le plus simple:

Simplified update

Les plans INSERT et UPDATE sont éligibles pour un plan trivial dans les deux cas. MERGE nécessite toujours une optimisation complète basée sur les coûts.

Reportez-vous aux questions et réponses Problème d'entrée simultanée de SQL Server 2014 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érifier les conditions puis réessayer).

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 .

8
Paul White 9