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

# はじめに

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

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

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

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

```rb
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?](https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-exist-3F)

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

## [fetch](https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-fetch)

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

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

```rb
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` をつけると必ずキャッシュミスされることができます。

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

```rb
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 のときにキャッシュさせないようにできます。

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

```rb
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 というオプションを指定することでキャッシュの有効時間を指定できます。

```rb
Rails.cache.fetch("fetch_exmple_com", expires_in: 5.minutes) do
  open("http://www.example.com/") rescue nil
end
```
とすると、結果を5分間だけキャッシュすることができます。

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

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

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

```rb
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](https://docs.ruby-lang.org/ja/latest/method/String/i/replace.html) を使って、文字列オブジェクトの内容を破壊的に変更しています。

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

（参考： exist? 、read、write を組み合わせる例）

```rb
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 ドキュメント](https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-fetch) を参照ください。


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

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

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