using Application.Abstractions.Cache; using StackExchange.Redis; using System.Text.Json; namespace Infrastructure.Cache; public sealed class RedisCacheService(IConnectionMultiplexer redis) : ICacheService { private readonly IDatabase _db = redis.GetDatabase(); private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; public async Task GetAsync(string key, CancellationToken ct = default) { var value = await _db.StringGetAsync(key); if (value.IsNullOrEmpty) { return default; } return JsonSerializer.Deserialize((string)value!, _jsonOptions); } public async Task SetAsync(string key, T value, CancellationToken ct = default) { await _db.StringSetAsync(key, JsonSerializer.Serialize(value, _jsonOptions)); } public async Task SetAsync(string key, T value, TimeSpan expiry, CancellationToken ct = default) { await _db.StringSetAsync(key, JsonSerializer.Serialize(value, _jsonOptions), expiry); } public async Task RemoveAsync(string key, CancellationToken ct = default) { await _db.KeyDeleteAsync(key); } public async Task RemoveByPrefixAsync(string prefix, CancellationToken ct = default) { // SCAN 기반 비블로킹 스캔. KEYS 명령은 운영 Redis 에서 단일 스레드 블로킹이라 절대 금지 var server = redis.GetServer(_db.Multiplexer.GetEndPoints().First()); var batch = new List(256); await foreach (var key in server.KeysAsync(database: _db.Database, pattern: $"{prefix}*", pageSize: 1000).WithCancellation(ct)) { batch.Add(key); if (batch.Count >= 500) { await _db.KeyDeleteAsync(batch.ToArray()); batch.Clear(); } } if (batch.Count > 0) { await _db.KeyDeleteAsync(batch.ToArray()); } } public async Task TryLockAsync(string key, TimeSpan expiry, CancellationToken ct = default) { return await _db.StringSetAsync(key, "1", expiry, When.NotExists); } }