Skip to content

Building a rails 3 application with memcache

shunter1112 edited this page Aug 1, 2013 · 1 revision

キャッシュをあなたのWebアプリケーションに追加することはに強大なパフォーマンスの改善になるかもしれません。複雑なデータベースのクエリの結果、高負荷な計算、または外部リソースへの遅い呼び出しは、計算量O(1)の早い参照を経由してアクセスされる単純なキーバリューストアになり得ます。

Rack::Cacheを使用したRails 3.1系の静的なアセットキャッシュは[この記事](rack-cache-memcached-rails31)に概説されています。

このチュートリアルは、簡単なRails3.2のアプリケーションを作成とHerokuへのデプロイ、そして高負荷なクエリをキャッシュするためにMemCachierアドオンを利用する過程を追います。

[デモ用Railsアプリケーション](https://github.com/memcachier/examples-rails3-heroku)はGithubで利用可能です。デモの実行中の物は[こちら](http://memcachier-examples-rails3.herokuapp.com/)で確認できます。

前提

  • 基本的なRuby/Railsの知識。インストールされているバージョンであるRuby1.9.2,Rubygems,Bundler,そしてRails3を含みます。
  • 基本的なGitの知識
  • Herokuのユーザアカウントを持っている事。こちらから無料で素早く取得できます

アプリケーションの作成

アプリのスケルトンを生成するために、railsコマンドを実行します:

:::term
$ rails new memcache-example
$ cd memcache-example/

次にデータベースです。Gemfileの中で、下記にある行 :

:::ruby
gem 'sqlite3'

を次のように変更します :

:::ruby
group :development do
  gem 'sqlite3'
end

group :production do
  gem 'pg'
end

これはアプリケーションがプロダクション環境でデータベースにPostgresを使う事を宣言しています。

次に以下を実行します。:

:::term
$ bundle install --without production

これは指定されたgemをインストールしGemfile.lockファイルを生成します。 --without productionオプションは、(プロダクション環境でしていされている)pg gemがローカルでインストールされるのを防ぎます。

このプロジェクトをGitリポジトリとし、変更をコミットします。:

:::term
$ git init
$ git add .
$ git commit -m "first commit"

Herokuへのデプロイ

herokuコマンドを使用して、新しいHerokuアプリケーションを追加します。

:::term
$ heroku create

そしてHerokuにデプロイします。

:::term
$ git push heroku master

MemCachierアドオンのインストールとキャッシュの設定

MemCachierの記事の中で言及している通り、アドオンとdalligemのインストールが必要になります。オプションであるmemcachiergemも推奨されます。

ターミナルにて以下を実行します :

:::term
$ heroku addons:add memcachier:dev

シンプルな設定を助けるgem、dalli、memcacheのクライアントライブラリ、そしてmemcachierを含める形でGemファイルを変更します。

:::ruby
gem 'dalli'
gem 'memcachier'

そしてRailsのデフォルトキャッシュの設定を、dalliが提供するキャッシュストアを利用する形に変更します。config/environments/production.rbに下記を含めます :

:::ruby
config.cache_store = :dalli_store

このサンプルがどのように動くのかを簡単に見るために、一時的に組み込まれているキャッシュを向こう化します。以下の行をコメントアウトします :

:::ruby
# config.action_controller.perform_caching = true

機能の追加

Railsのscaffoldジェネレータを使って、名前とメールアドレスが含まれる簡単な名簿を保存、閲覧できるインターフェースを生成します。

:::term
$ rails g scaffold contact name:string email:string
$ rake db:migrate

config/routes.rbを編集し、contacts#indexをルートとして設定します。

:::ruby
root :to => 'contacts#index'

そしてpublic/index.htmlを削除します。

変更をコミットし、Herokuへプッシュ、そして遠隔にあるデータベースをマイグレートするために以下のコマンドを使用します。

:::term
$ heroku run rake db:migrate

これでheroku openを使って連絡先のリストを見る事ができるようになっているはずです。"New Contact"リンクを辿り、いくつかの連絡先を追加してください。

キャッシュの追加

ContactsControllerの中にあるコードはこのようになっています。:

:::ruby
def index
    @contacts = Contact.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @contacts }
    end
end

/contactsと要求される時はいつも、このindexメソッドが実行され、Contactsテーブルの中にある全てのレコードを取得するデータベースのクエリが実行されます。

テーブルが小さくて、リクエストの量が低いときにはこれはそんなに問題にはなりません。しかし、ユーザ数とデータベースが育つ程に、このようなクエリはあなたのアプリケーションに影響を与える可能性があります。このページに訪れたときにいつもデータベースのクエリ実行されないように、Contact.allの結果をキャッシュしてみましょう。

`Rails.cache.fetch`メソッドはキーとなる属性とブロックを取ります。 もしキーが存在する場合は、一致する値が帰ってきます。もしそうでない場合はブロックが実行され、値は与えられたキーと一緒に保存され、そして戻されます。

app/models/contact.rb に以下のメソッドを追加します。 :

:::ruby
def self.all_cached
  Rails.cache.fetch('Contact.all') { all }
end

app/controllers/contacts_controller.rb にある以下の部分 :

:::ruby
@contacts = Contact.all

を以下の通りに変更します。 :

:::ruby
@contacts = Contact.all_cached

一緒にいくつかの統計情報もIndexページに載せてみましょう。以下の行をapp/controllers/contacts_controller.rbの中のindexメソッドに追加します。 :

:::ruby
@stats = Rails.cache.stats.first.last

そして、以下のタグをapp/views/contacts/index.html.erbの下に追加します :

:::html
<h1>Cache Stats</h1>

<table>
  <tr>
    <th>Metric</th>
    <th>Value</th>
  </tr>
  <tr>
    <td>Cache hits:</td>
    <td><%= @stats['get_hits'] %></td>
  </tr>
  <tr>
    <td>Cache misses:</td>
    <td><%= @stats['get_misses'] %></td>
  </tr>
  <tr>
    <td>Cache flushes:</td>
    <td><%= @stats['cmd_flush'] %></td>
  </tr>
</table>

結果をコミットし、Herokuにプッシュします。/contactsページを再読み込みすると、"Cache misses: 1"という表示が見えるでしょう。これはContact.allのキーを取得しようとしたけれども存在しなかったために起きています。再度再読み込みをすると"Cache hits: 1"と表示されるでしょう。今回は、前のリクエストによってContact.allのキーが保存されたため、キーが存在していたのです。

Herokuのコンソールからキャッシュを消す事で、この現象を再度確認することができます。

:::term
$ heroku run console
>> Rails.cache.clear

キャッシュの破棄

Contact.allはキャッシュされるようになりましたが、テーブルに更新があったら何が起こるでしょうか?試しに新しい連絡先を追加して、リストしている画面に戻ってみてください。きっと新しい連絡先が表示されていないはずです。Contact.allがキャッシュされたために、古い値がまだ提供されつづけているのです。そこで何か変更が会った場合に、キャッシュされた値を破棄する手段が必要になります。これはContactモデルの中のフィルターで実現する事が出来ます。

以下のコードをapp/models/contact.rbに追加します :

:::ruby
after_save    :expire_contact_all_cache
after_destroy :expire_contact_all_cache

def expire_contact_all_cache
  Rails.cache.delete('Contact.all')
end

これらの変更をコミットし、Herokuに追加します。これで、あなたが連絡先を保存(作成及び変更)または削除するときに、Contact.allのキャッシュのキーが削除されるようになりました。あなたがこのような連絡先の更新をしたあと/contactsに戻ってきたときには、いつも"Cache misses"の数字が1に繰り上がっているはずです。

Railsに組み込まれているアクションキャッシュ

上記の例は、どのように明示的にキャッシュを取得し破棄するかを説明してきました。便利な事に、Railsにはこのような機能が多く組み込まれています。もしshowのアクションの結果をキャッシュしたいと思ったならば、app/controllers/contacts_controller.rbに以下の行を追加します :

:::ruby
caches_action :show

適切な破棄のために、以下の行をcontacts_controller.rb内のupdatedestroyメソッドの両方に追加します :

:::ruby
expire_action :action => :show

アクションキャッシュはページキャッシュにとても似た形でビューとオブジェクトを保存します。しかし認証の様にbeforeフィルタが適応されるように、リクエストは実際にアプリケーションスタックを叩きます。

更に読み進めたいという方のために、Rails Guide on CachingはRailsに組み込まれているアクション、フラグメント、そして他の種類のキャッシュに関するすばらしい情報を持っています。

コード

このチュートリアルで作ったアプリケーションの完全なソースは、自由にGitHubからダウンロードすることが可能です。

Clone this wiki locally