Как избежать взаимоблокировок – как заблокировать на уровне строки

У меня возникла тупиковая ситуация, которую я не могу воспроизвести экспериментально. Я считаю, что в результате своих экспериментов я понял, как решить эту проблему, но мне хотелось бы изучить основной принцип.

Вот график блокировки, предоставленный SQL Server. По идентификатору объекта можно сделать вывод, что оба процесса пытаются получить доступ к одной и той же таблице.

Таблица выглядит следующим образом. (Для тестирования я использовал глобальную временную таблицу)

CREATE TABLE ##tMyTable(
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Fk_1_Id] [int] NOT NULL,
    [Fk_2_Id] [int] NULL,
    [Fk_3_Id] [int] NULL,
    [Day] [date] NOT NULL,
    [Quantity] [decimal](19, 3) NOT NULL,
 CONSTRAINT [Pk_MyTable_Id] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

+ foreign key stuff. 

Столбцы с префиксом Fk_ являются внешними ключами.

Также в таблице есть индекс:

CREATE NONCLUSTERED INDEX [Idx_IndexName] ON ##tMyTable
(
    [Fk_1_Id] ASC,
    [Fk_2_Id] ASC--,
    [Day] ASC,
    [Id] ASC
)
INCLUDE([Fk_3_Id],[Quantity]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

Прерванный процесс был оператором удаления:

delete from ##tMyTable where Fk_2_Id = 86416

Процесс, которому было позволено жить, был вставкой. (даты указаны в немецком формате).

insert into ##tMyTable (Fk_1_Id, Fk_2_Id, Day, Quantity) select Fk_1_Id, 86415, Day, Quantity from tOtherTableWithSimilarColumns where Fk_4_Id = 22 and Day >= '01.07.2024' and Day < '01.08.2024'

Для тестирования я установил два сеанса со следующим кодом.

Begin transaction

Select @@SPID as FirstTransactionProcessID

Query I want to test

commit

Если я выполняю код без включения commit, транзакции блокируют свои ресурсы, и я могу смоделировать ситуацию тупика.

С помощью exec sp_lock я могу потом проверить замки.

Прежде чем начать, я вставил в таблицу следующее:

insert into ##tMyTable (Fk_1_Id, Fk_2_Id, Fk_3_Id, Tag, Menge)
Select 1, 1, NULL, '2024-01-01', 22.404

В сеансе 1 я выполнил это:

insert into ##tMyTable (Fk_1_Id, Fk_2_Id, Fk_3_Id, Tag, Menge)
Select 2, 2, NULL, '2024-01-01', 22.404

И затем во втором сеансе я выполнил это:

delete from ##tMyTable where Fk_2_Id = 1

Результат был такой:

SPID 77 — это вставка, а SPID 69 — удаление.

Как вы можете видеть, команда удаления пытается получить U-блокировку ключа, но не получает ее, поскольку вставка X блокирует клавишу. Однако я ожидал увидеть, что вставка блокирует страницу с помощью блокировки IX, в то время как удаление требует той же страницы для блокировки U.

Вопрос: Что насчет моей тестовой настройки не так? Почему я получаю другие блокировки по сравнению с исходной тупиковой ситуацией? И почему удаление просто не устанавливает блокировку IX на ключ и не работает на уровне строки? Две блокировки IX совместимы, и запросы не должны мешать друг другу. В конце концов, они нацелены на разные идентификаторы и, следовательно, на разные строки.

Что, похоже, решает мою проблему: если я создаю индекс, который включает только Fk_2_Id, в моей тестовой настройке не возникает конфликтов блокировок. Однако я бы хотел избежать «исправления» чего-либо без понимания того, почему мое решение работает.

XML тупика:

<deadlock>
 <victim-list>
  <victimProcess id = "process1f4f20e5468" />
 </victim-list>
 <process-list>
  <process id = "process1f4f20e5468" taskpriority = "0" logused = "0" waitresource = "PAGE: 8:1:11652007 " waittime = "2865" ownerId = "213325556" transactionname = "DELETE" lasttranstarted = "2024-08-04T10:02:44.943" XDES = "0x1f4e33c3aa0" lockMode = "U" schedulerid = "2" kpid = "9392" status = "suspended" spid = "56" sbid = "0" ecid = "2" priority = "0" trancount = "0" lastbatchstarted = "2024-08-04T10:02:44.937" lastbatchcompleted = "2024-08-04T10:02:44.933" lastattention = "1900-01-01T00:00:00.933" clientapp = ".Net SqlClient Data Provider" hostname = "Our-Host" hostpid = "11672" isolationlevel = "read committed (2)" xactid = "213325556" currentdb = "8" currentdbname = "Our_DB" lockTimeout = "4294967295" clientoption1 = "671088672" clientoption2 = "128056">
   <executionStack>
    <frame procname = "adhoc" line = "1" stmtend = "152" sqlhandle = "0x02000000cff49033589c5bbc920f9e3e6c20dd52ec7b2a6b0000000000000000000000000000000000000000">
unknown    </frame>
    <frame procname = "adhoc" line = "1" stmtend = "152" sqlhandle = "0x02000000a28e5e12abc21669e1bfc6fb7c33f25bfee40b1c0000000000000000000000000000000000000000">
unknown    </frame>
   </executionStack>
   <inputbuf>
delete from tMyTable where Fk_2_Id = 86416   </inputbuf>
  </process>
  <process id = "process1f4f5011c28" taskpriority = "0" logused = "9944" waitresource = "PAGE: 8:1:11631539 " waittime = "4125" ownerId = "213327344" transactionname = "INSERT" lasttranstarted = "2024-08-04T10:02:46.687" XDES = "0x1f4e45e4460" lockMode = "IX" schedulerid = "4" kpid = "11516" status = "suspended" spid = "68" sbid = "0" ecid = "0" priority = "0" trancount = "2" lastbatchstarted = "2024-08-04T10:02:46.657" lastbatchcompleted = "2024-08-04T10:02:46.653" lastattention = "1900-01-01T00:00:00.653" clientapp = ".Net SqlClient Data Provider" hostname = "Our-Host" hostpid = "11672" loginname = "loginname" isolationlevel = "read committed (2)" xactid = "213327344" currentdb = "8" currentdbname = "Our_DB" lockTimeout = "4294967295" clientoption1 = "671088672" clientoption2 = "128056">
   <executionStack>
    <frame procname = "adhoc" line = "1" stmtend = "568" sqlhandle = "0x02000000679aaf081f4d1ffe0710dbf9b238a8db3e6adb6b0000000000000000000000000000000000000000">
unknown    </frame>
   </executionStack>
   <inputbuf>
       insert into tMyTable (Fk_1_Id, Fk_2_Id, Day, Quantity) select Fk_1t_Id, 86415, Day, Quantity from tOtherTableWithSimilarColumns where Fk_4_Id = 22 and Day &gt;= '01.07.2024' and Day &lt; '01.08.2024'   </inputbuf>
  </process>
 </process-list>
 <resource-list>
  <pagelock fileid = "1" pageid = "11652007" dbid = "8" subresource = "FULL" objectname = "Our_DB.dbo.tMyTable" id = "lock1f7bf84a200" mode = "IX" associatedObjectId = "72057661408477184">
   <owner-list>
    <owner id = "process1f4f5011c28" mode = "IX" />
   </owner-list>
   <waiter-list>
    <waiter id = "process1f4f20e5468" mode = "U" requestType = "wait" />
   </waiter-list>
  </pagelock>
  <pagelock fileid = "1" pageid = "11631539" dbid = "8" subresource = "FULL" objectname = "Our_DB.dbo.tMyTable" id = "lock1f4e3647700" mode = "U" associatedObjectId = "72057661408477184">
   <owner-list>
    <owner id = "process1f4f20e5468" mode = "U" />
   </owner-list>
   <waiter-list>
    <waiter id = "process1f4f5011c28" mode = "IX" requestType = "wait" />
   </waiter-list>
  </pagelock>
 </resource-list>
</deadlock>

Вставьте граф взаимоблокировок в формате XDL/XML. Также, пожалуйста, поделитесь планами запросов двух операторов взаимоблокировки через brentozar.com/pastetheplan

Charlieface 07.08.2024 15:04

DELETE не может использовать индекс Idx_IndexName, поскольку ведущим ключом является Fk_1_Id, который не ищется в DELETE. Вместо этого он сканирует всю таблицу, что вызывает серьезные проблемы с взаимоблокировками. Это классический тупик закладок. Если вы поменяете порядок двух первых столбцов в индексе (или создадите второй индекс), тогда он сможет напрямую искать правильные строки.

Charlieface 07.08.2024 15:07

Пожалуйста, также поделитесь планом запроса, как вас просили.

Charlieface 07.08.2024 17:05
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
3
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Каждый внешний ключ должен поддерживаться индексом, а в FK_2_id индекс отсутствует.

Команда DELETE не может использовать индекс Idx_IndexName, поскольку ведущим ключом является Fk_1_Id, который не ищется в команде DELETE. Вместо этого он сканирует всю таблицу, что вызывает серьезные проблемы с взаимоблокировками. Это классический тупик закладок.

Итак, вам нужен дополнительный индекс.

Другие вопросы по теме