From 547cb894bef5128d8d09cd85a837bb938cc2fe30 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Tue, 17 Mar 2026 13:50:06 +1000 Subject: [PATCH] =?UTF-8?q?feat(dal):=20RedisCacheService=20-=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20Hash=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=8A=D0=B5=D0=BA=D1=82=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API/Contracts/V1/ApiRoutes.cs | 13 -- PARR.API/Controllers/V1/TestController.cs | 163 ++++-------------- .../Cache/Services/Base/IRedisCacheService.cs | 70 ++++++++ .../Cache/Services/Base/RedisCacheService.cs | 92 +++++++++- PARR.DAL/ParrDalInstaller.cs | 11 +- 5 files changed, 200 insertions(+), 149 deletions(-) diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index ecb11262..f2ced40a 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -49,20 +49,7 @@ public const string GetNextRun = Base + "/tests/next-run"; - public const string GetNextScheduleDate = Base + "/tests/next-schedule-date/" + paramApplicationInWorkId + "/" + paramDate; - - public const string GetNextSchedule = Base + "/tests/next-schedule/" + paramApplicationInWorkId + "/" + paramDate; - public const string CreateCache = Base + "/tests/cache/"; - - public const string IsWorkDay = Base + "/tests/is-work-day/"; - - public const string TestHandler = Base + "/tests/test/"; - - public const string GetWorkDay = Base + "/tests/get-work-day/{date}"; - - public const string paramDate = "{lastRunDate}"; - public const string paramApplicationInWorkId = "{applicationInWorkId}"; } public static class Template diff --git a/PARR.API/Controllers/V1/TestController.cs b/PARR.API/Controllers/V1/TestController.cs index da19f183..e76fa8c6 100644 --- a/PARR.API/Controllers/V1/TestController.cs +++ b/PARR.API/Controllers/V1/TestController.cs @@ -1,16 +1,12 @@ -using AutoMapper; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Responses.Base; using PARR.API.Controllers.V1.Base; using PARR.API.Services.Interfaces; using PARR.DAL.Cache.Services.Base; -using PARR.DAL.DomainServices.UnitFilterService; using PARR.DAL.NextRunServices; using PARR.DAL.Services.Interfaces; -using PARR.DAL.Services.Interfaces.Job; -using PARR.DAL.Services.Interfaces.Unit; namespace PARR.API.Controllers.V1 @@ -19,43 +15,19 @@ namespace PARR.API.Controllers.V1 public class TestController : BaseApiController { private readonly IClientService clientService; - //private readonly IEsppScheduleTransformService esppScheduleTransformService; private readonly IRedisCacheService redisCacheService; - private readonly IWeekendDayService weekendDayService; - private readonly IUnitService unitService; - private readonly IFieldFilterService fieldFilterService; - private readonly IJobService jobService; - private readonly IJobUnitFilterService jobUnitFilterService; - private readonly IUnitFilterService unitFilterService; - private readonly IMapper mapper; private readonly INextRunService nextRunService; private readonly ITemplateService templateService; public TestController( IClientService clientService, - //IEsppScheduleTransformService esppScheduleTransformService, IRedisCacheService redisCacheService, - IWeekendDayService weekendDayService, - IUnitService unitService, - IFieldFilterService fieldFilterService, - IJobService jobService, - IJobUnitFilterService jobUnitFilterService, - IUnitFilterService unitFilterService, - IMapper mapper, INextRunService nextRunService, ITemplateService templateService ) { this.clientService = clientService; - //this.esppScheduleTransformService = esppScheduleTransformService; this.redisCacheService = redisCacheService; - this.weekendDayService = weekendDayService; - this.unitService = unitService; - this.fieldFilterService = fieldFilterService; - this.jobService = jobService; - this.jobUnitFilterService = jobUnitFilterService; - this.unitFilterService = unitFilterService; - this.mapper = mapper; this.nextRunService = nextRunService; this.templateService = templateService; } @@ -92,124 +64,47 @@ namespace PARR.API.Controllers.V1 } - ///// - ///// Получить следующую дату запуска - ///// - ///// - ///// - ///// - //[HttpGet(ApiRoutes.Test.GetNextScheduleDate)] - //public async Task GetNextScheduleDate([FromRoute] Guid applicationInWorkId, [FromRoute] DateTimeOffset lastRunDate) - //{ - // var result = await esppScheduleTransformService.GetNextDateAsync(applicationInWorkId, lastRunDate); - // return Ok(result); - //} - ///// - ///// Получить следующее расписание + ///// Записать в кэш ///// - ///// - ///// + ///// ///// - //[HttpGet(ApiRoutes.Test.GetNextSchedule)] - //public async Task GetNextSchedule([FromRoute] Guid applicationInWorkId, [FromRoute] DateTimeOffset lastRunDate) + //[HttpPost(ApiRoutes.Test.CreateCache)] + //public async Task CreateCache() //{ - // var result = await esppScheduleTransformService.GetNextScheduleAsync(applicationInWorkId, lastRunDate); - // return Ok(result); + // var hashKey = redisCacheService.GetKey(new[] { "test", "mxa" }); + // var ttl = TimeSpan.FromMinutes(10); + + // await redisCacheService.SetHashFieldAsync(hashKey, "t1", "val1", ttl); + // await redisCacheService.SetHashFieldAsync(hashKey, "t2", "val2"); + // await redisCacheService.SetHashFieldAsync(hashKey, "t3", "val3"); + + // var length = await redisCacheService.GetHashLengthAsync(hashKey); + + // var get = await redisCacheService.GetHashFieldAsync(hashKey, "t1"); + + // var getAll = await redisCacheService.GetAllHashFieldsAsync(hashKey); + + // await redisCacheService.DeleteHashFieldAsync(hashKey, "t1"); + // var getT1 = await redisCacheService.GetHashFieldAsync(hashKey, "t1"); + + // var exist = await redisCacheService.HashFieldExistsAsync(hashKey, "t2"); + + // await redisCacheService.DeleteHashAsync(hashKey); + // var getT_1 = await redisCacheService.GetHashFieldAsync(hashKey, "t1"); + + // await redisCacheService.SetHashTtlAsync(hashKey, ttl); + + // return Ok(); //} - /// - /// Записать в кэш и получить обратно значение из кэша - /// - /// - /// - [HttpPost(ApiRoutes.Test.CreateCache)] - public async Task CreateCache([FromBody] CaheRequestTest request) - { - await redisCacheService.SetCachedDataAsync(request.Key, request, TimeSpan.FromMinutes(1)); - - var fromCache = await redisCacheService.GetCachedDataAsync(request.Key); - - return Ok(new { fromCache }); - } - /// - /// Проверка, это рабочий день? - /// - /// - /// - [HttpGet(ApiRoutes.Test.IsWorkDay)] - public async Task IsWorkDay([FromQuery] DateOnly date) - { - return Ok(await weekendDayService.IsWorkDayAsync(date, true)); - } - /// - /// Тестовый метод - /// - /// - [HttpGet(ApiRoutes.Test.TestHandler)] - public async Task Test() - { - //var job = new Job - //{ - // Id = Guid.NewGuid(), - // Name = "ВИ-VMWARE ТО-1 Мониторинг и анализ работы виртуальной инфраструктуры", - // DateCreated = DateTimeOffset.Now, - // TemplateNameMask = "ЭИТИ_%РГ%_%ТНК%_%ЭК%", - // TnkId = Guid.Parse("9b7e0251-cb13-427c-ac71-16baa2a111dd") - //}; - //var jobDetails = new JobDetails { - // Id = Guid.NewGuid(), - // UnitFilter = "ВРТ-VMWARE-*", - // JobId = job.Id, - // ShortDescription = "Мониторинг и анализ работы виртуальной инфраструктуры", - // FullDescription = "Проверка журналов (лог-файла) событий (warning, alarm) объектов виртуальной инфраструктуры (ВМ, хост, кластер, датацентр, virtual center и др.)", - // Solution = "Работы проведены", - // Duration = "7 00:00:00", - // ReferenceDate = new DateTimeOffset(2026,01,01,00,00,00,new TimeSpan(0)) - //}; - - //var fieldFilter = new FieldFilter - //{ - // Id = Guid.NewGuid(), - // JobDetailsId = jobDetails.Id, - // FieldId = Guid.Parse("08b42860-09a7-4a1f-aa4f-0290e86df884"), - // ValueMask = "виртуальный комплекс" - //}; - ////await jobService.CreateAsync(job); - ////await jobService.CommitAsync(); - //if (await jobService.CreateAsync(job)) - // if (await jobDetailsService.CreateAsync(jobDetails)) - // if (await fieldFilterService.CreateAsync(fieldFilter)) - // await jobService.CommitAsync(); - //var unit = await unitService.Get().Include(t => t.Parents).Include(t => t.Childs).FirstOrDefaultAsync(t => t.Id == Guid.Parse("01fda0bd-3423-4b12-965b-2947153d1d87")); - - //var parent = unit?.Parents; - //var child = unit?.Childs; - - return Ok(); - } - - - //[HttpGet(ApiRoutes.Test.GetWorkDay)] - //public async Task GetWorkDay([FromRoute] DateTimeOffset date) - //{ - // var result = await esppScheduleTransformService.GetWorkDayAsync(date); - - // return Ok(result); - //} } - public class CaheRequestTest - { - public required string Key { get; set; } - - public required string Value { get; set; } - } } diff --git a/PARR.DAL/Cache/Services/Base/IRedisCacheService.cs b/PARR.DAL/Cache/Services/Base/IRedisCacheService.cs index 881fdf8a..01a48bb8 100644 --- a/PARR.DAL/Cache/Services/Base/IRedisCacheService.cs +++ b/PARR.DAL/Cache/Services/Base/IRedisCacheService.cs @@ -57,5 +57,75 @@ /// /// string GetKey(string[] keyParts, bool isUseHash = false); + + #region Работа с Hash + + /// + /// Изменить одно поле в Hash + /// + /// + /// + /// + /// + /// Если указано, обновится у всего Hash. Если не указано и hash не существовал, создастся Hash с бесокнечным ttl + /// + Task SetHashFieldAsync(string hashKey, string field, T value, TimeSpan? ttl = null); + + /// + /// Получить значение поля из Hash + /// + /// + /// + /// + /// + Task GetHashFieldAsync(string hashKey, string field); + + /// + /// Получить все значения из Hash + /// + /// + /// + /// + Task> GetAllHashFieldsAsync(string hashKey); + + /// + /// Удалить одну запись из Hash + /// + /// + /// + /// + Task DeleteHashFieldAsync(string hashKey, string field); + + /// + /// Есть ли запись в Hash + /// + /// + /// + /// + Task HashFieldExistsAsync(string hashKey, string field); + + /// + /// Удалить весь Hash + /// + /// + /// + Task DeleteHashAsync(string hashKey); + + /// + /// Получить количество записей в Hash + /// + /// + /// + Task GetHashLengthAsync(string hashKey); + + /// + /// Установить TTL для Hash + /// + /// + /// + /// + Task SetHashTtlAsync(string hashKey, TimeSpan ttl); + + #endregion } } diff --git a/PARR.DAL/Cache/Services/Base/RedisCacheService.cs b/PARR.DAL/Cache/Services/Base/RedisCacheService.cs index 38c902be..bb7fa130 100644 --- a/PARR.DAL/Cache/Services/Base/RedisCacheService.cs +++ b/PARR.DAL/Cache/Services/Base/RedisCacheService.cs @@ -1,17 +1,26 @@ using Microsoft.Extensions.Caching.Distributed; +using StackExchange.Redis; using System.Text.Json; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace PARR.DAL.Cache.Services.Base { internal class RedisCacheService : IRedisCacheService { private readonly IDistributedCache cache; + private readonly IDatabase redis; - public RedisCacheService(IDistributedCache cache) + public RedisCacheService( + IDistributedCache cache, + IConnectionMultiplexer connectionMultiplexer + ) { this.cache = cache; + this.redis = connectionMultiplexer.GetDatabase(); } + #region Распределенный кэш IDistributedCache + public async Task GetCachedDataAsync(string key) { var jsonData = await cache.GetStringAsync(key); @@ -69,6 +78,85 @@ namespace PARR.DAL.Cache.Services.Base cache.Remove(key); } + #endregion + + #region Нативные операции Redis, Redis Hash + + public async Task SetHashFieldAsync(string hashKey, string field, T value, TimeSpan? ttl = null) + { + // меняет одно поле в Hash + + var jsonData = JsonSerializer.Serialize(value); + + // true - поля не было, создалось новое. false - поле было, обновили значение + var result = await redis.HashSetAsync(hashKey, field, jsonData); + + // если указан ttl, обновим для всего Hash + // если не указан и ранее был создан hashKey, оставит его ttl; а если hashKey не было, то создаст его БЕССРОЧНЫМ!!! + if (ttl.HasValue) + await SetHashTtlAsync(hashKey, ttl.Value); + } + + public async Task GetHashFieldAsync(string hashKey, string field) + { + // получить значение поля из Hash + var value = await redis.HashGetAsync(hashKey, field); + + if (value.IsNullOrEmpty) + return default; + + return JsonSerializer.Deserialize(value); + } + + public async Task> GetAllHashFieldsAsync(string hashKey) + { + // получить все записи из Hash + var objs = await redis.HashGetAllAsync(hashKey); + + if (objs.Length == 0) + return new Dictionary(); + + var result = objs.ToDictionary( + t => t.Name.ToString(), + t => JsonSerializer.Deserialize(t.Value) + ); + + return result!; + } + + public async Task DeleteHashFieldAsync(string hashKey, string field) + { + // удалить запись из Hash + await redis.HashDeleteAsync(hashKey, field); + } + + public async Task HashFieldExistsAsync(string hashKey, string field) + { + // есть ли запись в Hash + return await redis.HashExistsAsync(hashKey, field); + } + + public async Task DeleteHashAsync(string hashKey) + { + // удалить весь Hash + await redis.KeyDeleteAsync(hashKey); + } + + public async Task GetHashLengthAsync(string hashKey) + { + // кол-во записей в hash + return await redis.HashLengthAsync(hashKey); + } + + public async Task SetHashTtlAsync(string hashKey, TimeSpan ttl) + { + // Установить ttl для Hash + await redis.KeyExpireAsync(hashKey, ttl); + } + + #endregion + + #region Helpers public string GetKey(string[] keyParts, bool isUseHash = false) { @@ -91,5 +179,7 @@ namespace PARR.DAL.Cache.Services.Base return keyStr; } + #endregion + } } diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index 5a92befb..f858bb7a 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -24,6 +24,7 @@ using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Schedule; using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.Settings; +using StackExchange.Redis; namespace PARR.DAL { @@ -45,16 +46,24 @@ namespace PARR.DAL #region Redis + cache services + services.AddSingleton(sp => + { + // IConnectionMultiplexer - для нативных операций Redis + var connectionString = configuration.GetConnectionString("RedisConnection"); + return ConnectionMultiplexer.Connect(connectionString); + }); + services.AddStackExchangeRedisCache(opt => { opt.Configuration = configuration.GetConnectionString("RedisConnection"); }); + var groupedShortcodesCacheSettings = new GroupedShortcodesCacheSettings(); configuration.GetSection(nameof(GroupedShortcodesCacheSettings)).Bind(groupedShortcodesCacheSettings); services.AddSingleton(groupedShortcodesCacheSettings); - services.AddTransient(); + services.AddSingleton(); #endregion