feat(core, dal): WorkloadCacheService - методы для работы с кэшем отчетности workload
This commit is contained in:
@@ -8,6 +8,7 @@ using PARR.API.Settings;
|
||||
using PARR.Core.Common.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Services.NextRunServices;
|
||||
using PARR.Core.Services.Workload.Implementations;
|
||||
|
||||
namespace PARR.API.Controllers.V1
|
||||
{
|
||||
@@ -18,19 +19,22 @@ namespace PARR.API.Controllers.V1
|
||||
private readonly IRedisCacheService redisCacheService;
|
||||
private readonly INextRunService nextRunService;
|
||||
private readonly ITemplateRepository templateService;
|
||||
private readonly WorkloadCacheService workloadCacheService;
|
||||
|
||||
public TestController(
|
||||
IClientService clientService,
|
||||
IRedisCacheService redisCacheService,
|
||||
INextRunService nextRunService,
|
||||
ITemplateRepository templateService,
|
||||
MqSettings mqSettings
|
||||
MqSettings mqSettings,
|
||||
WorkloadCacheService workloadCacheService
|
||||
)
|
||||
{
|
||||
this.clientService = clientService;
|
||||
this.redisCacheService = redisCacheService;
|
||||
this.nextRunService = nextRunService;
|
||||
this.templateService = templateService;
|
||||
this.workloadCacheService = workloadCacheService;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +78,9 @@ namespace PARR.API.Controllers.V1
|
||||
[HttpPost(ApiRoutes.Test.CreateCache)]
|
||||
public async Task<IActionResult> CreateCache()
|
||||
{
|
||||
|
||||
// var aaa = await workloadCacheService.GetTemplateReportDataAsync();
|
||||
|
||||
//var val = await redisCacheService.GetCachedDataAsync<object>("96dAoIH/8Q9Bg8VY");
|
||||
|
||||
//var hashKey = redisCacheService.GetKey(new[] { "test", "mxa" });
|
||||
|
||||
@@ -62,6 +62,15 @@
|
||||
string GetKey(string[] keyParts, string[]? keyPartsToHash = null);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Удалить ключи по маске (template*)
|
||||
/// </summary>
|
||||
/// <param name="pattern"></param>
|
||||
/// <param name="batchSize"></param>
|
||||
/// <returns></returns>
|
||||
Task<int> DeleteKeysByPatternAsync(string pattern, int batchSize = 100);
|
||||
|
||||
|
||||
#region Работа с Hash
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -74,10 +74,14 @@ namespace PARR.Core
|
||||
services.AddTransient<IShortcodesService, ShortcodesService>();
|
||||
services.AddTransient<IMatchingStatusService, MatchingStatusService>();
|
||||
|
||||
|
||||
//services.AddScoped<IUserService, UserService>();
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Workload
|
||||
|
||||
services.AddScoped<WorkloadCacheService>();
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.TaskRepositories;
|
||||
using PARR.Core.Services.Workload.Implementations;
|
||||
using PARR.Domain.Entities.TaskEntities;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.Core.Services.TaskServices.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Обработчик задачаи формирования отчета по загруженности (формирование КЭШ).
|
||||
/// Обработчик задачи формирования отчета по загруженности (формирование КЭШ).
|
||||
/// Формируется КЭШ шаблонов с ЗО и РГ
|
||||
/// </summary>
|
||||
public class WorkloadReportHandler : BaseTaskHandler
|
||||
{
|
||||
private readonly WorkloadCacheService workloadCacheService;
|
||||
|
||||
public WorkloadReportHandler(ITaskRepository taskRepository, ITaskErrorRepository taskErrorRepository, ILogger<WorkloadReportHandler> logger)
|
||||
: base(taskRepository, taskErrorRepository, logger) { }
|
||||
public WorkloadReportHandler(
|
||||
ITaskRepository taskRepository,
|
||||
ITaskErrorRepository taskErrorRepository,
|
||||
ILogger<WorkloadReportHandler> logger,
|
||||
WorkloadCacheService workloadCacheService)
|
||||
: base(taskRepository, taskErrorRepository, logger)
|
||||
{
|
||||
this.workloadCacheService = workloadCacheService;
|
||||
}
|
||||
|
||||
public override TaskTypeEnum SupportedType => TaskTypeEnum.Workload;
|
||||
|
||||
@@ -25,15 +35,23 @@ namespace PARR.Core.Services.TaskServices.Handlers
|
||||
// ? new ReportPayload()
|
||||
// : JsonSerializer.Deserialize<ReportPayload>(task.Payload);
|
||||
|
||||
//todo: тут логика построения отчета
|
||||
// тут логика построения отчета
|
||||
|
||||
|
||||
await workloadCacheService.DeleteWorkloadCacheAsync();
|
||||
await workloadCacheService.DeleteTemplateCacheAsync();
|
||||
await workloadCacheService.CreateTemplateCacheAsync();
|
||||
|
||||
await System.Threading.Tasks.Task.CompletedTask;
|
||||
//await System.Threading.Tasks.Task.CompletedTask;
|
||||
|
||||
// return HandlerResult.Success();
|
||||
return HandlerResult.Success();
|
||||
|
||||
return HandlerResult.Failure("Тут капец какая ошибка! Просто жуть", new Exception("А это я создал эксепшен!!!"));
|
||||
//return HandlerResult.Failure("Тут капец какая ошибка! Просто жуть", new Exception("А это я создал эксепшен!!!"));
|
||||
}
|
||||
|
||||
protected override Task OnSuccessAsync(TaskItem task, HandlerResult result)
|
||||
{
|
||||
// Пример хука: отправка уведомления после успеха
|
||||
return base.OnSuccessAsync(task, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
}
|
||||
|
||||
|
||||
public async System.Threading.Tasks.Task ProcessMessagesAsync(IMqSettings mqSettings)
|
||||
public async Task ProcessMessagesAsync(IMqSettings mqSettings)
|
||||
{
|
||||
// этот метод вызывается в воркере
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
throw new Exception("Ошибка при подключении к RabbitMq");
|
||||
}
|
||||
|
||||
public async System.Threading.Tasks.Task StopProcessingAsync()
|
||||
public async Task StopProcessingAsync()
|
||||
{
|
||||
await rabbitService.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -1,33 +1,82 @@
|
||||
using InfluxDB.Client.Api.Service;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Common.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Core.Services.Shortcodes;
|
||||
using PARR.Core.Services.Workload.Models;
|
||||
using PARR.Domain.Cache.Models;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.Core.Services.Workload.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс управления КЭШем для workload
|
||||
/// </summary>
|
||||
internal class WorkloadCacheService
|
||||
public class WorkloadCacheService
|
||||
{
|
||||
private readonly ILogger<WorkloadCacheService> logger;
|
||||
private readonly IRedisCacheService redisCacheService;
|
||||
private readonly ITemplateRepository templateRepository;
|
||||
private readonly IShortcodesService shortcodeService;
|
||||
|
||||
public WorkloadCacheService(
|
||||
ILogger<WorkloadCacheService> logger,
|
||||
IRedisCacheService redisCacheService,
|
||||
ITemplateRepository templateRepository
|
||||
//IShortcodeService
|
||||
ITemplateRepository templateRepository,
|
||||
IShortcodesService shortcodeService
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.redisCacheService = redisCacheService;
|
||||
this.templateRepository = templateRepository;
|
||||
this.shortcodeService = shortcodeService;
|
||||
}
|
||||
|
||||
|
||||
//----------- Настройки, вынести -----------
|
||||
/// <summary>
|
||||
/// Ключ КЭШ шаблонов
|
||||
/// </summary>
|
||||
private readonly string[] cacheTemplateKey = { "template", "report", "params" };
|
||||
|
||||
/// <summary>
|
||||
/// TTL кэша шаблонов
|
||||
/// </summary>
|
||||
private readonly TimeSpan cacheTemplateTtl = TimeSpan.FromHours(12);
|
||||
|
||||
/// <summary>
|
||||
/// Допустимая разница записей в БД и в кэш
|
||||
/// </summary>
|
||||
private readonly int maxCacheDiff = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Ключ КЭШ, отчет о загруженности, все ЗО
|
||||
/// </summary>
|
||||
private readonly string[] cacheWorkloadResponseAreaAllKey = { "workload", "response area", "all" };
|
||||
|
||||
/// <summary>
|
||||
/// Ключ КЭШ, отчет о загруженности, одна ЗО
|
||||
/// Формировать ключ, основа + Hash ЗО
|
||||
/// </summary>
|
||||
private readonly string[] cacheWorkloadResponseAreaItemKey = { "workload", "response area", "item" };
|
||||
|
||||
/// <summary>
|
||||
/// Ключ КЭШ, отчет о загруженности, все РГ
|
||||
/// </summary>
|
||||
private readonly string[] cacheWorkloadWorkGroupAllKey = { "workload", "work group", "all" };
|
||||
|
||||
/// <summary>
|
||||
/// Ключ КЭШ, отчет о загруженности, одна РГ
|
||||
/// Формировать ключ, основа + Hash РГ
|
||||
/// </summary>
|
||||
private readonly string[] cacheWorkloadWorkGroupItemKey = { "workload", "work group", "item" };
|
||||
|
||||
/// <summary>
|
||||
/// TTL кэша отчетов workload
|
||||
/// </summary>
|
||||
private readonly TimeSpan cacheWorkloadTtl = TimeSpan.FromMinutes(15);
|
||||
//=========== Настройки, вынести ===========
|
||||
|
||||
/// <summary>
|
||||
/// Создать КЭШ шаблонов.
|
||||
/// КЭШ рабочих групп и зон ответственности
|
||||
@@ -35,20 +84,51 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
/// <returns></returns>
|
||||
public async Task CreateTemplateCacheAsync()
|
||||
{
|
||||
await DeleteTemplateCacheAsync();
|
||||
await DeleteWorkloadCacheAsync();
|
||||
var dtStart = DateTimeOffset.UtcNow;
|
||||
|
||||
logger.LogInformation("Создание КЭШа шаблонов.");
|
||||
|
||||
//1. Получить из БД все шаблоны (тяжелый запрос, тянет вообще все данные, 8 сек)
|
||||
var templates = await templateRepository.Get()
|
||||
.AsNoTracking()
|
||||
.Include(t=>t.Job)
|
||||
// Загружаем дополнительные данные, чтоб быстрее считался шорткод
|
||||
.Include(t => t.Job).ThenInclude(t => t.Group).ThenInclude(t => t.GroupType)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
//2. Получить по каждому шаблону ЗО и РГ
|
||||
//3. Сохранить в Redis
|
||||
logger.LogInformation("Получил из БД шаблонов {TemplateCount} шт.", templates.Count);
|
||||
|
||||
//redisCacheService
|
||||
//2. Получить по каждому шаблону ЗО и РГ
|
||||
var cacheData = new List<TemplateCache>();
|
||||
|
||||
int templateCount = 0;
|
||||
foreach (var template in templates)
|
||||
{
|
||||
var (workGroup, responseArea) = await ApplyShortcodesAsync(template);
|
||||
|
||||
logger.LogDebug("[{TemplateCount}] Для шаблона {TemplateId} {TemplateName} получена ЗО '{ResponseArea}', РГ '{WorkGroup}'", templateCount, template.Id, template.Name, responseArea, workGroup);
|
||||
|
||||
var cacheItem = new TemplateCache
|
||||
{
|
||||
Data = new TemplateCacheDto { TemplateId = template.Id, ResponseArea = responseArea, WorkGroup = workGroup },
|
||||
Source = nameof(WorkloadCacheService),
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
cacheData.Add(cacheItem);
|
||||
|
||||
templateCount++;
|
||||
}
|
||||
|
||||
//3. Сохранить в Redis
|
||||
logger.LogDebug("Для сохранения в Redis подготовлен массив из {Count} строк", cacheData.Count);
|
||||
var hashKey = redisCacheService.GetKey(cacheTemplateKey);
|
||||
|
||||
foreach (var item in cacheData)
|
||||
{
|
||||
await redisCacheService.SetHashFieldAsync(hashKey, item.Data.TemplateId.ToString(), item, cacheTemplateTtl);
|
||||
}
|
||||
|
||||
var duration = DateTimeOffset.UtcNow - dtStart;
|
||||
logger.LogInformation("Завершено формирование кэша. Key: {HashKey}, ttl: {Ttl}, кол-во записей: {CacheCount}. Продолжительность формирования КЭШ {Duration}", hashKey, cacheTemplateTtl, cacheData.Count, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,7 +137,10 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
/// <returns></returns>
|
||||
public async Task DeleteTemplateCacheAsync()
|
||||
{
|
||||
|
||||
var hashKey = redisCacheService.GetKey(cacheTemplateKey);
|
||||
logger.LogDebug("Удаляю кэш шаблонов, {HashKey}", redisCacheService.GetKey(cacheTemplateKey));
|
||||
|
||||
await redisCacheService.DeleteHashAsync(hashKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -66,9 +149,134 @@ namespace PARR.Core.Services.Workload.Implementations
|
||||
/// <returns></returns>
|
||||
public async Task DeleteWorkloadCacheAsync()
|
||||
{
|
||||
logger.LogDebug("Удаляю кэш отчетов");
|
||||
|
||||
await redisCacheService.DeleteCachedDataAsync(redisCacheService.GetKey(cacheWorkloadResponseAreaAllKey));
|
||||
await redisCacheService.DeleteKeysByPatternAsync(redisCacheService.GetKey(cacheWorkloadResponseAreaItemKey) + "*");
|
||||
|
||||
await redisCacheService.DeleteCachedDataAsync(redisCacheService.GetKey(cacheWorkloadWorkGroupAllKey));
|
||||
await redisCacheService.DeleteKeysByPatternAsync(redisCacheService.GetKey(cacheWorkloadWorkGroupItemKey) + "*");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить кэш РГ и ЗО всех шаблонов, получить недостающие поля из БД.
|
||||
/// Сравнит кол-во записей в БД и в КЭШ, если не хватает до 1000 шаблонов, доформирует кэш,
|
||||
/// если больше 1000, вернет пустой список, так как считается кэш неактуальным.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<Dictionary<Guid, TemplateReportData>> GetTemplateReportDataAsync()
|
||||
{
|
||||
logger.LogDebug("Получаю данные для формирования отчета.");
|
||||
|
||||
// Получим кэш целиком
|
||||
var cacheData = await redisCacheService.GetAllHashFieldsAsync<TemplateCache>(redisCacheService.GetKey(cacheTemplateKey));
|
||||
if (cacheData.Count == 0)
|
||||
{
|
||||
logger.LogDebug("Записей в кэш 0. Конец.");
|
||||
return new Dictionary<Guid, TemplateReportData>();
|
||||
}
|
||||
|
||||
// Сравним кол-во записей в БД и в КЭШ, вдруг там разница больше допустимой
|
||||
var templatesCount = await templateRepository.Get().Select(t => t.Id).CountAsync();
|
||||
if (templatesCount - cacheData.Count > maxCacheDiff)
|
||||
{
|
||||
logger.LogInformation("Разница в количестве шаблонов в БД ({TemplateCount}) и в КЭШ ({CacheCount}) больше допустимой {MaxCacheDiff}. Считаем КЭШ неактуальным. Требуется повторное формирование КЭШа.",
|
||||
templatesCount, cacheData.Count, maxCacheDiff);
|
||||
return new Dictionary<Guid, TemplateReportData>();
|
||||
}
|
||||
|
||||
// Получаем из БД только нужные данные для отчета
|
||||
var templates = await templateRepository.Get()
|
||||
.AsNoTracking()
|
||||
.Select(t => new { Id = t.Id, IsActiveTemplate = t.IsActiveTemplate, IsActiveSchedule = t.IsActiveSchedule, Name = t.Name, NextRun = t.NextRun })
|
||||
.ToListAsync();
|
||||
|
||||
var reportData = new Dictionary<Guid, TemplateReportData>();
|
||||
|
||||
foreach (var template in templates)
|
||||
{
|
||||
cacheData.TryGetValue(template.Id.ToString(), out var cache);
|
||||
|
||||
var (workGroup, responseArea) = ("", "");
|
||||
var cacheDate = DateTimeOffset.UtcNow;
|
||||
|
||||
if (cache != null)
|
||||
{
|
||||
workGroup = cache.Data.WorkGroup;
|
||||
responseArea = cache.Data.ResponseArea;
|
||||
cacheDate = cache.Timestamp;
|
||||
}
|
||||
else
|
||||
{
|
||||
(workGroup, responseArea) = await GetTemplateProps(template.Id);
|
||||
}
|
||||
|
||||
var data = new TemplateReportData(
|
||||
template.Id,
|
||||
responseArea,
|
||||
workGroup,
|
||||
template.Name,
|
||||
template.NextRun,
|
||||
template.IsActiveTemplate,
|
||||
template.IsActiveSchedule,
|
||||
DateTimeOffset.UtcNow);
|
||||
reportData.Add(template.Id, data);
|
||||
}
|
||||
|
||||
logger.LogDebug("Сформировано итоговых данных для отчета, строк: {Count}", reportData.Count);
|
||||
|
||||
return reportData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить РГ и ЗО из шорткода. Сохранить в КЭШ.
|
||||
/// </summary>
|
||||
/// <param name="templateId"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<(string WorkGroup, string ResponseArea)> GetTemplateProps(Guid templateId)
|
||||
{
|
||||
logger.LogDebug("Дозагружаю недостающие данные из БД для шаблона {TemplateId}", templateId);
|
||||
|
||||
var template = await templateRepository.Get()
|
||||
.AsNoTracking()
|
||||
// Загружаем дополнительные данные, чтоб быстрее считался шорткод
|
||||
.Include(t => t.Job).ThenInclude(t => t.Group).ThenInclude(t => t.GroupType)
|
||||
.FirstOrDefaultAsync(t => t.Id == templateId);
|
||||
|
||||
if (template == null)
|
||||
{
|
||||
logger.LogWarning("Шаблон {TemplateId} не найден в БД", templateId);
|
||||
return ($"РГ {templateId}", $"ЗО {templateId}");
|
||||
}
|
||||
|
||||
var (workGroup, responseArea) = await ApplyShortcodesAsync(template);
|
||||
|
||||
// Запишем результат в кэш
|
||||
var cacheItem = new TemplateCache
|
||||
{
|
||||
Data = new TemplateCacheDto { TemplateId = template.Id, ResponseArea = responseArea, WorkGroup = workGroup },
|
||||
Source = nameof(WorkloadCacheService),
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await redisCacheService.SetHashFieldAsync(redisCacheService.GetKey(cacheTemplateKey), templateId.ToString(), cacheItem);
|
||||
logger.LogDebug("Добавлена недостающая запись в кэш. {TemplateId}, '{WorkGroup}', '{ResponseArea}'", templateId, workGroup, responseArea);
|
||||
|
||||
return (workGroup, responseArea);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить РГ и ЗО из шорткодов
|
||||
/// </summary>
|
||||
/// <param name="template"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<(string WorkGroup, string ResponseArea)> ApplyShortcodesAsync(Template template)
|
||||
{
|
||||
var workGroup = await shortcodeService.ApplyShortcodesAsync(template.Job!.WorkGroupMask, template);
|
||||
var responseArea = await shortcodeService.ApplyShortcodesAsync(template.Job!.ResponseAreaMask, template);
|
||||
|
||||
return (workGroup, responseArea);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
24
PARR.Core/Services/Workload/Models/TemplateReportData.cs
Normal file
24
PARR.Core/Services/Workload/Models/TemplateReportData.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace PARR.Core.Services.Workload.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Модель данных для формирования отчета по шаблонам.
|
||||
/// Склеенные данные полученные из кэш и БД
|
||||
/// </summary>
|
||||
/// <param name="TemplateId"></param>
|
||||
/// <param name="ResponseArea"></param>
|
||||
/// <param name="WorkGroup"></param>
|
||||
/// <param name="Name"></param>
|
||||
/// <param name="NextRun"></param>
|
||||
/// <param name="IsActiveTemplate"></param>
|
||||
/// <param name="IsActiveSchedule"></param>
|
||||
public record TemplateReportData(
|
||||
Guid TemplateId,
|
||||
string ResponseArea,
|
||||
string WorkGroup,
|
||||
string Name,
|
||||
DateTimeOffset NextRun,
|
||||
bool IsActiveTemplate,
|
||||
bool IsActiveSchedule,
|
||||
DateTimeOffset CacheDate
|
||||
);
|
||||
}
|
||||
@@ -573,7 +573,7 @@ namespace PARR.DAL.Context
|
||||
{
|
||||
f.HasData(new TaskType[]
|
||||
{
|
||||
new() { Code = TaskTypeEnum.Workload, Name = TaskTypeEnum.Workload.ToString(), Description = "Формирование данных для отчетности - Загруженность", MaxRetries = 2, IsSingleton=true, MaxExecutionTimeMinutes=30, RetentionDays=90 }
|
||||
new() { Code = TaskTypeEnum.Workload, Name = TaskTypeEnum.Workload.ToString(), Description = "Формирование данных для отчетности - Загруженность", MaxRetries = 2, IsSingleton=true, MaxExecutionTimeMinutes=60, RetentionDays=90 }
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
22
PARR.Domain/Cache/Models/TemplateCache.cs
Normal file
22
PARR.Domain/Cache/Models/TemplateCache.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using PARR.Domain.Cache.Models.Base;
|
||||
|
||||
namespace PARR.Domain.Cache.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Модель кэша для построения отчетности о загруженности.
|
||||
/// Шаблон + ЗО + РГ
|
||||
/// </summary>
|
||||
public record TemplateCache : IBaseCache<TemplateCacheDto>
|
||||
{
|
||||
public required TemplateCacheDto Data { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public required string Source { get; set; }
|
||||
}
|
||||
|
||||
public record TemplateCacheDto
|
||||
{
|
||||
public Guid TemplateId { get; init; }
|
||||
public required string WorkGroup { get; init; }
|
||||
public required string ResponseArea { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Common.Interfaces;
|
||||
using StackExchange.Redis;
|
||||
using System.Text.Json;
|
||||
@@ -8,14 +9,19 @@ namespace PARR.Infrastructure.Redis
|
||||
internal class RedisCacheService : IRedisCacheService
|
||||
{
|
||||
private readonly IDistributedCache cache;
|
||||
private readonly IConnectionMultiplexer connectionMultiplexer;
|
||||
private readonly ILogger<RedisCacheService> logger;
|
||||
private readonly IDatabase redis;
|
||||
|
||||
public RedisCacheService(
|
||||
IDistributedCache cache,
|
||||
IConnectionMultiplexer connectionMultiplexer
|
||||
IConnectionMultiplexer connectionMultiplexer,
|
||||
ILogger<RedisCacheService> logger
|
||||
)
|
||||
{
|
||||
this.cache = cache;
|
||||
this.connectionMultiplexer = connectionMultiplexer;
|
||||
this.logger = logger;
|
||||
this.redis = connectionMultiplexer.GetDatabase();
|
||||
}
|
||||
|
||||
@@ -80,6 +86,61 @@ namespace PARR.Infrastructure.Redis
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
public async Task<int> DeleteKeysByPatternAsync(string pattern, int batchSize = 100)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
return 0;
|
||||
|
||||
logger.LogDebug("Начало удаление ключей по маске '{Pattern}' из кэш", pattern);
|
||||
|
||||
var deletedCount = 0;
|
||||
var keysToDelete = new List<RedisKey>();
|
||||
|
||||
// Получаем сервер для выполнения SCAN
|
||||
//todo: Для кластера нужно обрабатывать каждый узел отдельно
|
||||
|
||||
var server = connectionMultiplexer.GetEndPoints()
|
||||
.Select(t => connectionMultiplexer.GetServer(t))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (server == null)
|
||||
{
|
||||
logger.LogError("Не удалось получить сервер Redis для операции SCAN");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// server.Keys() лениво сканирует ключи через SCAN (не блокирует!)
|
||||
// Важно: не вызывай .ToList() — это загрузит всё в память!
|
||||
foreach (var key in server.Keys(pattern: pattern, pageSize: batchSize))
|
||||
{
|
||||
keysToDelete.Add(key);
|
||||
|
||||
// Удаляем пакетом, чтобы не делать лишний сетевой запрос на каждый ключ
|
||||
if (keysToDelete.Count >= batchSize)
|
||||
{
|
||||
await redis.KeyDeleteAsync(keysToDelete.ToArray());
|
||||
deletedCount += keysToDelete.Count;
|
||||
|
||||
logger.LogDebug("Удалено {Count} ключей по масске '{Pattern}'", keysToDelete.Count, pattern);
|
||||
|
||||
keysToDelete.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем остаток, если остался
|
||||
if (keysToDelete.Count > 0)
|
||||
{
|
||||
await redis.KeyDeleteAsync(keysToDelete.ToArray());
|
||||
deletedCount += keysToDelete.Count;
|
||||
}
|
||||
|
||||
logger.LogInformation("Завершено удаление ключей по маске '{Pattern}'. Удалено: {DeletedCount}", pattern, deletedCount);
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
|
||||
#region Нативные операции Redis, Redis Hash
|
||||
|
||||
public async Task SetHashFieldAsync<T>(string hashKey, string field, T value, TimeSpan? ttl = null)
|
||||
|
||||
Reference in New Issue
Block a user