Deadlock SQL na mesma chave em cluster bloqueada exclusivamente (com NHibernate) na exclusão / inserção

29

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):

  1. 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).
  2. Leia todos os itens da visita para uma determinada visita do VisitId .
  3. 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).
  4. O código localiza que 1 item precisa ser adicionado e o adiciona usando NHibernate Visit.VisitItems.Add (..)
  5. 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).
  6. 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:

Três fechaduras exclusivas?

<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.

Ben
fonte
2
Não use SELECT *. Pode ser um fator que contribui para seus problemas. Veja stackoverflow.com/questions/3639861/...
JamieSee
Além disso, execute 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.
JamieSee
Você está enfrentando uma colisão de hash, talvez? dba.stackexchange.com/questions/80088/insert-only-deadlocks/...
Johnboy
Olá pessoal, receio não fazer mais parte deste projeto: - /, por isso não posso tentar suas sugestões. No entanto, encaminhei o tópico e todas as informações para alguns membros da equipe, para que eles possam analisá-lo em meu lugar.
Ben
Você pode usar minha resposta de script do PowerShell para esta pergunta para obter mais detalhes de conflito que podem ajudá-lo. Especificamente, ele recuperará as informações da instrução SQL para seus quadros de pilha "desconhecidos". dba.stackexchange.com/questions/28996/...
JamieSee

Respostas:

2

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.

Joe Hayes
fonte
2

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.VisitItemspor seu PK antes de fazer Addou, a Deletemenos que seja uma coleção enorme, nesse caso, eu classificaria SELECT.

A classificação provavelmente não é o seu problema aqui. Eu estou supondo que 2 threads agarram bloqueios compartilhados em todos os VisitItemIDs para um dado VisitIDe o segmento A DELETEnão pode ser concluído até que o segmento B libere seu bloqueio compartilhado, o que não ocorrerá até que seja DELETEconcluí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 outros SELECTs funcionam bem. Você também pode usar um bloqueio exclusivo na Visitmesa pelo dado, VisitIDmas, novamente, isso é potencialmente um exagero.

Eu recomendo transformar sua exclusão física em uma exclusão virtual (em UPDATE ... SET IsDeleted = 1vez de usar DELETE) 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 com DELETEs incluídos em uma SELECTtransação explícita.

Você também pode remover o SELECTda 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 o DELETEretorno de 0 linhas fosse afetado.

Ben Campbell
fonte
1

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.

stox
fonte
-1

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".

  1. Altere o nível de isolamento da transação de conexões do NHibernate para Read Committed
  2. Você precisa analisar a consulta para o seu segundo ponto e entender por que ela adquire bloqueios no PK se você tiver um índice na coluna visitID (pode ser por falta de colunas incluídas no seu índice).
Artashes Khachatryan
fonte