Ищу помощь в отладке взаимной блокировки SQL

Вопрос или проблема

Мне требуется руководство по устранению этой взаимной блокировки. Я использую 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.

Анализ вашего кода

Согласно предоставленному коду и логам взаимоблокировки, у вас имеется следующее:

  1. Удаление из таблицы: Находясь в процессе удаления из PickListTotes, вы параллельно пытаетесь удалить запись из InventoryTrack.
  2. Использование ключей блокировки: Логи показывают, что процессы ожидают освобождения ключей блокировки на одном и том же ресурсе (PickListItem).
  3. Каскадное удаление: У вас настроено каскадное удаление, что может позволить избежать непосредственного удаления из InventoryTrack, если записи, связанные с InventoryTrack, уже будут удалены.

Рекомендации по отладке и предотвращению взаимоблокировок

  1. Упрощение транзакций: Убедитесь, что ваши транзакции как можно более короткие, чтобы минимизировать время держания блокировок. Например, рассмотрите возможность выполнения SaveChanges() столько раз, сколько это необходимо. В вашем случае вы сначала удаляете PickListTotes, а затем загружаете и удаляете InventoryTracks.

  2. Изменение порядка операций: Попробуйте изменить порядок выполнения операций так, чтобы они всегда выполнялись в одном и том же порядке, чтобы избежать ситуации круговой блокировки.

  3. Исключение избыточных зависимостей: Если каскадное удаление настроено, и оно правильно работает, то нет необходимости явно вызывать Remove() для связанных записей, которые будут автоматически удалены.

  4. Логирование: Добавьте дополнительное логирование в вашем приложении для того, чтобы определить, какие именно операции выполняются и в каком порядке перед возникновением взаимоблокировки.

  5. Тестирование: Попробуйте проводить нагрузочное тестирование, чтобы выявить проблемные места, имитируя ситуации, схожие с реальными сценариями.

  6. Использование уровней изоляции: Возможно, имеет смысл рассмотреть использование более агрессивных уровней изоляции, таких как 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();
}

Заключение

Работа с взаимоблокировками в базе данных может быть сложной задачей, но с помощью упрощения транзакций и контроля порядка операций можно значительно уменьшить вероятность возникновения данной проблемы. Важно тщательно тестировать и анализировать вашу логику работы с данными.

Если у вас есть дополнительные вопросы или требуется дальнейшая помощь, не стесняйтесь обращаться!

Оцените материал
Добавить комментарий

Капча загружается...