Résoudre le blocage de 2 tables uniquement lié à la vue indexée

17

J'ai une situation où je reçois des blocages, et je pense que j'ai réduit les coupables, mais je ne sais pas trop ce que je peux faire pour y remédier.

Il s'agit d'un environnement de production exécutant SQL Server 2008 R2.

Pour vous donner une vue légèrement simplifiée de la situation:


J'ai 3 tableaux tels que définis ci-dessous:

TABLE activity (
    id, -- PK
    ...
)

TABLE member_activity (
    member_id, -- PK col 1
    activity_id, -- PK col 2
    ...
)

TABLE follow (
    id, -- PK
    follower_id,
    member_id,
    ...
)

La member_activitytable a une clé primaire composée définie comme member_id, activity_id, car je n'ai besoin que de rechercher des données sur cette table de cette façon.

J'ai également un index non clusterisé sur follow:

CREATE NONCLUSTERED INDEX [IX_follow_member_id_includes] 
ON follow ( member_id ASC ) INCLUDE ( follower_id )

De plus, j'ai une vue liée au schéma network_activityqui est définie comme suit:

CREATE VIEW network_activity
WITH SCHEMABINDING
AS

SELECT
    follow.follower_id as member_id,
    member_activity.activity_id as activity_id,
    COUNT_BIG(*) AS cb
FROM member_activity
INNER JOIN follow ON follow.member_id = member_activity.member_id
INNER JOIN activity ON activity.id = member_activity.activity_id
GROUP BY follow.follower_id, member_activity.activity_id

Qui a également un index cluster unique:

CREATE UNIQUE CLUSTERED INDEX [IX_network_activity_unique_member_id_activity_id] 
ON network_activity
(
    member_id ASC,
    activity_id ASC
)

Maintenant, j'ai deux procédures stockées bloquées. Ils passent par le processus suivant:

-- SP1: insert activity
-----------------------
INSERT INTO activity (...)
SELECT ... FROM member_activity WHERE member_id = @a AND activity_id = @b
INSERT INTO member_activity (...)


-- SP2: insert follow
---------------------
SELECT follow WHERE member_id = @x AND follower_id = @y
INSERT INTO follow (...)

Ces 2 procédures s'exécutent toutes les deux dans l'isolement READ COMMITTED. J'ai réussi à interroger la sortie des événements étendus 1222 et j'ai interprété ce qui suit en ce qui concerne les blocages:

SP1 attend un RangeS-Sverrou de clé sur l' IX_follow_member_id_includesindex tandis que SP2 détient un verrou (X) en conflit

SP2 attend un Sverrouillage de mode activé PK_member_activity tandis que SP1 détient un verrou (X) en conflit

Le blocage semble se produire sur la dernière ligne de chaque requête (les insertions). Ce qui n'est pas clair pour moi, c'est pourquoi SP1 veut un verrou sur l' IX_follow-member_id_includesindex. Le seul lien, pour moi, semble provenir de cette vue indexée, c'est pourquoi je l'ai inclus.

Quelle serait la meilleure façon pour moi d'empêcher ces blocages de se produire? Toute aide serait très appréciée. Je n'ai pas beaucoup d'expérience dans la résolution des problèmes de blocage.

Veuillez me faire savoir s'il y a plus d'informations que je peux fournir qui pourraient aider!

Merci d'avance.


Edit 1: Ajout de plus d'informations par demande.

Voici la sortie 1222 de ce blocage:

<deadlock>
    <victim-list>
        <victimProcess id="process4c6672748" />
    </victim-list>
    <process-list>
        <process id="process4c6672748" taskpriority="0" logused="332" waitresource="KEY: 8:72057594104905728 (25014f77eaba)" waittime="581" ownerId="474698706" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.287" XDES="0x298487970" lockMode="RangeS-S" schedulerid="1" kpid="972" status="suspended" spid="79" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T10:25:00.283" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698706" currentdb="8" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
            <executionStack>
                <frame procname="" line="7" stmtstart="1194" stmtend="1434" sqlhandle="0x02000000a26bb72a2b220406876cad09c22242e5265c82e6" />
                <frame procname="" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 1 --> </inputbuf>
        </process>
        <process id="process6cddc5b88" taskpriority="0" logused="456" waitresource="KEY: 8:72057594098679808 (89013169fc76)" waittime="567" ownerId="474698698" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.283" XDES="0x30c459970" lockMode="S" schedulerid="4" kpid="4204" status="suspended" spid="70" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T15:04:55.870" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698698" currentdb="8" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
            <executionStack>
                <frame procname="" line="18" stmtstart="942" stmtend="1250" sqlhandle="0x03000800ca458d315ee9130100a300000100000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 2 --> </inputbuf>
        </process>
    </process-list>
    <resource-list>
        <keylock hobtid="72057594104905728" dbid="8" objectname="" indexname="" id="lock33299fc00" mode="X" associatedObjectId="72057594104905728">
            <owner-list>
                <owner id="process6cddc5b88" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process4c6672748" mode="RangeS-S" requestType="wait" />
            </waiter-list>
        </keylock>
        <keylock hobtid="72057594098679808" dbid="8" objectname="" indexname="" id="lockb7e2ba80" mode="X" associatedObjectId="72057594098679808">
            <owner-list>
                <owner id="process4c6672748" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process6cddc5b88" mode="S" requestType="wait" />
            </waiter-list>
        </keylock>
    </resource-list>
</deadlock>

Dans ce cas,

associatedObjectId 72057594098679808 correspond à member_activity, PK_member_activity

associatedObjectId 72057594104905728 correspond à follow, IX_follow_member_id_includes

En outre, voici une image plus précise de ce que font SP1 et SP2

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m1 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m1, @activityId, @field1)

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m2 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m2, @activityId, @field1)

également SP2:

-- SP2: insert follow
---------------------

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

Edit 2: Après avoir relu les commentaires, j'ai pensé ajouter quelques informations sur les colonnes qui sont également des clés étrangères ...

  • member_activity.member_idest une clé étrangère vers une membertable
  • member_activity.activity_idest une clé étrangère de la activitytable
  • follow.member_idest une clé étrangère vers une membertable
  • follow.follower_idest une clé étrangère vers une membertable

Mise à jour 1:

J'ai apporté quelques changements qui, à mon avis, pourraient aider à éviter l'impasse, sans succès.

Les modifications que j'ai apportées étaient les suivantes:

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m1 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m2 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

et avec SP2:

-- SP2: insert follow
---------------------

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow WITH ( UPDLOCK )
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

COMMIT

Avec ces deux changements, je semble toujours avoir des blocages.

S'il y a autre chose que je peux fournir, faites-le moi savoir. Merci.

Leland Richardson
la source
read commit ne prend pas de verrous de plage de clés, seulement sérialisable. Si le blocage montre en fait la lecture validée (2), alors mon gues est que vous accédez à la modification d'une clé étrangère qui sera transformée en sérialisable sous les couvertures (bien que toujours dit lecture validée). Nous aurions honnêtement besoin de tous les ddl et sp pour aider davantage.
Sean dit Supprimer Sara Chipps le
@SeanGallardy, merci. J'ai édité pour inclure la sortie 1222 au cas où j'interpréterais mal, et j'ai ajouté plus de détails sur ce que font les SP. est-ce que cela aide?
Leland Richardson
2
@SeanGallardy La partie du plan de requête qui maintient la vue indexée s'exécute en interne à SERIALIZABLE(il y a un peu plus que cela, mais ce n'est pas une réponse :)
Paul White 9
@PaulWhite Merci pour la perspicacité, je ne savais pas ça! En faisant un test rapide, je peux certainement obtenir les modes de verrouillage sérialisables avec la vue indexée lors de l'insertion dans vos procédures stockées (RangeI-N, RangeS-S, RangeS-U). Il semble que l'impasse se produise à partir des modes de verrouillage incompatibles qui se heurtent au bon moment les uns contre les autres lors des insertions dans vos procédures stockées lorsqu'ils tombent à l'intérieur des limites du verrou (par exemple dans la zone tenue par le verrou de plage). À la fois un timing et une collision de données d'entrée, je pense.
Sean dit Supprimer Sara Chipps le
Question: Si j'ajoutais une indication HOLDLOCK sur les instructions SELECT, cela empêcherait-il le verrouillage de se produire lors de l'insertion?
Leland Richardson

Réponses:

5

Le conflit se résume à network_activityêtre une vue indexée qui doit être maintenue (en interne) dans les instructions DML. C'est probablement pourquoi SP1 souhaite un verrou sur l' IX_follow-member_id_includesindex car il est probablement utilisé par la vue (il semble être un index de couverture pour la vue).

Deux options possibles:

  1. Envisagez de supprimer l'index clusterisé sur la vue afin qu'il ne soit plus une vue indexée. L'avantage de l'avoir l'emporte-t-il sur le coût de maintenance? Le sélectionnez-vous assez fréquemment ou le gain de performances de l'indexation en vaut-il la peine? Si vous exécutez ces procs assez fréquemment, alors peut-être que le coût est supérieur à l'avantage?

  2. Si l'avantage de l'indexation de la vue l'emporte sur le coût, envisagez d'isoler les opérations DML par rapport aux tables de base de cette vue. Cela peut être fait en utilisant des verrous d'application (voir sp_getapplock et sp_releaseapplock ). Les verrous d'application vous permettent de créer des verrous autour de concepts arbitraires. Ce qui signifie que vous pouvez définir le @Resource"network_activity" dans vos deux Proc stockés, ce qui les forcera à attendre leur tour. Chaque proc suivrait la même structure:

    BEGIN TRANSACTION;
    EXEC sp_getapplock @Resource = 'network_activity', @LockMode = 'Exclusive';
    ...current proc code...
    EXEC sp_releaseapplock @Resource = 'network_activity';
    COMMIT TRANSACTION;

    Vous devez gérer les erreurs / ROLLBACKvous - même (comme indiqué dans la documentation MSDN liée), donc mettez-le dans l'habituel TRY...CATCH. Mais cela vous permet de gérer la situation.
    Veuillez noter: sp_getapplock / sp_releaseapplockdevrait être utilisé avec parcimonie; Les verrous d'application peuvent certainement être très pratiques (comme dans des cas comme celui-ci), mais ils ne doivent être utilisés qu'en cas de nécessité absolue.

Solomon Rutzky
la source
Merci pour l'aide. Je vais en lire un peu plus sur l'option # 2 et voir si cela fonctionne pour nous. La vue est lue d'un peu, et l'index clusterisé est une aide assez importante ... donc je préfère ne pas le supprimer encore. Je reviendrai une mise à jour une fois que je donnerai un coup de feu.
Leland Richardson
Je pense que l'utilisation de sp_getapplock fonctionnera. Je n'ai pas encore pu l'essayer dans notre environnement de production, mais je voulais m'assurer que vous avez obtenu la prime avant son expiration. Je mettrai à jour ici quand je peux confirmer que cela fonctionne!
Leland Richardson
Merci. Une bonne chose à propos des verrous d'application est que vous pouvez modifier le niveau de granularité de la concaténation dans quelque chose comme le member_iddans la @Resourcevaleur. Cela ne semble pas s'appliquer à cette situation particulière, mais je l'ai vu utilisé comme ça et c'est assez pratique, en particulier dans un système multi-locataire où vous souhaitez limiter le processus à un seul thread sur une base par client, mais l'ont toujours être multithread à travers les clients.
Solomon Rutzky
Je voulais faire une mise à jour et dire que cela a fini par fonctionner dans notre environnement de production. :)
Leland Richardson