Я работаю над этой тупиковой проблемой уже несколько дней и, что бы я ни делал, она так или иначе сохраняется.
Во-первых, общая предпосылка: у нас есть визиты с визитами в отношениях один ко многим.
ПосетитеItems соответствующую информацию:
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 одновременно следующим образом:
Отдельный веб-запрос создаст визит с визитом (обычно 1). Тогда (проблемный запрос):
- Приходит веб-запрос, открывается сеанс NHibernate, запускается транзакция NHibernate (с использованием Repeatable Read с включенным READ_COMMITTED_SNAPSHOT).
- Прочитайте все пункты посещения для данного посещения VisitId .
- Код оценивает, являются ли элементы все еще актуальными или нам нужны новые, использующие сложные правила (такие немного продолжительные, например, 40 мс).
- Код находит, что 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',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) выполняются только при необходимости. Большинство актуальных операторов update и delete из моей транзакции выполняются в конце, когда транзакция фиксируется (как видно из трассировки sql выше). Я действительно не имею никакого контроля над порядком исполнения; NHibernate решает, когда делать что. Мой первоначальный оператор блокировки - это всего лишь обходной путь.
Кроме того, с помощью оператора lock я просто читаю элементы в неиспользуемый список (я не пытаюсь переопределить список VisitItems в объекте Visit, поскольку NHibernate не работает, насколько я могу судить). Поэтому, хотя я сначала прочитал список с помощью пользовательского оператора, NHibernate все равно снова загрузит список в свою коллекцию прокси-объектов Visit.VisitItems, используя отдельный вызов sql, который я вижу в трассировке, когда пришло время лениво загрузить его куда-нибудь.
Но это не должно иметь значения, верно? У меня уже есть замок на указанный ключ? Загрузка снова не изменит это?
В качестве заключительного замечания можно уточнить: каждый процесс сначала добавляет свой собственный визит с помощью VisitItems, а затем входит и модифицирует его (что приведет к удалению, вставке и взаимоблокировке). В моих тестах никогда не было процессов, изменяющих одно и то же посещение или посещение.
У кого-нибудь есть идеи о том, как подойти к этому дальше? Что-нибудь, что я могу попытаться обойти это умным способом (никакие блокировки таблицы и т.д.)? Кроме того, я хотел бы узнать, почему эта блокировка tripple-x возможна даже для одного и того же объекта. Я не понимаю
Пожалуйста, дайте мне знать, если потребуется дополнительная информация для решения головоломки.
[РЕДАКТИРОВАТЬ] Я обновил вопрос с DDL для двух задействованных таблиц.
Кроме того, меня попросили уточнить ожидания: да, несколько тупиков здесь и там нормально, мы просто повторим попытку или заставим пользователя повторно отправить (в общем случае). Но на текущей частоте с 12 одновременными пользователями, я ожидаю, что будет только один раз в несколько часов. В настоящее время они появляются несколько раз в минуту.
В дополнение к этому я получил дополнительную информацию о trancount = 2, которая может указывать на проблему с вложенными транзакциями, которую мы на самом деле не используем. Я тоже исследую это и документирую результаты здесь.
SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)
sqlhandle для каждого фрейма executeStack, чтобы дополнительно определить, что на самом деле выполняется.