我已经在这个僵局问题上研究了好几天了,无论我做什么,它都会以一种或多种方式持续存在。

首先,一般的前提是:
我们有来访与VisitItems具有一对多关系。

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)


访问信息:

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]);


多个用户希望通过以下方式同时更新VisitItems表:

一个单独的Web请求将创建一个带有VisitItems(通常为1)的Visit。
然后(问题请求):


Web请求进入,打开NHibernate会话,启动NHibernate事务(使用具有READ_COMMITTED_SNAPSHOT的Repeatable Read)。
读取所有访问项
代码评估项目是否仍然相关,或者我们是否需要使用复杂规则(例如,运行时间稍长,例如40毫秒)的新项目。
代码发现需要添加1个项目,使用NHibernate Visit.VisitItems.Add(..)
代码标识需要删除的一项(不是我们刚刚添加的一项),请使用NHibernate Visit.VisitItems.Remove(item)将其删除。
代码使用工具I提交事务

模拟将来在生产环境中很可能发生的12个并发请求。
经过大量研究,下一步是考虑一种方法,如何将提示锁定在与where子句中使用的索引不同的索引上(即主键(因为它已用于删除),因此我将lock语句更改为:

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>();


这样可以稍微减少死锁的频率,但是死锁仍在发生。这就是我开始迷路的地方:



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


产生的查询数量跟踪看起来像这样。[编辑]哇。好一个星期。现在,我用未完成的相关语句的未编辑痕迹更新了该痕迹,我认为这会导致死锁。

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


现在我的锁似乎已经生效,因为它显示了在死锁图中。
那又如何呢?三个互斥锁和一个共享锁?在同一个对象/键上如何工作?我以为只要拥有独占锁,就无法从别人那里获得共享锁?反之亦然。如果您拥有共享锁,则没有人可以获得排他锁,他们必须等待。

我想在这里我对在相同的多个键上使用锁时锁如何工作缺乏深入的了解。表。

这是我尝试过的一些事情及其影响:


在lock语句上添加了IX_Visit_Id的另一个索引提示。否
更改
在IX_Visit_Id中添加了第二列(
VisitItem列的ID);牵强附会,但还是尝试了。没有变化
将隔离级别更改为已读回提交(我们项目中的默认设置),
死锁仍在发生
将隔离级别更改为可序列化。
死锁仍在发生,但更糟(不同的图形)。无论如何,我
真的不想这样做。
使用表锁可以使它们消失(很明显),但是谁愿意这么做呢?
使用悲观的应用程序锁(使用sp_getapplock)是可行的,但这与表锁几乎相同,不想这样做。
在READPAST提示中添加XLOCK提示没有什么区别。
我已经关闭了索引和PK上的PageLock,没有区别
我已经将ROWLOCK提示添加到XLOCK提示中,没有任何区别

关于NHibernate的一些说明:
我理解它的使用方式是,它会缓存sql语句,直到真正发现有必要执行它们为止,除非您调用flush,否则我们将尝试不这样做。因此,大多数语句(例如,懒惰加载的VisitItems => Visit.VisitItems的汇总列表)仅在必要时执行。提交事务后,我事务中的大多数实际更新和删除语句都会在提交时执行(从上面的sql跟踪中可以明显看出)。我真的无法控制执行顺序; NHibernate决定何时执行操作。我最初的lock语句实际上只是一个变通方法。

此外,使用lock语句,我只是将项目读入未使用的列表中(我并没有尝试覆盖上的VisitItems列表访问对象,因为据我所知,这不是NHibernate应该如何工作的。)因此,即使我先使用自定义语句阅读列表,NHibernate仍会再次将列表加载到其代理对象集合Visit中.VisitItems使用一个单独的sql调用,当我需要将其懒惰地加载到某个地方时,可以在跟踪中看到它。我已经在所说的钥匙上锁了吗?再次加载不会改变吗?

最后一点,也许需要澄清一下:每个进程都首先添加带有VisitItems的自己的Visit,然后进入并对其进行修改(这将触发删除并插入和死锁)。在我的测试中,从来没有任何过程可以更改完全相同的Visit或VisitItems。

是否有人对如何进一步解决此问题有任何想法?我可以尝试以一种聪明的方式解决此问题(没有表锁等)吗?另外,我想学习为什么在同一对象上甚至可以使用tripple-x锁定。我不明白。

如果需要更多信息来解决难题,请告诉我。

[编辑]
我用涉及到的两个表的DDL更新了问题。

还要求我澄清有关期望的信息:
是的,这里有些僵局,还可以,我们将重试或让用户重新提交(通常来说)。但是按照目前有12个并发用户的频率,我希望最多每隔几个小时只有一个。目前,它们每分钟弹出多次。我也将对此进行调查,并在此处记录结果。

评论

不要使用SELECT *。这可能是导致您的问题的一个因素。参见stackoverflow.com/questions/3639861/…

另外,对每个executeStack框架上的sqlhandle运行SELECT OBJECT_NAME(objectid,dbid)AS objectname * * from sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000)以进一步确定实际执行的操作。

您是否正在遇到哈希冲突? dba.stackexchange.com/questions/80088/insert-only-deadlocks / ...

大家好,恐怕我不再属于该项目了:-/,所以我不能尝试您的建议。但是,我已将线程和所有信息转发给了一些团队成员,以便他们代替我进行研究。

您可以使用我的PowerShell脚本回答该问题,以获取更多可能对您有帮助的死锁详细信息。具体而言,它将检索“未知”堆栈帧的SQL语句信息。 dba.stackexchange.com/questions/28996 / ...

#1 楼

我对此发表了一些评论,但是当您将“可重复读取”事务隔离级别与“读取已提交快照”结合使用时,我不确定您是否会获得理想的结果。

死锁列表中报告的TIL是可重复读取的,它比“读取已提交”的限制更严格,并且鉴于您描述的流程,很可能导致死锁。您可能想要做的是使您的数据库TIL保持可重复读取,但是将事务设置为通过设置的事务隔离级别语句显式使用快照TI​​L。参考:https://msdn.microsoft.com/zh-cn/library/ms173763.aspx
如果是这样,我认为您一定有不正确的地方。我对nHibernate并不熟悉,但是这里似乎有一个参考:http://www.anujvarma.com/fluent-nhibernate-setting-database-transaction-isolation-level/

如果您的应用程序的体系结构将允许这样做,一种选择是尝试在数据库级别读取已提交的快照,如果仍然遇到死锁,请启用带有行版本控制的快照。
请注意,如果执行此操作,则需要重新-如果启用快照(行版本控制),请考虑您的tempdb设置。如果需要,我可以为您提供各种各样的材料-让我知道。

#2 楼

我有几点想法。首先,避免死锁的最简单方法是始终以相同的顺序进行锁定。这意味着使用显式事务的不同代码应以相同的顺序访问对象,但显式事务中按键分别访问行也应在该键上进行排序。在执行Visit.VisitItemsAdd之前,请尝试按其PK对Delete进行排序,除非这是一个庞大的集合,在这种情况下,我将对SELECT进行排序。

虽然排序可能不是您的问题。我猜测对于给定的VisitItemID,有2个线程在所有VisitID上抢夺了共享锁,线程A的DELETE无法完成,直到线程B释放了它的共享锁,直到其DELETE完成为止。应用程序锁将在这里工作,并且没有表锁那么糟,因为它们仅按方法阻塞,其他SELECT可以正常工作。对于给定的Visit,您还可以在VisitID表上使用排他锁,但同样,这可能会导致过度杀伤。

我建议您将硬删除转换为软删除(UPDATE ... SET IsDeleted = 1而不是DELETE),并在以后使用一些不使用显式事务的清理作业来批量清理这些记录。显然,这将需要重构其他代码以忽略这些已删除的行,但这是我处理显式事务中DELETE中包含的SELECT的首选方法。

您还可以从事务中删除SELECT并切换到开放式并发模型。实体框架免费提供此功能,不确定NHibernate。如果您的DELETE返回受影响的0行,则EF会引发乐观并发异常。

#3 楼

在对visitItems进行任何修改之前,您是否尝试过移动“访问”更新?该x锁应该保护“子”行。

对获得的跟踪进行完全锁(并将其转换为人类可读的代码)是一项繁重的工作,但可能会更清楚地显示顺序。 >

#4 楼

如果您不知道为什么表格被卡住,有时会出现tran卡住的情况

SET XACT_ABORT ON->这应注意导致tran卡住的错误
BEGIN TRAN TRAN_NAME
-访问表的代码-
COMMIT TRAN TRAN_NAME

https://stackoverflow.com/questions/2277254/how-to-set-xact-abort-内部网

#5 楼

READ COMMITTED SNAPSHOT ON表示在READ COMMITTED ISOLATION LEVEL中运行的每个事务都将充当READ COMMITTED SNAPSHOT。

这意味着读者将不会阻止作家,而作家将不会阻止读者。 Read Committed(无快照)将行/页面上的锁保持到语句的结尾,而Repeatable Read则将锁保持到事务结束。

如果您查看死锁图,您会看到获得了“ S”锁。我认为这是第二点的锁定->“通过VisitId读取给定访问的所有访问项。”


将NHibernate连接事务隔离级别更改为Read Committed
。您需要分析第二点的查询,并了解如果有索引,为什么它会在PK上获得锁在您的visitID列上(可能是由于缺少索引中包含的列)。