Какова наилучшая практика в EF Core для использования параллельных асинхронных вызовов с Injected DbContext?

У меня есть API.NET Core 1.1 с EF Core 1.1 и с использованием настройки Microsoft vanilla с использованием Injection Dependency для предоставления DbContext для моих сервисов. (Ссылка: https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro#register-the-context-with-dependency-injection)

Теперь я рассматриваю распараллеливание базы данных как оптимизацию с использованием метода WhenAll

Поэтому вместо:

var result1 = await _dbContext.TableModel1.FirstOrDefaultAsync(x => x.SomeId == AnId);
var result2 = await _dbContext.TableModel2.FirstOrDefaultAsync(x => x.SomeOtherProp == AProp); 

Я использую:

var repositoryTask1 = _dbContext.TableModel1.FirstOrDefaultAsync(x => x.SomeId == AnId);     
var repositoryTask2 = _dbContext.TableModel2.FirstOrDefaultAsync(x => x.SomeOtherProp == AProp);   
(var result1, var result2) = await (repositoryTask1, repositoryTask2 ).WhenAll();

Все это хорошо и хорошо, пока я не использую одну и ту же стратегию вне этих классов доступа к репозиторию БД и вызываю эти же методы с помощью WhenAll в своем контроллере через несколько служб:

var serviceTask1 = _service1.GetSomethingsFromDb(Id);
var serviceTask2 = _service2.GetSomeMoreThingsFromDb(Id);
(var dataForController1, var dataForController2) = await (serviceTask1, serviceTask2).WhenAll();

Теперь, когда я вызываю это из своего контроллера, я случайно получаю ошибки параллелизма, такие как:

System.InvalidOperationException: ExecuteReader требует открытого и доступного соединения. Состояние тока соединения закрыто.

Причина, по которой я верю, заключается в том, что иногда эти потоки пытаются получить доступ к тем же таблицам одновременно. Я знаю, что это по дизайну в EF Core, и если бы я хотел, чтобы я каждый раз создавал новый dbContext, но я пытаюсь посмотреть, есть ли способ обхода проблемы. Это, когда я нашел этот хороший пост Мехди Эль Гуддари: http://mehdi.me/ambient-dbcontext-in-ef6/

В этом он признает это ограничение:

внедренный DbContext не позволяет вам вводить многопотоковые или любые параллельные потоки выполнения в ваших сервисах.

И предлагает DbContextScope решение проблемы с DbContextScope.

Тем не менее, он предлагает оговорку даже с DbContextScope в том, что он не будет работать параллельно (что я пытаюсь сделать выше):

если вы попытаетесь запустить несколько параллельных задач в контексте DbContextScope (например, создав несколько потоков или несколько задач TPL), вы столкнетесь с большими проблемами. Это связано с тем, что окружающая среда DbContextScope будет проходить через все потоки, которые используются ваши параллельные задачи.

Его последний момент приводит меня к моему вопросу:

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

Должен ли я не использовать WhenAll в этом случае в моих контроллерах и придерживаться его с помощью ожидания один за другим? Или это зависимость от DbContext, более фундаментальная проблема здесь, поэтому новый должен быть создан/поставлен каждый раз какой-то фабрикой?

Ответ 1

Это дошло до того, что на самом деле единственным способом ответить на дебаты было сделать тест производительности/нагрузки, чтобы получить сопоставимые, эмпирические статистические данные, чтобы я мог решить это раз и навсегда.

Вот что я тестировал:

Тест облачной нагрузки с максимальным количеством пользователей VSTS @200 в течение 4 минут на стандартном веб-сервере Azure.

Тестовый № 1:1 вызов API с зависимостью Инъекция DbContext и асинхронный/ожидающий для каждой службы.

Результаты теста №1: enter image description here

Тест № 2: 1 вызов API с новым созданием DbContext в каждом вызове метода службы и использование параллельного выполнения потока с помощью метода WhenAll.

Результаты теста №2: enter image description here

Вывод:

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

По моему мнению, увеличение производительности с параллельной обработкой незначительно, и это не оправдывает необходимость отказа от Injection Dependency, которая создавала бы накладные расходы на обслуживание/обслуживание, вероятность ошибок при неправильном обращении и отказ от официальных рекомендаций Microsoft.

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

Ответ 2

Использование любого метода context.XyzAsync() полезно только в том случае, если вы либо await вызванный метод, либо возвращаете управление вызывающему потоку, который не имеет context в своей области.

Экземпляр DbContext не является потокобезопасным: вы никогда не должны использовать его в параллельных потоках. Это означает, что, конечно, никогда не используйте его в нескольких потоках, даже если они не работают параллельно. Не пытайтесь обойти это.

Если по какой-то причине вы хотите запускать параллельные операции с базой данных (и думаете, что можете избежать взаимоблокировок, конфликтов параллелизма и т.д.), Убедитесь, что каждый из них имеет свой собственный экземпляр DbContext. Обратите внимание, однако, что распараллеливание в основном полезно для процессов, связанных с процессором, а не процессов, связанных с IO, таких как взаимодействие с базами данных. Возможно, вы можете воспользоваться параллельными независимыми операциями чтения, но я бы никогда не выполнял параллельные процессы записи. Помимо взаимоблокировок и т.д., Это также затрудняет выполнение всех операций в одной транзакции.

В ядре ASP.Net вы обычно используете шаблон context-per-request (ServiceLifetime.Scoped, см. Здесь), но даже это не может препятствовать передаче контекста нескольким потокам. В конце концов, это только программист, который может это предотвратить.

Если вы постоянно беспокоитесь об эффективности работы по созданию новых контекстов: не делайте этого. Создание контекста - это легкая операция, потому что базовая модель (модель хранилища, концептуальная модель + сопоставление между ними) создается один раз и затем сохраняется в домене приложения. Кроме того, новый контекст не создает физического подключения к базе данных. Все операции базы данных ASP.Net выполняются через пул соединений, который управляет пулом физических подключений.

Если все это означает, что вам нужно перенастроить свой DI, чтобы согласовать его с лучшими практиками, пусть будет так. Если ваша текущая настройка передает контексты нескольким потокам, в прошлом было плохое дизайнерское решение. Сопротивляйтесь соблазну отложить неизбежный рефакторинг при работе. Единственный рабочий процесс - это распараллеливать ваш код, поэтому в конце он может быть даже медленнее, чем если вы перепроектируете свой DI и код, чтобы придерживаться контекста на поток.