クラウドデザインパターンの Cache-Aside Pattern を ASP.NET Web API で実装してみました。Cache-Aside Pattern は、オンデマンドでデータをキャッシュに効率的に読み込むパターンです。キャッシュ機構には、Azure Redis Cache(Preview)を利用しています。
Azure Redis Cache の作成
事前準備として、新しいAzure 管理ポータルで Redis Cache を作成しておきます。任意の名前を入力し、プランとDCのリージョンを選択します。なお、フル機能の旧ポータルからは作成できません。後ほど使うので、Redis Cache の Keys(接続文字列)をメモしておきます。
プロジェクトの作成
Visual Studio 2013 で、WebApplication のプロジェクトを選択し、Web API のテンプレートで作成します。Redis Cache の C# 用ライブラリの NuGet パッケージをインストールします。
- Install-Package StackExchange.Redis
Person クラスを操作する Web API を作成します。ID と Name だけの Person クラスを定義し、スキャフォールディングで「Entity Framework を使用したアクションがある Web API 2 コントローラー」の PersonController を追加します。
Azure Redis Cache への接続
プログラムから Azure Redis Cache のインスタンスに接続するには、ConnectionMultiplexer クラスを使います。ConnectionMultiplexer のインスタンスは、アプリケーションで共有できるので、スタティックなプロパティとして実装します。Azure 管理ポータルから取得できる Redis Cache の Keys(接続文字列)を渡して接続します。
#PersonController.cs public class PersonController : ApiController { private static ConnectionMultiplexer connection; private static ConnectionMultiplexer Connection { get { if (connection == null || !connection.IsConnected) { connection = ConnectionMultiplexer.Connect("xxx.redis.cache.windows.net,ssl=true,password=xxx"); } return connection; } } }
.NET オブジェクト用の拡張メソッド
String や int などのプリミティブなデータ型であれば、StackExchange.Redis クライアントの StringSet と StringGet のメソッドでアクセスできますが、Person クラスのような任意の型の場合、オブジェクトをシリアル化する必要があります。今回は、Json.NET を使った JSON 形式のシリアル化を行う拡張メソッドを作成します。
#JsonNetRedisExtensions.cs public static class JsonNetRedisExtensions { public static async Task<T> GetAsync<T>(this IDatabase cache, string key) { return await DeserializeAsync<T>(cache.StringGet(key)); } public static async Task SetAsync(this IDatabase cache, string key, object value, TimeSpan? expiry = null) { await cache.StringSetAsync(key, await SerializeAsync(value), expiry); } static async Task<string> SerializeAsync(object target) { if (target == null) { return null; } return await JsonConvert.SerializeObjectAsync(target); } static async Task<T> DeserializeAsync<T>(string json) { if (json == null) { return default(T); } return await JsonConvert.DeserializeObjectAsync<T>(json); } }
データの取得
GetPerson メソッドにおいて、Cache-Aside Pattern のロジックを実装します。ConnectionMultiplexer.GetDatabase メソッドを呼び出すことで、Redis Cache への参照が返されます。キャッシュにデータがあればそのまま返却し、なければデータベースから取得したデータをキャッシュに追加してから返却します。キャッシュのキーはPerson の ID 、有効期間は10分を設定しています。
#PersonController.cs public async Task<IHttpActionResult> GetPerson(int id) { // キャッシュからデータを取得 IDatabase cache = Connection.GetDatabase(); var person = await cache.GetAsync<Person>(CacheKey(id)); if (person == null) { // キャッシュになければ、データベースから取得した結果を保存 person = await db.People.FindAsync(id); if (person == null) { return NotFound(); } await cache.SetAsync(CacheKey(id), person, TimeSpan.FromMinutes(10)); } return Ok(person); } static string CacheKey(int id) { return String.Format("PersonId:{0}", id); }
データの更新と削除
PutPerson メソッドにおいて、データベースの情報が更新されたら、キャッシュを削除します。複数クライアントからの同時更新を考えると、キャッシュを更新するよりも削除したほうが、キャッシュとデータベースの一貫性を確保し易くなります。
#PersonController.cs public async Task<IHttpActionResult> PutPerson(int id, Person person) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (id != person.Id) { return BadRequest(); } db.Entry(person).State = EntityState.Modified; try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!PersonExists(id)) { return NotFound(); } else { throw; } } // キャッシュを削除 IDatabase cache = Connection.GetDatabase(); await cache.KeyDeleteAsync(CacheKey(id)); return StatusCode(HttpStatusCode.NoContent); }
DeletePerson メソッドにおいても、キャッシュを削除しておます。
#PersonController.cs public async Task<IHttpActionResult> DeletePerson(int id) { Person person = await db.People.FindAsync(id); if (person == null) { return NotFound(); } db.People.Remove(person); await db.SaveChangesAsync(); // キャッシュを削除 IDatabase cache = Connection.GetDatabase(); await cache.KeyDeleteAsync(CacheKey(id)); return Ok(person); }
まとめ
より多くのリクエストを捌く必要が出てきた場合、Azure SQL Database はボトルネックになるケースが多いです。データベースに頼り過ぎず、静的データをキャッシュするなら、Cache-Aside Pattern は有効なパターンだと思います。Azure Redis Cache が東日本リージョンにも来たことですし、 Preview から GA することを待つばかりです。