Lavoro a questo problema di deadlock ormai da alcuni giorni e, qualunque cosa faccia, persiste in un modo o nell'altro.
Innanzitutto, la premessa generale: abbiamo visite con VisitItems in una relazione uno a molti.
Informazioni pertinenti su VisitItems:
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)
Informazioni sulla visita:
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]);
Più utenti desiderano aggiornare contemporaneamente la tabella VisitItems nel modo seguente:
Una richiesta web separata creerà una visita con VisitItems (in genere 1). Quindi (la richiesta del problema):
- La richiesta Web arriva, apre la sessione NHibernate, avvia la transazione NHibernate (utilizzando la lettura ripetibile con READ_COMMITTED_SNAPSHOT attivato).
- Leggi tutti gli elementi della visita per una determinata visita di VisitId .
- Il codice valuta se gli articoli sono ancora pertinenti o se ne abbiamo bisogno di nuovi utilizzando regole complesse (con una durata leggermente lunga, ad esempio 40 ms).
- Il codice trova 1 elemento da aggiungere, lo aggiunge utilizzando NHibernate Visit.VisitItems.Add (..)
- Il codice identifica che un elemento deve essere eliminato (non quello che abbiamo appena aggiunto), lo rimuove utilizzando NHibernate Visit.VisitItems.Remove (elemento).
- Il codice commette la transazione
Con uno strumento simulo 12 richieste simultanee che è molto probabile che accada in un futuro ambiente di produzione.
[EDIT] Su richiesta, rimosso molti dei dettagli dell'indagine che avevo aggiunto qui per farla breve.
Dopo molte ricerche, il passo successivo è stato pensare a un modo in cui posso bloccare il suggerimento su un indice diverso da quello usato nella clausola where (ovvero la chiave primaria, dal momento che viene utilizzata per l'eliminazione), quindi ho modificato la mia istruzione lock in :
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>();
Ciò ha ridotto leggermente i deadlock in frequenza, ma stavano ancora accadendo. Ed ecco dove sto iniziando a perdersi:
<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>
Una traccia del numero risultante di query è simile alla seguente.
[EDIT] Whoa. Che settimana. Ora ho aggiornato la traccia con la traccia non modificata dell'affermazione pertinente che penso porti al deadlock.
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
Ora il mio lucchetto sembra avere un effetto poiché viene mostrato nel grafico del deadlock. Ma cosa? Tre blocchi esclusivi e un blocco condiviso? Come funziona sullo stesso oggetto / chiave? Ho pensato che fintanto che hai un lucchetto esclusivo, non puoi ottenere un lucchetto condiviso da qualcun altro? E viceversa. Se hai un blocco condiviso, nessuno può ottenere un blocco esclusivo, devono aspettare.
Penso che mi manchi una comprensione più profonda qui su come funzionano i blocchi quando vengono presi su più chiavi sullo stesso tavolo.
Ecco alcune delle cose che ho provato e il loro impatto:
- Aggiunto un altro suggerimento indice su IX_Visit_Id all'istruzione lock. Nessun cambiamento
- Aggiunta una seconda colonna a IX_Visit_Id (l'id della colonna VisitItem); molto inverosimile, ma provato comunque. Nessun cambiamento
- Modificato il livello di isolamento per tornare a leggere il commit (predefinito nel nostro progetto), i deadlock continuano a verificarsi
- Livello di isolamento modificato in serializzabile. Si verificano ancora deadlock, ma peggio (grafici diversi). Non voglio davvero farlo, comunque.
- Prendere un lucchetto da tavolo li fa andare via (ovviamente), ma chi vorrebbe farlo?
- Prendere un blocco pessimistico dell'applicazione (usando sp_getapplock) funziona, ma è praticamente la stessa cosa del blocco della tabella, non voglio farlo.
- L'aggiunta del suggerimento READPAST al suggerimento XLOCK non ha fatto alcuna differenza
- Ho disattivato PageLock sull'indice e PK, nessuna differenza
- Ho aggiunto il suggerimento ROWLOCK al suggerimento XLOCK, senza alcuna differenza
Qualche nota a margine su NHibernate: il modo in cui viene usato e ho capito che funziona è che memorizza nella cache le istruzioni sql fino a quando non trova davvero necessario eseguirle, a meno che non si chiami flush, cosa che non stiamo cercando di fare. Pertanto, la maggior parte delle istruzioni (ad esempio l'elenco aggregato di VisitItems => Visit.VisitItems caricati pigramente) vengono eseguite solo quando necessario. La maggior parte delle istruzioni di aggiornamento ed eliminazione effettive dalla mia transazione viene eseguita alla fine quando viene eseguita la transazione (come è evidente dalla traccia sql sopra). Non ho davvero alcun controllo sull'ordine di esecuzione; NHibernate decide quando fare cosa. La mia dichiarazione di blocco iniziale è davvero solo una soluzione.
Inoltre, con l'istruzione lock, sto solo leggendo gli elementi in un elenco inutilizzato (non sto cercando di sovrascrivere l'elenco VisitItems sull'oggetto Visit poiché non è così che NHibernate dovrebbe funzionare per quanto posso dire). Quindi, anche se ho letto prima l'elenco con l'istruzione personalizzata, NHibernate caricherà nuovamente l'elenco nella sua raccolta di oggetti proxy Visit.VisitItems utilizzando una chiamata sql separata che posso vedere nella traccia quando è il momento di caricarlo pigramente da qualche parte.
Ma questo non dovrebbe importare, giusto? Ho già il lucchetto su detto tasto? Caricarlo di nuovo non lo cambierà?
Come nota finale, forse per chiarire: ogni processo aggiunge prima la propria Visita con VisitItems, quindi entra e lo modifica (che attiverà la cancellazione, l'inserimento e il deadlock). Nei miei test, non c'è mai stato alcun processo che modifica esattamente lo stesso Visit o VisitItems.
Qualcuno ha un'idea su come affrontarlo ulteriormente? Qualcosa che posso provare a aggirare in questo modo in modo intelligente (senza blocchi di tabella ecc.)? Inoltre, vorrei sapere perché questo blocco tripple-x è persino possibile sullo stesso oggetto. Non capisco.
Per favore fatemi sapere se sono necessarie ulteriori informazioni per risolvere il puzzle.
[EDIT] Ho aggiornato la domanda con il DDL per le due tabelle coinvolte.
Inoltre mi è stato chiesto un chiarimento sull'aspettativa: sì, alcuni deadlock qui e ci sono ok, riproveremo o faremo in modo che l'utente rispedisca (in generale). Ma alla frequenza attuale con 12 utenti simultanei, mi aspetto che ce ne sia solo uno ogni poche ore al massimo. Attualmente compaiono più volte al minuto.
Oltre a ciò, ho ottenuto alcune informazioni in più sul trancount = 2, che potrebbe indicare un problema con le transazioni nidificate, che non stiamo realmente utilizzando. Analizzerò anche quello e documenterò i risultati qui.
SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)
lo sqlhandle su ogni frame di esecuzioneStack per determinare ulteriormente ciò che viene effettivamente eseguito.