L'instruction de fusion se bloque

22

J'ai la procédure suivante (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey forment la clé composite de la table cible. CompanyId est une clé étrangère vers une table parent. En outre, il existe un index non clusterisé CompanyId asc, UserId asc.

Il est appelé à partir de nombreux threads différents, et je reçois constamment des blocages entre différents processus appelant cette même instruction. Ma compréhension était que le "avec (verrou)" était nécessaire pour éviter les erreurs d'insertion / mise à jour des conditions de concurrence.

Je suppose que deux threads différents verrouillent des lignes (ou des pages) dans des ordres différents lorsqu'ils valident les contraintes, et donc bloquent.

est-ce une supposition correcte?

Quelle est la meilleure façon de résoudre cette situation (c.-à-d. Pas de blocages, impact minimum sur les performances multi-thread)?

Image du plan de requête (Si vous affichez l'image dans un nouvel onglet, elle est lisible. Désolé pour la petite taille.)

  • Il y a au plus 28 lignes dans le @datatable.
  • J'ai retracé le code et je ne vois nulle part où nous commençons une transaction ici.
  • La clé étrangère est configurée pour cascade uniquement lors de la suppression, et il n'y a pas eu de suppression de la table parent.
Sako73
la source

Réponses:

12

OK, après avoir tout examiné plusieurs fois, je pense que votre hypothèse de base était correcte. Ce qui se passe probablement ici, c'est que:

  1. La partie MATCH du MERGE vérifie l'index pour les correspondances, verrouillant en lecture ces lignes / pages au fur et à mesure.

  2. Lorsqu'il a une ligne sans correspondance, il essaiera d'insérer la nouvelle ligne d'index en premier afin de demander un verrouillage en écriture ligne / page ...

Mais si un autre utilisateur est également passé à l'étape 1 sur la même ligne / page, le premier utilisateur sera bloqué de la mise à jour et ...

Si le deuxième utilisateur doit également insérer sur la même page, il se trouve dans une impasse.

AFAIK, il n'y a qu'une seule (simple) façon d'être sûr à 100% que vous ne pouvez pas obtenir un blocage avec cette procédure et ce serait d'ajouter un indice TABLOCKX au MERGE, mais cela aurait probablement un très mauvais impact sur les performances.

Il est possible que l'ajout d'un indice TABLOCK à la place soit suffisant pour résoudre le problème sans avoir d'effet important sur vos performances.

Enfin, vous pouvez également essayer d'ajouter PAGLOCK, XLOCK ou les deux PAGLOCK et XLOCK. Encore une fois, cela pourrait fonctionner et les performances pourraient ne pas être trop terribles. Vous devrez l'essayer pour voir.

RBarryYoung
la source
Pensez-vous que le niveau d'isolement de l'instantané (versionnage de ligne) pourrait être utile ici?
Mikael Eriksson
Peut être. Ou il peut transformer les exceptions de blocage en exceptions de concurrence.
RBarryYoung
2
La spécification du conseil TABLOCK sur une table qui est la cible d'une instruction INSERT a le même effet que la spécification du conseil TABLOCKX. (Source: msdn.microsoft.com/en-us/library/bb510625.aspx )
tuespetre
31

Il n'y aurait pas de problème si la variable de table ne contenait qu'une seule valeur. Avec plusieurs lignes, il existe une nouvelle possibilité de blocage. Supposons que deux processus simultanés (A et B) s'exécutent avec des variables de table contenant (1, 2) et (2, 1) pour la même société.

Le processus A lit la destination, ne trouve aucune ligne et insère la valeur «1». Il détient un verrou de ligne exclusif sur la valeur «1». Le processus B lit la destination, ne trouve aucune ligne et insère la valeur «2». Il détient un verrou de ligne exclusif sur la valeur «2».

Le processus A doit désormais traiter la ligne 2 et le processus B doit traiter la ligne 1. Aucun des deux processus ne peut progresser car il nécessite un verrou incompatible avec le verrou exclusif détenu par l'autre processus.

Pour éviter les blocages avec plusieurs lignes, les lignes doivent être traitées (et les tables accédées) dans le même ordre à chaque fois . La variable de table dans le plan d'exécution montré dans la question est un tas, donc les lignes n'ont pas d'ordre intrinsèque (elles sont très susceptibles d'être lues dans l'ordre d'insertion, bien que cela ne soit pas garanti):

Plan existant

L'absence d'un ordre de traitement des lignes cohérent conduit directement à l'opportunité de blocage. Une deuxième considération est que l'absence d'une garantie d'unicité clé signifie qu'une bobine de table est nécessaire pour fournir une protection Halloween correcte. Le spool est un spool enthousiaste, ce qui signifie que toutes les lignes sont écrites dans une table de travail tempdb avant d'être lues et relues pour l'opérateur d'insertion.

Redéfinir la TYPEvariable de table pour inclure un cluster PRIMARY KEY:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

Le plan d'exécution affiche désormais une analyse de l'index clusterisé et la garantie d'unicité signifie que l'optimiseur est en mesure de supprimer le spouleur de table en toute sécurité:

Avec clé primaire

Dans les tests avec 5000 itérations de l' MERGEinstruction sur 128 threads, aucun blocage n'a eu lieu avec la variable de table en cluster. Je dois souligner que ce n'est que sur la base de l'observation; la variable de table en cluster pourrait également ( techniquement ) produire ses lignes dans une variété d'ordres, mais les chances d'un ordre cohérent sont très considérablement améliorées. Le comportement observé devrait être retesté pour chaque nouvelle mise à jour cumulative, service pack ou nouvelle version de SQL Server, bien sûr.

Si la définition de la variable de table ne peut pas être modifiée, il existe une autre alternative:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Cela permet également d'éliminer le spool (et la cohérence de l'ordre des lignes) au prix de l'introduction d'un tri explicite:

Plan de tri

Ce plan n'a également produit aucun blocage en utilisant le même test. Script de reproduction ci-dessous:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;
Paul White dit GoFundMonica
la source
8

Je pense que SQL_Kiwi a fourni une très bonne analyse. Si vous devez résoudre le problème dans la base de données, vous devez suivre sa suggestion. Bien sûr, vous devez retester qu'il fonctionne toujours pour vous chaque fois que vous effectuez une mise à niveau, appliquez un Service Pack ou ajoutez / modifiez un index ou une vue indexée.

Il existe trois autres alternatives:

  1. Vous pouvez sérialiser vos insertions afin qu'elles ne se heurtent pas: vous pouvez invoquer sp_getapplock au début de votre transaction et acquérir un verrou exclusif avant d'exécuter votre MERGE. Bien sûr, vous devez encore le tester.

  2. Vous pouvez avoir un thread gérer toutes vos insertions, de sorte que votre serveur d'applications gère la concurrence.

  3. Vous pouvez réessayer automatiquement après les blocages - cela peut être l'approche la plus lente si la simultanéité est élevée.

Quoi qu'il en soit, vous seul pouvez déterminer l'impact de votre solution sur les performances.

En règle générale, nous n'avons pas de blocages dans notre système, bien que nous ayons beaucoup de potentiel pour les avoir. En 2011, nous avons commis une erreur dans un déploiement et une demi-douzaine de blocages se sont produits en quelques heures, tous suivant le même scénario. J'ai corrigé cela rapidement et c'était tous les blocages de l'année.

Nous utilisons principalement l'approche 1 dans notre système. Cela fonctionne vraiment bien pour nous.

AK
la source
-1

Une autre approche possible - j'ai trouvé que Merge présente parfois des problèmes de verrouillage et de performances - cela peut valoir la peine de jouer avec l'option de requête Option (MaxDop x)

Dans un passé sombre et lointain, SQL Server avait une option de verrouillage de niveau d'insertion de ligne - mais cela semble être mort, mais un PK en cluster avec une identité devrait rendre les insertions propres.

Ed Green
la source