Railsで中規模なサイトを作っていく上で
避けて通れないのが、増えてきたモデルを適切にキャッシュする仕組みのように思えます。
特に変更が少ないマスタ的なテーブルに対して、『多対多』で関連付け(アソシエーション: association)
がある場合などは、
それなりのSQLの発行コストになる事があります。そこを適切にキャッシュすることでDBへの負荷が減り、
ユーザーへのレスポンスが改善されると思います。
今回は、最近実装しているキャッシュの方法について、紹介したいと思います。 (というか偉い人、ぜひいい方法教えて下さい><)
前提条件: RailsからRedisにキャッシュ
今回は前提条件として、Railsのアプリ から『redis-store/redis-rails - GitHub』 のGemを使って、Redisにキャッシュをされているとします。 セットアップ方法は『redis-store/redis-rails - GitHub』 のREADMEを御覧ください。
またRedis自体のインストールについては拙著 「CentOS/Mac OSXへのRedis導入手順 - memcacheライクなKey-Value方式と、永続化対応のインメモリDB」 をよかったら御覧ください。
Railsでのキャッシュ(ActiveSupport::Cache)
Rails内では次のように書くことでRedisにxxx
というキーでキャッシュ済の場合は、Redisのキャッシュを取得します。
Redisにxxx
というキーがない場合は中の処理を実行してキャッシュにセットした上で値を取得します。
1 2 3 4 5 6 7 8 9 10 |
|
ちなみに、Redis側に格納されたデータは次のようになります。
1 2 3 |
|
Redis側にはActiveSupport::Cache::Entryというオブジェクトとして、値が格納されるようです。
ActiveRecord::Relation.to_a (一回呼び出して)のキャッシュ
いよいよ本題です。まずは、ActiveRecord::Relation.to_a
ってやるとキャッシュできるか試してみました。
1 2 3 |
|
1 2 3 4 5 |
|
キャッシュされてた。どうやら、Rails 4.2.1以降はActiveRecord::LazyAttributeHash
というオブジェクトでキャッシュされているっぽい。
Railsすごいな… (一部の環境でArelまでしかキャッシュされないことがある気がしますが、ライブラリとかのバージョン依存なのか、実装がしょぼいのか…)
あと、これとは別で状態が変化するオブジェクトをキャッシュするのはいかがなものかという議論もあります。
Confusion caching Active Record queries with Rails.cache.fetch - Stack Overflow
この方が言っていることは至極最もだと思います。あくまで変化が殆ど無いような、マスタデータに 関するキャッシュを想定しています。
最近 ActiveRecord / DB周り に対して思うこと
- ActiveRecord、まじ洗練されててすごい。でもその分、レコード数、カラム数が多くなるとオブジェクトの生成コストつらい気がする
- パフォーマンス保つためにも できるだけ SQL は発行したくないよね
- 仮に N+1 をさけて、include しても DB側のSQLのコストはそれなりに高い(index次第だけど)
- SQLの発行コストや発生頻度は常に意識する必要がある
- コストを正しく把握した上で、SQLを発行するかどうか選択すべき
- ただし、トレードオフとして生産性がある。ActiveRecordの便利な機能使えないってことは生産性が下がる
- 生産性を犠牲にはし過ぎないようにしたい。生産性を犠牲にしない程度にキャッシュを有効活用したい
ライブラリに依存すべき?
それっぽいGemがないかなと思って探していたら、shopify謹製のライブラリを発見しました。更新も頻繁に行われているようです。
便利そうなGemがあることはあるけど…
- 学習コストが高いライブラリはやっぱり怖い(チーム開発で使いづらい) - 実装をちゃんと読みきらないとブラックボックス化して怖そう...
models/concern でのキャッシュ実装の提案
Railsが前提にはなりますが、models/concern/cache_support.rb
を実装して、プロジェクトで使いやすい形、
チームメンバーが簡単に使える形で実装していくという提案です。
一応補足で、キャッシュのクリアを1時間に1回にしていますが、これはマスタ系のデータがほぼ更新されない ような特殊な環境を想定しています。正しくやるのであれば、after_saveとかのcallbackを使って、キャッシュを クリアして上げる仕組みも一緒に実装してあげると幸せになれると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
でこれを使うモデルでは CACHED_COLUMNS
を追加するとき、
もしくは、キャッシュを呼び出すメソッド 使うときは必ずテスト(RSpec
)書くって運用はどうでしょう?
この辺りの実装でよりいいプラクティスとかあればぜひ教えてほしいです><