From 66d1fe7b0d4325a5a22ae3825c6e71d96e286b69 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Wed, 3 Jun 2026 12:09:47 +1000 Subject: [PATCH] =?UTF-8?q?feat(core,=20infrastructure):=20RedisCacheServi?= =?UTF-8?q?ce=20-=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?/=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=B2=20=D0=BA=D1=8D=D1=88?= =?UTF-8?q?=D0=B5=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=D0=BC.=20UnitCach?= =?UTF-8?q?eService=20-=20=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API/Controllers/V1/TestController.cs | 2 +- .../Common/Interfaces/IRedisCacheService.cs | 10 +++ .../Implementations/UnitCacheService.cs | 21 +++++-- .../Redis/RedisCacheService.cs | 63 ++++++++++++++++++- 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/PARR.API/Controllers/V1/TestController.cs b/PARR.API/Controllers/V1/TestController.cs index 3adb08c8..e818bd37 100644 --- a/PARR.API/Controllers/V1/TestController.cs +++ b/PARR.API/Controllers/V1/TestController.cs @@ -91,7 +91,7 @@ namespace PARR.API.Controllers.V1 [HttpPost(ApiRoutes.Test.CreateCache)] public async Task 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) //{ diff --git a/PARR.Core/Common/Interfaces/IRedisCacheService.cs b/PARR.Core/Common/Interfaces/IRedisCacheService.cs index 6f7fdee2..8df631c8 100644 --- a/PARR.Core/Common/Interfaces/IRedisCacheService.cs +++ b/PARR.Core/Common/Interfaces/IRedisCacheService.cs @@ -86,6 +86,16 @@ /// Task SetHashFieldAsync(string hashKey, string field, T value, TimeSpan? ttl = null, bool useCompression = false); + /// + /// Изменить несколько полей в Hash + /// + /// + /// + /// Если указано, обновится у всего Hash. Если не указано и hash не существовал, создастся Hash с бесокнечным ttl + /// Сжимать данные + /// + Task SetHashFieldsAsync(List<(string HashKey, string FieldKey, T Value)> items, TimeSpan? ttl = null, bool useCompression = false); + /// /// Получить значение поля из Hash /// diff --git a/PARR.Core/Services/UnitService/Implementations/UnitCacheService.cs b/PARR.Core/Services/UnitService/Implementations/UnitCacheService.cs index 6fe0acc5..7c7acd06 100644 --- a/PARR.Core/Services/UnitService/Implementations/UnitCacheService.cs +++ b/PARR.Core/Services/UnitService/Implementations/UnitCacheService.cs @@ -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 /// private readonly TimeSpan CacheTtl = TimeSpan.FromHours(24); + /// + /// Размер порции данных, для запроса в БД + /// + private readonly int DatabaseBatchSize = 500; + private readonly ILogger 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(dataToCache, CacheTtl, true); + } return resultDictionary; } @@ -225,7 +236,7 @@ namespace PARR.Core.Services.UnitService.Implementations var dbUnits = new List(); // Выполняем порционно - 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(); - foreach (var chunk in allRelativeIds.Chunk(500)) + foreach (var chunk in allRelativeIds.Chunk(DatabaseBatchSize)) { var chunkRelatives = await unitRepository.Get() .AsNoTracking() diff --git a/PARR.Infrastructure/Redis/RedisCacheService.cs b/PARR.Infrastructure/Redis/RedisCacheService.cs index 2070064d..90781f73 100644 --- a/PARR.Infrastructure/Redis/RedisCacheService.cs +++ b/PARR.Infrastructure/Redis/RedisCacheService.cs @@ -14,6 +14,12 @@ namespace PARR.Infrastructure.Redis /// private const int compressionTreshold = 10 * 1024; // 10Kb + /// + /// Размер порции для Redis Hash. + /// Делим на порции по 5000, чтобы не перегружать буфер команд Redis. + /// + public const int RedisHashBatchSize = 5000; + private readonly IDistributedCache cache; private readonly IConnectionMultiplexer connectionMultiplexer; private readonly ILogger logger; @@ -212,6 +218,58 @@ namespace PARR.Infrastructure.Redis await SetHashTtlAsync(hashKey, ttl.Value); } + public async Task SetHashFieldsAsync(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(); + + // 1. Делим входящие данные на порции по 5000 (RedisHashBatchSize) + foreach (var chunk in items.Chunk(RedisHashBatchSize)) + { + var tasks = new List>(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(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 GetHashFieldAsync(string hashKey, string field, bool useCompression = false) { // получить значение поля из Hash @@ -245,7 +303,7 @@ namespace PARR.Infrastructure.Redis var allItems = new List(keys.Count); // Делим на порции по 5000 (оптимально) - foreach (var chunk in keys.Chunk(5000)) + foreach (var chunk in keys.Chunk(RedisHashBatchSize)) { // Формируем пайплайн ТОЛЬКО для 5 000 элементов var tasks = new List>(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>(chunk.Length); foreach (var key in chunk) @@ -483,6 +541,5 @@ namespace PARR.Infrastructure.Redis return JsonSerializer.Deserialize(json); } - } }