noxi雑記

.NET、Angularまわりの小ネタブログ

ASP.NET Coreで分散キャッシュを使用する

この記事は前回の記事の続きです。前回はASP.NET Coreのインメモリキャッシュを紹介しましたが、今回はASP.NET Core組み込みの分散キャッシュ( IDistributedCache )について紹介します。

noxi515.hateblo.jp

前提条件

この記事は以下のバージョンで検証しています。

分散キャッシュ(Distributed Cache)とは

分散キャッシュは単一の場所で使用される従来のキャッシュ概念の拡張。 分散キャッシュは複数のサーバーにまたがり容量やトランザクション能力を向上させることができる。 これは主にデータベースやWebセッションデータに存在するアプリケーションデータを格納するのに使用される。

Wikipediaから翻訳)

近年ではアプリケーションサーバーに限らずサーバーは1台だけでは無く複数台に機能や処理が分散されており、それらから共通で使用されるアプリケーションデータやキャッシュはそれぞれのサーバー上のメモリだけに置いておくことはできません。Webアプリをスケールアウトするには必須の機能になってきます。

ASP.NET Coreでは IDistributedCache インターフェースを通じて、分散キャッシュを使用します。標準でインメモリ、SQLServer、Redisの3実装が提供されています。なお分散キャッシュは複数を組み合わせることはできず、いずれか1つを選択して使用することになります。

インメモリ分散キャッシュ

まず、分散キャッシュ実装の1つであるインメモリ分散キャッシュ( MemoryDistributedCache )を紹介します。これは IDistributedCache の実装としてインメモリキャッシュ( MemoryCache )を使用するもので、分散キャッシュと言いつつ全く分散することができません。開発時に一時的に有効化するか、複数台にスケールしないことが確実な場合のみ使用しましょう。

インメモリ分散キャッシュを使用する

インメモリ分散キャッシュを有効かするには Startup.csConfigureServices メソッド内で次の1行を追加します。実際の使用方法はスキップします。

public void ConfigureServices(IServiceCollection services)
{
    // インメモリ分散キャッシュ有効化
    services.AddDistributedMemoryCache();
}

SQLServer分散キャッシュ

インメモリ分散キャッシュの次はSQLServer分散キャッシュ( SqlServerCache )です。もしアプリのデータベースとしてSQLServerを使用している場合、そのデータベースとは異なるSQLServerを指定することでパフォーマンスをより向上させることも可能です。

キャッシュ用のテーブル作成

SQLServer分散キャッシュを使用するにはアプリに設定するよりも先に、保存先のデータベースを用意して手動でテーブルを追加します。追加にはCLIコマンドが用意されています。 dotnet sql-cache create [connectionString] [schemaName] [tableName] を実行します。実行に失敗する場合は.NET SDK2.1.300 以上がインストールされているかを確認しましょう。次のコマンドはAzure上に作成したSQL Databaseに AppCache という名前でキャッシュテーブルを作るコマンドです。

dotnet sql-cache create "Server={server-name}.database.windows.net,1433;Initial Catalog={database-name};Persist Security Info=False;User ID={user-id};Password={password};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" dbo AppCache

コマンド実行に成功したら Table and index were created successfully. と表示されます。

これで生成されるテーブルはこんな感じです。

f:id:noxi515:20180902181618p:plain

SQLServer分散キャッシュをサービス登録する

テーブルが作成できたら次に Startup.csConfigureServices メソッドにSQLServer分散キャッシュを使用するための設定を追加します。

public void ConfigureServices(IServiceCollection services)
{
    // SQLServer分散キャッシュの有効化
    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = @"Server={server-name}.database.windows.net,1433;Initial Catalog={database-name};.....";
        options.SchemaName = "dbo";
        options.TableName = "AppCache";
    });
}

Startup内にベタ書いてしまうと開発時とリリース時で接続先が変更出来ないので、アプリケーションの設定に接続文字列を持たせるには下のように変更します。

public class Startup
{
    private IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // SQLServer分散キャッシュの有効化
        services.AddDistributedSqlServerCache(options =>
        {
            // アプリ設定から接続文字列を取得
            options.ConnectionString = Configuration.GetConnectionString("AppCache");
            options.SchemaName = "dbo";
            options.TableName = "AppCache";
        });

        ...
    }
}

そして appsettings.json に次のセクションを追加します。

{
    "ConnectionStrings": {
        "AppCache": "Server={server-name}.database.windows.net,1433;Initial Catalog={database-name};.....";
    }
}

SQLServer分散キャッシュを使用する

分散キャッシュを使用するには IDistributedCache をDIコンテナから受け取ります。MVCコントローラーではアクションメソッドの引数で、サービス等ではコンストラクタに記述します。

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;

namespace CacheWebApp1.Controllers
{
    [Route("sample")]
    public class SampleController : Controller
    {
        public IDistributedCache Cache { get; }

        public SampleController(IDistributedCache cache)
        {
            Cache = cache; // コンストラクタで受け取る場合
        }

        [HttpGet("")]
        public async Task<ActionResult<IEnumerable<string>>> GetListAsync([FromServices] IDistributedCache cache)  // MVCコントローラーのアクションメソッドの引数で受け取る場合
        {
            return Ok(new List<string> { "aaa", "bbb", "ccc" });
        }
    }
}

IDistributedCache はインメモリキャッシュとは異なり、やりとりは全て byte[] となります。キャッシュを登録する Set | SetAsync 、取得する Get | GetAsync 、削除する Remove | RemoveAsync 、期限を更新する Refresh | RefreshAsync のメソッドを利用して操作します。 Refresh を使用するのは主にユーザーセッションみたいなアクセスされる毎に期限を延ばす必要があるものに限られ、データキャッシュに対して使用することはないでしょう。

実際に分散キャッシュを利用してみます。キーに対応するものが無ければ登録し、あれば返すというものをサクッと実装してみましょう。

[HttpGet("")]
public async Task<ActionResult<IEnumerable<string>>> GetListAsync(
    [FromQuery, Required] string key,
    [FromServices] IDistributedCache cache)
{
    // キャッシュからデータを取得する。キャッシュに存在する場合JSONに変換して返す。
    var cached = await cache.GetStringAsync(key);
    if (cached != null)
    {
        return Ok(JsonConvert.DeserializeObject<List<string>>(cached));
    }

    // キャッシュに存在しない場合
    var result = new List<string> {"aaa", "bbb", "ccc"};
    cached = JsonConvert.SerializeObject(result);
    await cache.SetStringAsync(key, cached, new DistributedCacheEntryOptions {AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)});

    return Ok(result);
}

IDistributedCache への出し入れは基本的には byte[] ですが、文字列に限れば、文字列のまま出し入れするための拡張メソッド SetString | SetStringAsync GetString | GetStringAsync が用意されています。これらを使用すると、渡したstringをUTF-8シリアライズ・デシリアライズされて保存または取得されるため、ちょっとだけ楽ができます。

Redis分散キャッシュ

最後にRedis分散キャッシュ( RedisCache )です。Redisはオープンソースで開発されているKey-Value型のインメモリストア、いわゆるNoSQLの一種です。データが全てメモリ上に展開されるためアクセスが非常に高速なのが特徴です。

Redis分散キャッシュを使用する

Redis分散キャッシュを使用するには例によって Startup.csConfigureServices メソッドに追加するのですが、Redis分散キャッシュはASP.NET Coreの標準セットには含まれていません。まずはNuGetから Microsoft.Extensions.Caching.Redis を追加します。コマンドラインから追加する場合は追加するプロジェクトがカレントディレクトリになった状態で以下のコマンドを実行します。

dotnet add package Microsoft.Extensions.Caching.Redis

そして ConfigureServices に追加します。以下はAzureのRedis Cacheを使用した時の設定サンプルです。実際の使用方法はSQLServerの時と同じためスキップします。

public class Startup
{
    private IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // Redisキャッシュの有効化
        services.AddDistributedRedisCache(options =>
        {
            options.Configuration = Configuration.GetConnectionString("AppCache");
        });

        ...
    }
}
{
  "ConnectionStrings": {
    "AppCache": "{server-name}.cache.windows.net:6380,password={password},ssl=True,abortConnect=False"
  }
}