これはなに
- rails APIにredisによるキャッシュ機構を導入する際、接続するredisの設定や各キャッシュの設定(キー・期限など)を隠蔽してくれるredis-objectsが役に立ちます。今回、RailsAPIにRedisキャッシュ機構を実装したので、メモを残しておきます。
- 検証時の各種ライブラリのバージョンは以下です
- redis-objects 1.4.3
- rails 5.2.3
- ruby 2.6.1p33
前提
redisとは
インメモリKVS。データの格納・取得が鬼早い。詳しくはググってください。
なお、今回の記事では記載していませんが、redis-objectの導入前に、redis-cliを用いて色々コマンドを触ってみると、より理解が深まる気がします。
キャッシュとは
APIに飛んできたリクエストに対して毎回DBにデータを取りに行くのは非効率なので、同じパラメータを持つリクエストへのレスポンスはAPIサーバー側で保持しておいて、2回目以降のリクエストに対してはそれを返しちゃえばいいじゃん、というやつ。DBへのアクセス数が減る分、DBサーバーの負荷軽減、レスポンス速度の向上、などの効果が見込まれる。
気をつけないといけない点としては、キャッシュに有効期限を設定しないと、コンテンツ側の変更が反映されなかったり、キャッシュサーバーの容量が圧迫されたりするので、いい感じ(今回は1時間)の有効期限を設定してあげるとよい。
今回導入するキャッシュ機構
Cache-Aside Pattern と呼ばれるキャッシュパターンを採用しました。
- APIに対してリクエストがきた際、特定のルールに基づいてパラメータからキー(文字列)を生成
- そのキーに対応するキャッシュがあるかredisサーバーに問い合わせる
- あった場合
- redisサーバーから受け取った値を(必要に応じて整形し)レスポンスとして返す
- なかった場合
- DBに値を問い合わせ、先ほど生成したキーとDBから受け取った値をredisサーバーに格納
- DBから受け取った値をレスポンスとして返す
- あった場合
という感じです。
導入手順
要件
-
/items?item_category_id=${カテゴリID}
というエンドポイントへのリクエストに対するレスポンスを、カテゴリIDごとにJSON型でキャッシュする - キャッシュの期限は1時間とする
redis-objectsの導入
gemfile
gem "redis-objects", "~> 1.4.3"
terminal
bundle install
redis-objects用の設定追加
- 今回のプロジェクトにはglobalが導入されていたので、
Global.redis.host
という感じに環境ごとのホストとポートの設定を読み込めるよう、設定ファイルをconfig/globals/
に追加します - 今回の記事では本番環境などの場合の環境変数は省略しますが、必要な場合は適当に追加してください。
config/globals/redis.yml
default: &default
host: <%= ENV.fetch("REDIS_HOST", "redis") %>
port: <%= ENV.fetch("REDIS_PORT", "6379") %> # 6379はredisのデフォルトポート番号
development:
<<: *default
test:
<<: *default
- Railsアプリ起動時にRedisObjectとRedisサーバーの紐付けを行うために、
config/initializers
にredisの接続設定を記載します
config/initializers/redis.rb
host = Global.redis.host
port = Global.redis.port
Redis::Objects.redis = Redis.new(host: host, port: port)
キャッシュ用のクラス作成
- 今回はレスポンス用のJSONをまるごとキャッシュするので、
model/
配下にredis-objectsをincludesした*Response.rb
というクラスを作成します
app/models/items_response.rb
class ItemsResponse
include Redis::Objects
value :resource, expireat: -> { Time.current + 1.hour } # 有効期限1時間の設定
def initialize(item_category_id)
@item_category_id = item_category_id
end
def id # redis-objectがキーを生成するのに使用する。設定するキャッシュ単位ごとに一意である必要がある
@item_category_id
end
end
コントローラー側でキャッシュ機能作成
app/controllers/items_controller.rb
class ItemsController < ApplicationController
def show
if cache_exists
result.resource = JSON.parse(@cached_value)
else
... # DBからitemsを取得する処理
store_cache(result.resource)
end
render json: result.resource
end
private
def cache_exists
@cached_value = ItemsResponse.new(params[:item_id]).resource.value
end
def store_cache(resource)
ItemsResponse.new(params[:item_id]).resource = resource.to_json
end
end
その他
キーの変更
- redisに格納する際のキーはデフォルトでは
"#{モデル名}:#{self.id}:#{フィールド名}"
ですが、これを変えるときは、valueの宣言時にkeyオプションを渡すことでキーの変更が可能です。
ラムダを渡してキー変更
class ItemsResponse
include Redis::Objects
value :resource, key: ->(instance) { "it:is:new:key:#{instance.id}:good" }
...
end
文字列を渡してキー変更
class ItemsResponse
include Redis::Objectsfff
value :resource, key: 'it:is:new:key:#{instance.id}:good' # 文字列はシングルクオートで囲って渡す点に注意
...
end
redis DBの変更
アプリケーション内で複数のredisキャッシュ用クラスを持つ場合、クラスごとにredis dbの切り替えを行うことが可能です。
items_response.rb
class ItemsRespnose
include Redis::Objects
...
end
users_response.rb
class UsersRespnose
include Redis::Objects
...
end
redis.rb
host = Global.redis.host
port = Global.redis.port
ItemsResponse.redis = Redis.new(host: host, port: port, db: 0) # デフォルトは0
UsersResponse.redis = Redis.new(host: host, port: port, db: 1)