2秒かかっていたデータ取得が、0.3ミリ秒で返ってくる。

.NET 10に追加された HybridCache は、インメモリキャッシュと分散キャッシュを自動的に組み合わせ、APIやサービスの応答速度を劇的に改善するキャッシュ機構です。この記事では、Azure Database for PostgreSQLをバックエンドに使ったHybridCacheの導入方法と、実際のパフォーマンス効果を紹介します。

この記事でわかること:

  • HybridCacheが解決するキャッシュ設計の課題
  • .NET 10でのインストールと基本設定
  • GetOrCreateAsync によるキャッシュ実装
  • インメモリ・Postgresの2段階キャッシュのパフォーマンス実測値

https://devblogs.microsoft.com/dotnet/high-performance-distributed-caching-dotnet-postgres-azure/

キャッシュ設計の2つの課題

アプリケーションにキャッシュを導入するとき、主に2つの選択肢があります。

インメモリキャッシュはサブミリ秒の応答速度を出せますが、アプリプロセスが再起動するとデータが消えます。スケールアウト構成では複数インスタンス間でキャッシュが共有されないという問題もあります。

分散キャッシュ(RedisやPostgresなど)はデータの永続性と複数インスタンス間の共有を解決しますが、ネットワーク越しのアクセスが発生するためインメモリほどの速度は出ません。

HybridCacheはこの2つを統合します。「まずインメモリを確認し、なければ分散キャッシュを確認し、それもなければソースから取得してキャッシュに書く」という一連の処理を1つのAPIで実行します。プロセスが再起動してインメモリが消えても、Postgresに残ったエントリから応答を継続できます。

2つのパッケージをインストールする

HybridCacheとPostgresキャッシュには、それぞれ専用のパッケージが必要です。

dotnet add package Microsoft.Extensions.Caching.Postgres
dotnet add package Microsoft.Extensions.Caching.Hybrid

Microsoft.Extensions.Caching.Postgres がAzure Database for PostgreSQL上に分散キャッシュテーブルを構築し、Microsoft.Extensions.Caching.Hybrid がインメモリとの2層管理を担います。

接続文字列と設定ファイルを用意する

データベースの接続文字列はシークレットとして管理します。ソースコードへの直書きはしないようにします。

dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:PostgresCache" \
  "Host=your-server.postgres.database.azure.com;Port=5432;Username=your-user;Password=your-password;Database=your-database;Pooling=true;"

appsettings.json にはキャッシュの動作パラメーターを記載します。

{
  "PostgresCache": {
    "SchemaName": "public",
    "TableName": "cache",
    "CreateIfNotExists": true,
    "ExpiredItemsDeletionInterval": "00:30:00",
    "DefaultSlidingExpiration": "00:20:00"
  }
}

CreateIfNotExiststrue にしておくと、キャッシュ用テーブルが存在しない場合に自動で作成されます。既存のPostgresデータベースに追加するだけで済むため、Redisのような別サービスを新たに用意する必要はありません。

DI登録:AddHybridCacheの1行が核心

Program.cs でPostgresキャッシュとHybridCacheをDIコンテナに登録します。

builder.ConfigureServices((hostingContext, services) => {
    services.AddDistributedPostgresCache(options => {
        options.ConnectionString = hostingContext.Configuration
            .GetConnectionString("PostgresCache");
        options.SchemaName = hostingContext.Configuration
            .GetValue<string>("PostgresCache:SchemaName", "public");
        options.TableName = hostingContext.Configuration
            .GetValue<string>("PostgresCache:TableName", "cache");
        options.CreateIfNotExists = hostingContext.Configuration
            .GetValue<bool>("PostgresCache:CreateIfNotExists", true);
    });

    services.AddHybridCache();
});

services.AddHybridCache() の1行を追加するだけで、インメモリとPostgresの両方を管理する2層キャッシュが有効になります。以降の実装では HybridCache クラスだけを意識すればよく、個別のキャッシュ層を直接操作する必要はありません。

GetOrCreateAsyncでキャッシュを使う

サービスクラスのコンストラクタで HybridCache をDIして使います。

public class DataService : BackgroundService {
    private readonly HybridCache _cache;

    public DataService(HybridCache cache) {
        _cache = cache;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
        var result = await _cache.GetOrCreateAsync(
            "weather:forecast:next-day",
            async cancel => await GetDataFromSourceAsync(cancel),
            cancellationToken: stoppingToken,
            options: new HybridCacheEntryOptions {
                LocalCacheExpiration = TimeSpan.FromSeconds(3),
                Expiration = TimeSpan.FromSeconds(6),
            }
        );
    }
}

GetOrCreateAsync は3段階で動作します。キャッシュキーに対応するエントリがインメモリにあればそれを返します。なければPostgresを確認し、ヒットすればインメモリにも書き戻して返します。両方にない場合だけファクトリ関数(ここでは GetDataFromSourceAsync)を呼び出し、結果を両キャッシュに書き込んでから返します。

LocalCacheExpiration がインメモリキャッシュの有効期限、Expiration がPostgres側の有効期限です。インメモリが先に失効した後も、Postgresのエントリが残っている間は高速応答を維持できます。

実測パフォーマンス

Microsoftの公式デモコードで計測した結果は次のとおりです(参考)。

取得方法 応答時間
キャッシュなし(ソース直接取得) 約 2,061 ms
インメモリキャッシュヒット 0.3〜0.6 ms
Postgresキャッシュヒット 約 38 ms

インメモリヒット時はソース取得の約3,000倍、Postgresヒット時でも約50倍の高速化になります。インメモリキャッシュが失効した後もPostgresから38msで返ってくるため、短時間の失効期間でもユーザーに体感できるほどのレイテンシ劣化は起きません。

セキュリティと整合性の注意点

接続文字列は dotnet user-secrets や環境変数で管理し、本番環境ではAzure Key Vaultの使用が推奨されています。

インメモリキャッシュはプロセス単位で保持されるため、スケールアウト構成では複数インスタンス間でキャッシュ内容が一致しない期間が生じます。整合性が重要なデータは LocalCacheExpiration を短く設定するか、更新時に明示的にキャッシュを無効化する設計を検討してください。

GitHubにはWeb API + Aspire + Swagger構成や、Entra認証でのPostgres接続サンプルも公開されています。

https://github.com/Azure/Microsoft.Extensions.Caching.Postgres

HybridCacheは速さと耐障害性を同時に実現する設計パターンです。既存のPostgresインフラを使い回せるため、Redisを別途用意するコストをかけずに分散キャッシュの恩恵を受けられます。