/
afanasevn
/
RedisLab
Обзор
Документация
Войти
/
afanasevn
/
RedisLab
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/RedisLab.Infrastructure/Redis/TaskCardCache.cs
115 строк
4 KB
IBS\NAfanasev
Solution commit
24 июн 2026, 16:45
24 июн 2026, 16:45
bab80b7
Код
Авторство
О чём код?
using Microsoft.Extensions.Logging; using RedisLab.Application.Abstractions; using RedisLab.Application.Contracts.Tasks; using RedisLab.Application.Redis; using StackExchange.Redis; namespace RedisLab.Infrastructure.Redis; /// <summary> /// Cache-aside для карточки задачи через Redis Hash (не JSON String). /// </summary> public sealed class TaskCardCache( IConnectionMultiplexer redis, ILogger<TaskCardCache> logger) : ITaskCardCacheInvalidator { /// <inheritdoc /> public async Task InvalidateAsync( Guid taskId, CancellationToken ct) { var key = RedisKeyNames.Task(taskId); var database = redis.GetDatabase(); var deleted = await database.KeyDeleteAsync(key); logger.LogInformation( "Invalidation {Key} — ключ {Action}", key, deleted ? "удалён" : "отсутствовал"); } /// <summary> /// Пробует прочитать карточку из Hash; null — cache miss. /// </summary> public async Task<TaskCardDto?> TryGetAsync( Guid taskId, CancellationToken ct) { var key = RedisKeyNames.Task(taskId); var database = redis.GetDatabase(); // Hash удобнее одного JSON String: поля лежат отдельно (HGET/HSET), // читаем все поля через HGETALL. var entries = await database.HashGetAllAsync(key); if (entries.Length == 0) { return null; } var map = entries.ToDictionary(x => x.Name.ToString(), x => x.Value.ToString()); if (!TryParseHash(map, taskId, out var card)) { // Битый или неполный Hash — считаем miss, перезапишем из PG. logger.LogWarning("Hash {Key} неполный, будет перезаписан из PostgreSQL", key); return null; } logger.LogDebug("Redis cache HIT для Hash {Key}", key); return card; } /// <summary> /// Записывает поля карточки в Hash и выставляет TTL на весь ключ. /// </summary> public async Task SetAsync( TaskCardDto card, CancellationToken ct) { var key = RedisKeyNames.Task(card.Id); var database = redis.GetDatabase(); var entries = new HashEntry[] { new(RedisKeyNames.TaskHashFields.Title, card.Title), new(RedisKeyNames.TaskHashFields.Status, card.Status), new(RedisKeyNames.TaskHashFields.AssigneeName, card.AssigneeName), new(RedisKeyNames.TaskHashFields.DueDate, card.DueDate?.ToString("O") ?? string.Empty), }; // HMSET + EXPIRE: TTL на ключ Hash, не на отдельные поля. // В отличие от String с JSON, здесь каждое поле — атомарная пара field/value. await database.HashSetAsync(key, entries); await database.KeyExpireAsync(key, TimeSpan.FromSeconds(RedisKeyNames.TaskCardTtlSeconds)); logger.LogDebug( "Redis cache SET для Hash {Key}, TTL {Ttl} сек", key, RedisKeyNames.TaskCardTtlSeconds); } private static bool TryParseHash( IReadOnlyDictionary<string, string> map, Guid taskId, out TaskCardDto card) { card = null!; if (!map.TryGetValue(RedisKeyNames.TaskHashFields.Title, out var title) || !map.TryGetValue(RedisKeyNames.TaskHashFields.Status, out var status) || !map.TryGetValue(RedisKeyNames.TaskHashFields.AssigneeName, out var assigneeName)) { return false; } DateTime? dueDate = null; if (map.TryGetValue(RedisKeyNames.TaskHashFields.DueDate, out var dueRaw) && !string.IsNullOrEmpty(dueRaw) && DateTime.TryParse(dueRaw, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed)) { dueDate = parsed; } card = new TaskCardDto(taskId, title, status, assigneeName, dueDate); return true; } }