У меня есть следующая процедура (SQL Server 2008 R2):
create procedure usp_SaveCompanyUserData
@companyId bigint,
@userId bigint,
@dataTable tt_CoUserdata readonly
as
begin
set nocount, xact_abort on;
merge CompanyUser with (holdlock) as r
using (
select
@companyId as CompanyId,
@userId as UserId,
MyKey,
MyValue
from @dataTable) as newData
on r.CompanyId = newData.CompanyId
and r.UserId = newData.UserId
and r.MyKey = newData.MyKey
when not matched then
insert (CompanyId, UserId, MyKey, MyValue) values
(@companyId, @userId, newData.MyKey, newData.MyValue);
end;
CompanyId, UserId, MyKey формируют составной ключ для целевой таблицы. CompanyId - это внешний ключ родительской таблицы. Кроме того, существует некластеризованный индекс CompanyId asc, UserId asc
.
Он вызывается из разных потоков, и я постоянно получаю взаимные блокировки между разными процессами, вызывающими одно и то же утверждение. Насколько я понимаю, «with (holdlock)» было необходимо для предотвращения ошибок состояния вставки / обновления гонки.
Я предполагаю, что два разных потока блокируют строки (или страницы) в разных порядках, когда они проверяют ограничения, и, таким образом, блокируются.
Это правильное предположение?
Как лучше всего разрешить эту ситуацию (то есть отсутствие взаимоблокировок, минимальное влияние на многопоточную производительность)?
(Если вы просматриваете изображение на новой вкладке, оно доступно для чтения. Извините за небольшой размер.)
- В @datatable не более 28 строк.
- Я проследил весь код и нигде не вижу, чтобы мы начали транзакцию здесь.
- Внешний ключ настроен на каскад только при удалении, и из родительской таблицы не было удалений.
Не было бы проблемы, если бы табличная переменная содержала только одно значение. С несколькими рядами появилась новая возможность тупика. Предположим, что два параллельных процесса (A & B) выполняются с табличными переменными, содержащими (1, 2) и (2, 1) для одной и той же компании.
Процесс A считывает место назначения, не находит строки и вставляет значение «1». Он содержит исключительную блокировку строки со значением '1'. Процесс B читает пункт назначения, не находит строки и вставляет значение '2'. Он содержит эксклюзивную блокировку строки со значением '2'.
Теперь процессу A требуется обработать строку 2, а процессу B - обработать строку 1. Ни один из процессов не может добиться прогресса, поскольку ему требуется блокировка, несовместимая с исключительной блокировкой, удерживаемой другим процессом.
Чтобы избежать взаимных блокировок с несколькими строками, необходимо каждый раз обрабатывать строки (и обращаться к таблицам) в одном и том же порядке . Переменная таблицы в плане выполнения, показанная в вопросе, представляет собой кучу, поэтому строки не имеют внутреннего порядка (они, скорее всего, будут прочитаны в порядке вставки, хотя это не гарантируется):
Отсутствие согласованного порядка обработки строк приводит к возможности тупиковой ситуации. Второе соображение заключается в том, что отсутствие ключевой гарантии уникальности означает, что для обеспечения правильной защиты к Хэллоуину необходим Table Spool. Спул - это нетерпеливая шпуля, то есть все строки записываются на рабочий стол tempdb, а затем читаются и воспроизводятся для оператора Insert.
Переопределение
TYPE
табличной переменной для включения в кластерPRIMARY KEY
:План выполнения теперь показывает сканирование кластеризованного индекса, а гарантия уникальности означает, что оптимизатор может безопасно удалить таблицу Spool:
В тестах с 5000 итерациями
MERGE
оператора в 128 потоках не возникало взаимных блокировок с переменной кластеризованной таблицы. Я должен подчеркнуть, что это только на основе наблюдения; переменная кластеризованной таблицы также ( технически ) может производить свои строки в различных порядках, но шансы на согласованный порядок значительно повышаются. Разумеется, наблюдаемое поведение необходимо будет повторно тестировать для каждого нового накопительного обновления, пакета обновления или новой версии SQL Server.В случае, если определение табличной переменной не может быть изменено, есть другая альтернатива:
Это также обеспечивает исключение буфера (и согласованности порядка строк) за счет введения явной сортировки:
Этот план также не приводил к тупикам при использовании того же теста. Сценарий воспроизведения ниже:
источник
Я думаю, что SQL_Kiwi предоставил очень хороший анализ. Если вам нужно решить проблему в базе данных, вы должны следовать его предложению. Конечно, вам нужно повторно протестировать, что он по-прежнему работает для вас каждый раз, когда вы обновляете, устанавливаете пакет обновления или добавляете / изменяете индекс или индексированное представление.
Есть три других варианта:
Вы можете сериализовать ваши вставки так, чтобы они не сталкивались: вы можете вызвать sp_getapplock в начале вашей транзакции и получить эксклюзивную блокировку перед выполнением вашего MERGE. Конечно, вам все еще нужно провести стресс-тестирование.
Вы можете иметь один поток для обработки всех ваших вставок, чтобы ваш сервер приложений обрабатывал параллелизм.
Вы можете автоматически повторить попытку после взаимоблокировок - это может быть самый медленный подход, если уровень параллелизма высок.
В любом случае, только вы можете определить влияние вашего решения на производительность.
Обычно у нас вообще нет тупиков в нашей системе, хотя у нас есть большой потенциал для их наличия. В 2011 году мы допустили ошибку в одном развертывании, и за несколько часов произошло полдюжины тупиковых ситуаций, все по одному и тому же сценарию. Я исправил это скоро, и это были все тупики за год.
Мы в основном используем подход 1 в нашей системе. Это работает очень хорошо для нас.
источник
Еще один возможный подход - я обнаружил, что Merge иногда представляет проблемы с блокировкой и производительностью - возможно, стоит поиграть с опцией запроса Option (MaxDop x)
В неясном и далеком прошлом SQL Server имел опцию «Блокировка уровня вставки на уровне строки» - но это, похоже, умерло смертью, однако кластеризованный PK с идентификатором должен обеспечить чистоту вставок.
источник