Redis
 Computer >> コンピューター >  >> プログラミング >> Redis

.NET 9でRedisを使ったハイブリッドキャッシュの構築:インメモリキャッシュと分散キャッシュの統合

ハイブリッドキャッシュとは

ハイブリッドキャッシュは、インメモリキャッシュと外部キャッシュソース(分散キャッシュ)を統合的に扱うためのライブラリであり、いわゆる「マルチティア(多層)キャッシュ」を実現します。従来の IDistributedCacheIMemoryCache の置き換えとして機能し、.NET におけるキャッシュ利用を大幅にシンプルにすることを目的としています。

従来の分散キャッシュの実装では、データを適切にキャッシュしたり取得したりするために、追加のコードを書く必要がありました。このパッケージは、.NET でのキャッシュ処理を簡素化し、堅牢なキャッシュソリューションを構築するための優れた追加機能です。本記事では、Microsoft.Extensions.Caching.Hybrid NuGet パッケージを使用してハイブリッドキャッシュを実装する方法と、その全機能について解説します。

パッケージの主な特徴

  • 拡張性のあるコード: インメモリキャッシュ向けに書いたコードは、そのまま変更なしで Redis や SQL Server などの外部キャッシュサーバーとの統合に再利用できます。
  • 同時実行(コンカレンシー)管理: ハイブリッドキャッシュを利用するには、HybridCache クラスを依存性注入サービスとして使用します。このクラスは、同じキーに対して複数の同時リクエストが発生しても、実際のデータ取得は1つのインスタンスだけが行うことを保証します。他の同時リクエストは、この取得処理の完了を待機します。
  • マルチソースキャッシュ(プライマリ/セカンダリ方式): アプリケーションで複数のキャッシュソースが設定されている場合、データはすべての場所に保存されます。

データを取得する際は、まずプライマリソース(ローカルのインメモリキャッシュ)を確認します。そこにデータが存在しない場合は、セカンダリソース(Redis などの外部キャッシュ)へリクエストがフォールバックされます。以下の図のように、青色がシナリオ1、赤色がシナリオ2に相当します。

セットアップ手順

まず、Microsoft.Extensions.Caching.Hybrid パッケージをプロジェクトにインストールします。次に、Program.cs に以下のコードを記述して、ハイブリッドキャッシュを登録します。

builder.Services.AddHybridCache();

ハイブリッドキャッシュを詳細に構成したい場合は、AddHybridCache の中にセットアップアクションとして設定を記述します。以下はその例です。

builder.Services.AddHybridCache(options =>
{
    options.ReportTagMetrics = true;
    options.DefaultEntryOptions = new Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromSeconds(30),
        LocalCacheExpiration = TimeSpan.FromSeconds(30)
    };
});

インメモリキャッシュの実装

上記の依存サービスの設定だけで、インメモリキャッシュを利用できます。以下は、API コントローラーでアプリケーションメモリにキーと値を設定・取得するサンプルコードです。

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Hybrid;

namespace HybridCacheDemoApplication.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CacheController : ControllerBase
    {
        private readonly HybridCache _cache;
        const string cacheKey = "ping";

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

        [HttpGet(Name = "GetPingKey")]
        public async Task<IActionResult> Get()
        {
            var keydata = await _cache.GetOrCreateAsync<string>(
                cacheKey, // キャッシュエントリの一意なキー
                async cancel => await Task.FromResult("pong from get")
            );
            return Ok(new { message = $"{cacheKey} data: {keydata}" });
        }

        [HttpGet("set", Name = "SetPingKey")]
        public async Task<IActionResult> Set()
        {
            await _cache.SetAsync<string>(cacheKey, "ping");
            return Ok(new { message = $"{cacheKey} is set" });
        }
    }
}

このコードがどのように動作するか、以下の表で順番に確認してみましょう。

キャッシュ実行デモ表

順番エンドポイント出力結果説明
1 https://localhost:5154/cache/
{ "message": "ping data: pong from get" }
値が未設定の場合、このエンドポイントはキャッシュ値を「pong from get」として設定するため、「pong from get」が出力されます。
2 https://localhost:5154/cache/set
{ "message": "ping is set" }
このエンドポイントはキャッシュに値を明示的に設定します。
3 https://localhost:5154/cache/
{ "message": "ping data: ping" }
手順1と同じエンドポイントですが、今回はすでにキャッシュに値が存在しているため「ping」が返されます。

キャッシュキーの削除

キャッシュキーを削除するには、以下のような実装を使用します。

[HttpGet("del", Name = "DeletePingKey")]
public async Task<IActionResult> RemoveKey()
{
    await _cache.RemoveAsync(cacheKey); // データを削除
    return Ok(new { message = $"{cacheKey} removed" });
}

Redis サーバーによる分散キャッシュの実装

分散キャッシュを実装するには、追加の依存サービス構成が必要です。この例では、外部キャッシュソースとして Redis キャッシュを使用します。

まず、Microsoft.Extensions.Caching.StackExchangeRedis パッケージをインストールします。インストール後、Redis を有効化するために以下のコードを追加します。接続文字列は {ホスト名}:{ポート番号},password={パスワード} の形式になります。

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration =
        builder.Configuration.GetConnectionString("RedisConnectionString");
});

これで https://localhost:5154/cache/set を呼び出すと、データはシームレスに Redis キャッシュへ保存されます。Redis 側では以下のように表示されます。

.NET 9でRedisを使ったハイブリッドキャッシュの構築:インメモリキャッシュと分散キャッシュの統合

分散キャッシュサーバーである Redis の依存関係を追加しただけで、既存のコードがそのまま透過的に動作することが分かります。これこそがハイブリッドキャッシュの大きな魅力です。

シリアライズ(Serialization)

外部キャッシュサーバーへデータを送信する際、このパッケージは byte[]string、そして System.Text.Json を使用します。AddSerializer() メソッドや AddSerializerFactory() メソッドを AddHybridCache と組み合わせて使うことで、シリアライザーをカスタマイズすることも可能です。より最適化されたシリアライザーを採用することで、アプリケーションのパフォーマンス向上が期待できます。

さらなるカスタマイズ

依存関係の構成時にフラグを設定することで、キャッシュサービスの挙動をさらに細かく制御できます。以下は、キャッシュ時の圧縮を無効化するサンプルフラグです。

注意: 圧縮が必要ない・使用しないケースでは有効ですが、これは必ずしもベストプラクティスではなく、シナリオによって判断してください。

builder.Services.AddHybridCache(options =>
{
    options.ReportTagMetrics = true;
    options.DefaultEntryOptions = new Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions
    {
        Flags = Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryFlags.DisableCompression
    };
});

実験的機能:タグ(Tags)— 相互に関連するキーの操作

場合によっては、多数の相互に関連するキーを扱うことがあります。例: EC サイトのアプリケーションでは、特定ユーザーのカートアイテムに対応する多くのフィールドをキャッシュしているかもしれません。こうしたケースでは「タグ」を作成すると便利です。

以下のコードでは、相互に関連するキーを扱うために「testdata」という名前のタグを作成しています。異なるグルーピング要件に応じて、複数のタグを追加することも可能です。コードはほぼ自己説明的ですが、不明点があればコメントでお知らせください。

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Hybrid;

namespace HybridCacheDemoApplication.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CacheController : ControllerBase
    {
        private readonly HybridCache _cache;
        const string cacheKey = "ping";

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

        [HttpGet(Name = "GetPingKey")]
        public async Task<IActionResult> Get()
        {
            var keydata = await _cache.GetOrCreateAsync<string>(
                cacheKey, // キャッシュエントリの一意なキー
                async cancel => await Task.FromResult("pong from get"),
                tags: ["testdata"]
            );
            return Ok(new { message = $"{cacheKey} data: {keydata}" });
        }

        [HttpGet("set", Name = "SetPingKey")]
        public async Task<IActionResult> Set()
        {
            await _cache.SetAsync<string>(cacheKey,
                "ping",
                tags: ["testdata"]
            );
            return Ok(new { message = $"{cacheKey} is set" });
        }

        [HttpGet("del", Name = "DeletePingKey")]
        public async Task<IActionResult> RemoveKey()
        {
            await _cache.RemoveAsync(cacheKey);
            return Ok(new { message = $"{cacheKey} removed" });
        }

        [HttpGet("delbytag", Name = "DeleteByTestTags")]
        public async Task<IActionResult> RemoveKeyByTags()
        {
            await _cache.RemoveByTagAsync(["testdata"]);
            return Ok(new { message = $"{cacheKey} removed" });
        }
    }
}

まとめ

.NET 9 のハイブリッドキャッシュは、インメモリキャッシュと Redis などの分散キャッシュを統合し、同時実行制御やタグによる一括削除など、強力な機能を最小限のコードで提供してくれる非常に便利な仕組みです。最後までお読みいただきありがとうございました。皆さんの開発でも、ぜひこのハイブリッドキャッシュのメリットを活かしてみてください。

  1. RediSearchを実践しよう:Azure Cache for RedisとGoで作るリアルタイムツイート検索アプリ

    Redisは、シンプルなStringから強力な抽象化であるRedis Streamsまで、多彩なデータ構造を備えています。ネイティブなデータ型だけでも多くの用途に対応できますが、特定のユースケースでは追加の工夫が必要になることがあります。その代表例がセカンダリインデックス(二次インデックス)の活用です。キーによる検索・参照にとどまらず、より高度なクエリ機能を実現したい場合には、セカンダリインデックスが求められます。Sorted SetやListなどを駆使して実現することも可能ですが、いくつかのトレードオフを考慮する必要があります。そこで登場するのがRediSearchです。Redisモジュール

  2. React Server Componentsで実装するリアルタイム閲覧数カウンターの作り方

    このチュートリアルでは、React Server Components(RSC)の仕組みを理解した上で、その知識を活かしてアプリにシンプルな閲覧数カウンターを実装していきます。すぐに実装に取り掛かりたい方は、「プロジェクトのセットアップ」までスキップしてください。 React Server Componentsを理解する React Server Componentsがどのように動作するのかを理解するために、まずクライアントサイドレンダリング(CSR)とサーバーサイドレンダリング(SSR)について簡単におさらいしましょう。 クライアントサイドレンダリング(CSR) CSRでは、レンダリング処理