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.Respostas:
Fiz alguns comentários nesse sentido, mas não tenho certeza de que você está obtendo os resultados desejados ao combinar o nível de isolamento da transação de Leitura Repetível com o Instantâneo Confirmado de Leitura.
O TIL relatado em sua lista de deadlock é de leitura repetível, o que é ainda mais restritivo do que o Read Committed e, dado o fluxo que você descreve, provavelmente leva a deadlocks.
O que você pode estar tentando fazer é que seu DB TIL permaneça com leitura repetível, mas configure a transação para usar o instantâneo TIL explicitamente com uma instrução de nível de isolamento de transação definida. Referência: https://msdn.microsoft.com/en-us/library/ms173763.aspx Nesse caso, acho que você deve ter algo incorreto. Não estou familiarizado com o nHibernate, mas parece que há uma referência aqui: http://www.anujvarma.com/fluent-nhibernate-setting-database-transaction-isolation-level/
Se a arquitetura do seu aplicativo permitir, uma opção seria tentar ler o instantâneo confirmado no nível db e, se você ainda tiver conflitos, ative o instantâneo com o controle de versão de linha. Observe que, se você fizer isso, precisará repensar a configuração do tempdb se ativar o instantâneo (controle de versão de linha). Posso obter todo tipo de material sobre isso, se você precisar - me avise.
fonte
Eu tenho alguns pensamentos. Primeiro, a maneira mais fácil de evitar bloqueios é sempre usar bloqueios na mesma ordem. Isso significa que código diferente usando transações explícitas deve acessar objetos na mesma ordem, mas também acessar linhas individualmente por chave em uma transação explícita deve ser classificado nessa chave. Tente classificar
Visit.VisitItems
por seu PK antes de fazerAdd
ou, aDelete
menos que seja uma coleção enorme, nesse caso, eu classificariaSELECT
.A classificação provavelmente não é o seu problema aqui. Eu estou supondo que 2 threads agarram bloqueios compartilhados em todos os
VisitItemID
s para um dadoVisitID
e o segmento ADELETE
não pode ser concluído até que o segmento B libere seu bloqueio compartilhado, o que não ocorrerá até que sejaDELETE
concluído. Os bloqueios de aplicativo funcionarão aqui e não são tão ruins quanto os bloqueios de tabela, pois eles bloqueiam apenas por método e outrosSELECT
s funcionam bem. Você também pode usar um bloqueio exclusivo naVisit
mesa pelo dado,VisitID
mas, novamente, isso é potencialmente um exagero.Eu recomendo transformar sua exclusão física em uma exclusão virtual (em
UPDATE ... SET IsDeleted = 1
vez de usarDELETE
) e limpar esses registros posteriormente, em massa, usando algum trabalho de limpeza que não use transações explícitas. Obviamente, isso exigirá a refatoração de outro código para ignorar essas linhas excluídas, mas é o meu método preferido para lidar comDELETE
s incluídos em umaSELECT
transação explícita.Você também pode remover o
SELECT
da transação e mudar para um modelo de simultaneidade otimista. A estrutura de entidades faz isso de graça, não tenho certeza sobre o NHibernate. A EF geraria uma exceção de simultaneidade otimista se oDELETE
retorno de 0 linhas fosse afetado.fonte
Você já tentou mover a atualização de visitas antes de fazer modificações nos visitItems? Esse x-lock deve proteger as linhas "filho".
Fazer um rastreamento adquirido de bloqueios completos (e converter para legível por humanos) é muito trabalhoso, mas pode mostrar a sequência mais claramente.
fonte
Se você não tem idéia de por que uma mesa está travando, às vezes há um travamento
DEFINIR XACT_ABORT ON -> isso deve cuidar dos erros que causam o travamento da transe BEGIN TRAN TRAN_NAME --CODE acessando a tabela-- COMMIT TRAN TRAN_NAME
https://stackoverflow.com/questions/2277254/how-to-set-xact-abort-within-ado-net
fonte
LER INSTANTÂNEO COMPROMETIDO LIGADO significa que cada transação única executada no LEITOR COMPROMISSO NÍVEL DE ISOLAMENTO atuará como INSTANTÂNEO LIDO COMPROMETIDO.
Isso significa que os leitores não bloquearão os escritores e os escritores não bloquearão os leitores.
Você usa o nível de isolamento de transação de leitura repetível, é por isso que você tem um conflito. A leitura confirmada (sem captura instantânea) mantém os bloqueios nas linhas / páginas até o final da instrução , mas a leitura repetida mantém os bloqueios até o final da transação .
Se você der uma olhada no gráfico Deadlock, poderá ver um bloqueio "S" adquirido. Acho que esse é o bloqueio do segundo ponto -> "Leia todos os itens de uma visita do VisitId".
fonte