その他
    ホーム 技術発信 DoRuby ActiveSupport::Cache::StoreとMarshalの話
    ActiveSupport::Cache::StoreとMarshalの話
     

    ActiveSupport::Cache::StoreとMarshalの話

    この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

    RailsGuideではどんな情報でもキャッシュに保存できると豪語してますが誤りです。何でもはキャッシュできないわよ、Marshal.dumpできるobjectだけ。

    はじめに

    お久しぶりです、くろすです。
    突然ですが、皆さんは日常生活で

    「あ〜〜この情報キャッシュしておきたい〜〜〜」

    と思ったことはありませんか?

    私はあります。
    ライブ中のセトリ順なんか脳内メモリにキャッシュしといて打ち上げ中に引っ張り出したいです。
    だいたい書き込みに失敗して、他の人がメモ帳という名の外部記憶媒体に記録したものが検索に引っかかるまで待ってます。

    この待ち時間って無駄ですし、その間にも話をしたいわけです。
    そんな時にはこのモジュール

    ActiveSupport::Cache::Store

    Railsでアプリケーション層のキャッシングを考える場合は一考の余地があります。
    RailsGuideの低レベルキャッシュ の部分で説明されている Rails.cacheの正体です。
    もう少し詳しくいうなら ActiveSupport::Cache::Store を継承した MemoryCacheStore やFileCacheStore 等が正体になります。
    キャッシュの詳しいパフォーマンスなんかは RailsGuideキャッシュストアの項 や 少し古いですが、 Railsキャッシュの完全ガイド記事 が参考になります。

    MemoryStoreなら各プロセス単位、FileStoreなら各ホスト単位、 redis-storeや readthis などを使用するなら中央集権的にredisでキャッシュすることも可能で高速化の幅が広がります。

    どんな情報でもキャッシュできるわけではない

    そもそもRubyを使った際のオブジェクトのコピーには浅深があります。
    コピーについての詳しい話は先達の記事がありますので こちら
    ここで紹介されている Marshal.dump ですが、リファレンスにある通り名前のついていないClass/Moduleオブジェクトなど数種類のオブジェクトは書き出せません。
    普段はあまり意識しなくても問題ないでしょうが、私はRedisにProcのインスタンスを保存しようとした際にハマりました。
    というのも、上で紹介したActiveSupport::Cache::Storeは書き込みの際にオブジェクトをマーシャルデータで書き出してるんですね。
    ということはRails.cacheでキャッシュできるオブジェクトというのはその実、Marshalモジュールでマーシャルデータを作成することができるオブジェクト、つまりディープコピーできるオブジェクトということなのですね。

    実際、キャッシュストアに実データを投入する部分のソースを見てもその通りだとわかります。

    CacheStoreを使う際の注意点

    ActiveSupport::Cache::Storeはオブジェクトをキャッシュするという点に気をつけなければなりません。
    例えばActiveRecordなんかは賢いので、実データが必要になるまでクエリを吐きません。
    以下のように、思った通りキャッシュされなくて結果クエリの数は減らず高速化には繋がらない、みたいなこともあります。

    players = Player.where(id: 1..100);
    players.class
    # => Player::ActiveRecord_Relation
    memory_store = ActiveSupport::Cache::MemoryStore.new;
    memory_store.write('players', players)
    # => true
    players2 = memory_store.read('players');
    players2.class
    # => Player::ActiveRecord_Relation
    players2.map(&:name) # ここで初めてクエリが飛ぶ
    

    終わりに

    redisに値をキャッシュする場合なんかは、コードから剥き出しのredisを触りに行かなくても、RedisStoreやReadthisStoreを使いキャッシュとして触った方が、キャッシュ先を手軽に変更できる等のメリットがあります。
    直にredisを触る場合と比べての速度の問題は未検証ですが、個人的には#fetchexpires_inの使い勝手がいいのでキャッシュストアを使いたいです。