Je travaille sur ce problème de blocage depuis plusieurs jours maintenant et peu importe ce que je fais, il persiste d'une manière ou d'une autre.
Tout d'abord, la prémisse générale: nous avons des visites avec VisitItems dans une relation un à plusieurs.
VisitItems informations pertinentes:
CREATE TABLE [BAR].[VisitItems] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[VisitType] INT NOT NULL,
[FeeRateType] INT NOT NULL,
[Amount] DECIMAL (18, 2) NOT NULL,
[GST] DECIMAL (18, 2) NOT NULL,
[Quantity] INT NOT NULL,
[Total] DECIMAL (18, 2) NOT NULL,
[ServiceFeeType] INT NOT NULL,
[ServiceText] NVARCHAR (200) NULL,
[InvoicingProviderId] INT NULL,
[FeeItemId] INT NOT NULL,
[VisitId] INT NULL,
[IsDefault] BIT NOT NULL DEFAULT 0,
[SourceVisitItemId] INT NULL,
[OverrideCode] INT NOT NULL DEFAULT 0,
[InvoiceToCentre] BIT NOT NULL DEFAULT 0,
[IsSurchargeItem] BIT NOT NULL DEFAULT 0,
CONSTRAINT [PK_BAR.VisitItems] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_BAR.VisitItems_BAR.FeeItems_FeeItem_Id] FOREIGN KEY ([FeeItemId]) REFERENCES [BAR].[FeeItems] ([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.Visits_Visit_Id] FOREIGN KEY ([VisitId]) REFERENCES [BAR].[Visits] ([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.VisitTypes] FOREIGN KEY ([VisitType]) REFERENCES [BAR].[VisitTypes]([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.FeeRateTypes] FOREIGN KEY ([FeeRateType]) REFERENCES [BAR].[FeeRateTypes]([Id]),
CONSTRAINT [FK_BAR.VisitItems_CMN.Users_Id] FOREIGN KEY (InvoicingProviderId) REFERENCES [CMN].[Users] ([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.VisitItems_SourceVisitItem_Id] FOREIGN KEY ([SourceVisitItemId]) REFERENCES [BAR].[VisitItems]([Id]),
CONSTRAINT [CK_SourceVisitItemId_Not_Equal_Id] CHECK ([SourceVisitItemId] <> [Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.OverrideCodes] FOREIGN KEY ([OverrideCode]) REFERENCES [BAR].[OverrideCodes]([Id]),
CONSTRAINT [FK_BAR.VisitItems_BAR.ServiceFeeTypes] FOREIGN KEY ([ServiceFeeType]) REFERENCES [BAR].[ServiceFeeTypes]([Id])
)
CREATE NONCLUSTERED INDEX [IX_FeeItem_Id]
ON [BAR].[VisitItems]([FeeItemId] ASC)
CREATE NONCLUSTERED INDEX [IX_Visit_Id]
ON [BAR].[VisitItems]([VisitId] ASC)
Infos visite:
CREATE TABLE [BAR].[Visits] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[VisitType] INT NOT NULL,
[DateOfService] DATETIMEOFFSET NOT NULL,
[InvoiceAnnotation] NVARCHAR(255) NULL ,
[PatientId] INT NOT NULL,
[UserId] INT NULL,
[WorkAreaId] INT NOT NULL,
[DefaultItemOverride] BIT NOT NULL DEFAULT 0,
[DidNotWaitAdjustmentId] INT NULL,
[AppointmentId] INT NULL,
CONSTRAINT [PK_BAR.Visits] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_BAR.Visits_CMN.Patients] FOREIGN KEY ([PatientId]) REFERENCES [CMN].[Patients] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_BAR.Visits_CMN.Users] FOREIGN KEY ([UserId]) REFERENCES [CMN].[Users] ([Id]),
CONSTRAINT [FK_BAR.Visits_CMN.WorkAreas_WorkAreaId] FOREIGN KEY ([WorkAreaId]) REFERENCES [CMN].[WorkAreas] ([Id]),
CONSTRAINT [FK_BAR.Visits_BAR.VisitTypes] FOREIGN KEY ([VisitType]) REFERENCES [BAR].[VisitTypes]([Id]),
CONSTRAINT [FK_BAR.Visits_BAR.Adjustments] FOREIGN KEY ([DidNotWaitAdjustmentId]) REFERENCES [BAR].[Adjustments]([Id]),
);
CREATE NONCLUSTERED INDEX [IX_Visits_PatientId]
ON [BAR].[Visits]([PatientId] ASC);
CREATE NONCLUSTERED INDEX [IX_Visits_UserId]
ON [BAR].[Visits]([UserId] ASC);
CREATE NONCLUSTERED INDEX [IX_Visits_WorkAreaId]
ON [BAR].[Visits]([WorkAreaId]);
Plusieurs utilisateurs souhaitent mettre à jour la table VisitItems simultanément de la manière suivante:
Une demande Web distincte créera une visite avec VisitItems (généralement 1). Ensuite (la demande de problème):
- La demande Web arrive, ouvre la session NHibernate, démarre la transaction NHibernate (en utilisant la lecture répétable avec READ_COMMITTED_SNAPSHOT activé).
- Lisez tous les éléments de visite pour une visite donnée par VisitId .
- Le code évalue si les éléments sont toujours pertinents ou si nous en avons besoin de nouveaux en utilisant des règles complexes (donc un peu long terme, par exemple 40 ms).
- Le code trouve qu'un élément doit être ajouté, l'ajoute à l'aide de NHibernate Visit.VisitItems.Add (..)
- Le code identifie qu'un élément doit être supprimé (pas celui que nous venons d'ajouter), le supprime à l'aide de NHibernate Visit.VisitItems.Remove (élément).
- Le code valide la transaction
Avec un outil, je simule 12 requêtes simultanées, ce qui est très susceptible de se produire dans un futur environnement de production.
[MODIFIER] Sur demande, j'ai supprimé un grand nombre des détails de l'enquête que j'avais ajoutés ici pour être bref.
Après de nombreuses recherches, l'étape suivante consistait à trouver un moyen de verrouiller l'indication sur un index différent de celui utilisé dans la clause where (c'est-à-dire la clé primaire, car elle est utilisée pour la suppression), j'ai donc modifié ma déclaration de verrouillage en :
var items = (List<VisitItem>)_session.CreateSQLQuery(@"SELECT * FROM BAR.VisitItems WITH (XLOCK, INDEX([PK_BAR.VisitItems]))
WHERE VisitId = :visitId")
.AddEntity(typeof(VisitItem))
.SetParameter("visitId", qi.Visit.Id)
.List<VisitItem>();
Cela réduisait légèrement les blocages en fréquence, mais ils se produisaient toujours. Et c'est là que je commence à me perdre:
<deadlock-list>
<deadlock victim="process3f71e64e8">
<process-list>
<process id="process3f71e64e8" taskpriority="0" logused="0" waitresource="KEY: 5:72057594071744512 (a5e1814e40ba)" waittime="3812" ownerId="8004520" transactionname="user_transaction" lasttranstarted="2015-12-14T10:24:58.010" XDES="0x3f7cb43b0" lockMode="X" schedulerid="1" kpid="15788" status="suspended" spid="63" sbid="0" ecid="0" priority="0" trancount="1" lastbatchstarted="2015-12-14T10:24:58.013" lastbatchcompleted="2015-12-14T10:24:58.013" lastattention="1900-01-01T00:00:00.013" clientapp=".Net SqlClient Data Provider" hostname="ABC" hostpid="10016" loginname="bsapp" isolationlevel="repeatable read (3)" xactid="8004520" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="adhoc" line="1" stmtstart="18" stmtend="254" sqlhandle="0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000">
unknown
</frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown
</frame>
</executionStack>
<inputbuf>
(@p0 int)SELECT * FROM BAR.VisitItems WITH (XLOCK, INDEX([PK_BAR.VisitItems]))
WHERE VisitId = @p0
</inputbuf>
</process>
<process id="process4105af468" taskpriority="0" logused="1824" waitresource="KEY: 5:72057594071744512 (8194443284a0)" waittime="3792" ownerId="8004519" transactionname="user_transaction" lasttranstarted="2015-12-14T10:24:58.010" XDES="0x3f02ea3b0" lockMode="S" schedulerid="8" kpid="15116" status="suspended" spid="65" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2015-12-14T10:24:58.033" lastbatchcompleted="2015-12-14T10:24:58.033" lastattention="1900-01-01T00:00:00.033" clientapp=".Net SqlClient Data Provider" hostname="ABC" hostpid="10016" loginname="bsapp" isolationlevel="repeatable read (3)" xactid="8004519" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="adhoc" line="1" stmtstart="18" stmtend="98" sqlhandle="0x0200000075abb0074bade5aa57b8357410941428df4d54130000000000000000000000000000000000000000">
unknown
</frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown
</frame>
</executionStack>
<inputbuf>
(@p0 int)DELETE FROM BAR.VisitItems WHERE Id = @p0
</inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057594071744512" dbid="5" objectname="BAR.VisitItems" indexname="PK_BAR.VisitItems" id="lock449e27500" mode="X" associatedObjectId="72057594071744512">
<owner-list>
<owner id="process4105af468" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process3f71e64e8" mode="X" requestType="wait"/>
</waiter-list>
</keylock>
<keylock hobtid="72057594071744512" dbid="5" objectname="BAR.VisitItems" indexname="PK_BAR.VisitItems" id="lock46a525080" mode="X" associatedObjectId="72057594071744512">
<owner-list>
<owner id="process3f71e64e8" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process4105af468" mode="S" requestType="wait"/>
</waiter-list>
</keylock>
</resource-list>
</deadlock>
</deadlock-list>
Une trace du nombre de requêtes résultant ressemble à ceci.
[EDIT] Whoa. Quelle semaine. J'ai maintenant mis à jour la trace avec la trace non expurgée de la déclaration pertinente qui, je pense, a conduit à l'impasse.
exec sp_executesql N'SELECT * FROM BAR.VisitItems WITH (XLOCK, INDEX([PK_BAR.VisitItems]))
WHERE VisitId = @p0',N'@p0 int',@p0=3826
go
exec sp_executesql N'SELECT visititems0_.VisitId as VisitId1_, visititems0_.Id as Id1_, visititems0_.Id as Id37_0_, visititems0_.VisitType as VisitType37_0_, visititems0_.FeeItemId as FeeItemId37_0_, visititems0_.FeeRateType as FeeRateT4_37_0_, visititems0_.Amount as Amount37_0_, visititems0_.GST as GST37_0_, visititems0_.Quantity as Quantity37_0_, visititems0_.Total as Total37_0_, visititems0_.ServiceFeeType as ServiceF9_37_0_, visititems0_.ServiceText as Service10_37_0_, visititems0_.InvoiceToCentre as Invoice11_37_0_, visititems0_.IsDefault as IsDefault37_0_, visititems0_.OverrideCode as Overrid13_37_0_, visititems0_.IsSurchargeItem as IsSurch14_37_0_, visititems0_.VisitId as VisitId37_0_, visititems0_.InvoicingProviderId as Invoici16_37_0_, visititems0_.SourceVisitItemId as SourceV17_37_0_ FROM BAR.VisitItems visititems0_ WHERE visititems0_.VisitId=@p0',N'@p0 int',@p0=3826
go
exec sp_executesql N'INSERT INTO BAR.VisitItems (VisitType, FeeItemId, FeeRateType, Amount, GST, Quantity, Total, ServiceFeeType, ServiceText, InvoiceToCentre, IsDefault, OverrideCode, IsSurchargeItem, VisitId, InvoicingProviderId, SourceVisitItemId) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15); select SCOPE_IDENTITY()',N'@p0 int,@p1 int,@p2 int,@p3 decimal(28,5),@p4 decimal(28,5),@p5 int,@p6 decimal(28,5),@p7 int,@p8 nvarchar(4000),@p9 bit,@p10 bit,@p11 int,@p12 bit,@p13 int,@p14 int,@p15 int',@p0=1,@p1=452,@p2=1,@p3=0,@p4=0,@p5=1,@p6=0,@p7=1,@p8=NULL,@p9=0,@p10=1,@p11=0,@p12=0,@p13=3826,@p14=3535,@p15=NULL
go
exec sp_executesql N'UPDATE BAR.Visits SET VisitType = @p0, DateOfService = @p1, InvoiceAnnotation = @p2, DefaultItemOverride = @p3, AppointmentId = @p4, ReferralRequired = @p5, ReferralCarePlan = @p6, UserId = @p7, PatientId = @p8, WorkAreaId = @p9, DidNotWaitAdjustmentId = @p10, ReferralId = @p11 WHERE Id = @p12',N'@p0 int,@p1 datetimeoffset(7),@p2 nvarchar(4000),@p3 bit,@p4 int,@p5 bit,@p6 nvarchar(4000),@p7 int,@p8 int,@p9 int,@p10 int,@p11 int,@p12 int',@p0=1,@p1='2016-01-22 12:37:06.8915296 +08:00',@p2=NULL,@p3=0,@p4=NULL,@p5=0,@p6=NULL,@p7=3535,@p8=4246,@p9=2741,@p10=NULL,@p11=NULL,@p12=3826
go
exec sp_executesql N'DELETE FROM BAR.VisitItems WHERE Id = @p0',N'@p0 int',@p0=7919
go
Maintenant, mon verrou semble avoir un effet car il apparaît dans le graphique de blocage. Mais quoi? Trois verrous exclusifs et un verrou partagé? Comment cela fonctionne-t-il sur le même objet / clé? Je pensais que tant que vous avez un verrou exclusif, vous ne pouvez pas obtenir un verrou partagé de quelqu'un d'autre? Et l'inverse. Si vous avez un verrou partagé, personne ne peut obtenir un verrou exclusif, ils doivent attendre.
Je pense que je manque ici de compréhension plus approfondie sur le fonctionnement des verrous lorsqu'ils sont pris sur plusieurs clés sur la même table.
Voici quelques-unes des choses que j'ai essayées et leur impact:
- Ajout d'une autre indication d'index sur IX_Visit_Id à l'instruction de verrouillage. Pas de changement
- Ajout d'une deuxième colonne à IX_Visit_Id (l'ID de la colonne VisitItem); tiré par les cheveux, mais essayé quand même. Pas de changement
- Changement du niveau d'isolement en lecture validée (par défaut dans notre projet), des blocages se produisent toujours
- Changement du niveau d'isolement en sérialisable. Des blocages se produisent toujours, mais pire (graphiques différents). De toute façon, je ne veux pas vraiment faire ça.
- Prendre un verrou de table les fait disparaître (évidemment), mais qui voudrait faire ça?
- Prendre un verrou d'application pessimiste (en utilisant sp_getapplock) fonctionne, mais c'est à peu près la même chose que le verrou de table, je ne veux pas faire ça.
- L'ajout de l'indice READPAST à l'indice XLOCK n'a fait aucune différence
- J'ai désactivé PageLock sur l'index et PK, aucune différence
- J'ai ajouté l'indice ROWLOCK à l'indice XLOCK, cela n'a fait aucune différence
Une note secondaire sur NHibernate: La façon dont il est utilisé et je comprends que cela fonctionne est qu'il met en cache les instructions sql jusqu'à ce qu'il trouve vraiment nécessaire de les exécuter, sauf si vous appelez flush, ce que nous essayons de ne pas faire. Ainsi, la plupart des instructions (par exemple la liste agrégée paresseusement chargée de VisitItems => Visit.VisitItems) sont exécutées uniquement lorsque cela est nécessaire. La plupart des instructions de mise à jour et de suppression réelles de ma transaction sont exécutées à la fin lorsque la transaction est validée (comme le montre la trace SQL ci-dessus). Je n'ai vraiment aucun contrôle sur l'ordre d'exécution; NHibernate décide quand faire quoi. Ma déclaration de verrouillage initiale n'est vraiment qu'une solution de contournement.
De plus, avec l'instruction lock, je ne fais que lire les éléments dans une liste inutilisée (je n'essaye pas de remplacer la liste VisitItems sur l'objet Visit car ce n'est pas comme cela que NHibernate est censé fonctionner pour autant que je sache). Donc, même si j'ai lu la liste en premier avec l'instruction personnalisée, NHibernate chargera toujours la liste dans sa collection d'objets proxy Visit.VisitItems en utilisant un appel sql distinct que je peux voir dans la trace quand il est temps de la charger paresseusement quelque part.
Mais cela ne devrait pas avoir d'importance, non? J'ai déjà le verrou sur ladite clé? Le recharger ne changera pas cela?
Pour finir, peut-être pour clarifier: chaque processus ajoute d'abord sa propre visite avec VisitItems, puis entre et la modifie (ce qui déclenchera la suppression et l'insertion et le blocage). Dans mes tests, il n'y a jamais de processus modifiant exactement la même visite ou VisitItems.
Quelqu'un a-t-il une idée sur la façon d'aborder cela plus loin? Tout ce que je peux essayer de contourner cela de manière intelligente (pas de verrous de table, etc.)? J'aimerais aussi savoir pourquoi ce verrou tripple-x est même possible sur le même objet. Je ne comprends pas.
Veuillez me faire savoir si des informations supplémentaires sont nécessaires pour résoudre le puzzle.
[EDIT] J'ai mis à jour la question avec le DDL pour les deux tables concernées.
On m'a également demandé des éclaircissements sur l'attente: oui, quelques blocages ici et là sont ok, nous allons simplement réessayer ou demander à l'utilisateur de soumettre à nouveau (en général). Mais à la fréquence actuelle avec 12 utilisateurs simultanés, je m'attends à ce qu'il n'y en ait qu'un au maximum toutes les quelques heures. Actuellement, ils apparaissent plusieurs fois par minute.
En plus de cela, j'ai obtenu plus d'informations sur le trancount = 2, ce qui pourrait indiquer un problème avec les transactions imbriquées, que nous n'utilisons pas vraiment. Je vais également enquêter sur cela et documenter les résultats ici.
SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)
le sqlhandle sur chaque trame executionStack pour déterminer davantage ce qui est réellement exécuté.