2012年8月31日金曜日

ASP.NET MVCのキャッシュについてあれこれ

一年半前ぐらいにASP.NET MVC 3のキャッシュ周りについて調べまくったので今更ながらまとめておく。一年以上前の話なのであやふやな部分も少々あるけれどそこはご愛嬌ということで話半分に読んで欲しい。

で、キャッシュについて。キャッシュを有効活用できれば一番手っ取り早くサーバのスループットをあげることができるわけで、もちろんASP.NET MVC 3にもキャッシュ機能はある。


OutputCacheAttribute
OutputCacheAttributeがそれにあたる。この属性をControllerかActionに付与することによってそれらの出力結果をキャッシュしてくれる。使用方法は下記のような感じ。
[OutputCache( Duration = 5, VaryByParam = "fish;angler", VaryByHeader = "X-Requested-With" )]
public class ChinuController : Controller{
}
Durationで何秒間キャッシュするのかを指定。VaryByParamやVaryByHeaderの指定でQueryStringのパラメータとかヘッダーごとにキャッシュを分けることが可能となっている。その他にもいくつか設定できる項目があるみたいだけれど使ったことがないのでよく分からない。

で、この属性を使う上で一つ注意が必要なのが、さきほども強調表示してあるけれどこの属性は出力結果をキャッシュするようになっている。つまりこの属性を使用するとControllerのアクションは当たり前だけどViewのレンダリングもスキップされることになる。

で、それの何が問題なの?ってのは次の場合。


ドーナツキャッシュ
ログインユーザに「ようこそYooさん」のようなメッセージを表示する会員制のサイトがあったとする。そういう場合にOutputCacheだとディモールト(非常に)都合がよろしくない。というのもOutputCacheは出力結果をキャッシュするので、Yooさんの内容がキャッシュされた状態でSasukeさん(うちの猫)がそのページにアクセスすると「ようこそYooさん」と表示されてしまうのである。じゃぁそういう場合はどうするの?ってのでドーナツキャッシュという手法がある。下図のようにある一箇所だけを除外してキャッシュするのでドーナツキャッシュと呼ばれる。


その除外された箇所の描画用コールバックを用意しておくとそれが後から呼ばれるという寸法だ。で、詳細は下記を参照して欲しい。
Donut Caching in ASP.NET MVC

うん、冒頭のUPDATEを読んでびっくりだと思うけどこの機能はASP.NET MVC 3には組み込まれてない。キャッシュについて調べてたときも上記のサイトの説明を飛ばしてソースコードから入ったので、実装したら動かなくてちゃんと読み直したらびっくりしたね。F***って思ったよ。

で、困ったなーということで自前のキャッシュフィルターを作ることにした。


ResultCache
要するに出力結果をキャッシュされちゃうと柔軟性に欠けるというわけで、それならControllerのアクションの結果(ActionResult)だけをキャッシュして、そのキャッシュしといたActionResultをViewに渡して描画するようにすれば大体の問題は解決できるので下記のようなアクションフィルターを作ろうと思ったら既に作っている人(ASP.NET MVC Result Cache)がいたのでそれを拝借した。
public class ResultCacheAttribute : ActionFilterAttribute
{
 public ResultCacheAttribute()
 {
  Duration = 1200;    // 20 mins
 }
 public string CacheKey{ get; private set; }
 public CacheDependency Dependency { get; set; }
 private CacheItemPriority _priority = CacheItemPriority.Default;
 public CacheItemPriority Priority
 {
  get { return _priority; }
  set { _priority = value; }
 }
 public int Duration{ get; set; }

 public override void OnActionExecuting(ActionExecutingContext filterContext)
 {
  var url = filterContext.HttpContext.Request.Url.PathAndQuery;
  this.CacheKey = "ResultCache-" + url;
  if (filterContext.HttpContext.Cache[this.CacheKey] != null)
  {
   filterContext.Result = (ActionResult)filterContext.HttpContext.Cache[this.CacheKey];
  }
  base.OnActionExecuting(filterContext);
 }

 public override void OnActionExecuted(ActionExecutedContext filterContext)
 {
  filterContext.Controller.ViewData["CachedStamp"] = DateTime.Now;
  filterContext.HttpContext.Cache.Add(this.CacheKey, filterContext.Result, Dependency, DateTime.Now.AddSeconds(Duration), System.Web.Caching.Cache.NoSlidingExpiration, Priority, null);
  base.OnActionExecuted(filterContext);
 }
}
リクエストされたURLをキャッシュのキーにしてActionResultを保持しておき、再度同じURLがリクエストされた場合はキャッシュからActionResultを取得してfilterContextのResultに渡すというだけの至極シンプルなフィルター。OnActionExecutingを見て不思議に思うかもしれないけれど、実はfilterContextのResultにActionResultが設定されるとControllerのアクションは呼び出されないのだ。


というわけで、サーバリソース節約のためにもキャッシュはガンガン使っていくべきなので機会があれば快適なWebを実現するためにもドシドシ有効にしていただきたい。ただ一点注意が必要なのはなんでもかんでもキャッシュしていると予期せぬ出力結果になることがままあるのでキャッシュが有効になっている箇所の周りを開発する場合はその箇所だけをテストするのではなく、複数人からのリクエストを想定したテストなどを行わないと本番環境にリリースしてから痛い目にあうことがあるのでくれぐれも注意して欲しい。かく言う私もチヌかかり釣りMEGAのスマホ版をリリースしたさいに、ControllerのアクションでPC版とスマホ版のViewを動的に切り替えていたのでActionResultがキャッシュされてしまい予期せぬ動作になりとても焦った。ただその問題は前述のアクションフィルターにスマホ用のキャッシュキーを追加することによってサクッと事なきを得たので良かったけれども。

0 件のコメント:

コメントを投稿