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?