はじめに
Ruby on Railsを使ったサービス運用時に(Ruby on Railsに限った話ではありませんが)サービスの成長に連れてサイトへのアクセス増大によりDBへの負荷が原因でサイトのレスポンスが遅くなってしまうことがあります。
そのような場合の対応策の一つとしてクエリの結果をキャッシュしてDBアクセスの回数を減らして負荷を軽減するという方法があります。
この記事では簡単なサンプルを使ってクエリの結果をキャッシュする方法を紹介します。
実行環境
ruby 2.5.1
rails 5.2.1
以下のサンプルではキャッシュストアとしてredisを使用するため localhost:6379
でredisにアクセスできる状態を前提としています。
サンプルプロジェクトの準備
サンプルとしてArticle(model)を持つプロジェクトを作成します
# プロジェクの作成
$ bundle exec rails new redis_cash
# フォルダを移動
$ cd redis_cash
# DB作成
$ bundle exec rails db:create
# articleの定義を作成
$ bundle exec rails g scaffold article title:string body:text
# テーブルの作成
$ bundle exec rails db:migrate
データの準備
# ./db/seeds.rb
ActiveRecord::Base.transaction do
Article.delete_all
5.times do |index|
Article.create!(
title: "タイトル_#{index}",
body: "記事本文_#{index}"
)
end
end
$ bundle exec rails db:seed
データの確認
railsのサーバを起動してデータの確認
$ bundle exec rails s
ブラウザで http://localhost:3000/articles
にアクセスして以下の表示されることを確認
redisキャッシュの有効化
RailsアプリにRedisを導入
下記をGemfileに追記し bundle install
を実行。
# Gemfile
gem 'redis'
gem 'redis-rails'
キャッシュストアの設定変更
以下のようにキャッシュストアとしてredisを利用するようにconfigファイルを修正します。
# ./config/environments/development.rb
Rails.application.configure do
# config.cache_store = :null_store
config.cache_store = :redis_store, "redis://localhost:6379/0/cache"
end
:null_store
となっているところを :redis_store
に変更し、ローカルで起動しているredisを使用するようにします。
変更後は、Railsのサーバを再起動して設定してを反映させます。
Rails.cache.fetchを使ってクエリ結果をストアへ保存
Redisへの保存はRails.cache.fetch
を使うことで簡単に書くことができます。このメソッドは引数で指定したキーに対応するキャッシュがあったらキャッシュを返す/無かったら生成して返すという動きをします。
コントローラを以下のように修正します。
# ./app/controllers/articles_controller.rb
# 追加したメソッドからarticlesを取得するように変更
def index
# @articles = Article.all
@articles = cache_articles
end
private
# Rails.cache.fetchを使いキャッシュからArticle.allを取得する
# cache_articlesというキーで保存。cache_articlesはキャッシュの有効期間
def cache_articles
Rails.cache.fetch("cache_articles", expires_in: 60.minutes) do
Article.all
end
end
キャッシュに保存されるデータについて
ブラウザで http://localhost:3000/articles
に複数回アクセスしてログを確認します。
# 1回目のページアクセス
Started GET "/articles" for 127.0.0.1 at 2018-11-10 17:04:28 +0900
Processing by ArticlesController#index as HTML
Rendering articles/index.html.erb within layouts/application
Article Load (0.3ms) SELECT "articles".* FROM "articles"
↳ app/views/articles/index.html.erb:15
Rendered articles/index.html.erb within layouts/application (10.6ms)
Completed 200 OK in 206ms (Views: 194.5ms | ActiveRecord: 0.9ms)
# 2回目のページアクセス
Started GET "/articles" for 127.0.0.1 at 2018-11-10 17:04:33 +0900
Processing by ArticlesController#index as HTML
Rendering articles/index.html.erb within layouts/application
Article Load (0.2ms) SELECT "articles".* FROM "articles"
↳ app/views/articles/index.html.erb:15
Rendered articles/index.html.erb within layouts/application (2.2ms)
Completed 200 OK in 25ms (Views: 20.6ms | ActiveRecord: 0.2ms)
2回目以降のページアクセスでは、DBアクセスが発生してほしくないところですが, Article Load (0.2ms) SELECT "articles".* FROM "articles"
のようにDBアクセスが発生してしいます。
キャッシュ保存時の注意点
キャッシュを保存する際は、クエリが発行されるタイミングに注意する必要があります。
上の例で cache_articles
メソッドを実行後に保存された結果を確認するとクエリの実行した結果ではなく、実行前のクエリの情報がActiveRecord_Relation
として保存されます。実際にクエリが実行されるのは、例えばviewのループ処理の中であったりするため、キャッシュしても毎回クエリが実行されることになります。
以下のようにto_a
を追加するなどクエリ実行後の結果を保存することで回避できます。
# ./app/controllers/articles_controller.rb
private
def cache_articles
Rails.cache.fetch("cache_articles", expires_in: 60.minutes) do
# Article.all
Article.all.to_a
end
end
修正後の確認を行う前にredis-cliからキャッシュを削除しておきます。
(redis-cliがインストールされてない場合は、 Rails.cache.delete('cache_articles')
のコードを実行することでも削除できます)
$ redis-cli
127.0.0.1:6379>
127.0.0.1:6379> DEL cache:cache_articles
修正後にもう一度ログを確認すると2回目移行はクエリが実行されていないことがわかります。
# 修正後のログ
# 1回目のページアクセス
Started GET "/articles" for 127.0.0.1 at 2018-11-10 17:40:19 +0900
Processing by ArticlesController#index as HTML
Article Load (0.4ms) SELECT "articles".* FROM "articles"
↳ app/controllers/articles_controller.rb:78
Rendering articles/index.html.erb within layouts/application
Rendered articles/index.html.erb within layouts/application (1.7ms)
Completed 200 OK in 41ms (Views: 26.9ms | ActiveRecord: 1.0ms)
# 2回目のページアクセス
Started GET "/articles" for 127.0.0.1 at 2018-11-10 17:40:24 +0900
Processing by ArticlesController#index as HTML
Rendering articles/index.html.erb within layouts/application
Rendered articles/index.html.erb within layouts/application (1.5ms)
Completed 200 OK in 24ms (Views: 20.1ms | ActiveRecord: 0.0ms)
まとめ
Rails.cache.fetch
を使ってRailsアプリケーションでクエリの結果をキャッシュする方法を紹介しました。
キャッシュを使ってDBアクセスを減らす方法は、動的だが更新頻度が少ないページや決まったクエリを発行することが多いページでは有用な方法だと思います。
私が開発しているWebサービスではサイトのトップページに人気の商品やおすすめ商品などいくつかの商品のリストをDBから取得して表示しますが、アクセスの度に内容が変更されるようなものではないためキャッシュに持たせるようにすることで表示速度が改善されました。
サイトの速度改善などの参考になればと思います。