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

ASP.NET CoreとAzure Cache for Redisで学ぶキャッシュアサイドパターンの実装入門

ソフトウェア開発において、アプリケーションのパフォーマンスは常に重要な関心事です。パフォーマンスを改善する手法は数多くありますが、その中でもモダンなクラウドアプリケーションで最も広く採用されているのが「キャッシュアサイド(Cache-Aside)パターン」です。本記事では、キャッシュアサイドパターンの概要と、ASP.NET CoreおよびAzure Cache for Redis(旧・Azure Redis Cache)を使った具体的な実装方法を解説します。

キャッシュアサイドパターンとは

キャッシュアサイドパターンは非常にシンプルな設計で、その目的はただひとつ――必要に応じてデータソースからキャッシュへデータを読み込むことです。これにより、キャッシュ内のデータと基盤となるデータストアとの整合性を保つことができます。

このパターンの基本的な動作は以下の通りです。

  • アプリケーションがデータを必要とするとき、まずキャッシュを確認します。
  • キャッシュにデータが存在すれば(キャッシュヒット)、そのデータを利用します。
  • 存在しなければ(キャッシュミス)、データソースからデータを取得します。

この流れを図に示すと以下のようになります。

ASP.NET CoreとAzure Cache for Redisで学ぶキャッシュアサイドパターンの実装入門

また、アプリケーションによってデータが更新された際には、対応するキャッシュ項目を無効化(削除)する必要があります。

ASP.NET CoreとAzure Cache for Redisで学ぶキャッシュアサイドパターンの実装入門

ここで重要になるのはキャッシュを無効化する順序です。必ず先にデータストアを更新してから、キャッシュの項目を削除してください。逆にキャッシュだけを先に削除すると、データストアの更新が完了する前にクライアントが古いデータを取得し、それがキャッシュに再格納される恐れがあります。その結果、データストアとキャッシュの間に不整合が発生してしまいます。

このパターンが有効なケース

  • データをオンデマンドで読み込む仕組みのため、リソースへの需要が予測できない場合に適しています。
  • リードスルー/ライトスルー機能を持たないキャッシュシステムを利用している場合。

補足:リードスルーとライトスルー

  • リードスルー(Read-Through):キャッシュがデータベースと直列に配置され、キャッシュミス発生時に自動的にデータベースからデータを読み込んでキャッシュを補充する方式です。
  • ライトスルー(Write-Through):キャッシュがデータベースと直列に配置され、すべての書き込みが必ずキャッシュを経由してメインのデータベースへ反映される方式です。

Azureリソースの作成

今回の構成では、データベースとしてAzure SQL Database、キャッシュとしてAzure Cache for Redisを使用します。もちろん、用途に応じて任意のデータベースやキャッシュサービスを選択できます。以下はAzure CLIを使ったリソース作成のコマンド例です。

$resourceGroup="<Resource Group>"
$location="<location>"
$redisCacheName="<Redis cache name>"
$sqlServerName="<Azure SQL Server Name>"
$sqlDBName="<Azure SQL DB Name>"
$adminName="<admin name of SQL server>"
$adminPassword="<admin password of SQL Server>"

# リソースグループを作成
az group create --name $resourceGroup --location $location

# Basic SKUでRedis Cacheを作成
az redis create --name $redisCacheName --resource-group $resourceGroup --location $location --sku Basic --vm-size c0

# SQL Serverを作成
az sql server create -l $location -g $resourceGroup -n $sqlServerName -u $adminName -p $adminPassword

# Basic SKUでSQLデータベースを作成
az sql db create -g $resourceGroup -s $sqlServerName -n $sqlDBName --service-objective Basic

ASP.NET Coreでの実装

それでは実際の実装に取り掛かりましょう。まずASP.NET Core Web APIプロジェクトを作成し、RedisキャッシュとEntity Framework Coreに必要なNuGetパッケージを追加します。

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

続いて、国(Country)情報を表すモデルクラスを作成します。

public class Country 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
    public bool IsActive { get; set; } 
} 

次に、StartupクラスのConfigureServicesメソッドで、EF CoreとRedisキャッシュの依存関係を登録します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddDbContext<CountryContext>(optionsAction =>
        optionsAction.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    services.AddStackExchangeRedisCache(setupAction =>
    {
        setupAction.Configuration = Configuration.GetConnectionString("RedisConnectionString");
    });
}

appsettings.jsonファイルに、Redis CacheとSQL Databaseの接続文字列を追記します。

"ConnectionStrings": {
    "RedisConnectionString": "<Redis Cache ConnectionString>",
    "DefaultConnection": "<SQL Server Connection string>"
}

DbContextクラスを追加します。

public class CountryContext : DbContext
{
    public DbSet<Country> Countries { get; set; }

    public CountryContext(DbContextOptions dbContextOptions) : base(dbContextOptions)
    {
    }
}

GetCountriesメソッドでは、まずキーを指定してキャッシュからアイテムの取得を試みます。一致するデータが見つかればそれを返し、見つからなければデータベースからデータを取得してキャッシュに格納します。このとき、キャッシュされたアイテムには5分で失効する絶対有効期限(AbsoluteExpirationRelativeToNow)を設定しています。

[Route("api/[controller]")]
[ApiController]
public class CountryController : ControllerBase
{
    private readonly IDistributedCache cache;
    private readonly CountryContext countryContext;

    public CountryController(IDistributedCache cache, CountryContext countryContext)
    {
        this.cache = cache;
        this.countryContext = countryContext;
    }

    // GET: api/<CountryController>
    [HttpGet]
    public async Task<IEnumerable<Country>> GetCountries()
    {
        var countriesCache = await cache.GetStringAsync("countries");
        var value = (countriesCache == null) ? default : JsonConvert.DeserializeObject<IEnumerable<Country>>(countriesCache);

        if (value == null)
        {
            var countries = countryContext.Countries.ToList();
            if (countries != null && countries.Any())
            {
                await cache.SetStringAsync("countries", JsonConvert.SerializeObject(countries), new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
                });
                return countries;
            }
        }
        return value;
    }
}

ポイント:キャッシュキーは読み書きで完全に同一の文字列を使用してください。大文字小文字が異なるだけでも別のキーとして扱われ、キャッシュが常にミスする原因となります。

続くAddCountriesメソッドは、データベースへの追加・更新後に実施すべきキャッシュの無効化の実装例です。RemoveAsyncを呼び出して対象のキャッシュエントリを明示的に削除することで、次回のリクエスト時に最新データがデータベースから読み込まれ、キャッシュが再構築されます。

// POST api/<CountryController>
[HttpPost]
public async Task<ActionResult<string>> AddCountries([FromBody] Country country, CancellationToken cancellationToken)
{
    if (country == null)
        return BadRequest("country is null");

    await countryContext.AddAsync(country);
    await countryContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
    await cache.RemoveAsync("countries", cancellationToken).ConfigureAwait(false);

    return Ok("cache has been invalidated");
}

まとめ

本記事では、キャッシュアサイドパターンの基本概念と、ASP.NET CoreおよびAzure Cache for Redisを使った実装方法について解説しました。「まずキャッシュを確認し、なければデータストアから取得してキャッシュに格納する」「データ更新時は先にデータストアを更新してからキャッシュを無効化する」というシンプルなルールを守るだけで、アプリケーションのパフォーマンスとデータ整合性を両立できます。Happy Caching!

本記事がお役に立ちましたら、ぜひシェアやフィードバックをお寄せください。

  1. Redis GEORADIUSBYMEMBERコマンドの使い方を実例付きで解説 – Redisチュートリアル

    このチュートリアルでは、Redisに保存された地理空間データ(ジオスペーシャル値)の中から、特定の範囲内に含まれる要素を取得する方法を学びます。そのために使用するのが GEORADIUSBYMEMBER コマンドです。 GEORADIUSBYMEMBERコマンドとは GEORADIUSBYMEMBERコマンドは、キーに保存された地理空間値(ソート済みセット)のメンバーのうち、指定したメンバーの経度・緯度と半径の引数から算出される円形エリアの境界内にある1つ以上のメンバーを返すために使用します。このエリアは、指定したメンバーの経度・緯度を円の中心位置とし、指定した単位による半径を円の半径として計

  2. DBaaSプロバイダー選びで必ずチェックすべき6つの重要ポイント

    クラウドベースのデータベース管理システム(DBMS)の成功を左右するのが、Database-as-a-Service(DBaaS)環境の選択です。この決定は年々重要性を増しており、Research and Marketsの最新レポートによると、グローバルのクラウドデータベースおよびDBaaS市場は、2020年の推定120億ドルから2025年には248億ドルへと成長すると予測されています。 この成長を牽引しているのは何でしょうか?それは、最小限のレイテンシでクエリを処理したいという絶え間ない需要です。 DBaaSモデルは、マネージド型データベース資産をクラウド上に迅速に展開するための魅力的な