Files
parr_api/PARR.Infrastructure/Redis/RedisCacheService.cs

322 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
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;
private readonly IDatabase redis;
public RedisCacheService(
IDistributedCache cache,
IConnectionMultiplexer connectionMultiplexer,
ILogger<RedisCacheService> logger
)
{
this.cache = cache;
this.connectionMultiplexer = connectionMultiplexer;
this.logger = logger;
this.redis = connectionMultiplexer.GetDatabase();
}
#region Распределенный кэш IDistributedCache
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);
if (jsonData == null)
return default(T);
return JsonSerializer.Deserialize<T>(jsonData);
}
catch (Exception ex)
{
throw new Exception("Формат данных не строка.", ex);
}
}
}
//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
};
if (useCompression)
{
// храним в байтах, если надо то сжимаем
var jsonData = SerializeWithCompression(data);
await cache.SetAsync(key, jsonData, options);
}
else
{
// храним в строке
var jsonData = JsonSerializer.Serialize(data);
await cache.SetStringAsync(key, jsonData, options);
}
}
public async Task DeleteCachedDataAsync(string key)
{
await cache.RemoveAsync(key);
}
public void DeleteCachedData(string key)
{
cache.Remove(key);
}
#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)
{
// меняет одно поле в 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<T?> GetHashFieldAsync<T>(string hashKey, string field)
{
// получить значение поля из Hash
var value = await redis.HashGetAsync(hashKey, field);
if (value.IsNullOrEmpty)
return default;
return JsonSerializer.Deserialize<T>(value);
}
public async Task<Dictionary<string, T>> GetAllHashFieldsAsync<T>(string hashKey)
{
// получить все записи из Hash
var objs = await redis.HashGetAllAsync(hashKey);
if (objs.Length == 0)
return new Dictionary<string, T>();
var result = objs.ToDictionary(
t => t.Name.ToString(),
t => JsonSerializer.Deserialize<T>(t.Value)
);
return result!;
}
public async Task DeleteHashFieldAsync(string hashKey, string field)
{
// удалить запись из Hash
await redis.HashDeleteAsync(hashKey, field);
}
public async Task<bool> 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<long> 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, string[]? keyPartsToHash = null)
{
// разделитель между ИД
var mainSeparator = "_";
// разделитель между словами
var wordSeparator = "-";
if (keyParts.Length == 0)
{
throw new ArgumentNullException("keyParts не может быть пустым");
}
var keyStr = string.Join(mainSeparator, keyParts).Replace(" ", wordSeparator);
if (keyPartsToHash != null && keyPartsToHash.Length > 0)
{
var partsToHashStr = string.Join(mainSeparator, keyPartsToHash).Replace(" ", wordSeparator);
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hashedBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(partsToHashStr));
var base64str = Convert.ToBase64String(hashedBytes).Substring(0, 16);
return (keyStr + mainSeparator + base64str).ToLower();
}
return keyStr.ToLower();
}
#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);
}
}
}