feat(core, infrastructure): RedisCacheService - изменение/добавление полей в кэше списокм. UnitCacheService - оптимизация при получении данных.

This commit is contained in:
Mikhail Trubnikov
2026-06-03 12:09:47 +10:00
parent d1de63e939
commit 66d1fe7b0d
4 changed files with 87 additions and 9 deletions

View File

@@ -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)
//{

View File

@@ -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>

View File

@@ -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()

View File

@@ -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);
}
}
}