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,7 +33,28 @@ 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)
{
if (useCompression)
{
// хранили в байтах
try
{
var data = await cache.GetAsync(key);
if (data == null)
return default(T);
return DeserializeWithCompression<T>(data);
}
catch (Exception ex)
{
throw new Exception("Формат данных не байты.", ex);
}
}
else
{
// хранили в строке
try
{ {
var jsonData = await cache.GetStringAsync(key); var jsonData = await cache.GetStringAsync(key);
@@ -36,41 +63,58 @@ namespace PARR.Infrastructure.Redis
return JsonSerializer.Deserialize<T>(jsonData); return JsonSerializer.Deserialize<T>(jsonData);
} }
catch (Exception ex)
public T? GetCachedData<T>(string key)
{ {
var jsonData = cache.GetString(key); throw new Exception("Формат данных не строка.", ex);
}
if (jsonData == null) }
return default(T);
return JsonSerializer.Deserialize<T>(jsonData);
} }
public void SetCachedData<T>(string key, T data, TimeSpan cacheDuration) //public T? GetCachedData<T>(string key)
//{
// var jsonData = cache.GetString(key);
// if (jsonData == null)
// return default(T);
// return JsonSerializer.Deserialize<T>(jsonData);
//}
//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); {
// храним в байтах, если надо то сжимаем
var jsonData = SerializeWithCompression(data);
await cache.SetAsync(key, jsonData, options);
} }
else
public async Task SetCachedDataAsync<T>(string key, T data, TimeSpan cacheDuration)
{ {
var options = new DistributedCacheEntryOptions // храним в строке
{
AbsoluteExpirationRelativeToNow = cacheDuration
};
var jsonData = JsonSerializer.Serialize(data); var jsonData = JsonSerializer.Serialize(data);
await cache.SetStringAsync(key, jsonData, options); await cache.SetStringAsync(key, jsonData, options);
} }
}
public async Task DeleteCachedDataAsync(string key) public async Task DeleteCachedDataAsync(string key)
@@ -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);
}
} }
} }