using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StackExchange.Redis; using TakeoutSaaS.Application.Dictionary.Abstractions; using TakeoutSaaS.Domain.Dictionary.Entities; using TakeoutSaaS.Domain.Dictionary.Enums; using TakeoutSaaS.Domain.Dictionary.Repositories; using TakeoutSaaS.Shared.Abstractions.Security; namespace TakeoutSaaS.Infrastructure.Dictionary.Caching; /// /// 两级缓存封装:L1 内存 + L2 Redis。 /// public sealed class HybridCacheService : IDictionaryHybridCache { private static readonly RedisChannel InvalidationChannel = RedisChannel.Literal("dictionary:cache:invalidate"); private readonly MemoryCacheService _memoryCache; private readonly RedisCacheService _redisCache; private readonly ISubscriber? _subscriber; private readonly ILogger? _logger; private readonly CacheMetricsCollector? _metrics; private readonly IServiceScopeFactory? _scopeFactory; /// /// 初始化两级缓存服务。 /// public HybridCacheService( MemoryCacheService memoryCache, RedisCacheService redisCache, IConnectionMultiplexer? multiplexer = null, ILogger? logger = null, CacheMetricsCollector? metrics = null, IServiceScopeFactory? scopeFactory = null) { _memoryCache = memoryCache; _redisCache = redisCache; _logger = logger; _subscriber = multiplexer?.GetSubscriber(); _metrics = metrics; _scopeFactory = scopeFactory; if (_subscriber != null) { _subscriber.Subscribe(InvalidationChannel, (_, value) => { var prefix = value.ToString(); if (!string.IsNullOrWhiteSpace(prefix)) { _memoryCache.RemoveByPrefix(prefix); } }); } } /// /// 获取缓存,如果不存在则创建并回填。 /// public async Task GetOrCreateAsync( string key, TimeSpan ttl, Func> factory, CancellationToken cancellationToken = default) { var stopwatch = Stopwatch.StartNew(); var dictionaryCode = CacheMetricsCollector.ExtractDictionaryCode(key); var l1Hit = false; var l2Hit = false; var cached = await _memoryCache.GetAsync(key, cancellationToken); if (cached != null) { l1Hit = true; _metrics?.RecordHit("L1", dictionaryCode); _metrics?.RecordDuration(dictionaryCode, stopwatch.Elapsed.TotalMilliseconds); _metrics?.RecordQuery(dictionaryCode, l1Hit, l2Hit, stopwatch.Elapsed.TotalMilliseconds); return cached; } _metrics?.RecordMiss("L1", dictionaryCode); try { cached = await _redisCache.GetAsync(key, cancellationToken); if (cached != null) { l2Hit = true; _metrics?.RecordHit("L2", dictionaryCode); await _memoryCache.SetAsync(key, cached, ttl, cancellationToken); _metrics?.RecordDuration(dictionaryCode, stopwatch.Elapsed.TotalMilliseconds); _metrics?.RecordQuery(dictionaryCode, l1Hit, l2Hit, stopwatch.Elapsed.TotalMilliseconds); return cached; } _metrics?.RecordMiss("L2", dictionaryCode); } catch (Exception ex) { _metrics?.RecordMiss("L2", dictionaryCode); _logger?.LogWarning(ex, "读取 Redis 缓存失败,降级为数据库查询。"); } var created = await factory(cancellationToken); if (created == null) { _metrics?.RecordDuration(dictionaryCode, stopwatch.Elapsed.TotalMilliseconds); _metrics?.RecordQuery(dictionaryCode, l1Hit, l2Hit, stopwatch.Elapsed.TotalMilliseconds); return default; } await _memoryCache.SetAsync(key, created, ttl, cancellationToken); try { await _redisCache.SetAsync(key, created, ttl, cancellationToken); } catch (Exception ex) { _logger?.LogWarning(ex, "写入 Redis 缓存失败。"); } _metrics?.RecordDuration(dictionaryCode, stopwatch.Elapsed.TotalMilliseconds); _metrics?.RecordQuery(dictionaryCode, l1Hit, l2Hit, stopwatch.Elapsed.TotalMilliseconds); return created; } /// /// 失效指定前缀的缓存键。 /// public async Task InvalidateAsync( string prefix, CacheInvalidationOperation operation = CacheInvalidationOperation.Update, CancellationToken cancellationToken = default) { var dictionaryCode = CacheMetricsCollector.ExtractDictionaryCode(prefix); _metrics?.RecordInvalidation(dictionaryCode); var removedCount = _memoryCache.RemoveByPrefixWithCount(prefix); long redisRemoved = 0; try { redisRemoved = await _redisCache.RemoveByPrefixWithCountAsync(prefix, cancellationToken); } catch (Exception ex) { _logger?.LogWarning(ex, "删除 Redis 缓存失败。"); } var totalRemoved = removedCount + (int)Math.Min(redisRemoved, int.MaxValue); if (_subscriber != null && !string.IsNullOrWhiteSpace(prefix)) { await _subscriber.PublishAsync(InvalidationChannel, prefix); } _ = WriteInvalidationLogAsync(prefix, dictionaryCode, totalRemoved, operation); } private async Task WriteInvalidationLogAsync( string prefix, string dictionaryCode, int removedCount, CacheInvalidationOperation operation) { if (_scopeFactory == null) { return; } try { using var scope = _scopeFactory.CreateScope(); var repo = scope.ServiceProvider.GetService(); if (repo == null) { return; } var currentUser = scope.ServiceProvider.GetService(); var tenantId = TryExtractTenantId(prefix) ?? 0; var scopeType = tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business; var log = new CacheInvalidationLog { TenantId = tenantId, Timestamp = DateTime.UtcNow, DictionaryCode = dictionaryCode, Scope = scopeType, AffectedCacheKeyCount = removedCount, OperatorId = currentUser?.IsAuthenticated == true ? currentUser.UserId : 0, Operation = operation }; await repo.AddAsync(log); await repo.SaveChangesAsync(); } catch (Exception ex) { _logger?.LogWarning(ex, "写入缓存失效日志失败。"); } } private static long? TryExtractTenantId(string prefix) { if (string.IsNullOrWhiteSpace(prefix)) { return null; } if (prefix.StartsWith("dict:groups:", StringComparison.Ordinal)) { var token = prefix.Replace("dict:groups:", string.Empty, StringComparison.Ordinal).Trim(':'); return long.TryParse(token.Split(':', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(), out var tenantId) ? tenantId : null; } if (prefix.StartsWith("dict:", StringComparison.Ordinal) && !prefix.StartsWith("dict:items:", StringComparison.Ordinal)) { var token = prefix.Replace("dict:", string.Empty, StringComparison.Ordinal); return long.TryParse(token.Split(':', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(), out var tenantId) ? tenantId : null; } return null; } }