LoginSignup
15
8

More than 3 years have passed since last update.

ActiveSupport::Cache::Store の使い方について

Last updated at Posted at 2020-12-12

はじめに

特にサーバ外にあるリソースを取得するなど、一部遅い処理がある場合、そこについてキャッシュすることは一般的です。

ActiveSupport::Cache::Store はそのようなキャッシュを行うときに使えるライブラリです。Ruby on Rails と組合わせて使われることが多いですね。

ActiveSupport::Cache::Store の基本的な使い方

ドキュメントでは下記のようなコードが紹介されています。

cache = ActiveSupport::Cache::MemoryStore.new

cache.read('city')   # => nil
cache.write('city', "Duckburgh")
cache.read('city')   # => "Duckburgh"

read というのがキャッシュを取得するメソッド、write が保存するメソッドです。

キャッシュストアの種別

MemoryStore

MemoryStore は Ruby プロセスのメモリ内にオブジェクトを保持するためのクラスです。プロセス内のメモリに保存するため、ワーカープロセスが複数ある場合、そのワーカープロセス間ではキャッシュは共有されません。その点考慮して利用する必要があります。

MemCacheStore

バックエンドとして Memcached を利用します。

RedisCacheStore

バックエンドとして Redis に保存します。

NullStore

実際には何もキャッシュに保存しません。開発やテストのときにキャッシュされると厄介なときに使うと便利な場合があります。

よく使うメソッド

上記の例でも使われている read や write がよくつかわれますが、それ以外でいうと下記のメソッドがよく使います。

exist?

そのキーが存在するかどうかを確認することができます。

fetch

fetch は一番よく使うメソッドかもしれません。

ドキュメントの例を引用します。

cache.fetch('city')   # => nil
cache.fetch('city') do
  'Duckburgh'
end
cache.fetch('city')   # => "Duckburgh"
  1. cache.fetch('city') はまだ、保存されていなので nil
  2. cache.fetch('city') do のところ
    • キャッシュミスするのでブロック内を実行する
    • city というキャッシュキーに対してブロックの評価結果 'Duckburgh' を保存する
  3. 最後の cache.fetch('city') ではキャッシュヒットするのでキャッシュを返す

というかんじで動作します。

fetch メソッドには有用なオプションがいくつかあります。

force: true

force: true をつけると必ずキャッシュミスされることができます。

たとえば、下記のようなコード例で利用できます。

Rails.cache.fetch("User.analytics_result(#{Time.current.strftime("%F")}", force: Rails.env.test?) do
    User.analytics_result
end

上記の例ではユーザの分析をする analytics_result メソッドがあって、その結果をキャッシュしているという想定です。
キャッシュキーに日付を含めていて、毎時0時0分に再計算します。
テストで実行しているときはデータがいろいろ書き換わることがあったりしてキャッシュすると不都合があるような場合において、強制的にキャッシュミスさせ、必ず再計算するようにできます。

skip_nil: true

skip_nil: true を指定すると、結果が nil のときにキャッシュさせないようにできます。

たとえば下記のように使います。

Rails.cache.fetch("fetch_exmple_com", skip_nil: true) do
  open("http://www.example.com/") rescue nil
end

上記のコードで www.example.com へのアクセスがエラーになったと仮定します。
すると rescue nil によって nil が返ります。skip_nil: true ですので nil はキャッシュされません。
毎回再試行させることができます。

expires_in: 5.minutes

expires_in というオプションを指定することでキャッシュの有効時間を指定できます。

Rails.cache.fetch("fetch_exmple_com", expires_in: 5.minutes) do
  open("http://www.example.com/") rescue nil
end

とすると、結果を5分間だけキャッシュすることができます。

デフォルトとは違う時間キャッシュしたいときに指定します。

試行結果に応じてキャッシュ時間を増減したい場合

ところで、成功したときはキャッシュ時間を長く、失敗したときはキャッシュ時間を短くしたい場合があります。
実行してみないと成功するか失敗するかは分かりません。
ソースコードを確認したところ、この expires_in は to_f できるオブジェクトなら何でもいいようですので
たとえば

expires_in = "300" # 300 means 5 minutes
Rails.cache.fetch("fetch_exmple_com", expires_in: expires_in) do
  (open("http://www.example.com/") rescue nil).tap do |body|
    expires_in.replace("5") if body.nil? # 5 means 5 seconds
  end
end

とすると、失敗したときは 5秒、成功したときは 5分キャッシュすることができます。
ここでは String#replace を使って、文字列オブジェクトの内容を破壊的に変更しています。

ライブラリ側がシャロウコピーではなくディープコピーするように変更されることもあり得ます。この String#replace を使う方法はいつまでも使える方法ではないかもしれません。
素直に実行するなら、exist? 、read 、 write を組み合わせるのが良いですが、高頻度でアクセスされる箇所で後述の race_condition_ttl を組合せて利用したい場合はこの方法もありえます。

(参考: exist? 、read、write を組み合わせる例)

cache_key = "fetch_wxampl_com"
if Rails.cache.exist?(cache_key)
  Rails.cache.read(cache_key)
else
  (open("http://www.example.com/") rescue nil).tap do |body|
    expires_in = body ? 5.minutes : 5.seconds
    Rails.cache.write(cache_key, body, expires_in: expires_in)
  end
end

race_condition_ttl

race_condition_ttl は高頻度にアクセスされるキャッシュに対して用いると効果的です。

高頻度にアクセスされるキャッシュのキャッシュ期限が切れたとき多くののプロセスで、同時にキャッシュミスし、再試行する事象が発生します。多くのプロセスで再試行し、キャッシュに書き込もうとするのはムダです。どれか1つのプロセスが再試行して、キャッシュに書き込めば十分です。race_condition_ttl を使うと、最初のプロセスだけが再試行した上で、ほかのプロセスは以前のキャッシュをしばらく使い続けるという動作にできます。

race_condition_ttl は少し難しいので詳しくは API ドキュメント を参照ください。

終わりに

今回の記事では ActiveSupport::Cache::Store の主な使い方について解説しました。

キャッシュを効果的に活用するとアプリケーションを劇的に高速化できる場合もあります。

この記事がお役に立てばうれしいです。

15
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
8