Я працюю над проблемою тупикової ситуації вже досить декілька днів, і що б я не робив, воно так чи інакше зберігається.
По-перше, загальна передумова: у нас є відвідування з 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 таким чином:
Окремий веб-запит створить відвідування з VisitItems (зазвичай це 1). Потім (запит проблеми):
- Заходить веб-запит, відкривається сесія NHibernate, починається транзакція NHibernate (з використанням повторного читання з увімкненою READ_COMMITTED_SNAPSHOT).
- Прочитайте всі пункти відвідування для даного візиту від VisitId .
- Код оцінює, чи елементи все ще є актуальними або якщо нам потрібні нові, використовуючи складні правила (настільки дещо тривалі, наприклад, 40 мс).
- Код знаходить 1 елемент потрібно додати, додавши його за допомогою NHibernate Visit.VisitItems.Add (..)
- Код визначає, що один елемент потрібно видалити (не той, який ми щойно додали), видаляє його за допомогою NHibernate Visit.VisitItems.Remove (item).
- Код здійснює транзакцію
За допомогою інструменту я моделюю 12 одночасних запитів, що, швидше за все, трапиться в майбутньому виробничому середовищі.
[EDIT] За запитом видалено багато деталей розслідування, які я додав сюди, щоб тримати це коротко.
Після багатьох досліджень наступним кроком було продумати спосіб, як я можу заблокувати підказку на інший індекс до того, який використовується у пункті де (тобто первинний ключ, оскільки він використовується для видалення), тому я змінив свою заяву блокування на :
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
Тепер, схоже, мій замок має ефект, оскільки він відображається в графіку тупикового кута. Але що? Три ексклюзивні замки та один загальний замок? Як це працює на одному об’єкті / ключі? Я думав, що поки у вас є ексклюзивний замок, ви не можете отримати загальний замок від когось іншого? А навпаки. Якщо у вас є загальний замок, ніхто не може отримати ексклюзивний замок, їм доведеться чекати.
Я думаю, що мені не вистачає глибшого розуміння того, як працюють замки, коли їх переносять на кілька клавіш однієї таблиці.
Ось кілька речей, які я спробував, та їх вплив:
- Доданий ще один натяк на індекс на IX_Visit_Id до оператора блокування. Без змін
- Додано другий стовпець до IX_Visit_Id (ідентифікатор стовпця VisitItem); далеко не вдалося, але все-таки спробував. Без змін
- Змінено рівень ізоляції назад, щоб прочитати скоєне (за замовчуванням у нашому проекті), тупики все ще відбуваються
- Змінено рівень ізоляції на серіалізаційний. Тупики все ще відбуваються, але гірше (різні графіки). Я все одно не хочу цього робити.
- Якщо взяти замок для столу, вони змушують їх піти (очевидно), але хто б хотів це зробити?
- Беручи песимістичний блокування додатків (використовуючи sp_getapplock) працює, але це майже те саме, що і блокування таблиці, не хочу цього робити.
- Додавання підказки READPAST до підказки XLOCK не мало значення
- Я вимкнув PageLock на індексі та ПК, різниці немає
- Я додав підказку ROWLOCK до підказки XLOCK, не маючи ніякої різниці
Деякі сторонні зауваження щодо NHibernate: те, як воно використовується, і я розумію, що це працює, - це те, що він кешує заяви sql, поки дійсно не вважає за потрібне їх виконувати, якщо тільки ви не викликаєте flush, чого ми намагаємося не робити. Тож більшість висловлювань (наприклад, ліниво завантажений агрегатний список VisitItems => Visit.VisitItems) виконуються лише за необхідності. Більшість фактичних заяв про оновлення та видалення з моєї транзакції виконуються наприкінці, коли транзакція здійснена (як це видно з сліду sql вище). Я дійсно не маю контролю над наказом про виконання; NHibernate вирішує, коли робити що. Моя початкова заява про блокування - це справді лише обхід.
Крім того, з твердженням про блокування я просто читаю елементи у невикористаному списку (я не намагаюся переосмислити список VisitItems об’єкта Visit, оскільки це не так, як NHibernate повинен працювати, наскільки я можу сказати). Тож хоч я спочатку прочитав список зі спеціальним висловом, NHibernate все одно завантажить цей список у свою колекцію проксі-об'єктів Visit.VisitItems, використовуючи окремий виклик sql, який я можу побачити в трасі, коли прийде час ліниво його десь завантажити.
Але це не має значення, правда? У мене вже є замок на вказаному ключі? Завантаження його ще раз це не змінить?
Як остаточне зауваження, можливо, для уточнення: Кожен процес спочатку додає власний візит із VisitItems, потім входить і змінює його (що ініціює видалення та вставлення та тупик). У моїх тестах жодного разу не змінюється такий самий візит або відвідування.
Хтось має ідею, як далі до цього підходити? Що-небудь, що я можу спробувати обійти цим розумним способом (без замків таблиці тощо)? Також я хотів би дізнатися, чому цей замок tripple-x можливий навіть на одному об’єкті. Я не розумію.
Будь ласка, дайте мені знати, чи потрібно більше інформації для вирішення головоломки.
[EDIT] Я оновив питання з DDL для двох задіяних таблиць.
Також мене попросили роз'яснити в очікуванні: Так, кілька тупиків тут і там добре, ми просто повторимо або змусимо користувача повторно подати (загалом кажучи). Але при нинішній частоті з 12 одночасними користувачами я б очікував, що там буде лише один кожні кілька годин. В даний час вони спливають кілька разів на хвилину.
На додаток до цього, я отримав додаткову інформацію про транзакцію = 2, яка може вказувати на проблему з вкладеними транзакціями, які ми насправді не використовуємо. Я теж буду досліджувати це і задокументувати результати.
SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)
sqlhandle на кожному кадрі ExecuStack для подальшого визначення того, що насправді виконується.