EasyCaching:シンプルで効率的な.NETキャッシュパッケージ

EasyCaching:シンプルで効率的な.NETキャッシュパッケージ

EasyCachingという名前は、その役割を大きく説明しています。easyとcachingを組み合わせることで、最終的な目的はキャッシュ操作をより便利にすることです。

最終更新 2023/11/05 23:14
Catcher Wong
読了目安 9 分
カテゴリ
.NET
タグ
.NET C#

はじめに

2017年11月11日にGitHubでEasyCachingリポジトリを作成してから、約1年半が経ちました。基本は仕事終わりや休暇中にこのプロジェクトを改良してきました。

EasyCachingには現在、英語のドキュメントしかなく、Read the Docsにホスティングされていますが、当初選んだMkDocsはまだ多言語対応していないため、日本語のドキュメントは対応後に計画する予定です。

以前、グループ内で「EasyCachingの紹介が見つからない」という声があったため、このブログ記事を書くことにしました。

まずは、EasyCachingについて簡単に紹介します。

EasyCachingとは

img

EasyCachingという名前は、その役割をよく表しています。「easy(簡単)」と「caching(キャッシュ)」を組み合わせており、最終的な目的はキャッシュ操作をより便利にすることです。

その発展には、以下の重要なマイルストーンがあります。

  1. 2018年3月:茶叔の助力によりNCCに参加
  2. 2019年1月:鎮汐から多くの改善提案
  3. 2019年3月:NopCommerceがEasyCachingを導入(このコミット履歴を参照)
  4. 2019年4月:awesome-dotnet-coreに掲載(自分でPRを送りました。少し自己愛がありますね…)

EasyCachingが登場する前は、多くの人がCacheManagerに慣れ親しんでいました。両者の位置づけや機能は似ているため、比較されることもよくありました。

比較をしやすくするために、ここではEasyCachingの現在の機能を重点的に紹介します。

EasyCachingの主な機能

EasyCachingは主に以下の機能を提供します。

  1. 統一された抽象キャッシュインターフェース
  2. 複数の一般的なキャッシュプロバイダー(InMemory、Redis、Memcached、SQLite)
  3. 分散キャッシュのデータシリアライズに複数の選択肢
  4. ハイブリッドキャッシュ(2層キャッシュ)
  5. キャッシュのAOP操作(able、put、evict)
  6. マルチインスタンスのサポート
  7. Diagnosticsのサポート
  8. Redis専用プロバイダー

これら8つの他にも小規模な機能がありますが、ここでは説明しません。

以下で、上記8つの機能をそれぞれ紹介します。

統一された抽象キャッシュインターフェース

キャッシュ自体もデータソースの一種であり、一連のCRUD操作を含むため、統一された抽象インターフェースが用意されています。インターフェース指向プログラミングにより、EasyCachingはシンプルな実装を提供していますが、必要に応じて独自のプロバイダーを実装することも可能です。

キャッシュ操作としては、以下のものが提供されています。基本的に同期・非同期の両方の操作があります。

  • TrySet/TrySetAsync
  • Set/SetAsync
  • SetAll/SetAllAsync
  • Get/GetAsync(データ取得関数あり)
  • Get/GetAsync(データ取得関数なし)
  • GetByPrefix/GetByPrefixAsync
  • GetAll/GetAllAsync
  • Remove/RemoveAsync
  • RemoveByPrefix/RemoveByPrefixAsync
  • RemoveAll/RemoveAllAsync
  • Flush/FlushAsync
  • GetCount
  • GetExpiration/GetExpirationAsync
  • Refresh/RefreshAsync(これは今後非推奨になり、代わりにsetを使用します)

名前から機能は推測できると思いますので、ここでは詳細は省略します。

複数の一般的なキャッシュプロバイダー

プロバイダーは2種類に分類されます。ローカルキャッシュと分散キャッシュです。

現在の実装は以下の5つです。

  • ローカルキャッシュ:InMemory、SQLite
  • 分散キャッシュ:StackExchange.Redis、csredis、EnyimMemcachedCore

使用方法は非常に簡単です。以下にInMemoryプロバイダーを例に説明します。

まず、NuGetで対応するパッケージをインストールします。

dotnet add package EasyCaching.InMemory

次に、設定を追加します。

public void ConfigureServices(IServiceCollection services)
{
    // EasyCachingの追加
    services.AddEasyCaching(option =>
    {
        // InMemoryの最も簡単な設定
        option.UseInMemory("default");

        //// InMemoryのカスタム設定
        //option.UseInMemory(options =>
        //{
        //     // DBConfigは各プロバイダー固有の設定
        //     options.DBConfig = new InMemoryCachingOptions
        //     {
        //         // InMemoryの期限切れスキャン頻度(デフォルトは60秒)
        //         ExpirationScanFrequency = 60,
        //         // InMemoryの最大キャッシュ数(デフォルトは10000)
        //         SizeLimit = 100
        //     };
        //     // キャッシュの一斉失効を防ぐため、各キーの有効期限にランダムな秒数を追加(デフォルトは120秒)
        //     options.MaxRdSecond = 120;
        //     // ログ出力を有効にするか(デフォルトはfalse)
        //     options.EnableLogging = false;
        //     // ミューテックスの有効期間(デフォルトは5000ミリ秒)
        //     options.LockMs = 5000;
        //     // ミューテックス取得失敗時のスリープ時間(デフォルトは300ミリ秒)
        //     options.SleepMs = 300;
        // }, "m2");

        //// 設定ファイルから読み込み
        //option.UseInMemory(Configuration, "m3");
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // MemcachedやSQLiteを使用する場合は、初期化のために以下が必要
    app.UseEasyCaching();
}

設定ファイルの例

"easycaching": {
    "inmemory": {
        "MaxRdSecond": 120,
        "EnableLogging": false,
        "LockMs": 5000,
        "SleepMs": 300,
        "DBConfig":{
            "SizeLimit": 10000,
            "ExpirationScanFrequency": 60
        }
    }
}

設定について一点注意があります。それはMaxRdSecondの値です。これで老猫子さんを一度ハメてしまったので、特に説明します。この値は、大量のキャッシュキーが同時に失効するのを防ぐために、各キーの元の有効期限にランダムな秒数を追加します。これにより有効期限を分散させます。この機能が必要ない場合は、0に設定してください。

最後に使用します。

[Route("api/[controller]")]
public class ValuesController : Controller
{
    // プロバイダーが1つだけの場合、IEasyCachingProviderを直接使用可能
    private readonly IEasyCachingProvider _provider;

    public ValuesController(IEasyCachingProvider provider)
    {
        this._provider = provider;
    }

    // GET api/values/sync
    [HttpGet]
    [Route("sync")]
    public string Get()
    {
        var res1 = _provider.Get("demo", () => "456", TimeSpan.FromMinutes(1));
        var res2 = _provider.Get<string>("demo");

        _provider.Set("demo", "123", TimeSpan.FromMinutes(1));

        _provider.Remove("demo");

        // others..
        return "sync";
    }

    // GET api/values/async
    [HttpGet]
    [Route("async")]
    public async Task<string> GetAsync(string str)
    {
        var res1 = await _provider.GetAsync("demo", async () => await Task.FromResult("456"), TimeSpan.FromMinutes(1));
        var res2 = await _provider.GetAsync<string>("demo");

        await _provider.SetAsync("demo", "123", TimeSpan.FromMinutes(1));

        await _provider.RemoveAsync("demo");

        // others..
        return "async";
    }
}

もう一つ注意点として、データ取得関数を伴うGetメソッドを使用する場合、キャッシュミス時にデータベースへのクエリ前にロック処理が行われます。これは、同じキーに対して同時にn回データベースに問い合わせるのを防ぐためです。このロックの有効期間とスリープ時間は、設定のLockMsSleepMsで決まります。

分散キャッシュのシリアライズ選択

分散キャッシュを操作する際、シリアライズの問題は避けられません。

現在は主にRedisとMemcachedを対象としています。シリアライズに関しては、デフォルトでBinaryFormatterを使用する実装が用意されています。これはサードパーティライブラリに依存しないため、他に指定がない場合はこれが使用されます。

デフォルト実装の他に、Newtonsoft.Json、MessagePack、Protobufの3つの選択肢が用意されています。以下に、RedisプロバイダーでMessagePackを使用する例を示します。

services.AddEasyCaching(option=>
{
    // redisの使用
    option.UseRedis(config =>
    {
        config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
    }, "redis1")
    // BinaryFormatterをMessagePackに置き換え
    .WithMessagePack()
    //// BinaryFormatterをNewtonsoft.Jsonに置き換え
    //.WithJson()
    //// BinaryFormatterをProtobufに置き換え
    //.WithProtobuf()
    ;
});

ただし、現在これらのシリアライザーはプロバイダー単位で指定できません。つまり、「このプロバイダーはmessagepack、あのプロバイダーはjson」という使い方はできず、シリアライザーは1種類のみです。この点は今後改善が必要かもしれません。

マルチインスタンスのサポート

「マルチインスタンス」とは、同じプロジェクト内で複数のプロバイダーを同時に使用することを指します。同じ種類のプロバイダーを複数、あるいは異なる種類のプロバイダーを混在させることができます。

具体的な例を挙げると、より明確になるでしょう。

商品のキャッシュはRedisクラスタ1、ユーザー情報はRedisクラスタ2、商品レビューはMemcachedクラスタ、簡単な設定情報はアプリケーションサーバのローカルキャッシュに保存されているとします。

このような場合、単にIEasyCachingProviderを使ってこれらすべての異なるキャッシュを直接操作することはできません。

このような状況で複数のキャッシュを同時に操作するには、IEasyCachingProviderFactoryを使用して、使用するプロバイダーを指定します。

このファクトリは、プロバイダーの名前を使用してプロバイダーを取得します。

例を見てみましょう。

まず、異なる名前の2つのInMemoryキャッシュを追加します。

services.AddEasyCaching(option =>
{
    // 現在のプロバイダー名をm1に指定
    option.UseInMemory("m1");

    // 現在のプロバイダー名をm2に指定
    config.UseInMemory(options =>
    {
        options.DBConfig = new InMemoryCachingOptions
        {
            SizeLimit = 100
        };
    }, "m2");
});

使用時:

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly IEasyCachingProviderFactory _factory;

    public ValuesController(IEasyCachingProviderFactory factory)
    {
        this._factory = factory;
    }

    // GET api/values
    [HttpGet]
    [Route("")]
    public string Get()
    {
        // 名前がm1のプロバイダーを取得
        var provider_1 = _factory.GetCachingProvider("m1");
        // 名前がm2のプロバイダーを取得
        var provider_2 = _factory.GetCachingProvider("m2");

        // provider_1.xxx
        // provider_2.xxx

        return $"multi instances";
    }
}

この例では、provider_1とprovider_2は互いに干渉しません。別々のプロバイダーだからです。

直感的には、リージョン(領域)のような概念に似ていますが、厳密にはリージョンではありません。

キャッシュのAOP操作

AOPと言えば、多くの人は最初にログ操作を思い浮かべるでしょう。パラメータを記録したり、結果を記録したり。

しかし、キャッシュ操作においても同様に簡略化の効果があります。

通常、キャッシュ操作は次のようになります。

public async Task<Product> GetProductAsync(int id)
{
    string cacheKey = $"product:{id}";

    var val = await _cache.GetAsync<Product>(cacheKey);

    if(val.HasValue)
        return val.Value;

    var product = await _db.GetProductAsync(id);

    if(product != null)
        _cache.Set<Product>(cacheKey, product, expiration);

    return val;
}

キャッシュを使用する箇所が多くなると、煩雑に感じるでしょう。

AOPを使用して、この操作を簡略化できます。

public interface IProductService
{
    [EasyCachingAble(Expiration = 10)]
    Task<Product> GetProductAsync(int id);
}

public class ProductService : IProductService
{
    public Task<Product> GetProductAsync(int id)
    {
        return Task.FromResult(new Product { ... });
    }
}

Attributeをインターフェース定義に追加するだけで済みます。

ただし、Attributeを追加するだけでは機能しません。設定も必要です。以下にEasyCaching.Interceptor.AspectCoreを例に、対応する設定を追加します。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IProductService, ProductService>();

    services.AddEasyCaching(options =>
    {
        options.UseInMemory("m1");
    });

    return services.ConfigureAspectCoreInterceptor(options =>
    {
        // 使用するプロバイダーをここで指定可能
        // またはAttributeで指定
        options.CacheProviderName = "m1";
    });
}

これで、メソッド呼び出し時にキャッシュがあればそれを優先し、なければメソッドを実行するようになります。

次に、3つのAttributeのパラメータについて説明します。

まずは共通の設定です。

設定名 説明
CacheKeyPrefix 生成されるキャッシュキーのプレフィックスを指定。通常は変更・削除のキャッシュで使用
CacheProviderName 特定のプロバイダー名を指定可能
IsHightAvailability キャッシュ関連操作で例外が発生した場合でも、ビジネスメソッドの実行を続行するか

EasyCachingAble と EasyCachingPut には同じ名前の設定があります。

設定名 説明
Expiration キーの有効期限(秒単位)

EasyCachingEvict には2つの特別な設定があります。

設定名 説明
IsAll CacheKeyPrefix と組み合わせて使用。このプレフィックスを持つすべてのキーを削除
IsBefore ビジネスメソッド実行前にキャッシュを削除するか、実行後か

Diagnosticsのサポート

サードパーティのAPMに容易に接続できるよう、Diagnosticsをサポートしています。トレーシングの実装に便利です。

以下は、当社でJaegerに接続した例です。

img

ハイブリッドキャッシュ(2層キャッシュ)

ハイブリッドキャッシュ、マルチレベルキャッシュは、キャッシュの世界では重要な要素です。

最も頭を悩ませる問題は、異なるレベルのキャッシュをどのようにほぼリアルタイムで同期するかです。

EasyCachingにおけるハイブリッドキャッシュの実装ロジックは、以下の図のようになります。

img

あるサーバーのローカルキャッシュが変更されると、キャッシュバスを介して他のサーバーに通知し、該当するローカルキャッシュを削除します。

簡単な使用例を見てみましょう。

まず、NuGetパッケージを追加します。

dotnet add package EasyCaching.InMemory
dotnet add package EasyCaching.Redis
dotnet add package EasyCaching.HybridCache
dotnet add package EasyCaching.Bus.Redis

次に、設定を追加します。

services.AddEasyCaching(option =>
{
    // 2つの基本プロバイダーを追加
    option.UseInMemory("m1");
    option.UseRedis(config =>
    {
        config.DBConfig.Endpoints.Add(new Core.Configurations.ServerEndPoint("127.0.0.1", 6379));
        config.DBConfig.Database = 5;
    }, "myredis");

    // ハイブリッドの使用
    option.UseHybrid(config =>
    {
        config.EnableLogging = false;
        // キャッシュバスの購読トピック
        config.TopicName = "test_topic";
        // ローカルキャッシュの名前
        config.LocalCacheProviderName = "m1";
        // 分散キャッシュの名前
        config.DistributedCacheProviderName = "myredis";
    });

    // キャッシュバスとしてRedisを使用
    option.WithRedisBus(config =>
    {
        config.Endpoints.Add(new Core.Configurations.ServerEndPoint("127.0.0.1", 6379));
        config.Database = 6;
    });
});

最後に使用します。

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly IHybridCachingProvider _provider;

    public ValuesController(IHybridCachingProvider provider)
    {
        this._provider = provider;
    }

    // GET api/values
    [HttpGet]
    [Route("")]
    public string Get()
    {
        _provider.Set(cacheKey, "val", TimeSpan.FromSeconds(30));

        return $"hybrid";
    }
}

不明な点があれば、完全な例EasyCachingHybridDemoを参照してください。

Redis専用プロバイダー

ご存知のように、Redisは複数のデータ構造やアトミックなインクリメント/デクリメント操作などをサポートしています。これらの操作をサポートするために、EasyCachingは専用のインターフェースIRedisCachingProviderを提供しています。

このインターフェースは、現在一般的な操作の60~70%程度をサポートしています。使用頻度の低いものは含まれていません。

このインターフェースもマルチインスタンスをサポートしており、IEasyCachingProviderFactoryを使用して異なるプロバイダーインスタンスを取得できます。

注入時に特別な操作は必要なく、Redisの追加と同じです。異なる点は、使用時にIEasyCachingProviderではなくIRedisCachingProviderを使用することです。

以下は簡単な使用例です。

[Route("api/mredis")]
public class MultiRedisController : Controller
{
    private readonly IRedisCachingProvider _redis1;
    private readonly IRedisCachingProvider _redis2;

    public MultiRedisController(IEasyCachingProviderFactory factory)
    {
        this._redis1 = factory.GetRedisProvider("redis1");
        this._redis2 = factory.GetRedisProvider("redis2");
    }

    // GET api/mredis
    [HttpGet]
    public string Get()
    {
        _redis1.StringSet("keyredis1", "val");

        var res1 = _redis1.StringGet("keyredis1");
        var res2 = _redis2.StringGet("keyredis1");

        return $"redis1 cached value: {res1}, redis2 cached value : {res2}";
    }
}

これらの基本機能に加えて、拡張機能もあります。yrinleung氏がEasyCachingをWebApiClientやCAPなどのプロジェクトと連携させてくれました。興味があれば、このプロジェクトEasyCaching.Extensionsをご覧ください。

最後に

以上がEasyCachingが現在サポートしている機能の紹介です。使用中に問題が発生した場合は、ぜひフィードバックをお寄せください。EasyCachingをより良くするためにご協力ください。

このプロジェクトに興味があれば、GitHubでStarを付けていただくか、開発やメンテナンスに参加してください。

先日、EasyCachingを使用しているユーザーや事例を記録するためのIssueを作成しました。EasyCachingを使用していて、情報を公開しても差し支えない場合は、このIssueに返信してください。

img

img

この記事が役に立った、または何か得るものがあったと思われましたら、右下の**【おすすめ】**ボタンをクリックしてください。あなたのサポートが、私が執筆・共有を続ける最大の原動力です!

作者:Catcher Wong (黄文清)

出典:http://catcher1994.cnblogs.com/

声明: 本文の著作権は作者と博客園に帰属します。転載は歓迎しますが、作者の同意なくこの声明を削除することはできません。また、記事ページの明らかな位置に元のリンクを記載してください。違反した場合、法的責任を追及する権利を留保します。ブログに誤りを見つけたり、より良い提案やアイデアがあれば、遠慮なくご連絡ください。個人的に連絡を取りたい場合は、プライベートメッセージまたはWeChatでご連絡ください。

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2026/04/22

各OSバージョンの.NETサポート状況(250707更新)

仮想マシンとテストマシンを使用して、各OSバージョンの.NETサポート状況を確認します。OSインストール後、対応するランタイムをインストールし、Stardustエージェントを実行できることを確認します(合格条件)。

続きを読む
同じカテゴリ / 同じタグ 2026/02/07

AOTの使用経験のまとめ

プロジェクト作成当初から、新機能を追加したり新しい構文を使用したりした場合には、すぐにAOT公開テストを実施するという良い習慣を身につけるべきです。

続きを読む