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でのキャッシュ(Active Support::Cache)
Rails内では次のように書くことでRedisにxxx
というキーでキャッシュ済の場合は、Redisのキャッシュを取得します。
Redisにxxx
というキーがない場合は中の処理を実行してキャッシュにセットしたうえで値を取得します。
array = Rails.cache.fetch('xxx') do |
ちなみに、Redis側に格納されたデータは次のようになります。
$ redis-cli |
Redis側にはActive Support::Cache::Entryというオブジェクトとして、値が格納されるようです。
🎃 Active Record::Relation.to_a (1回呼び出して)のキャッシュ
いよいよ本題です。まずは、ActiveRecord::Relation.to_a
ってやるとキャッシュできるか試してみました。
Rails.cache.fetch('prefecture') do |
$ redis-cli |
キャッシュされてた。どうやら、Rails 4.2.1以降はActiveRecord::LazyAttributeHash
というオブジェクトでキャッシュされているっぽい。
Railsすごいな。.. (一部の環境でArelまでしかキャッシュされないことがある気がしますが、ライブラリとかのバージョン依存なのか、実装がしょぼいのか。..)
あと、これとは別で状態が変化するオブジェクトをキャッシュするのはいかがなものかという議論もあります。
Confusion caching Active Record queries with Rails.cache.fetch - Stack Overflow
この方が言っていることは至極もっともだと思います。あくまで変化が殆どなどような、マスタデータに
関するキャッシュを想定しています。
😎 最近Active Record / DB周りに対して思うこと
- Active Record、まじ洗練されてすごい。でもそのぶん、レコード数、カラム数が多くなるとオブジェクトの生成コストつらい気がする
- パフォーマンス保つためにもできるだけSQLは発行したくないよね
- 仮にN+1をさけて、includeしてもDB側のSQLのコストはそれなりに高い(index次第だけど)
- SQLの発行コストや発生頻度は常に意識する必要がある
- コストを正しく把握したうえで、SQLを発行するかどうか選択すべき
- ただし、トレードオフとして生産性がある。Active Recordの便利な機能使えないってことは生産性が下がる
- 生産性を犠牲にはし過ぎないようにしたい。生産性を犠牲にしない程度にキャッシュを有効活用したい
🎂 ライブラリに依存すべき?
それっぽいGemがないかなと思って探していたら、shopify謹製のライブラリを発見しました。更新も頻繁に行われているようです。
便利そうなGemがあることはあるけど。..
- 学習コストが高いライブラリはやっぱり怖い(チーム開発で使いづらい) - 実装をちゃんと読みきらないとブラックボックス化して怖そう...
🎉 models/concernでのキャッシュ実装の提案
Railsが前提にはなりますが、models/concern/cache_support.rb
を実装して、プロジェクトで使いやすい形、
チームメンバーが簡単に使える形で実装していくという提案です。
一応補足で、キャッシュのクリアを1時間に1回にしていますが、これはマスタ系のデータがほぼ更新されない
ような特殊な環境を想定しています。正しくやるのであれば、after_saveとかのcallbackを使って、キャッシュを
クリアして上げるしくみも一緒に実装してあげると幸せになれると思います。
# キャッシュ制御に関するモジュール |
11/8に少し更新をしました。
- カラムを限定する仕組みを取りの時期ました。チームでの使いやすさを重視しました。 - ハッシュではなく、ActiveRecordにしました。ActiveRecordに戻すコストはかかりますが、使いやすさ重視です。 - キャッシュをレコード単位に変更しました。RedisのIOとハッシュに戻す部分のコストが大きかったので減らすのが目的です。 - 全レコードを一括で取得するメソッドを追加しました。Rails.cache.read_multi 便利。
このあたりの実装でよりいいプラクティスとかあればぜひ教えてほしいです><