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