Je travaille sur ce problème de blocage depuis quelques jours maintenant et quoi que je fasse, 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):
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 a réduit 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.
[MODIFIER] 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:
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 rechange.
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.)? Aussi, je voudrais 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 impliqué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.
J'ai quelques réflexions. Tout d'abord, le moyen le plus simple d'éviter les interblocages est de toujours prendre les verrous dans le même ordre. Cela signifie qu'un code différent utilisant des transactions explicites doit accéder aux objets dans le même ordre, mais également accéder aux lignes individuellement par clé dans une transaction explicite doit être trié sur cette clé. Essayez de trier Visit.VisitItems
par son PK avant de faire Add
ou Delete
sauf s'il s'agit d'une énorme collection auquel cas je trierais sur SELECT
.
Le tri n'est probablement pas votre problème ici. Je suppose que 2 threads saisissent des verrous partagés sur tous les VisitItemID
s pour un VisitID
donné et que le thread A DELETE
ne peut pas terminer tant que le thread B n'a pas libéré son verrou partagé qu'il a gagné jusqu'à ce que son DELETE
se termine. Les verrous d'application fonctionneront ici et ne sont pas aussi mauvais que les verrous de table car ils ne bloquent que par méthode et les autres SELECT
fonctionneront très bien. Vous pouvez également prendre un verrou exclusif sur la table Visit
pour le VisitID
donné, mais là encore, c'est potentiellement exagéré.
Je vous recommande de transformer votre suppression matérielle en une suppression logicielle (UPDATE ... SET IsDeleted = 1
au lieu d'utiliser DELETE
) et de nettoyer ces enregistrements plus tard, en bloc, à l'aide d'une tâche de nettoyage qui n'utilise pas de transactions explicites. Cela nécessitera évidemment la refactorisation d'un autre code pour ignorer ces lignes supprimées, mais c'est ma méthode préférée pour gérer les DELETE
s inclus dans un SELECT
dans une transaction explicite.
Vous pouvez également supprimer le SELECT
de la transaction et passer à un modèle de concurrence optimiste. Le framework d'entité le fait gratuitement, pas sûr de NHibernate. EF déclencherait une exception de concurrence optimiste si votre DELETE
renvoie 0 lignes affectées.
J'ai fait quelques commentaires à cet effet, mais je ne suis pas sûr que vous obteniez les résultats souhaités lorsque vous combinez le niveau d'isolement des transactions en lecture répétable avec un instantané validé en lecture.
Le TIL signalé dans votre liste d'interblocages est une lecture répétable, ce qui est encore plus restrictif que Read Committed, et étant donné le flux que vous décrivez, conduit probablement à des blocages.
Ce que vous essayez peut-être de faire, c'est que votre DB TIL reste reproductible en lecture, mais définissez la transaction pour utiliser le snapshot TIL explicitement avec une instruction set transaction isolation level. Référence: https://msdn.Microsoft.com/en-us/library/ms173763.aspx Si c'est le cas, je pense que vous devez avoir quelque chose de incorrect. Je ne connais pas nHibernate, mais il semble qu'il y ait une référence ici: http://www.anujvarma.com/fluent-nhibernate-setting-database-transaction-isolation-level/
Si l'architecture de votre application le permet, une option serait d'essayer de lire l'instantané validé au niveau de la base de données, et si vous obtenez toujours des blocages, activez l'instantané avec le versionnage des lignes. REMARQUE: si vous faites cela, vous devez repenser votre configuration tempdb si vous activez l'instantané (versioning de ligne). Je peux vous fournir toutes sortes de documents à ce sujet si vous en avez besoin - faites le moi savoir.
Avez-vous essayé de déplacer la mise à jour des visites avant toute modification de visitItems? Ce verrou X devrait protéger les lignes "enfants".
Faire une trace acquise des verrous complets (et la conversion en lisible par l'homme) est beaucoup de travail, mais pourrait montrer la séquence plus clairement.