From 395ef6a0ea614f123f689d1e8ba521a8d2e49d15 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Thu, 7 May 2026 12:27:57 +1000 Subject: [PATCH] =?UTF-8?q?feat(core,=20infrastructure):=20IRedisCacheServ?= =?UTF-8?q?ice=20-=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=B0=D1=80=D1=85=D0=B8=D0=B2=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/Interfaces/IRedisCacheService.cs | 35 +++--- .../Implementations/WorkloadCacheService.cs | 11 +- .../Redis/Helpers/CompressionHelper.cs | 51 ++++++++ .../Redis/RedisCacheService.cs | 119 ++++++++++++++---- 4 files changed, 164 insertions(+), 52 deletions(-) create mode 100644 PARR.Infrastructure/Redis/Helpers/CompressionHelper.cs diff --git a/PARR.Core/Common/Interfaces/IRedisCacheService.cs b/PARR.Core/Common/Interfaces/IRedisCacheService.cs index fa5cfe52..f6bf40b2 100644 --- a/PARR.Core/Common/Interfaces/IRedisCacheService.cs +++ b/PARR.Core/Common/Interfaces/IRedisCacheService.cs @@ -5,30 +5,31 @@ /// public interface IRedisCacheService { - /// - /// Получить кэшированные данные - /// - /// - /// - /// - T? GetCachedData(string key); + ///// + ///// Получить кэшированные данные + ///// + ///// + ///// + ///// + //T? GetCachedData(string key); /// /// Получить кэшированные данные асинхронно /// /// /// + /// Разрешить сжатие данных /// - Task GetCachedDataAsync(string key); + Task GetCachedDataAsync(string key, bool useCompression = false); - /// - /// Добавить в кэш данные - /// - /// - /// - /// - /// - void SetCachedData(string key, T data, TimeSpan cacheDuration); + ///// + ///// Добавить в кэш данные + ///// + ///// + ///// + ///// + ///// + //void SetCachedData(string key, T data, TimeSpan cacheDuration); /// /// Добавить в кэш данные асинхронно @@ -38,7 +39,7 @@ /// /// /// - Task SetCachedDataAsync(string key, T data, TimeSpan cacheDuration); + Task SetCachedDataAsync(string key, T data, TimeSpan cacheDuration, bool useCompression = false); /// /// Удалить кэшированные данные diff --git a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs index e327e39f..6053b5c5 100644 --- a/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs +++ b/PARR.Core/Services/Workload/Implementations/WorkloadCacheService.cs @@ -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(key); + return await redisCacheService.GetCachedDataAsync(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; } - - - - - - } } diff --git a/PARR.Infrastructure/Redis/Helpers/CompressionHelper.cs b/PARR.Infrastructure/Redis/Helpers/CompressionHelper.cs new file mode 100644 index 00000000..3d25aa54 --- /dev/null +++ b/PARR.Infrastructure/Redis/Helpers/CompressionHelper.cs @@ -0,0 +1,51 @@ +using System.IO.Compression; + +namespace PARR.Infrastructure.Redis.Helpers +{ + /// + /// Архивация/разарзивация данных + /// + internal static class CompressionHelper + { + // Не сжимать маленькие значения + private const int MinCompressionSize = 1024; // 1Кб + + /// + /// Сжать + /// + /// + /// + 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(); + } + + /// + /// Разархивировать если это GZip + /// + /// + /// + 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(); + } + } +} diff --git a/PARR.Infrastructure/Redis/RedisCacheService.cs b/PARR.Infrastructure/Redis/RedisCacheService.cs index d7a0fff4..fba9f1ae 100644 --- a/PARR.Infrastructure/Redis/RedisCacheService.cs +++ b/PARR.Infrastructure/Redis/RedisCacheService.cs @@ -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 { + /// + /// Минимальный размер для сжатия + /// + private const int compressionTreshold = 10 * 1024; // 10Kb + private readonly IDistributedCache cache; private readonly IConnectionMultiplexer connectionMultiplexer; private readonly ILogger logger; @@ -27,49 +33,87 @@ namespace PARR.Infrastructure.Redis #region Распределенный кэш IDistributedCache - public async Task GetCachedDataAsync(string key) + public async Task GetCachedDataAsync(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 default(T); + return DeserializeWithCompression(data); + } + catch (Exception ex) + { + throw new Exception("Формат данных не байты.", ex); + } + } + else + { + // хранили в строке + try + { + var jsonData = await cache.GetStringAsync(key); - return JsonSerializer.Deserialize(jsonData); + if (jsonData == null) + return default(T); + + return JsonSerializer.Deserialize(jsonData); + } + catch (Exception ex) + { + throw new Exception("Формат данных не строка.", ex); + } + + } } - public T? GetCachedData(string key) - { - var jsonData = cache.GetString(key); + //public T? GetCachedData(string key) + //{ + // var jsonData = cache.GetString(key); - if (jsonData == null) - return default(T); + // if (jsonData == null) + // return default(T); - return JsonSerializer.Deserialize(jsonData); - } + // return JsonSerializer.Deserialize(jsonData); + //} - public void SetCachedData(string key, T data, TimeSpan cacheDuration) + //public void SetCachedData(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(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); - } - - - public async Task SetCachedDataAsync(string key, T data, TimeSpan cacheDuration) - { - var options = new DistributedCacheEntryOptions + if (useCompression) { - AbsoluteExpirationRelativeToNow = cacheDuration - }; - - var jsonData = JsonSerializer.Serialize(data); - await cache.SetStringAsync(key, jsonData, options); + // храним в байтах, если надо то сжимаем + var jsonData = SerializeWithCompression(data); + await cache.SetAsync(key, jsonData, options); + } + else + { + // храним в строке + var jsonData = JsonSerializer.Serialize(data); + await cache.SetStringAsync(key, jsonData, options); + } } @@ -250,5 +294,28 @@ namespace PARR.Infrastructure.Redis #endregion + private byte[] SerializeWithCompression(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(byte[] data) + { + var json = CompressionHelper.Decompress(data); + + return JsonSerializer.Deserialize(json); + } + + } }