feat(core, infrastructure): IRedisCacheService - данные теперь можно архивировать

This commit is contained in:
Mikhail Trubnikov
2026-05-07 12:27:57 +10:00
parent 277bf0caa3
commit 395ef6a0ea
4 changed files with 164 additions and 52 deletions

View File

@@ -5,30 +5,31 @@
/// </summary> /// </summary>
public interface IRedisCacheService public interface IRedisCacheService
{ {
/// <summary> ///// <summary>
/// Получить кэшированные данные ///// Получить кэшированные данные
/// </summary> ///// </summary>
/// <typeparam name="T"></typeparam> ///// <typeparam name="T"></typeparam>
/// <param name="key"></param> ///// <param name="key"></param>
/// <returns></returns> ///// <returns></returns>
T? GetCachedData<T>(string key); //T? GetCachedData<T>(string key);
/// <summary> /// <summary>
/// Получить кэшированные данные асинхронно /// Получить кэшированные данные асинхронно
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="useCompression">Разрешить сжатие данных</param>
/// <returns></returns> /// <returns></returns>
Task<T?> GetCachedDataAsync<T>(string key); Task<T?> GetCachedDataAsync<T>(string key, bool useCompression = false);
/// <summary> ///// <summary>
/// Добавить в кэш данные ///// Добавить в кэш данные
/// </summary> ///// </summary>
/// <typeparam name="T"></typeparam> ///// <typeparam name="T"></typeparam>
/// <param name="key"></param> ///// <param name="key"></param>
/// <param name="data"></param> ///// <param name="data"></param>
/// <param name="cacheDuration"></param> ///// <param name="cacheDuration"></param>
void SetCachedData<T>(string key, T data, TimeSpan cacheDuration); //void SetCachedData<T>(string key, T data, TimeSpan cacheDuration);
/// <summary> /// <summary>
/// Добавить в кэш данные асинхронно /// Добавить в кэш данные асинхронно
@@ -38,7 +39,7 @@
/// <param name="data"></param> /// <param name="data"></param>
/// <param name="cacheDuration"></param> /// <param name="cacheDuration"></param>
/// <returns></returns> /// <returns></returns>
Task SetCachedDataAsync<T>(string key, T data, TimeSpan cacheDuration); Task SetCachedDataAsync<T>(string key, T data, TimeSpan cacheDuration, bool useCompression = false);
/// <summary> /// <summary>
/// Удалить кэшированные данные /// Удалить кэшированные данные

View File

@@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NodaTime;
using PARR.Core.Common.Interfaces; using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces; using PARR.Core.Repositories.Interfaces;
using PARR.Core.Services.Shortcodes; using PARR.Core.Services.Shortcodes;
@@ -308,7 +307,7 @@ namespace PARR.Core.Services.Workload.Implementations
{ {
var key = GetReportCacheKey(reportType, dateStart, durationDays, offset, filterParam); var key = GetReportCacheKey(reportType, dateStart, durationDays, offset, filterParam);
return await redisCacheService.GetCachedDataAsync<WorkloadReport>(key); return await redisCacheService.GetCachedDataAsync<WorkloadReport>(key, true);
} }
@@ -325,7 +324,7 @@ namespace PARR.Core.Services.Workload.Implementations
public async Task SetWorkloadReport(WorkloadReport report, WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam) public async Task SetWorkloadReport(WorkloadReport report, WorkloadReportType reportType, DateOnly dateStart, int durationDays, TimeSpan offset, string? filterParam)
{ {
var key = GetReportCacheKey(reportType, dateStart, durationDays, offset, filterParam); var key = GetReportCacheKey(reportType, dateStart, durationDays, offset, filterParam);
await redisCacheService.SetCachedDataAsync(key, report, cacheWorkloadTtl); await redisCacheService.SetCachedDataAsync(key, report, cacheWorkloadTtl, true);
} }
@@ -377,11 +376,5 @@ namespace PARR.Core.Services.Workload.Implementations
return keyStr; return keyStr;
} }
} }
} }

View File

@@ -0,0 +1,51 @@
using System.IO.Compression;
namespace PARR.Infrastructure.Redis.Helpers
{
/// <summary>
/// Архивация/разарзивация данных
/// </summary>
internal static class CompressionHelper
{
// Не сжимать маленькие значения
private const int MinCompressionSize = 1024; // 1Кб
/// <summary>
/// Сжать
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static byte[] Compress(byte[] data)
{
if (data.Length < MinCompressionSize)
return data;
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionLevel.Optimal))
{
gzip.Write(data, 0, data.Length);
}
return output.ToArray();
}
/// <summary>
/// Разархивировать если это GZip
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static byte[] Decompress(byte[] data)
{
// Проверяем сигнатуру GZip (0x1F, 0x8B)
if (data.Length < 2 || data[0] != 0x1F || data[1] != 0x8B)
return data; // Не сжато
using var input = new MemoryStream(data);
using var gzip = new GZipStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
gzip.CopyTo(output);
return output.ToArray();
}
}
}

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces; using PARR.Core.Common.Interfaces;
using PARR.Infrastructure.Redis.Helpers;
using StackExchange.Redis; using StackExchange.Redis;
using System.Text.Json; using System.Text.Json;
@@ -8,6 +9,11 @@ namespace PARR.Infrastructure.Redis
{ {
internal class RedisCacheService : IRedisCacheService internal class RedisCacheService : IRedisCacheService
{ {
/// <summary>
/// Минимальный размер для сжатия
/// </summary>
private const int compressionTreshold = 10 * 1024; // 10Kb
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;
@@ -27,49 +33,87 @@ namespace PARR.Infrastructure.Redis
#region Распределенный кэш IDistributedCache #region Распределенный кэш IDistributedCache
public async Task<T?> GetCachedDataAsync<T>(string key) public async Task<T?> GetCachedDataAsync<T>(string key, bool useCompression = false)
{ {
var jsonData = await cache.GetStringAsync(key); if (useCompression)
{
// хранили в байтах
try
{
var data = await cache.GetAsync(key);
if (data == null)
return default(T);
if (jsonData == null) return DeserializeWithCompression<T>(data);
return default(T); }
catch (Exception ex)
{
throw new Exception("Формат данных не байты.", ex);
}
}
else
{
// хранили в строке
try
{
var jsonData = await cache.GetStringAsync(key);
return JsonSerializer.Deserialize<T>(jsonData); if (jsonData == null)
return default(T);
return JsonSerializer.Deserialize<T>(jsonData);
}
catch (Exception ex)
{
throw new Exception("Формат данных не строка.", ex);
}
}
} }
public T? GetCachedData<T>(string key) //public T? GetCachedData<T>(string key)
{ //{
var jsonData = cache.GetString(key); // var jsonData = cache.GetString(key);
if (jsonData == null) // if (jsonData == null)
return default(T); // return default(T);
return JsonSerializer.Deserialize<T>(jsonData); // return JsonSerializer.Deserialize<T>(jsonData);
} //}
public void SetCachedData<T>(string key, T data, TimeSpan cacheDuration) //public void SetCachedData<T>(string key, T data, TimeSpan cacheDuration)
//{
// var options = new DistributedCacheEntryOptions
// {
// AbsoluteExpirationRelativeToNow = cacheDuration
// };
// var jsonData = JsonSerializer.Serialize(data);
// cache.SetString(key, jsonData, options);
//}
public async Task SetCachedDataAsync<T>(string key, T data, TimeSpan cacheDuration, bool useCompression = false)
{ {
var options = new DistributedCacheEntryOptions var options = new DistributedCacheEntryOptions
{ {
AbsoluteExpirationRelativeToNow = cacheDuration AbsoluteExpirationRelativeToNow = cacheDuration
}; };
var jsonData = JsonSerializer.Serialize(data); if (useCompression)
cache.SetString(key, jsonData, options);
}
public async Task SetCachedDataAsync<T>(string key, T data, TimeSpan cacheDuration)
{
var options = new DistributedCacheEntryOptions
{ {
AbsoluteExpirationRelativeToNow = cacheDuration // храним в байтах, если надо то сжимаем
}; var jsonData = SerializeWithCompression(data);
await cache.SetAsync(key, jsonData, options);
var jsonData = JsonSerializer.Serialize(data); }
await cache.SetStringAsync(key, jsonData, options); else
{
// храним в строке
var jsonData = JsonSerializer.Serialize(data);
await cache.SetStringAsync(key, jsonData, options);
}
} }
@@ -250,5 +294,28 @@ namespace PARR.Infrastructure.Redis
#endregion #endregion
private byte[] SerializeWithCompression<T>(T data)
{
var json = JsonSerializer.SerializeToUtf8Bytes(data);
logger.LogDebug("Сериализовано: {Size} байт, тип: {Type}", json.Length, typeof(T).Name);
if (json.Length < compressionTreshold)
return json;
var compressed = CompressionHelper.Compress(json);
logger.LogDebug("После сжатия: {Size} байт (экономия: {Percent}%)", compressed.Length, Math.Round((1 - (double)compressed.Length / json.Length) * 100, 1));
return compressed;
}
private T? DeserializeWithCompression<T>(byte[] data)
{
var json = CompressionHelper.Decompress(data);
return JsonSerializer.Deserialize<T>(json);
}
} }
} }