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é sur 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)?
(Si vous affichez l'image dans un nouvel onglet, elle est lisible. Désolé pour la petite taille.)
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:
La partie MATCH du MERGE vérifie l'index pour les correspondances, verrouillant en lecture ces lignes/pages au fur et à mesure.
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 serait suffisant pour résoudre le problème sans avoir un effet important sur vos performances.
Enfin, vous pouvez également essayer d'ajouter PAGLOCK, XLOCK ou les deux PAGLOCK et XLOCK. Encore une fois, pourrait travail et performances pourrait ne pas être trop horrible. Vous devrez l'essayer pour voir.
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):
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. La bobine est une bobine enthousiaste, ce qui signifie que toutes les lignes sont écrites dans un tempdb table de travail avant d'être relue et relue pour l'opérateur d'insertion.
Redéfinir le TYPE
de la variable de table pour inclure un PRIMARY KEY
En cluster:
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é:
Dans les tests avec 5000 itérations de l'instruction MERGE
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 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.
Dans le cas où la définition de 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:
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;
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:
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.
Vous pouvez avoir un thread gérer toutes vos insertions, de sorte que votre serveur d'applications gère la concurrence.
Vous pouvez réessayer automatiquement après les blocages - cela peut être l'approche la plus lente si la concurrence 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 fait 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 pour l'année.
Nous utilisons principalement l'approche 1 dans notre système. Cela fonctionne vraiment bien pour nous.