近期同事介紹了 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.
這篇「Step by Step」的官方說明簡潔有力,直接說明了各功能的用途和效果,還提供了一些維持服務穩定的調整方向:FusionCache/docs/StepByStep.md at main · ZiggyCreatures/FusionCache · GitHub
這裡就挑「Step by Step」裡幾個有趣的功能簡單說明。
基本使用 只要安裝 FusionCache 的套件,裡面實作了 IMemoryCache
,使用 in-memory 的方式在本機建立快取。只要 DI 註冊、調整資料存取相關程式就能馬上使用,相當方便。
必裝套件:
ZiggyCreatures.FusionCache
直接使用 1 services.AddFusionCache();
自訂快取有效期 設定說明:
Duration
: In-memory cache 資料的有效期。
1 2 3 4 5 6 services.AddFusionCache() .WithDefaultEntryOptions(new FusionCacheEntryOptions { Duration = TimeSpan.FromMinutes(1 ) }) ;
調整資料存取的程式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 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} " ; } public async Task<Product> CreateAsync (Product product ) { _context.Product.Add(product); await _context.SaveChangesAsync(); await _fusionCache.SetAsync( GetCacheKey("Product" , product.Id.ToString()), product, options => { options.DistributedCacheDuration = TimeSpan.FromMinutes(10 ); options.DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(10 ); options.DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(500 ); options.Duration = TimeSpan.FromSeconds(5 ); } ); return product; } public async Task<Product> GetByIdAsync (int id ) { var product = await _fusionCache.GetOrSetAsync( GetCacheKey("Product" , id.ToString()), async ct => { _logger.LogError("Getting product {id} from the database." , id); return await _context.Product.FindAsync(id, ct); }, options => { options.DistributedCacheDuration = TimeSpan.FromMinutes(10 ); options.DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(10 ); options.DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(500 ); options.Duration = TimeSpan.FromSeconds(5 ); } ); return product; } 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 => { options.DistributedCacheDuration = TimeSpan.FromMinutes(10 ); options.DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(10 ); options.DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(500 ); options.Duration = TimeSpan.FromSeconds(5 ); } ); return true ; } 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
: 每次有請求來時,會讓這筆過期資料在這段時間內暫時當作還沒過期,可以繼續讀取,避免每次都嘗試連資料庫。
1 2 3 4 5 6 7 8 9 10 services.AddFusionCache() .WithDefaultEntryOptions(new FusionCacheEntryOptions { Duration = TimeSpan.FromMinutes(1 ), 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 的執行。
1 2 3 4 5 6 7 8 9 10 11 12 13 services.AddFusionCache() .WithDefaultEntryOptions(new FusionCacheEntryOptions { Duration = TimeSpan.FromMinutes(1 ), IsFailSafeEnabled = true , FailSafeMaxDuration = TimeSpan.FromHours(2 ), FailSafeThrottleDuration = TimeSpan.FromSeconds(30 ), FactorySoftTimeout = TimeSpan.FromMilliseconds(100 ), FactoryHardTimeout = TimeSpan.FromMilliseconds(1500 ) }) ;
Distributed cache 單純使用 FusionCache 的話,只會使用 in-memory 的快取 (實作 IMemoryCache
),多節點的話還是需要個分散式快取。FusionCache 實作了 IDistributedCache
,所以可以使用各種服務來儲存 (Redis, Memcached, MongoDB 等)。這裡以 Redis 作為範例。
IMemoryCache
和 IDistributedCache
可以看這裡:
使用 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
1 2 3 4 5 6 7 8 9 10 11 12 13 services.AddFusionCache() .WithDefaultEntryOptions(new FusionCacheEntryOptions { Duration = TimeSpan.FromMinutes(1 ), }) .WithSerializer( new FusionCacheNeueccMessagePackSerializer() ) .WithDistributedCache( new RedisCache(new RedisCacheOptions() { Configuration = "CONNECTION STRING" }) ) ;
Distributed cache 的設定 設定說明:
DistributedCacheCircuitBreakerDuration
: 斷路器的持續時間。如果分散式快取出現硬性錯誤 (如壞掉拋例外),就會觸發啟動斷路器,在指定的時間內停止使用分散式快取。
DistributedCacheSoftTimeout
: 軟性 timeout。超過這個時間後,FusionCache 會先放棄等待結果,進入下一步,但還是會在背景繼續執行。
DistributedCacheHardTimeout
: 硬性 timeout。超過這時間後,會直接取消分散式快取操作(例如用 CancellationToken)。
AllowBackgroundDistributedCacheOperations
: 允許在背景操作分散式快取,加快回應速度。讀取不會有影響,新增、修改、刪除才會影響。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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" }) ) .WithBackplane( new RedisBackplane(new RedisBackplaneOptions() { Configuration = "CONNECTION STRING" }) ) ;
最後 除了上述的功能之外,還有功能和套件的使用概念在官方文件中都有寫到,建議去裡面看看。對於快取的概念,其實也可以從裡面學到很多各種問題的處理方式。原本以為裡面的連線問題或重試會使用 Polly,結果並沒有使用,看來可以再仔細研究裡面的做法。
參考
延伸閱讀