---
# System prepended metadata

title: FusionCache 快取套件介紹
tags: [FusionCache]

---

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