# 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)