Повесть о двух схемах: одна зашла в тупик, другая нет

Фон

У меня есть приложение, которое периодически получает большие объемы данных из внешнего источника (например, XML-файла) и вставляет эти данные в базу данных. Во время этой операции ничто другое не обращается к базе данных. Поскольку имеется большой объем данных, я использую настраиваемое количество потоков для выполнения вставки.

Приложение использует Entity Framework. Однако для этой конкретной операции из-за соображений производительности ADO.Net используется для выполнения хранимой процедуры для вставки данных в таблицу заголовков (ответы) и таблицу сведений (ListAnswerSelections).

Этот проект предшествует EF Migrations. Оригинальный DbContext реализует IDatabaseInitializer для создания представлений, индексов и т. д. Работает как шарм. Я только что повторно реализовал наш DbContext, чтобы правильно использовать миграцию EF. Вот где все становится странным.

Проблема

Когда операция массовой загрузки выполняется с одним потоком для новой схемы, созданной с помощью повторно реализованного DbContext, она работает нормально. Однако когда я использую 2 или более потоков, я получаю высокий уровень взаимоблокировок в индексе первичного ключа ListAnswerSelections (подробная таблица). Диаграмма взаимоблокировки выглядит так:

Схема блокировки

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

В обоих случаях для каждого запуска теста создается новая схема и импортируются идентичные XML-файлы. Тесты проводились несколько раз для каждой схемы, каждый раз с одним и тем же результатом.

Когда я использую инструменты сравнения схем в Visual Studio 2012, чтобы найти различия между схемами, созданными исходным и повторно реализованным DbContext, я не вижу никаких различий ни для одной из двух таблиц, задействованных в этой операции.

Кодекс

Код C#, вызывающий хранимую процедуру, выглядит так:

cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "dbo.uspInsertAnswer";
// Add appropriate parameters
int resultCount = cmd.ExecuteNonQuery();

Сама хранимая процедура выглядит так:

CREATE PROCEDURE [dbo].[uspInsertAnswer] @defId INT, @partId INT, @weight FLOAT, @questionId INT, @listValues tvpInt32List READONLY AS
    DECLARE @type INT
    DECLARE @id INT

    BEGIN TRY   
        INSERT INTO dbo.Answers VALUES(@questionId, @partId, NULL, 0, @weight, GETDATE(), NULL, @partId, @defId, @type)

        INSERT INTO dbo.ListAnswerSelections SELECT n, '', GETDATE(), @questionId, @partId FROM @listValues
    END TRY
    BEGIN CATCH
        -- Error Handling
    END CATCH

Задействованные таблицы выглядят так (некоторые неиндексированные столбцы удалены для краткости):

CREATE TABLE [dbo].[Answers](
    [RelatedQuestionId] [int] NOT NULL,
    [RelatedParticipantId] [int] NOT NULL,
    [Text] [nvarchar](max) NULL,
    [Response_ParticipantId] [int] NULL,
    [Response_DefinitionId] [int] NULL,
    [Type] [nvarchar](128) NOT NULL,
 CONSTRAINT [PK_dbo.Answers] PRIMARY KEY CLUSTERED 
(
    [RelatedQuestionId] ASC,
    [RelatedParticipantId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]


CREATE TABLE [dbo].[ListAnswerSelections](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [NumericValue] [int] NOT NULL,
    [ListAnswer_RelatedQuestionId] [int] NOT NULL,
    [ListAnswer_RelatedParticipantId] [int] NOT NULL,
 CONSTRAINT [PK_dbo.ListAnswerSelections] 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]

Мой вопрос

Что может отличаться между двумя схемами (и не показано в инструменте сравнения схем VS), что может привести к тому, что одна из них будет испытывать частые взаимоблокировки с записью 2 (или более) потоков, в то время как другая испытывает нулевые взаимоблокировки с записью 8 потоков?

Обновление: взаимоблокировка XML

<deadlock-list>
 <deadlock victim="process53ace08">
  <process-list>
   <process id="process53ace08" taskpriority="0" logused="1360" waitresource="KEY: 11:72057594041663488 (2c3c53413efb)" waittime="7106" ownerId="6158342" transactionname="user_transaction" lasttranstarted="2014-08-25T18:29:49.570" XDES="0xbf3cf950" lockMode="RangeS-S" schedulerid="7" kpid="2808" status="suspended" spid="59" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-08-25T18:30:01.240" lastbatchcompleted="2014-08-25T18:30:01.237" lastattention="2014-08-25T18:22:27.293" clientapp=".Net SqlClient Data Provider" hostname="CRUNCHBOX" hostpid="10164" loginname="Crunchbox\Eric" isolationlevel="read committed (2)" xactid="6158342" currentdb="11" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="Survey_DEV_BAT.dbo.uspInsertAnswer" line="17" stmtstart="1088" stmtend="1332" sqlhandle="0x03000b004e241b1505a42b0192a300000100000000000000">
INSERT INTO dbo.Answers VALUES(@questionId, @partId, NULL, 0, @weight, GETDATE(), NULL, @partId, @defId, @type)     </frame>
    </executionStack>
    <inputbuf>
Proc [Database Id = 11 Object Id = 354100302]    </inputbuf>
   </process>
   <process id="process534ee08" taskpriority="0" logused="1360" waitresource="KEY: 11:72057594041663488 (354416591f4b)" waittime="4983" ownerId="6158339" transactionname="user_transaction" lasttranstarted="2014-08-25T18:29:49.570" XDES="0xc1d01950" lockMode="RangeS-S" schedulerid="4" kpid="13824" status="suspended" spid="54" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-08-25T18:30:01.237" lastbatchcompleted="2014-08-25T18:30:01.237" clientapp=".Net SqlClient Data Provider" hostname="CRUNCHBOX" hostpid="10164" loginname="Crunchbox\Eric" isolationlevel="read committed (2)" xactid="6158339" currentdb="11" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="Survey_DEV_BAT.dbo.uspInsertAnswer" line="17" stmtstart="1088" stmtend="1332" sqlhandle="0x03000b004e241b1505a42b0192a300000100000000000000">
INSERT INTO dbo.Answers VALUES(@questionId, @partId, NULL, 0, @weight, GETDATE(), NULL, @partId, @defId, @type)     </frame>
    </executionStack>
    <inputbuf>
Proc [Database Id = 11 Object Id = 354100302]    </inputbuf>
   </process>
  </process-list>
  <resource-list>
   <keylock hobtid="72057594041663488" dbid="11" objectname="Survey_DEV_BAT.dbo.ListAnswerSelections" indexname="PK_dbo.ListAnswerSelections" id="lock8be36880" mode="RangeX-X" associatedObjectId="72057594041663488">
    <owner-list>
     <owner id="process534ee08" mode="RangeX-X"/>
    </owner-list>
    <waiter-list>
     <waiter id="process53ace08" mode="RangeS-S" requestType="wait"/>
    </waiter-list>
   </keylock>
   <keylock hobtid="72057594041663488" dbid="11" objectname="Survey_DEV_BAT.dbo.ListAnswerSelections" indexname="PK_dbo.ListAnswerSelections" id="lock8544da00" mode="X" associatedObjectId="72057594041663488">
    <owner-list>
     <owner id="process53ace08" mode="X"/>
    </owner-list>
    <waiter-list>
     <waiter id="process534ee08" mode="RangeS-S" requestType="wait"/>
    </waiter-list>
   </keylock>
  </resource-list>
 </deadlock>
</deadlock-list>

ОБНОВЛЕНИЕ: ограничение внешнего ключа

ALTER TABLE [dbo].[ListAnswerSelections]  WITH NOCHECK ADD  CONSTRAINT [FK_dbo.ListAnswerSelections_dbo.Answers_ListAnswer_RelatedQuestionId_ListAnswer_RelatedParticipantId] FOREIGN KEY([ListAnswer_RelatedQuestionId], [ListAnswer_RelatedParticipantId])
REFERENCES [dbo].[Answers] ([RelatedQuestionId], [RelatedParticipantId])
ON DELETE CASCADE

person Eric J.    schedule 26.08.2014    source источник


Ответы (1)


Опубликуйте график взаимоблокировок. Не его изображение, а реальный XML-граф взаимоблокировки. Изображение похоже на 1% информации в XML. Например, XML ответит, есть ли у вас хэш блокировки столкновение.

Замки диапазонов. Почему? Это подразумевает сериализуемую изоляцию, и ничто в вашем описании не оправдывает этого. Используйте зафиксированное чтение.

Не имеет отношения к взаимоблокировке, но если вы действительно заботитесь о производительности, используйте массовую вставку. Как и в true массовой вставке. Используйте SqlBulkCopy. который использует настоящий API массовой вставки, а не обычный оператор INSERT. Только API массовой вставки может достичь минимального ведения журнала и только API массовой вставки может выполнять потоковую передачу данных во время вставки.

Некоторые операции внутренне используют сериализуемый уровень изоляции. Ограничение внешнего ключа и поддержка индексированных представлений — вот два примера, см. Conor и обновление уровня изоляции при UPDATE/DELETE Cascading RI:

Это же условие применяется к обслуживанию индексированного представления.

Захват планов выполнения двух случаев немедленно укажет на проблему.

person Remus Rusanu    schedule 26.08.2014
comment
Добавлен взаимоблокирующий XML. Я посмотрю уровень изоляции. Можно ли использовать SqlBulkCopy при вставке в таблицы заголовков/деталей? Кажется, что класс принимает только DestinationTableName. - person Eric J.; 27.08.2014
comment
Вам нужно вставлять заголовки в один проход, детали в другой. - person Remus Rusanu; 27.08.2014
comment
В настоящее время база данных присваивает значение первичного ключа. При выполнении двухпроходного SqlBulkCopy код должен назначать значение первичного ключа? Если нет, то как связать строки сведений с правильной родительской строкой? - person Eric J.; 27.08.2014
comment
У вас есть внешние ключи, ссылающиеся на ListAnswerSelections? Ваша изоляция зафиксирована для чтения, но у вас есть блокировки диапазона, эти обычно указывает ограничения ссылочной целостности. - person Remus Rusanu; 27.08.2014
comment
Что касается идентификационных значений в master-detail: после первого прохода вы должны прочитать вставленные ключи master-значений, присвоить их набору деталей в памяти, а затем выполнить второй проход. В качестве альтернативы можно предварительно вычислить идентификаторы на клиенте и вставить в SET IDENTITY_INSERT (это вполне возможно, когда БД в основном отключена во время массовой вставки, как вы описываете) - person Remus Rusanu; 27.08.2014
comment
Да, ListAnswer_RelatedQuestionId и ListAnswer_RelatedParticipantId являются частью ограничения. Это ограничение отключено во время этой операции. Добавлен DDL, описывающий ограничение (взятое во время массовой операции). Также обратите внимание, что ограничение идентично в каждой схеме. Только один из них демонстрирует тупиковое поведение. - person Eric J.; 27.08.2014
comment
Изоляция: я явно не указываю уровень изоляции в коде. Из статьи, на которую вы ссылаетесь, похоже, что SQL Server в некоторых случаях может автоматически обновлять уровень изоляции до сериализуемого. - person Eric J.; 27.08.2014
comment
Да, и это такой случай. Вставка находится в Answers, но взаимоблокировка возникает в диапазоне ListAnswerSelections, указывая на FK как на виновника. Похоже, в плохом случае он остается включенным, я бы дважды проверил логику отключения FK. Попробуй совсем скинуть. - person Remus Rusanu; 27.08.2014
comment
Я полностью удалил FK, и проблема осталась :-( - person Eric J.; 27.08.2014
comment
Запишите план выполнения INSERT INTO Answer ... и опубликуйте его здесь (исходный XML-файл .sqlplan). Если возможно, зафиксируйте оба случая. - person Remus Rusanu; 27.08.2014
comment
Вы прокомментировали Do you have indexed views based on the two tables?, а затем отредактировали комментарий? Оказывается, проблема в этом :-) Предыдущий метод создания базы данных удалял индексированное представление при выполнении массовых операций. Это было упущено из виду в новой реализации. Если вы хотите обновить свой ответ с помощью этого аспекта, я с радостью приму его. - person Eric J.; 27.08.2014
comment
У меня было это в качестве комментария, затем я решил спросить о планах, поскольку планы точно ответят, в чем проблема, без зондирования. - person Remus Rusanu; 27.08.2014