Как избежать блокировки базы данных в инфраструктуре Entity 4 при выполнении многих обновлений

Этот вопрос касается наилучшей практики для обработки многих вставок или обновлений с использованием Microsoft Entity Framework. Проблема в том, что мы написали долговременную программу, которая отбрасывает тысячи записей из базы данных, а затем обновляет одно поле в каждой из этих записей, один за другим. К большому сожалению, мы поняли, что каждая из этих обновленных записей была заблокирована в течение времени, в течение которого ObjectContext не был удален. Ниже приведен некоторый псевдокод (на самом деле не выполняется), чтобы проиллюстрировать:

using(ObjectContext context = new ObjectContext())
{

    var myRecords = context.CreateObjectSet<MyType>().AsQueryable();

    foreach(var record in myRecords)
    {
       record.MyField = "updated!";
       context.SaveChanges();

       //--do something really slow like call an external web service
   }
}

Проблема заключается в том, что нам нужно делать много обновлений без какого-либо учета транзакций. Мы были удивлены, осознав, что вызов context.SaveChanges() фактически создает блокировку записей и не освобождает его до тех пор, пока ObjectContext не будет удален. Мы особо НЕ хотим блокировать записи в базе данных, так как это система с высоким трафиком, и программа может работать в течение нескольких часов.

Итак, вопрос заключается в следующем: каков оптимальный способ сделать много обновлений в Microsoft Entity Framework 4 БЕЗ делать все это на одной длинной транзакции, которая блокирует БД? Мы надеемся, что ответ заключается не в создании нового ObjectContext для каждого обновления...

Ответ 1

Структура Entity поверх SQL-сервера по умолчанию использует прочитанный уровень изоляции транзакции, а транзакция завершается в конце SaveChanges. Если вы подозреваете другое поведение, оно должно быть остальным кодом (вы используете TransactionScope? - вы не показывали его в своем коде), или это должна быть некоторая ошибка.

Также ваш подход неверен. Если вы хотите сохранить каждую запись отдельно, вы также должны загружать каждую запись отдельно. EF определенно является плохим выбором для такого типа приложений. Даже если вы используете только один SaveChange для обновления всех своих записей, он все равно сделает однократное обращение к базе данных для каждого обновления.

Ответ 2

Эти блокировки не создаются Entity Framework. EF поддерживает только оптимистичный concurrency, пессимистическая блокировка не поддерживается с помощью EF.

Я думаю, что блокировка, которую вы испытываете, является результатом вашей конфигурации SQL Server. Возможно, если ваш уровень изоляции транзакций на сервере будет установлен на REPEATABLE READ, это может вызвать блокировки после каждого запроса. Но я не уверен, какая настройка конфигурации может быть именно такой проблемой. Более подробно здесь.

Edit:

Еще одна полезная статья об транзакциях и изоляции транзакций в EF здесь. Он настоятельно рекомендует всегда задавать уровень изоляции явно. Цитата из статьи:

Если вы не контролируете [ уровень изоляции], , о котором вы не знаете в уровень изоляции транзакции запросы будут выполняться. В конце концов, вы не знаете, где соединение что вы получили из бассейна, было [...] Вы просто наследуете последний использованный уровень изоляции на соединении, поэтому вы не знаете, какой тип замков (или хуже: проигнорировано) вашим запросов и как долго эти блокировки будет проведено. В загруженной базе данных безусловно, приведет к случайным ошибкам, тайм-ауты и взаимоблокировки.

Ответ 3

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

Ответ 4

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

  • Если вы хотите прочитать все заранее

Решение A) Используйте область транзакций, которая включает чтение и обновление PRO: данные безопасно обновляются CONS: Замки, вызванные чтением (повторяемые чтения) и обновлением

Решение B) Не используйте транзакцию и обновляйте все данные вместе PRO: данные безопасно обновлены, но данные, которые вы прочитали, могут быть изменены в то же время CONS: блокировки, вызванные обновлением на весь период (EF создает по умолчанию транзакцию)

Решение C) Обновление пакетами вместо всех данных вместе (можно использовать только в том случае, если выбор не блокирует таблицы, в противном случае вы получите то же поведение, что и B PRO: более короткие и малые блокировки в обновленных таблицах CONS: вы увеличиваете изменение воздействия на устаревание данных

  1. Если вы хотите (и можете) читать партиями

Решение D). Разрушение проблемы и разделение чтения могут помочь вам уменьшить блокировку, чтобы вы могли использовать область транзакций для обертывания как чтения, так и записи (как sol. A) PRO: данные безопасно обновляются CONS: Замки, вызванные чтением (повторяемыми чтениями) и обновлением, воздействия зависят от размера партии и характера самого запроса

Решение E) Не используйте транзакции, поэтому только обновление будет производить небольшие блокировки (как зонд B) PRO: данные безопасно обновлены, но данные, которые вы прочитали, могут быть изменены в то же время CONS: Замки, вызванные обновлениями

Как правильно указала @Ladislav, несколько вложений действительно неэффективны, и быстрое профилирование в базе данных показывает вам, как магия ORM не срабатывает в этом случае. Если вы хотите использовать EF для выполнения пакетных операций таких вставок, обновления и удаления, я рекомендую вам посмотреть на это: EF Utilities

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

SELECT
    OBJECT_NAME(p.OBJECT_ID) AS TableName,
    resource_type,
    resource_description
FROM
    sys.dm_tran_locks l JOIN 
    sys.partitions p ON 
    l.resource_associated_entity_id = p.hobt_id