EF Конкурирующие вызовы SaveChanges () - PullRequest
5 голосов
/ 07 октября 2011

Я строю систему пакетной обработки.Партии Units поступают в количестве от 20 до 1000.Каждый Unit представляет собой иерархию моделей (одна основная модель и несколько дочерних моделей).Моя задача заключается в сохранении каждой иерархии модели в базе данных в виде одной транзакции (либо каждая иерархия фиксируется, либо откатывается).К сожалению, EF не смог обработать две части иерархии моделей из-за их способности содержать тысячи записей.

То, что я сделал для решения этой проблемы, настроено SqlBulkCopy для обработки этих двух моделей с потенциально высоким числом и для EF обработки остальных вставок (и ссылочной целостности).

Пакетная петля:

foreach (var unitDetails in BatchUnits)
{
  var unitOfWork = new Unit(unitDetails);
  Task.Factory.StartNew(() =>
    {
      unitOfWork.ProcessX(); // data preparation
      unitOfWork.ProcessY(); // data preparation
      unitOfWork.PersistCase();
    });
}

Единица измерения:

class Unit
{
  public PersistCase()
  {
    using (var dbContext = new CustomDbContext())
    {
      // Need an explicit transaction so that 
      // EF + SqlBulkCopy act as a single block
      using (var scope = new TransactionScope(TransactionScopeOption.Required,
        new TransactionOptions() {
          IsolationLevel = System.Transaction.IsolationLevel.ReadCommitted
        }))
      {
        // Let EF Insert most of the records
        // Note Insert is all it is doing, no update or delete
        dbContext.Units.Add(thisUnit);
        dbContext.SaveChanges();  // deadlocks, DbConcurrencyExceptions here

        // Copy Auto Inc Generated Id (set by EF) to DataTables
        // for referential integrity of SqlBulkCopy inserts
        CopyGeneratedId(thisUnit.AutoIncrementedId, dataTables);

        // Execute SqlBulkCopy for potentially numerous model #1
        SqlBulkCopy bulkCopy1 = new SqlBulkCopy(...);
        ...
        bulkCopy1.WriteToServer(dataTables["#1"]);

        // Execute SqlBulkCopy for potentially number model #2
        SqlBulkCopy bulkCopy2 = new SqlBulkCopy(...);
        ...
        bulkCopy2.WriteToServer(dataTables["#2"]);

        // Commit transaction
        scope.Complete();
      }
    }
  }
}

Сейчас я по сути застрял между камнем и наковальней,Если я оставлю для IsolationLevel значение ReadCommitted, я получу взаимоблокировку между EF INSERT операторами в разных Tasks.

Если я установлю IsolationLevel на ReadUncommitted (что, я думаю, будет в порядке, поскольку я не делаю SELECTs), я получу DbConcurrencyExceptions.

У меня естьНе удалось найти какую-либо полезную информацию о DbConcurrencyExceptions и Entity Framework, но я предполагаю, что ReadUncommitted по существу заставляет EF получать недопустимую информацию о вставленных строках.

UPDATE

Вот некоторая справочная информация о том, что на самом деле вызывает мои проблемы с блокировкой при выполнении INSERTS:

http://connect.microsoft.com/VisualStudio/feedback/details/562148/how-to-avoid-using-scope-identity-based-insert-commands-on-sql-server-2005

Видимо, эта же проблема присутствовала несколько лет назадкогда вышел Linq To SQL и Microsoft исправила это, изменив способ выбора scope_identity ().Не уверен, почему их позиция изменилась на проблему SQL Server, когда та же проблема возникла с Entity Framework.

1 Ответ

3 голосов
/ 14 ноября 2011

Эта проблема достаточно хорошо объяснена здесь: http://connect.microsoft.com/VisualStudio/feedback/details/562148/how-to-avoid-using-scope-identity-based-insert-commands-on-sql-server-2005

По сути, это внутренняя проблема EF.Я перенес свой код для использования Linq To SQL, и теперь он работает нормально (больше не делает ненужных SELECT для значения идентификатора).

Соответствующая цитата из точно такой же проблемы в Linq To Sql, которая была исправлена:

Когда в таблице есть столбец идентификаторов, Linq to SQL генерирует крайне неэффективный SQL для вставки в такую ​​таблицу.Предположим, что таблица - это Order, а столбец identiy - Id.Генерируемый SQL:

exec sp_executesql N'INSERT INTO [dbo]. [Order] ([Colum1], [Column2]) VALUES (@ p0, @ p1)

SELECT [t0]. [Id] FROM [dbo]. [Порядок] AS [t0] ГДЕ [t0]. [Id] = (SCOPE_IDENTITY ()) ', N' @ p0 int, @ p1 int, @ p0 = 124, @ p1= 432

Как можно увидеть вместо непосредственного возврата SCOPE_IDENTITY () с помощью SELECT SCOPE_IDENTITY (), сгенерированный SQL выполняет SELECT для столбца Id, используя значение, возвращаемое SCOPE_IDENTITY ().Когда количество записей в таблице велико, это значительно замедляет вставку.Когда таблица разделена, проблема становится еще хуже.

...