feat(core, infrastructure): RedisCacheService - изменение/добавление полей в кэше списокм. UnitCacheService - оптимизация при получении данных.
This commit is contained in:
@@ -91,7 +91,7 @@ namespace PARR.API.Controllers.V1
|
||||
[HttpPost(ApiRoutes.Test.CreateCache)]
|
||||
public async Task<IActionResult> CreateCache([FromBody] Guid unitId)
|
||||
{
|
||||
//var units = await unitRepository.Get().Take(100).Select(t => t.Id).ToListAsync();
|
||||
//var units = await unitRepository.Get().Take(100000).Select(t => t.Id).ToListAsync();
|
||||
//var listData = await unitService.GetWithCachingAsync(units);
|
||||
//foreach(var unit in units)
|
||||
//{
|
||||
|
||||
@@ -86,6 +86,16 @@
|
||||
/// <returns></returns>
|
||||
Task SetHashFieldAsync<T>(string hashKey, string field, T value, TimeSpan? ttl = null, bool useCompression = false);
|
||||
|
||||
/// <summary>
|
||||
/// Изменить несколько полей в Hash
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="items"></param>
|
||||
/// <param name="ttl">Если указано, обновится у всего Hash. Если не указано и hash не существовал, создастся Hash с бесокнечным ttl</param>
|
||||
/// <param name="useCompression">Сжимать данные</param>
|
||||
/// <returns></returns>
|
||||
Task SetHashFieldsAsync<T>(List<(string HashKey, string FieldKey, T Value)> items, TimeSpan? ttl = null, bool useCompression = false);
|
||||
|
||||
/// <summary>
|
||||
/// Получить значение поля из Hash
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PARR.Core.Common.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Domain.Cache;
|
||||
@@ -17,6 +18,11 @@ namespace PARR.Core.Services.UnitService.Implementations
|
||||
/// </summary>
|
||||
private readonly TimeSpan CacheTtl = TimeSpan.FromHours(24);
|
||||
|
||||
/// <summary>
|
||||
/// Размер порции данных, для запроса в БД
|
||||
/// </summary>
|
||||
private readonly int DatabaseBatchSize = 500;
|
||||
|
||||
private readonly ILogger<UnitCacheService> logger;
|
||||
private readonly IUnitRepository unitRepository;
|
||||
private readonly IRedisCacheService redisCacheService;
|
||||
@@ -102,6 +108,9 @@ namespace PARR.Core.Services.UnitService.Implementations
|
||||
|
||||
var dbData = await GetUnitsAsync(missingIds);
|
||||
|
||||
// список объектов для сохранения в кэш
|
||||
var dataToCache = new List<(string HashKey, string FieldKey, UnitInfo Value)>();
|
||||
|
||||
foreach (var unit in dbData)
|
||||
{
|
||||
resultDictionary[unit.Id] = unit;
|
||||
@@ -110,11 +119,13 @@ namespace PARR.Core.Services.UnitService.Implementations
|
||||
var hashKey = redisCacheService.GetKey(CacheKeys.Unit.UnitHashWithBucket(unit.Id));
|
||||
var unitKey = redisCacheService.GetKey(CacheKeys.Unit.UnitItem(unit.Id));
|
||||
|
||||
await redisCacheService.SetHashFieldAsync(hashKey, unitKey, unit, CacheTtl, true);
|
||||
//await redisCacheService.SetHashFieldAsync(hashKey, unitKey, unit, CacheTtl, true);
|
||||
dataToCache.Add((hashKey, unitKey, unit));
|
||||
}
|
||||
}
|
||||
|
||||
// сохранить докаченные в кэш + ттл + не забыть указать сжатие (может вообще этот метод вынести отдельно, так как пересекается когда по одному)
|
||||
if (dataToCache.Count > 0)
|
||||
await redisCacheService.SetHashFieldsAsync<UnitInfo>(dataToCache, CacheTtl, true);
|
||||
}
|
||||
|
||||
return resultDictionary;
|
||||
}
|
||||
@@ -225,7 +236,7 @@ namespace PARR.Core.Services.UnitService.Implementations
|
||||
var dbUnits = new List<dynamic>();
|
||||
|
||||
// Выполняем порционно
|
||||
foreach (var chank in ids.Chunk(500))
|
||||
foreach (var chank in ids.Chunk(DatabaseBatchSize))
|
||||
{
|
||||
var chunkUnits = await unitRepository.Get()
|
||||
.AsNoTracking()
|
||||
@@ -260,7 +271,7 @@ namespace PARR.Core.Services.UnitService.Implementations
|
||||
// Пакетно загружаем базовую информацию обо всех родственниках за один раз
|
||||
var relativesDictionary = new Dictionary<Guid, UnitInfoBase>();
|
||||
|
||||
foreach (var chunk in allRelativeIds.Chunk(500))
|
||||
foreach (var chunk in allRelativeIds.Chunk(DatabaseBatchSize))
|
||||
{
|
||||
var chunkRelatives = await unitRepository.Get()
|
||||
.AsNoTracking()
|
||||
|
||||
@@ -14,6 +14,12 @@ namespace PARR.Infrastructure.Redis
|
||||
/// </summary>
|
||||
private const int compressionTreshold = 10 * 1024; // 10Kb
|
||||
|
||||
/// <summary>
|
||||
/// Размер порции для Redis Hash.
|
||||
/// Делим на порции по 5000, чтобы не перегружать буфер команд Redis.
|
||||
/// </summary>
|
||||
public const int RedisHashBatchSize = 5000;
|
||||
|
||||
private readonly IDistributedCache cache;
|
||||
private readonly IConnectionMultiplexer connectionMultiplexer;
|
||||
private readonly ILogger<RedisCacheService> logger;
|
||||
@@ -212,6 +218,58 @@ namespace PARR.Infrastructure.Redis
|
||||
await SetHashTtlAsync(hashKey, ttl.Value);
|
||||
}
|
||||
|
||||
public async Task SetHashFieldsAsync<T>(List<(string HashKey, string FieldKey, T Value)> items, TimeSpan? ttl = null, bool useCompression = false)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return;
|
||||
|
||||
// Множество для отслеживания уникальных HashKey в рамках всей операции,
|
||||
// чтобы обновить для них TTL всего один раз в самом конце.
|
||||
var uniqueHashKeys = new HashSet<string>();
|
||||
|
||||
// 1. Делим входящие данные на порции по 5000 (RedisHashBatchSize)
|
||||
foreach (var chunk in items.Chunk(RedisHashBatchSize))
|
||||
{
|
||||
var tasks = new List<Task<bool>>(chunk.Length);
|
||||
|
||||
// 2. Формируем пайплайн для текущей порции
|
||||
foreach (var item in chunk)
|
||||
{
|
||||
uniqueHashKeys.Add(item.HashKey);
|
||||
|
||||
if (useCompression)
|
||||
{
|
||||
var byteData = SerializeWithCompression(item.Value);
|
||||
tasks.Add(redis.HashSetAsync(item.HashKey, item.FieldKey, byteData));
|
||||
}
|
||||
else
|
||||
{
|
||||
var jsonData = JsonSerializer.Serialize(item.Value);
|
||||
tasks.Add(redis.HashSetAsync(item.HashKey, item.FieldKey, jsonData));
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogDebug("Сформировал {Count} одновременных запросов на запись в кэш", tasks.Count);
|
||||
|
||||
// 3. Отправляем пачку в Redis и ждем завершения
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
// 4. Если указан TTL, обновляем его для всех затронутых хешей
|
||||
if (ttl.HasValue)
|
||||
{
|
||||
var ttlTasks = new List<Task>(uniqueHashKeys.Count);
|
||||
foreach (var hashKey in uniqueHashKeys)
|
||||
{
|
||||
ttlTasks.Add(SetHashTtlAsync(hashKey, ttl.Value));
|
||||
}
|
||||
|
||||
await Task.WhenAll(ttlTasks);
|
||||
}
|
||||
|
||||
logger.LogDebug("Успешно сохранил {Count} элементов в кэш.", items.Count);
|
||||
}
|
||||
|
||||
public async Task<T?> GetHashFieldAsync<T>(string hashKey, string field, bool useCompression = false)
|
||||
{
|
||||
// получить значение поля из Hash
|
||||
@@ -245,7 +303,7 @@ namespace PARR.Infrastructure.Redis
|
||||
var allItems = new List<T>(keys.Count);
|
||||
|
||||
// Делим на порции по 5000 (оптимально)
|
||||
foreach (var chunk in keys.Chunk(5000))
|
||||
foreach (var chunk in keys.Chunk(RedisHashBatchSize))
|
||||
{
|
||||
// Формируем пайплайн ТОЛЬКО для 5 000 элементов
|
||||
var tasks = new List<Task<RedisValue>>(chunk.Length);
|
||||
@@ -383,7 +441,7 @@ namespace PARR.Infrastructure.Redis
|
||||
int deletedCount = 0;
|
||||
|
||||
// Делим на порции по 5000, чтобы не перегружать буфер команд Redis
|
||||
foreach (var chunk in keys.Chunk(5000))
|
||||
foreach (var chunk in keys.Chunk(RedisHashBatchSize))
|
||||
{
|
||||
var tasks = new List<Task<bool>>(chunk.Length);
|
||||
foreach (var key in chunk)
|
||||
@@ -483,6 +541,5 @@ namespace PARR.Infrastructure.Redis
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user