Saya telah mengerjakan masalah kebuntuan ini selama beberapa hari sekarang dan tidak peduli apa yang saya lakukan, masalah ini tetap ada.
Pertama, premis umum: Kami memiliki Kunjungan dengan VisitItems dalam hubungan satu ke banyak.
Info relevan 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)
Kunjungi info:
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]);
Beberapa pengguna ingin memperbarui tabel VisitItems secara bersamaan dengan cara berikut:
Permintaan web terpisah akan membuat Kunjungan dengan VisitItems (biasanya 1). Kemudian (permintaan masalah):
- Permintaan web masuk, buka sesi NHibernate, mulai transaksi NHibernate (menggunakan Baca Berulang dengan READ_COMMITTED_SNAPSHOT aktif).
- Baca semua item kunjungan untuk kunjungan yang diberikan oleh VisitId .
- Kode menilai apakah item tersebut masih relevan atau jika kita membutuhkan yang baru menggunakan aturan yang kompleks (berjalan agak lama, misalnya 40 ms).
- Kode menemukan 1 item perlu ditambahkan, menambahkannya menggunakan NHibernate Visit.VisitItems.Add (..)
- Kode mengidentifikasi bahwa satu item perlu dihapus (bukan yang baru saja kita tambahkan), menghapusnya menggunakan NHibernate Visit.VisitItems.Remove (item).
- Kode melakukan transaksi
Dengan alat saya mensimulasikan 12 permintaan bersamaan yang sangat mungkin terjadi di lingkungan produksi masa depan.
[EDIT] Atas permintaan, menghapus banyak detail investigasi yang saya tambahkan di sini untuk membuatnya singkat.
Setelah banyak penelitian, langkah selanjutnya adalah memikirkan cara bagaimana saya bisa mengunci petunjuk pada indeks yang berbeda dengan yang digunakan di mana klausa (yaitu kunci utama, karena itu digunakan untuk penghapusan), jadi saya mengubah pernyataan kunci saya ke :
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>();
Ini mengurangi deadlock dalam frekuensi sedikit, tetapi mereka masih terjadi. Dan di sinilah saya mulai tersesat:
<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>
Jejak dari jumlah kueri yang dihasilkan terlihat seperti ini.
[EDIT] Whoa. Seminggu sekali. Saya sekarang telah memperbarui jejak dengan jejak pernyataan yang relevan yang saya pikir menyebabkan kebuntuan.
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
Sekarang kunci saya tampaknya memiliki efek karena ditampilkan di grafik jalan buntu. Tapi apa? Tiga kunci eksklusif dan satu kunci bersama? Bagaimana cara kerjanya pada objek / kunci yang sama? Saya pikir selama Anda memiliki kunci eksklusif, Anda tidak bisa mendapatkan kunci bersama dari orang lain? Dan sebaliknya. Jika Anda memiliki kunci bersama, tidak ada yang bisa mendapatkan kunci eksklusif, mereka harus menunggu.
Saya pikir saya kurang memiliki pemahaman yang lebih dalam di sini tentang bagaimana kunci bekerja ketika mereka diambil pada beberapa tombol di meja yang sama.
Berikut adalah beberapa hal yang telah saya coba dan dampaknya:
- Menambahkan petunjuk indeks lain pada IX_Visit_Id ke pernyataan kunci. Tidak ada perubahan
- Menambahkan kolom kedua ke IX_Visit_Id (Id dari kolom VisitItem); jauh diambil, tetapi tetap mencoba. Tidak ada perubahan
- Tingkat Isolasi yang diubah kembali menjadi komit (default dalam proyek kami), deadlock masih terjadi
- Tingkat Isolasi Berubah menjadi serializable. Kebuntuan masih terjadi, tetapi lebih buruk (grafik berbeda). Lagipula aku sebenarnya tidak ingin melakukan itu.
- Mengambil kunci meja membuat mereka pergi (jelas), tetapi siapa yang mau melakukan itu?
- Mengambil kunci aplikasi pesimistis (menggunakan sp_getapplock) berfungsi, tapi itu hampir sama dengan kunci tabel, tidak ingin melakukan itu.
- Menambahkan petunjuk READPAST ke petunjuk XLOCK tidak ada bedanya
- Saya telah mematikan PageLock pada indeks dan PK, tidak ada perbedaan
- Saya telah menambahkan petunjuk ROWLOCK ke petunjuk XLOCK, tidak ada bedanya
Beberapa catatan di NHibernate: Cara ini digunakan dan saya mengerti itu berfungsi adalah bahwa cache pernyataan sql sampai benar-benar merasa perlu untuk mengeksekusi mereka, kecuali jika Anda memanggil flush, yang kami coba tidak lakukan. Jadi sebagian besar pernyataan (mis., Daftar Agregat yang malas dari VisitItems => Visit.VisitItems) dieksekusi hanya jika diperlukan. Sebagian besar pembaruan aktual dan menghapus pernyataan dari transaksi saya dieksekusi pada akhir ketika transaksi dilakukan (seperti yang terlihat dari jejak sql di atas). Saya benar-benar tidak memiliki kendali atas perintah eksekusi; NHibernate memutuskan kapan melakukan apa. Pernyataan kunci awal saya benar-benar hanya solusi.
Juga, dengan pernyataan kunci, saya hanya membaca item ke dalam daftar yang tidak digunakan (saya tidak mencoba untuk menimpa daftar VisitItems pada objek Visit karena bukan bagaimana seharusnya NHibernate bekerja sejauh yang saya tahu). Jadi, meskipun saya membaca daftar terlebih dahulu dengan pernyataan kustom, NHibernate masih akan memuat daftar lagi ke dalam koleksi objek proxy-nya. Visit.VisitItems menggunakan panggilan sql terpisah yang dapat saya lihat di dalam jejak saat saatnya memuatnya secara malas di suatu tempat.
Tapi itu tidak masalah, kan? Saya sudah memiliki kunci pada kata kunci? Memuatnya lagi tidak akan mengubah itu?
Sebagai catatan terakhir, mungkin untuk mengklarifikasi: Setiap proses menambahkan Kunjungan sendiri dengan VisitItems terlebih dahulu, kemudian masuk dan memodifikasinya (yang akan memicu hapus dan masukkan dan kebuntuan). Dalam pengujian saya, tidak pernah ada proses mengubah Visit atau VisitItems yang sama persis.
Adakah yang punya ide tentang cara mendekati ini lebih jauh? Adakah yang bisa saya coba untuk menyiasatinya dengan cara yang cerdas (tidak ada kunci meja dll)? Juga, saya ingin mempelajari mengapa kunci tripple-x ini bahkan dimungkinkan pada objek yang sama. Saya tidak mengerti.
Tolong beri tahu saya jika ada informasi lebih lanjut yang diperlukan untuk menyelesaikan puzzle.
[EDIT] Saya memperbarui pertanyaan dengan DDL untuk dua tabel yang terlibat.
Juga saya diminta untuk klarifikasi tentang harapan: Ya, beberapa kebuntuan di sini dan ada ok, kami hanya akan mencoba lagi atau meminta pengguna untuk mengirimkan kembali (secara umum). Tetapi pada frekuensi saat ini dengan 12 pengguna secara bersamaan, saya berharap hanya ada satu setiap beberapa jam paling banyak. Saat ini mereka muncul beberapa kali per menit.
Selain itu, saya mendapatkan beberapa informasi lebih lanjut tentang trancount = 2, yang mungkin mengindikasikan masalah dengan transaksi bersarang, yang sebenarnya tidak kami gunakan. Saya akan menyelidiki itu juga, dan mendokumentasikan hasilnya di sini.
SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)
untuk sqlhandle pada setiap frame eksekusiStack untuk lebih menentukan apa yang sebenarnya dieksekusi.