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

View File

@@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NodaTime;
using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Services.Shortcodes;
@@ -308,7 +307,7 @@ namespace PARR.Core.Services.Workload.Implementations
{
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)
{
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;
}
}
}

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.Logging;
using PARR.Core.Common.Interfaces;
using PARR.Infrastructure.Redis.Helpers;
using StackExchange.Redis;
using System.Text.Json;
@@ -8,6 +9,11 @@ namespace PARR.Infrastructure.Redis
{
internal class RedisCacheService : IRedisCacheService
{
/// <summary>
/// Минимальный размер для сжатия
/// </summary>
private const int compressionTreshold = 10 * 1024; // 10Kb
private readonly IDistributedCache cache;
private readonly IConnectionMultiplexer connectionMultiplexer;
private readonly ILogger<RedisCacheService> logger;
@@ -27,7 +33,28 @@ namespace PARR.Infrastructure.Redis
#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);
@@ -36,41 +63,58 @@ namespace PARR.Infrastructure.Redis
return JsonSerializer.Deserialize<T>(jsonData);
}
public T? GetCachedData<T>(string key)
catch (Exception ex)
{
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
{
AbsoluteExpirationRelativeToNow = cacheDuration
};
var jsonData = JsonSerializer.Serialize(data);
cache.SetString(key, jsonData, options);
if (useCompression)
{
// храним в байтах, если надо то сжимаем
var jsonData = SerializeWithCompression(data);
await cache.SetAsync(key, jsonData, options);
}
public async Task SetCachedDataAsync<T>(string key, T data, TimeSpan cacheDuration)
else
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = cacheDuration
};
// храним в строке
var jsonData = JsonSerializer.Serialize(data);
await cache.SetStringAsync(key, jsonData, options);
}
}
public async Task DeleteCachedDataAsync(string key)
@@ -250,5 +294,28 @@ namespace PARR.Infrastructure.Redis
#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);
}
}
}