dramaling-vocab-learning/backend/DramaLing.Api/Services/Caching/HybridCacheService.cs

538 lines
17 KiB
C#

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.EntityFrameworkCore;
using DramaLing.Api.Data;
using DramaLing.Api.Models.Entities;
using System.Text.Json;
using System.Text;
using System.Security.Cryptography;
namespace DramaLing.Api.Services.Caching;
/// <summary>
/// 混合快取服務實作,支援記憶體快取和分散式快取的多層架構
/// </summary>
public class HybridCacheService : ICacheService
{
private readonly IMemoryCache _memoryCache;
private readonly IDistributedCache? _distributedCache;
private readonly DramaLingDbContext _dbContext;
private readonly ILogger<HybridCacheService> _logger;
private readonly CacheStats _stats;
private readonly JsonSerializerOptions _jsonOptions;
public HybridCacheService(
IMemoryCache memoryCache,
DramaLingDbContext dbContext,
ILogger<HybridCacheService> logger,
IDistributedCache? distributedCache = null)
{
_memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
_distributedCache = distributedCache;
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_stats = new CacheStats { LastUpdated = DateTime.UtcNow };
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
_logger.LogInformation("HybridCacheService initialized with Memory Cache and {DistributedCache}",
_distributedCache != null ? "Distributed Cache" : "No Distributed Cache");
}
#region
public async Task<T?> GetAsync<T>(string key) where T : class
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
try
{
// L1: 記憶體快取 (最快)
if (_memoryCache.TryGetValue(key, out T? memoryResult))
{
_stats.HitCount++;
_logger.LogDebug("Cache hit from memory for key: {Key}", key);
return memoryResult;
}
// L2: 分散式快取
if (_distributedCache != null)
{
var distributedData = await _distributedCache.GetAsync(key);
if (distributedData != null)
{
var distributedResult = DeserializeFromBytes<T>(distributedData);
if (distributedResult != null)
{
// 回填到記憶體快取
var memoryExpiry = CalculateMemoryExpiry(key);
_memoryCache.Set(key, distributedResult, memoryExpiry);
_stats.HitCount++;
_logger.LogDebug("Cache hit from distributed cache for key: {Key}", key);
return distributedResult;
}
}
}
// L3: 資料庫快取 (僅適用於分析結果)
if (key.StartsWith("analysis:"))
{
var dbResult = await GetFromDatabaseCacheAsync<T>(key);
if (dbResult != null)
{
// 回填到上層快取
await SetMultiLevelCacheAsync(key, dbResult);
_stats.HitCount++;
_logger.LogDebug("Cache hit from database for key: {Key}", key);
return dbResult;
}
}
_stats.MissCount++;
_logger.LogDebug("Cache miss for key: {Key}", key);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting cache for key: {Key}", key);
_stats.MissCount++;
return null;
}
}
public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
if (value == null)
throw new ArgumentNullException(nameof(value));
try
{
var smartExpiry = expiry ?? CalculateSmartExpiry(key, value);
// 同時設定記憶體和分散式快取
var tasks = new List<Task<bool>>();
// L1: 記憶體快取
tasks.Add(Task.Run(() =>
{
try
{
var memoryExpiry = TimeSpan.FromMinutes(Math.Min(smartExpiry.TotalMinutes, 30)); // 記憶體快取最多30分鐘
_memoryCache.Set(key, value, memoryExpiry);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting memory cache for key: {Key}", key);
return false;
}
}));
// L2: 分散式快取
if (_distributedCache != null)
{
tasks.Add(SetDistributedCacheAsync(key, value, smartExpiry));
}
var results = await Task.WhenAll(tasks);
var success = results.Any(r => r);
if (success)
{
_stats.TotalKeys++;
_logger.LogDebug("Cache set for key: {Key}, expiry: {Expiry}", key, smartExpiry);
}
return success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting cache for key: {Key}", key);
return false;
}
}
public async Task<bool> RemoveAsync(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
try
{
var tasks = new List<Task<bool>>();
// 從記憶體快取移除
tasks.Add(Task.Run(() =>
{
try
{
_memoryCache.Remove(key);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing from memory cache for key: {Key}", key);
return false;
}
}));
// 從分散式快取移除
if (_distributedCache != null)
{
tasks.Add(Task.Run(async () =>
{
try
{
await _distributedCache.RemoveAsync(key);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing from distributed cache for key: {Key}", key);
return false;
}
}));
}
var results = await Task.WhenAll(tasks);
var success = results.Any(r => r);
if (success)
{
_logger.LogDebug("Cache removed for key: {Key}", key);
}
return success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing cache for key: {Key}", key);
return false;
}
}
public async Task<bool> ExistsAsync(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
try
{
// 檢查記憶體快取
if (_memoryCache.TryGetValue(key, out _))
{
return true;
}
// 檢查分散式快取
if (_distributedCache != null)
{
var distributedData = await _distributedCache.GetAsync(key);
return distributedData != null;
}
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking cache existence for key: {Key}", key);
return false;
}
}
public async Task<bool> ExpireAsync(string key, TimeSpan expiry)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
try
{
// 重新設定過期時間(需要重新設定值)
var value = await GetAsync<object>(key);
if (value != null)
{
return await SetAsync(key, value, expiry);
}
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting expiry for key: {Key}", key);
return false;
}
}
public async Task<bool> ClearAsync()
{
try
{
var tasks = new List<Task>();
// 清除記憶體快取(如果支援)
if (_memoryCache is MemoryCache memoryCache)
{
tasks.Add(Task.Run(() =>
{
// MemoryCache 沒有直接清除所有項目的方法
// 這裡只能重新建立或等待自然過期
_logger.LogWarning("Memory cache clear is not directly supported");
}));
}
// 分散式快取清除(取決於實作)
if (_distributedCache != null)
{
tasks.Add(Task.Run(() =>
{
_logger.LogWarning("Distributed cache clear implementation depends on the provider");
}));
}
await Task.WhenAll(tasks);
_logger.LogInformation("Cache clear operation completed");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error clearing cache");
return false;
}
}
#endregion
#region
public async Task<Dictionary<string, T?>> GetManyAsync<T>(IEnumerable<string> keys) where T : class
{
var keyList = keys.ToList();
var result = new Dictionary<string, T?>();
if (!keyList.Any())
return result;
try
{
var tasks = keyList.Select(async key =>
{
var value = await GetAsync<T>(key);
return new KeyValuePair<string, T?>(key, value);
});
var results = await Task.WhenAll(tasks);
return results.ToDictionary(r => r.Key, r => r.Value);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting multiple cache values");
return result;
}
}
public async Task<bool> SetManyAsync<T>(Dictionary<string, T> keyValuePairs, TimeSpan? expiry = null) where T : class
{
if (!keyValuePairs.Any())
return true;
try
{
var tasks = keyValuePairs.Select(async kvp =>
await SetAsync(kvp.Key, kvp.Value, expiry));
var results = await Task.WhenAll(tasks);
return results.All(r => r);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting multiple cache values");
return false;
}
}
#endregion
#region
public Task<CacheStats> GetStatsAsync()
{
_stats.LastUpdated = DateTime.UtcNow;
return Task.FromResult(_stats);
}
#endregion
#region
private async Task<bool> SetDistributedCacheAsync<T>(string key, T value, TimeSpan expiry) where T : class
{
try
{
var serializedData = SerializeToBytes(value);
var options = new DistributedCacheEntryOptions
{
SlidingExpiration = expiry
};
await _distributedCache!.SetAsync(key, serializedData, options);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting distributed cache for key: {Key}", key);
return false;
}
}
private byte[] SerializeToBytes<T>(T value) where T : class
{
var json = JsonSerializer.Serialize(value, _jsonOptions);
return Encoding.UTF8.GetBytes(json);
}
private T? DeserializeFromBytes<T>(byte[] data) where T : class
{
try
{
var json = Encoding.UTF8.GetString(data);
return JsonSerializer.Deserialize<T>(json, _jsonOptions);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deserializing cache data");
return null;
}
}
private TimeSpan CalculateSmartExpiry<T>(string key, T value)
{
// 根據不同的快取類型和鍵的特性計算智能過期時間
return key switch
{
var k when k.StartsWith("analysis:") => TimeSpan.FromHours(2), // AI 分析結果快取2小時
var k when k.StartsWith("user:") => TimeSpan.FromMinutes(30), // 用戶資料快取30分鐘
var k when k.StartsWith("flashcard:") => TimeSpan.FromMinutes(15), // 詞卡資料快取15分鐘
var k when k.StartsWith("stats:") => TimeSpan.FromMinutes(5), // 統計資料快取5分鐘
_ => TimeSpan.FromMinutes(10) // 預設快取10分鐘
};
}
private TimeSpan CalculateMemoryExpiry(string key)
{
// 記憶體快取時間通常比分散式快取短
return key switch
{
var k when k.StartsWith("analysis:") => TimeSpan.FromMinutes(30),
var k when k.StartsWith("user:") => TimeSpan.FromMinutes(10),
var k when k.StartsWith("flashcard:") => TimeSpan.FromMinutes(5),
var k when k.StartsWith("stats:") => TimeSpan.FromMinutes(2),
_ => TimeSpan.FromMinutes(5)
};
}
#region (L3)
private async Task<T?> GetFromDatabaseCacheAsync<T>(string key) where T : class
{
try
{
if (!key.StartsWith("analysis:")) return null;
var hash = key.Replace("analysis:", "");
var cached = await _dbContext.SentenceAnalysisCache
.AsNoTracking()
.FirstOrDefaultAsync(c => c.InputTextHash == hash && c.ExpiresAt > DateTime.UtcNow);
if (cached != null)
{
// 更新訪問統計
cached.AccessCount++;
cached.LastAccessedAt = DateTime.UtcNow;
await _dbContext.SaveChangesAsync();
var result = JsonSerializer.Deserialize<T>(cached.AnalysisResult, _jsonOptions);
return result;
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting from database cache for key: {Key}", key);
return null;
}
}
private async Task SaveToDatabaseCacheAsync<T>(string key, T value, TimeSpan expiry) where T : class
{
try
{
if (!key.StartsWith("analysis:")) return;
var hash = key.Replace("analysis:", "");
var expiresAt = DateTime.UtcNow.Add(expiry);
var existing = await _dbContext.SentenceAnalysisCache
.FirstOrDefaultAsync(c => c.InputTextHash == hash);
if (existing != null)
{
existing.AnalysisResult = JsonSerializer.Serialize(value, _jsonOptions);
existing.ExpiresAt = expiresAt;
existing.AccessCount++;
existing.LastAccessedAt = DateTime.UtcNow;
}
else
{
var cacheItem = new SentenceAnalysisCache
{
Id = Guid.NewGuid(),
InputTextHash = hash,
InputText = "", // 需要從其他地方獲取原始文本
AnalysisResult = JsonSerializer.Serialize(value, _jsonOptions),
CreatedAt = DateTime.UtcNow,
ExpiresAt = expiresAt,
AccessCount = 1,
LastAccessedAt = DateTime.UtcNow
};
_dbContext.SentenceAnalysisCache.Add(cacheItem);
}
await _dbContext.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving to database cache for key: {Key}", key);
}
}
private async Task SetMultiLevelCacheAsync<T>(string key, T value) where T : class
{
var expiry = CalculateSmartExpiry(key, value);
// 設定記憶體快取
var memoryExpiry = CalculateMemoryExpiry(key);
_memoryCache.Set(key, value, memoryExpiry);
// 設定分散式快取
if (_distributedCache != null)
{
await SetDistributedCacheAsync(key, value, expiry);
}
}
#endregion
#endregion
}