# FusionCache 快取套件介紹 近期同事介紹了 FusionCache 這個快取套件,試玩了一下發現裡面的功能非常完整,說明文件也很詳細。FusionCache 解決了很多使用多層快取時會遇到的問題,也比微軟在 .NET 9 新出的 HybridCache 完整和好用很多。 FusionCache 除了提供基本的 in-memory 快取之外,還可以疊加更多層次的快取,例如 Redis, Memcached 等。還有提供各種 timeout 的處理、快取同步、重新連線、熔斷等功能,減少了各種連線問題的處理。 FusionCache 的 GitHub: [GitHub - ZiggyCreatures/FusionCache: FusionCache is an easy to use, fast and robust hybrid cache with advanced resiliency features.](https://github.com/ZiggyCreatures/FusionCache) 這篇「Step by Step」的官方說明簡潔有力,直接說明了各功能的用途和效果,還提供了一些維持服務穩定的調整方向: [FusionCache/docs/StepByStep.md at main · ZiggyCreatures/FusionCache · GitHub](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/StepByStep.md) 這裡就挑「Step by Step」裡幾個有趣的功能簡單說明。 ## 基本使用 只要安裝 FusionCache 的套件,裡面實作了 `IMemoryCache`,使用 in-memory 的方式在本機建立快取。只要 DI 註冊、調整資料存取相關程式就能馬上使用,相當方便。 必裝套件: - `ZiggyCreatures.FusionCache` ### 直接使用 ```csharp services.AddFusionCache(); ``` ### 自訂快取有效期 設定說明: - `Duration`: In-memory cache 資料的有效期。 ```csharp services.AddFusionCache() .WithDefaultEntryOptions(new FusionCacheEntryOptions { // In-memory cache 的到期時間 Duration = TimeSpan.FromMinutes(1) }) ; ``` ### 調整資料存取的程式 ```csharp public class ProductRepository : IProductRepository { private readonly EcShopContext _context; private readonly IFusionCache _fusionCache; private readonly ILogger _logger; public ProductRepository(EcShopContext context, IFusionCache fusionCache, ILoggerFactory loggerFactory) { _context = context; _fusionCache = fusionCache; _logger = loggerFactory.CreateLogger<ProductRepository>(); } private string GetCacheKey(string prefix, string identifier = "") { return $"{prefix}:{identifier}"; } // Create public async Task<Product> CreateAsync(Product product) { _context.Product.Add(product); await _context.SaveChangesAsync(); // 建立快取 await _fusionCache.SetAsync( GetCacheKey("Product", product.Id.ToString()), product, options => { // 客製化設定,用來方便測試,實務上並不會這樣設定,請勿照抄。 // 這裡的設定只會針對這筆資料,會覆蓋 DI 內的設定。 options.DistributedCacheDuration = TimeSpan.FromMinutes(10); options.DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(10); options.DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(500); options.Duration = TimeSpan.FromSeconds(5); } ); return product; } // Read - By Id public async Task<Product> GetByIdAsync(int id) { var product = await _fusionCache.GetOrSetAsync( GetCacheKey("Product", id.ToString()), async ct => // 這段就是 factory,先記得 { _logger.LogError("Getting product {id} from the database.", id); return await _context.Product.FindAsync(id, ct); }, options => { // 客製化設定,用來方便測試,實務上並不會這樣設定,請勿照抄。 // 這裡的設定只會針對這筆資料,會覆蓋 DI 內的設定。 options.DistributedCacheDuration = TimeSpan.FromMinutes(10); options.DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(10); options.DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(500); options.Duration = TimeSpan.FromSeconds(5); } ); return product; } // Update public async Task<bool> UpdateAsync(Product product) { var existing = await _context.Product.FindAsync(product.Id); if (existing == null) return false; _context.Entry(existing).CurrentValues.SetValues(product); await _context.SaveChangesAsync(); // 更新相關快取 await _fusionCache.SetAsync( GetCacheKey("Product", product.Id.ToString()), product, options => { // 客製化設定,用來方便測試,實務上並不會這樣設定,請勿照抄。 // 這裡的設定只會針對這筆資料,會覆蓋 DI 內的設定。 options.DistributedCacheDuration = TimeSpan.FromMinutes(10); options.DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(10); options.DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(500); options.Duration = TimeSpan.FromSeconds(5); } ); return true; } // Delete public async Task<bool> DeleteAsync(int id) { var product = await _context.Product.FindAsync(id); if (product == null) return false; _context.Product.Remove(product); await _context.SaveChangesAsync(); // 刪除相關快取 await _fusionCache.RemoveAsync(GetCacheKey("Product", id.ToString())); return true; } ``` ## Fail-Safe 當 factory (也就是 factory 裡面的實作,存取資料庫) 壞掉時,就會觸發這裡。當資料庫發生錯誤時,仍然可以持續提供服務,會去讀取已經過期的快取資料。搭配 `Factory Timeouts`,效果更佳,縮短 timeouts 時間,儘早回傳資料。 設定說明: - `IsFailSafeEnabled`: 是否啟用。 - `FailSafeMaxDuration`: 一個已經過期的快取資料,最多還可以被使用多久(從原本過期時間開始算)。 - `FailSafeThrottleDuration`: 每次有請求來時,會讓這筆過期資料在這段時間內暫時當作還沒過期,可以繼續讀取,避免每次都嘗試連資料庫。 ```csharp services.AddFusionCache() .WithDefaultEntryOptions(new FusionCacheEntryOptions { Duration = TimeSpan.FromMinutes(1), // Fail-Safe 的設定 IsFailSafeEnabled = true, FailSafeMaxDuration = TimeSpan.FromHours(2), FailSafeThrottleDuration = TimeSpan.FromSeconds(30) }) ; ``` ## Factory Timeouts 當遇到 factory (也就是 factory 裡面的實作,存取資料庫) 的回傳時間比較久,甚至等滿了 timeout 時間才回傳資料庫壞掉。這時,使用 `Fatory Timeouts` 就可以設定較短的時間,縮短 timeout,繼續下一步進入到 `Fail-Safe`。 設定說明: - `FactorySoftTimeout`: 軟性 timeout。超過這個時間後,FusionCache 會先放棄等待結果,但 factory 還是會在背景繼續執行。若在 hard timeout 之前能收到資料,還是會在背景建立快取。 - `FactoryHardTimeout`: 硬性 timeout。超過這個時間後,FusionCache 會強制取消執行(例如透過 CancellationToken)。 流程: - 先到 soft timeout 時 → FusionCache 嘗試用 fail-safe 或回傳 null,但 factory 還會繼續執行(可補快取)。 - 再到 hard timeout 時 → 就會真的取消 factory 的執行。 ```csharp services.AddFusionCache() .WithDefaultEntryOptions(new FusionCacheEntryOptions { Duration = TimeSpan.FromMinutes(1), IsFailSafeEnabled = true, FailSafeMaxDuration = TimeSpan.FromHours(2), FailSafeThrottleDuration = TimeSpan.FromSeconds(30), // Factory timeout 的設定 FactorySoftTimeout = TimeSpan.FromMilliseconds(100), FactoryHardTimeout = TimeSpan.FromMilliseconds(1500) }) ; ``` ## Distributed cache 單純使用 FusionCache 的話,只會使用 in-memory 的快取 (實作 `IMemoryCache`),多節點的話還是需要個分散式快取。FusionCache 實作了 `IDistributedCache`,所以可以使用各種服務來儲存 (Redis, Memcached, MongoDB 等)。這裡以 Redis 作為範例。 `IMemoryCache` 和 `IDistributedCache` 可以看這裡: - [ASP.NET Core 中的記憶體中快取 | Microsoft Learn](https://learn.microsoft.com/zh-tw/aspnet/core/performance/caching/memory) - [ASP.NET Core 中的分散式快取 | Microsoft Learn](https://learn.microsoft.com/zh-tw/aspnet/core/performance/caching/distributed) ### 使用 Distributed cache 必裝套件: - `Microsoft.Extensions.Caching.StackExchangeRedis` 擇一安裝: - `ZiggyCreatures.FusionCache.Serialization.SystemTextJson` - `ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson` - `ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack` 上面的序列化套件依需求擇一安裝。這裡選 `MessagePack`,因為資料佔用空間小。除了上面那三種,還有這些(要搭配支援的分散式快取服務): - `ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack` - `ZiggyCreatures.FusionCache.Serialization.ServiceStackJson` - `ZiggyCreatures.FusionCache.Serialization.ProtoBufNet` ```csharp services.AddFusionCache() .WithDefaultEntryOptions(new FusionCacheEntryOptions { Duration = TimeSpan.FromMinutes(1), }) // 使用 Message Pack 作為序列化工具 .WithSerializer( new FusionCacheNeueccMessagePackSerializer() ) // 使用 Redis 作為分散式快取的服務 .WithDistributedCache( new RedisCache(new RedisCacheOptions() { Configuration = "CONNECTION STRING" }) ) ; ``` ### Distributed cache 的設定 設定說明: - `DistributedCacheCircuitBreakerDuration`: 斷路器的持續時間。如果分散式快取出現硬性錯誤 (如壞掉拋例外),就會觸發啟動斷路器,在指定的時間內停止使用分散式快取。 - `DistributedCacheSoftTimeout`: 軟性 timeout。超過這個時間後,FusionCache 會先放棄等待結果,進入下一步,但還是會在背景繼續執行。 - `DistributedCacheHardTimeout`: 硬性 timeout。超過這時間後,會直接取消分散式快取操作(例如用 CancellationToken)。 - `AllowBackgroundDistributedCacheOperations`: 允許在背景操作分散式快取,加快回應速度。讀取不會有影響,新增、修改、刪除才會影響。 ```csharp services.AddFusionCache() .WithOptions(options => { // 分散式快取斷路器的設定 options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(2); }) .WithDefaultEntryOptions(new FusionCacheEntryOptions { Duration = TimeSpan.FromMinutes(1), // 分散式快取的設定 DistributedCacheSoftTimeout = TimeSpan.FromSeconds(1), DistributedCacheHardTimeout = TimeSpan.FromSeconds(2), AllowBackgroundDistributedCacheOperations = true }) .WithSerializer( new FusionCacheNeueccMessagePackSerializer() ) .WithDistributedCache( new RedisCache(new RedisCacheOptions() { Configuration = "CONNECTION STRING" }) ) ; ``` ## Backplane 有了分散式快取,多節點都可以使用了,但是要處理同步的問題。當 A 節點更新資料後,A 自己的記憶體快取更新了,可是其他節點不知道資料已經修改,各節點裡面的記憶體快取依舊是舊資料,這時就產生資料不一致的問題。所以需要主動通知其他節點要更新記憶體快取內的資料,不用等到快取記憶體過期才更新,加快資料更新的時間,消除資料不一致的問題。Backplane 就是在處理這件事的功能。 這裡以使用 Redis 為例,FusionCache 背後的實作是利用 Redis 的 Pub/Sub 功能來達成。 必裝套件: - `ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis` ```csharp services.AddFusionCache() .WithOptions(options => { options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(2); }) .WithDefaultEntryOptions(new FusionCacheEntryOptions { Duration = TimeSpan.FromMinutes(1), DistributedCacheSoftTimeout = TimeSpan.FromSeconds(1), DistributedCacheHardTimeout = TimeSpan.FromSeconds(2), AllowBackgroundDistributedCacheOperations = true, }) .WithSerializer( new FusionCacheNeueccMessagePackSerializer() ) .WithDistributedCache( new RedisCache(new RedisCacheOptions() { Configuration = "CONNECTION STRING" }) ) // 使用 Redis 作為 Backplane .WithBackplane( new RedisBackplane(new RedisBackplaneOptions() { Configuration = "CONNECTION STRING" }) ) ; ``` ## 最後 除了上述的功能之外,還有功能和套件的使用概念在官方文件中都有寫到,建議去裡面看看。對於快取的概念,其實也可以從裡面學到很多各種問題的處理方式。原本以為裡面的連線問題或重試會使用 Polly,結果並沒有使用,看來可以再仔細研究裡面的做法。 ## 參考 - [FusionCache/docs/StepByStep.md at main · ZiggyCreatures/FusionCache · GitHub](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/StepByStep.md) - [FusionCache/docs/CoreMethods.md at main · ZiggyCreatures/FusionCache · GitHub](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CoreMethods.md) ## 延伸閱讀 - [ASP.NET Core 中的記憶體中快取 | Microsoft Learn](https://learn.microsoft.com/zh-tw/aspnet/core/performance/caching/memory) - [ASP.NET Core 中的分散式快取 | Microsoft Learn](https://learn.microsoft.com/zh-tw/aspnet/core/performance/caching/distributed) - [ASP.NET Core 中的 HybridCache 程式庫 | Microsoft Learn](https://learn.microsoft.com/zh-tw/aspnet/core/performance/caching/hybrid) - [redis - 快取雪崩、擊穿、穿透 - Po-Ching Liu - Medium](https://totoroliu.medium.com/redis-%E5%BF%AB%E5%8F%96%E9%9B%AA%E5%B4%A9-%E6%93%8A%E7%A9%BF-%E7%A9%BF%E9%80%8F-8bc02f09fe8f) - [Polly - GitHub](https://github.com/App-vNext/Polly)