Я работаю над этой тупиковой проблемой уже несколько дней и, что бы я ни делал, она так или иначе сохраняется.
Во-первых, общая предпосылка: у нас есть визиты с визитами в отношениях один ко многим.
Посетите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, чтобы дополнительно определить, что на самом деле выполняется.Ответы:
Я сделал пару комментариев по этому поводу, но я не уверен, что вы получаете желаемые результаты, когда вы комбинируете уровень изоляции транзакции Repeatable Read с Read Committed Snapshot.
TIL, указанный в вашем списке взаимоблокировок, является повторяемым чтением, которое является еще более ограничительным, чем Read Committed, и, учитывая поток, который вы описываете, вероятно, приводит к взаимоблокировкам.
Возможно, вы пытаетесь сделать так, чтобы ваш TIL БД оставался повторяемым для чтения, но при этом установите для транзакции явное использование TIL снимка с заданным оператором уровня изоляции транзакции. Ссылка: https://msdn.microsoft.com/en-us/library/ms173763.aspx Если это так, я думаю, что вы должны что-то неправильно. Я не знаком с nHibernate, но, похоже, здесь есть ссылка: http://www.anujvarma.com/fluent-nhibernate-setting-database-transaction-isolation-level/
Если архитектура вашего приложения позволяет это сделать, можно попробовать сделать моментальный снимок для чтения на уровне базы данных, а если вы по-прежнему получаете взаимоблокировки, включите снимок с контролем версий строк. Обратите внимание, что, если вы сделаете это, вам нужно переосмыслить настройку tempdb, если вы включите снимок (управление версиями строк). Я могу дать вам все виды материалов по этому вопросу, если вам это нужно - дайте мне знать.
источник
У меня есть пара мыслей. Прежде всего, самый простой способ избежать взаимоблокировок - всегда брать блокировки в одном и том же порядке. Это означает, что другой код, использующий явные транзакции, должен обращаться к объектам в одном и том же порядке, но также должен сортироваться по ключам в явной транзакции по этому ключу. Попробуйте
Visit.VisitItems
выполнить сортировку по его PK, прежде чем делать,Add
илиDelete
если это не огромная коллекция, в этом случае я бы сортировалSELECT
.Сортировка, вероятно, не ваша проблема здесь, хотя. Я предполагаю, что 2 потока захватывают общие блокировки на все
VisitItemID
s для данногоVisitID
и поток A неDELETE
может завершить, пока поток B не освободит свою общую блокировку, которая не будет, пока неDELETE
завершится. Блокировки приложений будут работать здесь и не так плохо, как блокировки таблиц, так как они блокируются только по методу, а другиеSELECT
будут работать нормально. Вы также можете взять эксклюзивную блокировку наVisit
столе для данного,VisitID
но опять же, это потенциально излишним.Я бы порекомендовал превратить ваше жесткое удаление в мягкое удаление (
UPDATE ... SET IsDeleted = 1
вместо использованияDELETE
) и очистить эти записи позже, массово, с помощью некоторого задания очистки, которое не использует явные транзакции. Это, очевидно, потребует рефакторинга другого кода, чтобы игнорировать эти удаленные строки, но это мой предпочтительный метод для обработкиDELETE
s, включенныхSELECT
в явную транзакцию.Вы также можете удалить
SELECT
из транзакции и переключиться на оптимистическую модель параллелизма. Entity Framework делает это бесплатно, не уверен насчет NHibernate. EF вызовет исключение оптимистичного параллелизма, если ваши результатыDELETE
вернутся на 0 строк.источник
Вы пытались переместить обновление Visits перед любыми изменениями для посещения элементов? Этот х-замок должен защищать «дочерние» строки.
Выполнение трассировки с полной блокировкой (и преобразование в удобочитаемое человеком) - большая работа, но она может показать последовательность более четко.
источник
Если вы понятия не имеете, почему стол застревает, иногда возникает застревание
SET XACT_ABORT ON -> это должно позаботиться об ошибках, приводящих к зависанию файла tran. BEGIN TRAN TRAN_NAME --CODE доступ к таблице-- COMMIT TRAN TRAN_NAME
https://stackoverflow.com/questions/2277254/how-to-set-xact-abort-within-ado-net
источник
READ COMMITTED SNAPSHOT ON означает, что каждая отдельная транзакция, выполняемая в READ COMMITTED ISOLATION LEVEL, будет действовать как READ COMMITTED SNAPSHOT.
Это означает, что читатели не будут блокировать писателей, а писатели не будут блокировать читателей.
Вы используете повторяющийся уровень изоляции транзакции чтения, поэтому у вас есть тупик. Read Committed (без моментального снимка) удерживает блокировки строк / страниц до конца инструкции , а Repeatable Read удерживает блокировки до конца транзакции .
Если вы посмотрите на график Deadlock, вы можете увидеть полученную блокировку «S». Я думаю, что это блокировка по второму пункту -> «Прочитать все элементы посещения для данного посещения с помощью VisitId».
источник