.NET 9 的混合式快取 HybridCache

在現代 Web 應用程式開發中,快取是提升應用程式效能不可或缺的技術。在過去,我們會使用 IMemoryCache 做記憶體快取,或者使用 IDistributedCache 做分散式快取。

.NET 9 引入了全新的 HybridCache,它結合了記憶體快取(L1)和分散式快取(L2)的優勢,讓我們能夠在同一個 API 中享受兩層快取的效能提升,同時還提供了快取擊穿保護和標籤管理等進階功能。

開發環境

  • Windows 11 Pro
  • ASP.NET Core 9.0
  • Microsoft.Extensions.Caching.Hybrid
  • Microsoft.Extensions.Caching.StackExchangeRedis
  • Rider 2025.2

HybridCache 是什麼?

HybridCache 是 .NET 9 新推出的快取程式庫,透過 Microsoft.Extensions.Caching.Hybrid 套件提供。所謂「混合快取」,就是同時管理兩層:

  • L1 快取:記憶體內快取,存取速度最快,但只活在當前 process 裡
  • L2 快取:分散式快取(Redis、SQL Server 等),多個 instance 共用,速度慢一點但可以跨 process 共享

查詢邏輯很直觀:L1 沒有 → 問 L2 → L2 也沒有 → 執行 factory 回源,結果同時寫入 L1 與 L2。

graph TB
    A[應用程式請求] --> B[HybridCache]
    B --> C[L1 記憶體快取]
    C --> D{命中?}
    D -->|是| E[直接回傳]
    D -->|否| F[L2 分散式快取]
    F --> G{命中?}
    G -->|是| H[回傳並存入L1]
    G -->|否| I[執行資料來源]
    I --> J[存入L1與L2]
    J --> K[回傳結果]

除了雙層快取,HybridCache 還有幾個亮點:

  • 避免快取擊穿 (Cache Breakdown):同一個 process 內,相同 key 的並發請求,只有第一個會執行 factory,其他請求等待結果,避免瞬間打爆資料庫
  • 標籤式快取管理:可以用標籤批次清除相關快取,不用一個 key 一個 key 刪
  • 自訂序列化器:預設 System.Text.Json,也可以換成 protobuf 或 XML

快取擊穿防護的邊界

這裡有一個很重要的細節,要特別說清楚。

HybridCache 的快取擊穿防護是 process 內的 in-flight deduplication。根據官方文件,快取資料儲存在 process 內(each server has a separate cache),因此防護範圍自然只在當前 process 內有效。

換句話說,如果你部署了 3 個 Pod,同一時間三個 Pod 都發生快取 miss:

Pod A → 執行 factory,打一次 DB
Pod B → 執行 factory,打一次 DB
Pod C → 執行 factory,打一次 DB

每個 Pod 各自保護自己 process 內的並發請求,但 Pod 之間沒有協調,結果還是會打 3 次 DB(而不是全部幾百個並發請求都打)。

這是設計邊界,不是 bug。 大多數情況下,3 次 vs 數百次已經差很多了,實際上通常可以接受。

若真的需要跨 process 的保護,常見做法:

  1. Redis Distributed LockRedLockSETNX + TTL):在取資料前先搶鎖,複雜度比較高
  2. 接受 N 次打穿(N = instance 數量):instance 少、DB 撐得住的情況下,直接接受即可

如何安裝?

安裝 HybridCache 套件:

dotnet add package Microsoft.Extensions.Caching.Hybrid

如果要用 Redis 當 L2 快取,再安裝:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

基本設定

只使用 L1 記憶體快取(最簡單的設定):

builder.Services.AddHybridCache();

加上 Redis 作為 L2 快取:

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
});

builder.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(5),        // L2 過期時間
        LocalCacheExpiration = TimeSpan.FromMinutes(1) // L1 過期時間(應比 L2 短)
    };
});

NOTE:L1 快取時間應比 L2 短,避免 L1 資料比 L2 還新鮮造成不一致。


快取資料的方式

在應用程式端注入 Microsoft.Extensions.Caching.Hybrid.HybridCache,然後呼叫 GetOrCreateAsync

var weather = await _cache.GetOrCreateAsync(
    key: $"weather-{city}",
    factory: async token =>
    {
        await Task.Delay(1000, token); // 模擬外部 API
        return new WeatherInfo(city, 28, "晴朗", DateTime.UtcNow);
    },
    options: new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(10),
        LocalCacheExpiration = TimeSpan.FromMinutes(2)
    },
    tags: [$"weather-{city}"]);

我的範例是在 Minimal API 直接注入使用,完整程式碼如下:

app.MapGet("/weatherforecast/hybrid-cache",
    async (Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache) =>
    {
        var forecast = await hybridCache.GetOrCreateAsync(
            key: "weather-forecast",
            factory: async cancellationToken =>
            {
                await Task.Delay(TimeSpan.FromMicroseconds(2), cancellationToken);
                Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 從資料來源生成天氣預報資料");

                return Enumerable.Range(1, 5).Select(index =>
                        new WeatherForecast
                        {
                            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                            TemperatureC = Random.Shared.Next(-20, 55),
                            Summary = summaries[Random.Shared.Next(summaries.Length)]
                        })
                    .ToArray();
            },
            tags: ["weather-forecast"]
        );

        return Results.Ok(new
        {
            Data = forecast,
            CachedAt = DateTime.Now,
            Message = "資料來自 HybridCache (L1: 記憶體 + L2: Redis)"
        });
    })
    .WithName("GetWeatherForecastWithHybridCache")
    .WithSummary("使用 HybridCache 的天氣預報")
    .WithDescription("展示 HybridCache L1(記憶體)+ L2(Redis)雙層快取功能");

標籤式快取管理

HybridCache 支援標籤,可以批次清除相關快取,在產品更新、類別變動時非常實用:

// 清除特定產品的快取
await _cache.RemoveByTagAsync("product-123");

// 清除某個類別底下所有快取
await _cache.RemoveByTagAsync("category-electronics");

自訂序列化器

預設使用 System.Text.Json,如果要改用 protobuf,可以這樣設定:

options.WithSerializerFactory(type =>
{
    if (type == typeof(Product))
        return new ProtobufSerializer<Product>();
    return null;
});

心得

HybridCache 讓我們不再需要在記憶體快取和分散式快取之間二選一,一個 API 搞定兩層。

幾個要記住的使用原則:

  • L1 過期時間要比 L2 短,避免資料不一致
  • 所有快取都打上標籤,日後清除方便很多
  • 快取擊穿防護只在 單一 process 內有效,多 instance 部署時,每個 instance 還是各自打一次 DB;若要跨 process 保護,需要額外搭配分散式鎖

參考資料


範例位置

sample.dotblog/Cache/Lab.HybridCache at master · yaochangyu/sample.dotblog

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2025 .NET

Image result for microsoft+mvp+logo