422 lines
13 KiB
C#
422 lines
13 KiB
C#
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Caching.Distributed;
|
|
using System.Text.Json;
|
|
using System.Text;
|
|
|
|
namespace DramaLing.Api.Services.Caching;
|
|
|
|
/// <summary>
|
|
/// 混合快取服務實作,支援記憶體快取和分散式快取的多層架構
|
|
/// </summary>
|
|
public class HybridCacheService : ICacheService
|
|
{
|
|
private readonly IMemoryCache _memoryCache;
|
|
private readonly IDistributedCache? _distributedCache;
|
|
private readonly ILogger<HybridCacheService> _logger;
|
|
private readonly CacheStats _stats;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
|
|
public HybridCacheService(
|
|
IMemoryCache memoryCache,
|
|
ILogger<HybridCacheService> logger,
|
|
IDistributedCache? distributedCache = null)
|
|
{
|
|
_memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
|
|
_distributedCache = distributedCache;
|
|
_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;
|
|
}
|
|
}
|
|
}
|
|
|
|
_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)
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
} |