一,一般前提:
我们通过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,[Quantity] INT NOT NULL,[Total] DECIMAL (18,[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,[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).
然后(问题请求):
> Web请求进来,打开NHibernate会话,启动NHibernate事务(使用带有READ_COMMITTED_SNAPSHOT的Repeatable Read).
>阅读VisitId给定访问的所有访问项目.
>代码评估项目是否仍然相关或者我们是否需要使用复杂规则的新项目(如此长时间运行,例如40ms).
>代码发现需要添加1个项目,使用NHibernate Visit.VisitItems.Add(..)添加它
>代码标识需要删除一个项目(不是我们刚刚添加的项目),使用NHibernate Visit.VisitItems.Remove(item)删除它.
>代码提交交易
使用工具,我可以模拟12个并发请求,这很可能在未来的生产环境中发生.
[编辑]根据要求,删除了我在这里添加的很多调查细节,以保持简短.
经过大量研究后,下一步是想办法如何锁定与where子句中使用的索引不同的索引(即主键,因为它用于删除),所以我将我的锁定语句改为:
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',@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,@p5 int,@p6 decimal(28,@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',@p1 datetimeoffset(7),@p2 nvarchar(4000),@p3 bit,@p4 int,@p5 bit,@p6 nvarchar(4000),@p8 int,@p9 int,@p10 int,@p12 int',@p1='2016-01-22 12:37:06.8915296 +08:00',@p2=NULL,@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',@p0=7919 go
现在我的锁似乎有效,因为它显示在死锁图中.
但是什么?三个独家锁和一个共享锁?这对同一个对象/密钥有什么作用?我想只要你有一个独家锁,你就不能从别人那里获得共享锁?反过来说.如果您有共享锁,没有人可以获得独占锁,他们必须等待.
我认为我对这些锁在同一个桌子上的多个键上进行操作时的工作方式缺乏深入的了解.
以下是我尝试过的一些事情及其影响:
>在IX_Visit_Id上向lock语句添加了另一个索引提示.没有
更改
>在IX_Visit_Id(Id的ID)中添加了第二列
VisitItem专栏);远远不过,但无论如何都试过了.没变
>将隔离级别更改回读取已提交(在我们的项目中为默认值),
死锁仍在发生
>将隔离级别更改为可序列化.
死锁仍在发生,但更糟(不同的图表).我不
无论如何,真的很想这样做.
>拿一把桌子锁会让它们消失(显然),但是谁会想要这样做呢?
>使用悲观的应用程序锁(使用sp_getapplock)可以工作,但这与表锁几乎完全相同,不想这样做.
>将READPAST提示添加到XLOCK提示没有任何区别
>我已经关闭了索引和PK的PageLock,没有区别
>我已经在XLOCK提示中添加了ROWLOCK提示,没有任何区别
关于NHibernate的一些注意事项:
它的使用方式,我理解它的工作原理是它缓存sql语句,直到它真正发现它必须执行它们,除非你调用flush,我们试图不这样做.因此,大多数语句(例如,懒惰加载的VisitItems的聚合列表=> Visit.VisitItems)仅在必要时执行.当事务提交时,我的事务中的大多数实际更新和删除语句都会在结束时执行(从上面的sql trace中可以看出).我真的无法控制执行顺序; NHibernate决定何时做什么.我最初的锁定声明实际上只是解决方法.
另外,使用lock语句,我只是将项目读入一个未使用的列表(我不是试图覆盖Visit对象上的VisitItems列表,因为根据我的判断,NHibernate不应该如何工作).
因此,即使我首先使用自定义语句读取列表,NHibernate仍然会使用单独的sql调用将列表再次加载到其代理对象集合Visit.VisitItems中,我可以在跟踪中看到懒惰时将其加载到某处.
但这不重要,对吧?我已经锁上了所说的钥匙?再次加载不会改变它?
作为最后一点,也许可以澄清一下:每个进程首先添加自己的Visit with VisitItems,然后进入并修改它(这将触发删除和插入以及死锁).在我的测试中,从来没有任何过程改变完全相同的Visit或VisitItems.
有没有人知道如何进一步解决这个问题?我可以尝试以聪明的方式解决这个问题(没有表锁等)?另外,我想了解为什么这个tripple-x锁甚至可以在同一个对象上.我不明白.
如果需要更多信息来解决这个难题,请告诉我.
[编辑]
我用DDL更新了所涉及的两个表的问题.
我还被要求澄清期望:
是的,这里有一些死锁,没关系,我们只是重试或让用户重新提交(一般来说).但是目前有12个并发用户的频率,我希望最多只有每几个小时一个.目前,它们每分钟弹出多次.
除此之外,我还获得了有关trancount = 2的更多信息,这可能表明嵌套事务存在问题,我们并未真正使用它.我也会对此进行调查,并在此处记录结果.