Вопрос или проблема
Мне требуется руководство по устранению этой взаимной блокировки. Я использую EF Core и считаю, что строка, вызывающая проблему, выглядит так: _context.InventoryTracks.Remove(id). Это единственное место в коде, где выполняется удаление из этой таблицы. Это генерирует оператор DELETE, показанный ниже. Есть блокировка ключа, которая может быть вызвана предыдущим запросом с Include (или чем-то другим), но я не понимаю, почему здесь есть два процесса, выполняющих одно и то же действие (удаление), и один из них становится жертвой. Перед вызовом .Remove есть .RemoveRange и .SaveChanges на связанной таблице. Интересно, что если этого совсем не делать, удаление записи InventoryTracks автоматически очистит то, что удаляется в вызове .RemoveRange, поскольку каскадное удаление настроено в БД. Буду признателен за любые предложения по улучшению отладки. Спасибо.
<deadlock>
<victim-list>
<victimProcess id="process15c4352d468" />
</victim-list>
<process-list>
<process id="process15c4352d468" taskpriority="0" logused="604" waitresource="KEY: 5:72057654391668736 (b4d7f128fa4c)" waittime="1111" ownerId="2312262978" transactionname="user_transaction" lasttranstarted="2024-09-25T10:07:12.913" XDES="0x154f76b8428" lockMode="RangeS-U" schedulerid="4" kpid="4460" status="suspended" spid="96" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2024-09-25T10:07:12.933" lastbatchcompleted="2024-09-25T10:07:12.923" lastattention="1900-01-01T00:00:00.923" clientapp="MyAPI" hostname="ProdServer" hostpid="14488" loginname="AD\user" isolationlevel="read committed (2)" xactid="2312262978" currentdb="5" currentdbname="MyDB" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
<executionStack>
<frame procname="adhoc" line="2" stmtstart="86" stmtend="258" sqlhandle="0x0200000060b625062105144680a22bc60c73056ff9771a4c0000000000000000000000000000000000000000">
unknown </frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
(@p2 int,@p3 varbinary(8))SET NOCOUNT ON;
DELETE FROM [dbo].[InventoryTrack]
WHERE [InventoryTrackId] = @p2 AND [RowVersion] = @p3;
SELECT @@ROWCOUNT;
</inputbuf>
</process>
<process id="process15aef12e108" taskpriority="0" logused="960" waitresource="KEY: 5:72057654391668736 (cf6d78acbc8a)" waittime="1121" ownerId="2312262977" transactionname="user_transaction" lasttranstarted="2024-09-25T10:07:12.903" XDES="0x1552a7c0428" lockMode="RangeS-U" schedulerid="7" kpid="9872" status="suspended" spid="125" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2024-09-25T10:07:12.923" lastbatchcompleted="2024-09-25T10:07:12.913" lastattention="1900-01-01T00:00:00.913" clientapp="MyAPI" hostname="ProdServer" hostpid="14488" loginname="AD\user" isolationlevel="read committed (2)" xactid="2312262977" currentdb="5" currentdbname="MyDB" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
<executionStack>
<frame procname="adhoc" line="2" stmtstart="86" stmtend="258" sqlhandle="0x0200000060b625062105144680a22bc60c73056ff9771a4c0000000000000000000000000000000000000000">
unknown </frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
(@p2 int,@p3 varbinary(8))SET NOCOUNT ON;
DELETE FROM [dbo].[InventoryTrack]
WHERE [InventoryTrackId] = @p2 AND [RowVersion] = @p3;
SELECT @@ROWCOUNT;
</inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057654391668736" dbid="5" objectname="MyDB.dbo.PickListItem" indexname="PK_PickListItem" id="lock150c6d0f000" mode="RangeS-U" associatedObjectId="72057654391668736">
<owner-list>
<owner id="process15aef12e108" mode="RangeS-U" />
</owner-list>
<waiter-list>
<waiter id="process15c4352d468" mode="RangeS-U" requestType="wait" />
</waiter-list>
</keylock>
<keylock hobtid="72057654391668736" dbid="5" objectname="MyDB.dbo.PickListItem" indexname="PK_PickListItem" id="lock15db6cb0200" mode="X" associatedObjectId="72057654391668736">
<owner-list>
<owner id="process15c4352d468" mode="X" />
</owner-list>
<waiter-list>
<waiter id="process15aef12e108" mode="RangeS-U" requestType="wait" />
</waiter-list>
</keylock>
</resource-list>
</deadlock>
Вот код EF из проекта, который был создан некоторое время назад людьми, которых уже нет, и нет сопроводительной документации. Я убрал код до и после рассматриваемого участка.
//код выше, опущен для краткости
incompleteTotesForPick = _context.PickListTotes
.Where(plt => plt.PickStatusId != (int)PickStatus.Completed)
.Include(pli => pli.PickListItem.InventoryTrack)
.Any(
ip =>
ip.PickListItem.InventoryTrack.InventoryTrackId ==
pickListTote.PickListItem.InventoryTrackId);
var inventoryTrack = pickListTote.PickListItem.InventoryTrack;
var pickListItemTotes = _context.PickListTotes
.Where(pl => pl.PickListItem.PickListItemId == pickListTote.PickListItemId);
totalPickQty = pickListItemTotes.Sum(q => q.PickedQty);
_context.PickListTotes.RemoveRange(pickListItemTotes);
_context.SaveChanges();
if (!incompleteTotesForPick)
{
_context.InventoryTracks.Remove(InventoryTrack); //это должно генерировать удаление и являться местом возникновения взаимной блокировки
_context.SaveChanges();
}
//больше кода, опущено для краткости
Ответ или решение
Конечно, давайте разберемся с проблемой взаимоблокировки (deadlock) в вашем SQL, особенно учитывая, что вы используете Entity Framework Core.
Понимание взаимоблокировки
Взаимоблокировка происходит, когда два или более процессов ждут освободившихся ресурсов, которые удерживаются друг другом. В вашем случае это происходит при попытке удалить записи из таблицы InventoryTrack
и, возможно, связанных с ней таблиц, таких как PickListItem
.
Анализ вашего кода
Согласно предоставленному коду и логам взаимоблокировки, у вас имеется следующее:
- Удаление из таблицы: Находясь в процессе удаления из
PickListTotes
, вы параллельно пытаетесь удалить запись изInventoryTrack
. - Использование ключей блокировки: Логи показывают, что процессы ожидают освобождения ключей блокировки на одном и том же ресурсе (
PickListItem
). - Каскадное удаление: У вас настроено каскадное удаление, что может позволить избежать непосредственного удаления из
InventoryTrack
, если записи, связанные сInventoryTrack
, уже будут удалены.
Рекомендации по отладке и предотвращению взаимоблокировок
-
Упрощение транзакций: Убедитесь, что ваши транзакции как можно более короткие, чтобы минимизировать время держания блокировок. Например, рассмотрите возможность выполнения
SaveChanges()
столько раз, сколько это необходимо. В вашем случае вы сначала удаляетеPickListTotes
, а затем загружаете и удаляетеInventoryTracks
. -
Изменение порядка операций: Попробуйте изменить порядок выполнения операций так, чтобы они всегда выполнялись в одном и том же порядке, чтобы избежать ситуации круговой блокировки.
-
Исключение избыточных зависимостей: Если каскадное удаление настроено, и оно правильно работает, то нет необходимости явно вызывать
Remove()
для связанных записей, которые будут автоматически удалены. -
Логирование: Добавьте дополнительное логирование в вашем приложении для того, чтобы определить, какие именно операции выполняются и в каком порядке перед возникновением взаимоблокировки.
-
Тестирование: Попробуйте проводить нагрузочное тестирование, чтобы выявить проблемные места, имитируя ситуации, схожие с реальными сценариями.
-
Использование уровней изоляции: Возможно, имеет смысл рассмотреть использование более агрессивных уровней изоляции, таких как
Read Uncommitted
, чтобы уменьшить блокировки, но имейте в виду, что это может привести к «грязным» чтениям.
Пример улучшенной логики
Следующий код иллюстрирует более безопасный подход к удалению:
// Удаляем все связанные PickListTotes
var pickListItemTotes = _context.PickListTotes
.Where(pl => pl.PickListItem.PickListItemId == pickListTote.PickListItemId);
_context.PickListTotes.RemoveRange(pickListItemTotes);
_context.SaveChanges(); // Обратите внимание на отдельный вызов SaveChanges
// Удаляем InventoryTrack только если нет незавершенных Tote
if (!incompleteTotesForPick)
{
var inventoryTrack = pickListTote.PickListItem.InventoryTrack;
_context.InventoryTracks.Remove(inventoryTrack);
_context.SaveChanges();
}
Заключение
Работа с взаимоблокировками в базе данных может быть сложной задачей, но с помощью упрощения транзакций и контроля порядка операций можно значительно уменьшить вероятность возникновения данной проблемы. Важно тщательно тестировать и анализировать вашу логику работы с данными.
Если у вас есть дополнительные вопросы или требуется дальнейшая помощь, не стесняйтесь обращаться!