SQL Deadlock en la misma clave agrupada bloqueada exclusivamente (con NHibernate) al eliminar / insertar

29

He estado trabajando en este problema de bloqueo durante bastantes días y no importa lo que haga, persiste de una forma u otra.

Primero, la premisa general: tenemos visitas con VisitItems en una relación de uno a muchos.

VisitItems información relevante:

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)

Información de la 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]);

Varios usuarios desean actualizar la tabla VisitItems simultáneamente de la siguiente manera:

Una solicitud web separada creará una visita con VisitItems (generalmente 1). Entonces (la solicitud del problema):

  1. Entra la solicitud web, abre la sesión de NHibernate, comienza la transacción de NHibernate (usando lectura repetible con READ_COMMITTED_SNAPSHOT activado).
  2. Lea todos los artículos de visita para una visita dada por VisitId .
  3. El código evalúa si los elementos siguen siendo relevantes o si necesitamos otros nuevos que usen reglas complejas (por lo tanto, un plazo ligeramente largo, por ejemplo, 40 ms).
  4. El código encuentra que 1 elemento debe agregarse, lo agrega usando NHibernate Visit.VisitItems.Add (..)
  5. El código identifica que un elemento debe eliminarse (no el que acabamos de agregar), lo elimina con NHibernate Visit.VisitItems.Remove (elemento).
  6. El código confirma la transacción

Con una herramienta, simulo 12 solicitudes simultáneas que es muy probable que suceda en un entorno de producción futuro.

[EDITAR] A solicitud, eliminé muchos de los detalles de la investigación que había agregado aquí para mantenerlo breve.

Después de mucha investigación, el siguiente paso fue pensar en una forma de cómo puedo bloquear la pista en un índice diferente al que se usa en la cláusula where (es decir, la clave principal, ya que se usa para eliminar), por lo que modifiqué mi declaración de bloqueo 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>();

Esto redujo los puntos muertos en frecuencia ligeramente, pero todavía estaban sucediendo. Y aquí es donde estoy empezando a perderme:

¿Tres cerraduras 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>

Este es un rastro del número resultante de consultas.
[EDITAR] Whoa. Que semana. Ahora he actualizado la traza con la traza no redactada de la declaración relevante que creo que conduce al punto muerto.

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

Ahora mi bloqueo parece tener un efecto ya que se muestra en el gráfico de punto muerto. ¿Pero que? ¿Tres cerraduras exclusivas y una cerradura compartida? ¿Cómo funciona eso en el mismo objeto / clave? Pensé que mientras tengas un bloqueo exclusivo, ¿no puedes obtener un bloqueo compartido de otra persona? Y a la inversa. Si tiene un bloqueo compartido, nadie puede obtener un bloqueo exclusivo, tienen que esperar.

Creo que me falta una comprensión más profunda aquí sobre cómo funcionan las cerraduras cuando se toman en varias claves en la misma tabla.

Estas son algunas de las cosas que he probado y su impacto:

  • Se agregó otra sugerencia de índice en IX_Visit_Id a la declaración de bloqueo. Ningún cambio
  • Se agregó una segunda columna a IX_Visit_Id (el Id de la columna VisitItem); descabellado, pero lo intenté de todos modos. Ningún cambio
  • Se cambió el nivel de aislamiento a lectura confirmada (valor predeterminado en nuestro proyecto), todavía se producen puntos muertos
  • Se cambió el nivel de aislamiento a serializable. Los puntos muertos siguen sucediendo, pero peor (gráficos diferentes). Realmente no quiero hacer eso, de todos modos.
  • Tomar un candado de mesa los hace desaparecer (obviamente), pero ¿quién querría hacer eso?
  • Tomar un bloqueo de aplicación pesimista (usando sp_getapplock) funciona, pero eso es casi lo mismo que el bloqueo de la tabla, no quiero hacer eso.
  • Agregar la sugerencia READPAST a la sugerencia XLOCK no hizo ninguna diferencia
  • He desactivado PageLock en el índice y PK, no hay diferencia
  • He agregado la pista ROWLOCK a la pista XLOCK, no hice ninguna diferencia

Alguna nota al margen sobre NHibernate: la forma en que se usa y entiendo que funciona es que almacena en caché las instrucciones sql hasta que realmente considera necesario ejecutarlas, a menos que llame a flush, que estamos tratando de no hacer. Por lo tanto, la mayoría de las declaraciones (p. Ej., La lista de agregados de VisitItems => Visit.VisitItems) con carga lenta se ejecutan solo cuando es necesario. La mayoría de las declaraciones de actualización y eliminación reales de mi transacción se ejecutan al final cuando se confirma la transacción (como se evidencia en el seguimiento de sql anterior). Realmente no tengo control sobre la orden de ejecución; NHibernate decide cuándo hacer qué. Mi declaración de bloqueo inicial es realmente solo una solución alternativa.

Además, con la declaración de bloqueo, solo estoy leyendo los elementos en una lista no utilizada (no estoy tratando de anular la lista VisitItems en el objeto Visit, ya que no es así como se supone que NHibernate funciona). Entonces, aunque leí la lista primero con la declaración personalizada, NHibernate aún cargará la lista nuevamente en su colección de objetos proxy Visit.VisitItems usando una llamada sql separada que puedo ver en el seguimiento cuando es hora de cargarla perezosamente en algún lugar.

Pero eso no debería importar, ¿verdad? Ya tengo la cerradura de dicha llave? ¿Cargarlo de nuevo no cambiará eso?

Como nota final, tal vez para aclarar: cada proceso agrega primero su propia visita con VisitItems, luego entra y la modifica (lo que activará la eliminación e inserción y el punto muerto). En mis pruebas, nunca hay ningún proceso que cambie exactamente la misma visita o visititetems.

¿Alguien tiene una idea sobre cómo abordar esto más? ¿Algo que pueda intentar solucionar de una manera inteligente (sin bloqueos de mesa, etc.)? Además, me gustaría saber por qué este bloqueo tripple-x es incluso posible en el mismo objeto. No entiendo.

Avíseme si necesita más información para resolver el rompecabezas.

[EDITAR] Actualicé la pregunta con el DDL para las dos tablas involucradas.

También se me pidió una aclaración sobre la expectativa: Sí, algunos puntos muertos aquí y allá están bien, solo volveremos a intentarlo o haremos que el usuario vuelva a enviar (en general). Pero en la frecuencia actual con 12 usuarios simultáneos, esperaría que solo haya uno cada pocas horas como máximo. Actualmente aparecen varias veces por minuto.

Además de eso, obtuve más información sobre trancount = 2, lo que podría indicar un problema con las transacciones anidadas, que realmente no estamos usando. También investigaré eso y documentaré los resultados aquí.

Ben
fuente
2
No use SELECT *. Podría ser un factor contribuyente en sus problemas. Ver stackoverflow.com/questions/3639861/…
JamieVer
Además, ejecute SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)el sqlhandle en cada marco executeStack para determinar aún más lo que realmente se está ejecutando.
JamieVer
¿Te encuentras con una colisión hash, tal vez? dba.stackexchange.com/questions/80088/insert-only-deadlocks/…
Johnboy
Hola chicos, me temo que ya no soy parte de este proyecto: - /, así que no puedo probar sus sugerencias. Sin embargo, he enviado el hilo y toda la información a algunos miembros del equipo para que puedan verlo en mi lugar.
Ben
Puede usar la respuesta de mi script de PowerShell a esta pregunta para obtener más detalles de punto muerto que pueden ayudarlo. Específicamente, recuperará la información de la declaración SQL para sus marcos de pila "desconocidos". dba.stackexchange.com/questions/28996/…
JamieVer

Respuestas:

2

Hice un par de comentarios en este sentido, pero no estoy seguro de que esté obteniendo los resultados deseados cuando combina el nivel de aislamiento de transacciones de lectura repetible con la instantánea confirmada de lectura.

El TIL informado en su lista de puntos muertos es una lectura repetible, que es aún más restrictiva que la lectura confirmada, y dado el flujo que describe, es probable que conduzca a puntos muertos.

Lo que podría estar tratando de hacer es que su DB TIL permanezca como lectura repetible, pero configure la transacción para usar la instantánea TIL explícitamente con una instrucción de nivel de aislamiento de transacción establecida. Referencia: https://msdn.microsoft.com/en-us/library/ms173763.aspx Si es así, creo que debe tener algo incorrecto. No estoy familiarizado con nHibernate, pero parece que hay una referencia aquí: http://www.anujvarma.com/fluent-nhibernate-setting-database-transaction-isolation-level/

Si la arquitectura de su aplicación lo permite, una opción sería intentar leer una instantánea confirmada en el nivel de base de datos, y si aún tiene puntos muertos, active la instantánea con el control de versiones de fila. TENGA EN CUENTA que, si hace esto, debe repensar la configuración de tempdb si habilita la instantánea (control de versiones de fila). Puedo obtener todo tipo de material sobre esto si lo necesita, hágamelo saber.

Joe Hayes
fuente
2

Tengo un par de pensamientos En primer lugar, la forma más fácil de evitar bloqueos es tomar siempre los bloqueos en el mismo orden. Eso significa que un código diferente que usa transacciones explícitas debería acceder a los objetos en el mismo orden, pero también acceder a las filas individualmente por clave en una transacción explícita debería clasificarse en esa clave. Intente ordenar Visit.VisitItemspor su PK antes de hacerlo Addo, a Deletemenos que sea una gran colección, en cuyo caso ordenaría SELECT.

Sin embargo, la clasificación probablemente no sea tu problema aquí. Supongo que 2 hilos capturan bloqueos compartidos en todos los VisitItemIDs para un determinado VisitIDy el hilo A DELETEno puede completarse hasta que el hilo B libera su bloqueo compartido, que no lo hará hasta que se DELETEcomplete. Los bloqueos de aplicaciones funcionarán aquí y no son tan malos como los bloqueos de tablas, ya que solo se bloquean por método y otros SELECTs funcionarán bien. También puede tomar un bloqueo exclusivo en la Visitmesa para lo dado, VisitIDpero nuevamente, eso es potencialmente exagerado.

Recomiendo convertir su eliminación dura en una eliminación suave (en UPDATE ... SET IsDeleted = 1lugar de usar DELETE) y limpiar estos registros más tarde, a granel, utilizando algún trabajo de limpieza que no use transacciones explícitas. Obviamente, esto requerirá refactorizar otro código para ignorar estas filas eliminadas, pero es mi método preferido para manejar DELETEs incluidos en una SELECTtransacción explícita.

También puede eliminar el SELECTde la transacción y cambiar a un modelo de simultaneidad optimista. Entity Framework lo hace de forma gratuita, no estoy seguro acerca de NHibernate. EF generaría una excepción de concurrencia optimista si su DELETEretorno 0 filas afectadas.

Ben Campbell
fuente
1

¿Has intentado mover la actualización de Visitas antes de realizar modificaciones en visitItems? Ese bloqueo x debe proteger las filas "secundarias".

Hacer un seguimiento completo de los bloqueos adquiridos (y la conversión a legible para humanos) es mucho trabajo, pero podría mostrar la secuencia más claramente.

stox
fuente
-1

READ COMMITTED SNAPSHOT ON significa que cada transacción que se ejecute en READ COMMITTED ISVELATION LEVEL actuará como READ COMMITTED SNAPSHOT.

Esto significa que los lectores no bloquearán a los escritores y los escritores no bloquearán a los lectores.

Utiliza el nivel de aislamiento de transacciones de lectura repetible, es por eso que tiene un punto muerto. La lectura confirmada (sin instantánea) retiene los bloqueos en las filas / páginas hasta el final de la declaración , pero la lectura repetible retiene los bloqueos hasta el final de la transacción .

Si echa un vistazo a su gráfico Deadlock, puede ver un bloqueo "S" adquirido. Creo que este es el bloqueo en el segundo punto -> "Leer todos los elementos de visita para una visita dada por VisitId"

  1. Cambie su nivel de aislamiento de transacción de conexiones de NHibernate a Compromiso de lectura
  2. Debe analizar la consulta para su segundo punto y comprender por qué adquiere bloqueos en el PK si tiene un índice en su columna visitID (podría deberse a la falta de columnas incluidas en su índice).
Artashes Khachatryan
fuente