Estou trabalhando nesse problema de impasse há alguns dias e, não importa o que eu faça, ele persiste de uma maneira ou de outra.
Primeiro, a premissa geral: temos visitas com o VisitItems em um relacionamento um para muitos.
Informações relevantes sobre os itens 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)
Informações da 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]);
Vários usuários desejam atualizar a tabela VisitItems simultaneamente da seguinte maneira:
Uma solicitação da Web separada criará uma Visita com o VisitItems (geralmente 1). Então (a solicitação do problema):
- A solicitação da Web entra, abre a sessão do NHibernate, inicia a transação do NHibernate (usando a leitura repetida com READ_COMMITTED_SNAPSHOT ativado).
- Leia todos os itens da visita para uma determinada visita do VisitId .
- O código avalia se os itens ainda são relevantes ou se precisamos de novos usando regras complexas (de modo um pouco demorado, por exemplo, 40ms).
- O código localiza que 1 item precisa ser adicionado e o adiciona usando NHibernate Visit.VisitItems.Add (..)
- O código identifica que um item precisa ser excluído (não o que acabamos de adicionar) e o remove usando o NHibernate Visit.VisitItems.Remove (item).
- Código confirma a transação
Com uma ferramenta, simulo 12 solicitações simultâneas, o que provavelmente ocorrerá em um ambiente de produção futuro.
[EDITAR] A pedido, removemos muitos detalhes da investigação que adicionei aqui para mantê-lo breve.
Depois de muita pesquisa, o próximo passo foi pensar em uma maneira de bloquear uma dica em um índice diferente daquele usado na cláusula where (ou seja, a chave primária, uma vez que é usada para exclusão), então alterei minha instrução de bloqueio para :
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>();
Isso reduziu ligeiramente os impasses na frequência, mas eles ainda estavam acontecendo. E aqui é onde estou começando a me perder:
<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>
Um rastreamento do número resultante de consultas se parece com isso.
[EDIT] Whoa. Que semana. Atualizei agora o rastreamento com o rastreamento não deduzido da declaração relevante que acho que leva ao 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
Agora, meu bloqueio parece ter efeito, pois está sendo exibido no gráfico de deadlock. Mas o que? Três bloqueios exclusivos e um bloqueio compartilhado? Como isso funciona no mesmo objeto / chave? Pensei que, desde que você tenha um bloqueio exclusivo, não será possível obter um bloqueio compartilhado de outra pessoa? E o contrário. Se você tiver um bloqueio compartilhado, ninguém poderá obter um bloqueio exclusivo, eles terão que esperar.
Eu acho que estou faltando um entendimento mais profundo aqui de como os bloqueios funcionam quando são executados em várias chaves na mesma tabela.
Aqui estão algumas das coisas que tentei e seu impacto:
- Adicionada outra dica de índice em IX_Visit_Id à instrução de bloqueio. Sem alteração
- Adicionada uma segunda coluna ao IX_Visit_Id (o ID da coluna VisitItem); muito buscado, mas tentou de qualquer maneira. Sem alteração
- Alterado o nível de isolamento de volta para a leitura confirmada (padrão em nosso projeto), os conflitos ainda estão acontecendo
- Nível de isolamento alterado para serializável. Os impasses ainda estão acontecendo, mas pior (gráficos diferentes). Realmente não quero fazer isso.
- Tomar um bloqueio de mesa os faz desaparecer (obviamente), mas quem iria querer fazer isso?
- Usar um bloqueio de aplicativo pessimista (usando sp_getapplock) funciona, mas é praticamente a mesma coisa que o bloqueio de tabela, não quero fazer isso.
- Adicionar a dica READPAST à dica XLOCK não fez diferença
- Desativei o PageLock no índice e no PK, não há diferença
- Adicionei a dica ROWLOCK à dica XLOCK, não fez diferença
Alguma observação ao lado do NHibernate: O modo como é usado e eu entendo que ele funciona é que ele armazena em cache as instruções sql até que realmente seja necessário executá-las, a menos que você chame flush, o que estamos tentando não fazer. Portanto, a maioria das instruções (por exemplo, a lista Agregada lenta de VisitItems => Visit.VisitItems) é executada apenas quando necessário. A maioria das instruções reais de atualização e exclusão da minha transação é executada no final quando a transação é confirmada (como é evidente no rastreamento do sql acima). Eu realmente não tenho controle sobre a ordem de execução; O NHibernate decide quando fazer o que. Minha declaração de bloqueio inicial é realmente apenas uma solução alternativa.
Além disso, com a instrução lock, estou apenas lendo os itens em uma lista não utilizada (não estou tentando substituir a lista VisitItems no objeto Visit, pois não é assim que o NHibernate deve funcionar, tanto quanto eu sei). Portanto, mesmo que eu tenha lido a lista primeiro com a instrução personalizada, o NHibernate ainda carregará a lista novamente em sua coleção de objetos proxy Visit.VisitItems usando uma chamada sql separada que eu posso ver no rastreamento quando chegar a hora de carregá-la lentamente em algum lugar.
Mas isso não deveria importar, certo? Eu já tenho o cadeado na chave? Carregá-lo novamente não vai mudar isso?
Como observação final, talvez para esclarecer: cada processo está adicionando sua própria Visita com o VisitItems primeiro e depois a modifica (o que acionará a exclusão, a inserção e o impasse). Nos meus testes, nunca há nenhum processo alterando exatamente a mesma Visita ou VisitItems.
Alguém tem uma idéia de como abordar isso ainda mais? Alguma coisa que eu possa tentar contornar isso de uma maneira inteligente (sem travas de mesa, etc.)? Além disso, eu gostaria de saber por que esse bloqueio tripple-x é possível no mesmo objeto. Eu não entendo
Entre em contato se precisar de mais informações para resolver o quebra-cabeça.
[EDIT] Atualizei a pergunta com o DDL para as duas tabelas envolvidas.
Também me pediram esclarecimentos sobre a expectativa: Sim, alguns impasses aqui e ali estão ok, apenas tentaremos novamente ou levaremos o usuário a reenviar (de modo geral). Mas na frequência atual com 12 usuários simultâneos, eu esperaria que houvesse apenas um a cada poucas horas, no máximo. Atualmente, eles aparecem várias vezes por minuto.
Além disso, obtive mais informações sobre o trancount = 2, o que pode indicar um problema com transações aninhadas, as quais não estamos realmente usando. Também vou investigar isso e documentar os resultados aqui.
SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)
o sqlhandle em cada quadro de execuçãoStack para determinar ainda mais o que realmente está sendo executado.