Tôi đã làm việc về vấn đề bế tắc này trong vài ngày nay và bất kể tôi làm gì, nó vẫn tồn tại theo cách này hay cách khác.
Đầu tiên, tiền đề chung: Chúng tôi có Lượt truy cập với VisitItems trong mối quan hệ một đến nhiều.
VisitItems thông tin liên quan:
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)
Truy cập thông tin:
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]);
Nhiều người dùng muốn cập nhật đồng thời bảng VisitItems theo cách sau:
Một yêu cầu web riêng biệt sẽ tạo một Lượt truy cập với VisitItems (thường là 1). Sau đó (yêu cầu vấn đề):
- Yêu cầu web xuất hiện, mở phiên NHibernate, bắt đầu giao dịch NHibernate (sử dụng Đọc lặp lại với READ_COMMITTED_SNAPSHOT trên).
- Đọc tất cả các mục truy cập cho một chuyến thăm nhất định của VisitId .
- Mã đánh giá nếu các mục vẫn còn có liên quan hoặc nếu chúng ta cần những mục mới bằng cách sử dụng các quy tắc phức tạp (vì vậy hơi lâu, ví dụ 40ms).
- Mã tìm thấy 1 mục cần được thêm vào, thêm nó bằng cách sử dụng NHibernate Visit.VisitItems.Add (..)
- Mã xác định rằng một mục cần phải bị xóa (không phải mục chúng tôi vừa thêm), xóa mục đó bằng NHibernate Visit.VisitItems.Remove (mục).
- Mã cam kết giao dịch
Với một công cụ tôi mô phỏng 12 yêu cầu đồng thời có khả năng xảy ra trong môi trường sản xuất trong tương lai.
[EDIT] Theo yêu cầu, đã xóa rất nhiều chi tiết điều tra mà tôi đã thêm vào đây để giữ cho nó ngắn gọn.
Sau nhiều nghiên cứu, bước tiếp theo là nghĩ cách tôi có thể khóa gợi ý về một chỉ mục khác với chỉ mục được sử dụng trong mệnh đề where (tức là khóa chính, vì nó được sử dụng để xóa), vì vậy tôi đã thay đổi câu lệnh khóa của mình thành :
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>();
Điều này làm giảm các bế tắc về tần số một chút, nhưng chúng vẫn xảy ra. Và đây là nơi tôi bắt đầu bị lạc:
<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>
Một dấu vết của số lượng truy vấn kết quả trông như thế này.
[EDIT] Whoa. Thật là một tuần. Bây giờ tôi đã cập nhật dấu vết với dấu vết chưa được xác minh của tuyên bố có liên quan mà tôi nghĩ dẫn đến bế tắc.
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
Bây giờ khóa của tôi dường như có hiệu lực vì nó hiển thị trong biểu đồ khóa chết. Nhưng cái gì? Ba khóa độc quyền và một khóa chia sẻ? Làm thế nào mà nó hoạt động trên cùng một đối tượng / khóa? Tôi nghĩ miễn là bạn có một khóa độc quyền, bạn không thể nhận được một khóa chia sẻ từ người khác? Và cách khác xung quanh. Nếu bạn có một khóa chia sẻ, không ai có thể có được khóa độc quyền, họ phải chờ.
Tôi nghĩ rằng tôi đang thiếu một số hiểu biết sâu sắc hơn ở đây về cách các khóa hoạt động khi chúng được thực hiện trên nhiều khóa trên cùng một bảng.
Dưới đây là một số điều tôi đã thử và tác động của chúng:
- Đã thêm một gợi ý chỉ mục khác trên IX_Visit_Id vào câu lệnh khóa. Không thay đổi
- Đã thêm một cột thứ hai vào IX_Visit_Id (Id của cột VisitItem); xa xôi, nhưng dù sao cũng đã thử. Không thay đổi
- Đã thay đổi mức Cô lập trở lại để đọc cam kết (mặc định trong dự án của chúng tôi), các bế tắc vẫn xảy ra
- Thay đổi mức cô lập để tuần tự hóa. Bế tắc vẫn xảy ra, nhưng tồi tệ hơn (đồ thị khác nhau). Dù sao thì tôi cũng không muốn làm điều đó.
- Lấy một cái khóa bàn làm cho chúng biến mất (rõ ràng), nhưng ai sẽ muốn làm điều đó?
- Sử dụng khóa ứng dụng bi quan (sử dụng sp_getapplock) hoạt động, nhưng điều đó khá giống với khóa bảng, không muốn làm điều đó.
- Việc thêm gợi ý READPAST vào gợi ý XLOCK không tạo ra sự khác biệt
- Tôi đã tắt PageLock trên chỉ mục và PK, không có sự khác biệt
- Tôi đã thêm gợi ý ROWLOCK vào gợi ý XLOCK, không có sự khác biệt
Một số lưu ý phụ trên NHibernate: Cách nó được sử dụng và tôi hiểu nó hoạt động là nó lưu trữ các câu lệnh sql cho đến khi nó thực sự thấy cần thiết để thực thi chúng, trừ khi bạn gọi tuôn ra, điều mà chúng tôi đang cố gắng không làm. Vì vậy, hầu hết các câu lệnh (ví dụ: danh sách Tổng hợp được tải một cách lười biếng của VisitItems => Visit.VisitItems) chỉ được thực thi khi cần thiết. Hầu hết các báo cáo cập nhật và xóa thực tế khỏi giao dịch của tôi được thực hiện vào cuối khi giao dịch được thực hiện (như hiển nhiên từ dấu vết sql ở trên). Tôi thực sự không kiểm soát được lệnh thi hành án; NHibernate quyết định khi nào nên làm gì. Tuyên bố khóa ban đầu của tôi thực sự chỉ là một công việc xung quanh.
Ngoài ra, với tuyên bố khóa, tôi chỉ đọc các mục vào danh sách không sử dụng (Tôi không cố ghi đè danh sách VisitItems trên đối tượng Lượt truy cập vì đó không phải là cách NHibernate hoạt động theo như tôi có thể nói). Vì vậy, mặc dù tôi đã đọc danh sách đầu tiên bằng câu lệnh tùy chỉnh, NHibernate vẫn sẽ tải lại danh sách đó vào bộ sưu tập đối tượng proxy của nó Visit.VisitItems bằng cách sử dụng một cuộc gọi sql riêng biệt mà tôi có thể thấy trong dấu vết khi đến lúc tải nó một cách lười biếng ở đâu đó.
Nhưng điều đó không quan trọng, phải không? Tôi đã có khóa trên chìa khóa nói? Tải lại nó sẽ không thay đổi điều đó?
Như một lưu ý cuối cùng, có thể để làm rõ: Mỗi quy trình sẽ thêm Lượt truy cập riêng với VisitItems trước, sau đó đi vào và sửa đổi nó (sẽ kích hoạt xóa và chèn và bế tắc). Trong các thử nghiệm của tôi, không bao giờ có bất kỳ quá trình nào thay đổi chính xác Visit hoặc VisitItems.
Có ai có ý tưởng về cách tiếp cận điều này hơn nữa không? Bất cứ điều gì tôi có thể cố gắng khắc phục điều này một cách thông minh (không có khóa bảng, v.v.)? Ngoài ra, tôi muốn tìm hiểu lý do tại sao khóa tripple-x này thậm chí có thể trên cùng một đối tượng. Tôi không hiểu
Xin vui lòng cho tôi biết nếu có thêm thông tin được yêu cầu để giải câu đố.
[EDIT] Tôi đã cập nhật câu hỏi với DDL cho hai bảng có liên quan.
Ngoài ra, tôi đã được yêu cầu làm rõ về kỳ vọng: Có, một vài bế tắc ở đây và vẫn ổn, chúng tôi sẽ thử lại hoặc yêu cầu người dùng gửi lại (nói chung). Nhưng với tần suất hiện tại với 12 người dùng đồng thời, tôi hy vọng sẽ chỉ có một người cứ sau vài giờ. Hiện tại họ bật lên nhiều lần mỗi phút.
Ngoài ra, tôi có thêm một số thông tin về trancount = 2, có thể chỉ ra sự cố với các giao dịch lồng nhau, mà chúng tôi không thực sự sử dụng. Tôi cũng sẽ điều tra điều đó, và ghi lại kết quả ở đây.
SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)
cho sqlhandle trên mỗi khung execStack để xác định thêm những gì đang thực sự được thực thi.