Бекенд на C# Web Api
Чтение и запись большого числа данных
Рассмотрим некоторые оптимизации при чтении и записи больших объемов данных в БД.
Если серверу нужно что-то прочитать из стороннего источника используя HTTP запрос, то это можно сделать с помощью класса HttpClient. Однако если получаемые данные большие, то вызов методов client.GetAsync() или client.Get() приведут к тому что оперативная память быстро забьётся, т.к. приложение будет пытаться держать в памяти все данные, получаемые запросом.
Вместо этого используйте метод client.GetStreamAsync() и работайте с результатом как с потоком. Однако этот метод не позволяет узнать статус-код запроса. Чтобы быстро получить статус-код запроса без нагрузки на оперативную память можно использовать обычный метод client.GetAsync(), но с дополнительным параметром HttpCompletionOption.ResponseHeadersRead.
HttpClient client = new();
// читаем только заголовки
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (response.StatusCode == HttpStatusCode.OK)
{
// создаем Stream контента
using Stream contentStream = await client.GetStreamAsync(url);
}Чтобы прочитать большой объем данных последовательно, не нагрузив оперативную память, используют стримы.
Рассмотрим данные, которые приходят в формате json. Допустим в массиве data очень большой объем данных.
{
"metadata": {
"version": "1.0",
"source": "example"
},
"data": [
{
"id": 1,
"name": "Item 1",
"value": 100
},
{
"id": 2,
"name": "Item 2",
"value": 200
},
{
"id": 3,
"name": "Item 3",
"value": 300
},
...
]
}Чтобы прочитать эти данные можно использовать такой метод:
public static async Task ReadStreamData<T>(Stream jsonStream, bool gzip, Func<T, Task> onFetched)
{
using var gzipStream = gzip ? new GZipStream(jsonStream, CompressionMode.Decompress) : null;
using var textReader = new StreamReader(gzipStream ?? jsonStream);
using var reader = new JsonTextReader(textReader);
JsonSerializer serializer = new JsonSerializer();
while (await reader.ReadAsync())
{
if (reader.ValueType == typeof(string) && (string)reader.Value == "data")
{
await reader.ReadAsync();
break;
}
}
while (await reader.ReadAsync())
{
if (reader.TokenType == JsonToken.EndArray)
break;
var data = serializer.Deserialize<T>(reader);
await onFetched?.Invoke(data);
}
}var headerResponse = await _client.GetAsync(responseUrl, HttpCompletionOption.ResponseHeadersRead);
if (headerResponse.StatusCode != HttpStatusCode.OK)
return;
using Stream jsonStream = await _client.GetStreamAsync(streamUrl);
await UploadStreamData<MyData>(
jsonStream,
headerResponse.Content.Headers.ContentEncoding.Contains("gzip"),
async (analyticsData) => {...}
);Рассмотрим подробнее метод ReadStreamData.
Если флаг
gzipустановлен вtrue, потокjsonStreamоборачивается вGZipStreamдля декомпрессии. В противном случае используется самjsonStream. Подробнее про использованиеgzipсмотрите в документации, откуда скачиваются данные.Создается
StreamReaderдля чтения из потока и передается вJsonTextReader, который работает с JSON.Метод асинхронно читает JSON, пока не находит свойство
"data". Как только"data"обнаружен, чтение продолжается, переходя к массиву данных, который нужно обработать.Чтение данных:
После ключа
"data"предполагается, что идет массив JSON-объектов, каждый из которых соответствует типуT.Каждый элемент массива десериализуется из
readerв объектT.Метод
onFetchedвызывается для каждого объектаT(асинхронно), передавая этот объект как параметр.
Когда достигнут конец массива (
JsonToken.EndArray), метод завершает выполнение.
Библиотека EFCore.BulkExtensions используется для эффективного выполнения массовых операций с данными в Entity Framework Core (EF Core), таких как Bulk Insert, Bulk Update, Bulk Delete, и Bulk Read. Она позволяет значительно улучшить производительность при обработке больших объемов данных, обходя ограничения производительности стандартных методов EF Core.
Используйте эту библиотеку, если необходимо работать с большим числом записей.
await _dbContext.BulkInsertAsync(...);
await _dbContext.BulkInsertOrUpdateAsync(...);
await _dbContext.BulkInsertOrUpdateOrDeleteAsync(...);
await _dbContext.BulkUpdateAsync(...);
await _dbContext.BulkDeleteAsync(...);
await _dbContext.BulkSaveChangesAsync(...);Чтобы не нагружать БД вставкой или удалением огромного числа записей за раз, можно разбить операцию на несколько шагов и вставлять/удалять по порциям.
Также и в обратной ситуации: вместо того чтобы вставлять огромное число записей по одной штуке, лучше группировать их и вставлять вместе за раз.
В C# WebAPI использование транзакций позволяет управлять согласованностью данных при выполнении нескольких операций в базе данных. Методы BeginTransactionAsync, CommitAsync, и RollbackAsync помогают гарантировать, что все операции внутри транзакции будут завершены успешно, или ни одна из них не будет применена, если произошла ошибка.
using (var transaction = await _dbContext.Database.BeginTransactionAsync())
{
try
{
await _dbContext.BulkInsertAsync(insertData);
await transaction.CommitAsync();
}
catch (Exception)
{
await transaction.RollbackAsync();
throw;
}
}6. "Сырой" SQL запрос
В некоторых случаях бывает полезно отправлять "сырой" SQL запрос вместо использования встроенных методов Entity Framework. При использовании прямого SQL запроса можно вручную оптимизировать его или использовать специфичные функции SQL, которые недоступны в EF. Однако, такой запрос никак не защищен от ошибок.
_dbContext.Database.ExecuteSqlRawAsync("...")При неправильном использовании IEnumerable или IQueryable можно очень быстро забить оперативную память. Нужно понимать различие этих интерфейсов, если они используются.
Старайтесь также не использовать методы ToList или ToArray, либо используйте с осторожностью, т.к. эти методы загружают сразу все данные в оперативную память.
Может быть полезно использовать кеш, который хранит некоторые данные в небольшом промежутке времени жизни приложения. Для примера можно использовать Memory Cache (хранит данные в оперативной памяти).
builder.Services.AddMemoryCache(); // подключаем в Program.cs// Сохраняем значение на 6 часов
var cacheEntryOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6) };
_memoryCache.Set("key", "value", cacheEntryOptions);
// читаем значение из кеша
_memoryCache.TryGetValue("key", out valueObject);Автор документа: Артём Ветик