Rails Model Caching bằng Redis

enter image description here

Caching ở tầng model thường bị bỏ qua, thậm chí với những lập trình viên lâu năm. Phần lớn đó là do quan niệm sai lầm rằng, khi bạn cache dữ liệu, bạn không cần bộ nhớ cache ở các cấp thấp hơn. Trong khi sự thật là vấn đề thắt nút cổ chai trong Rails nằm trong lớp View, đó không phải luôn luôn như vậy.

Cache ở cấp độ thấp thì linh hoạt và có thể làm việc bất cứ nơi nào trong ứng dụng. Trong hướng dẫn này, tôi sẽ chứng minh làm thế nào để cache model với Redis.

1. Caching làm việc như thế nào?

Theo truyền thống, truy cập vào ổ đĩa thì tốn kém. Thường xuyên truy cập dữ liệu từ ổ đĩa sẽ gây ra chậm, hiệu năng sẽ giam. Để chống lại điều này, chúng ta có thể thực hiện một lớp bộ nhớ đệm ở giữa ứng dụng và máy chủ cơ sở dữ liệu.

Lớp caching thì không lưu dữ liệu ở lần đầu. Khi nhận yêu cầu dữ liệu, nó sẽ truy cập cơ sở dữ liệu và lưu kết qủa vào trong bộ nhớ (gọi là cache). Những yêu cầu dữ liệu ở lần tiếp theo sẽ được cung cấp từ lớp dữ liệu cache này, vậy nên không cần thiết phải truy cập database, nên hiệu suất sẽ tăng lên.

2. Tại sao sử dụng Redis?

Redis là một in-memory, dạng cấu trúc lưu trữ dữ liệu, sử dụng như là databse, lưu trữ dạng key-value. Ưu điểm là tốc độ nhanh và phục hồi dữ liệu gần như tức thời. Redis hỗ trợ cấu trúc dữ liệu cao cấp như lists, hashes, sets, và có thể tồn tại vào đĩa.

Trong khi nhiều lập trinhf viên thích Memcache với dalli cho việc caching của họ, tôi tìm thấy Redis thì đơn gian cho việc cài đặt và dẽ dàng để quản trị. Nếu bạn sử dụng reque hoặc sidekiq cho việc quản lý backgroud jobs, bạn cũng cần dùng redis.

2.1 cài đặt redis

$ wget http://download.redis.io/releases/redis-2.8.18.tar.gz
$ tar xzf redis-2.8.18.tar.gz
$ cd redis-2.8.18
$ make

Sau khi cài đặt xong, khởi chạy redis bằng dòng lệnh sau:

$ cd redis-2.8.18/src
$ ./redis-server

Để đo hiệu năng, chúng ta sử dụng thư viện ruby "rack-mini-profiler". Thư viện này giups chúng ta kiểm tra hiệu năng ở ngoài view.

Chúng ta sẽ thực hiện bằng cách xây dụng một ứng dụng rails với 2 model Categories và Languages:

app/models/category.rb

class Category
  include Mongoid::Document
  include Mongoid::Timestamps
  include Mongoid::Paranoia

  include CommonMeta
end

app/models/language.rb

class Language
  include Mongoid: :Document
  include Mongoid::Timestamps
  include Mongoid::Paranoia

  include CommonMeta
end

app/models/concerns/common_meta.rb

module CommonMeta
  extend ActiveSupport::Concern
  included do
    field :name, :type => String
    field :desc, :type => String
    field :page_title, :type => String
  end
end

cùng build dữ liệu với seed.rb file rake db:seed Thực hiện lấy toàn bộ danh sách Category trong action index như bên dưới:

app/controllers/category_controller.rb

class CategoryController < ApplicationController
  include CategoryHelper
  def index
    @categories = Category.all
  end
end

app/helpers/category_helper.rb

module CategoryHelper
  def fetch_categories
    @categories = Category.all
  end
end

app/views/category/index.html.haml

%h1
  Category Listing
%ul#categories
  - @categories.each do |cat|
      %li
        %h3
          = cat.name
        %p
          = cat.desc

config.routes.rb

Rails.application.routes.draw do
  resources :languages
  resources :category
end

Khi sử dụng trình duyệt và trỏ đến /category, bạn sẽ tìm thấy mini-profiler điểm chuẩn thời gian thực hiện của mỗi hành động thực hiện. Việc này sẽ cung cấp cho bạn một ý tưởng để kiểm tra hiệu năng ứng dụng và làm thế nào để tối ưu hóa chúng. Trang này được thực hiện hai lệnh SQL và các truy vấn đã thực hiện khoảng 5ms để hoàn thành

enter image description here

2.1 Triển khai Redis

Có một Ruby client cho Redis giup chung ta kết nối với redis dễ dàng:

gem 'redis'
gem 'redis-namespace'
gem 'redis-rails'
gem 'redis-rack-cache'

Sau khi cài đặt xong, thực hiện config redis như sau:

config/application.rb

#...........
config.cache_store = :redis_store, 'redis://localhost:6379/0/cache', { expires_in: 90.minutes }
#.........

config/initializers/redis.rb

$redis = Redis::Namespace.new("site_point", :redis => Redis.new)

Bây gio, redis function thì có thể được gọi từ bất cứ đâu thông qua bến toàn cục $redis

Thực hiện việc caching và lấy dữ liệu

app/helpers/category_helper.rb

module CategoryHelper
  def fetch_categories
    categories =  $redis.get("categories")
    if categories.nil?
      categories = Category.all.to_json
      $redis.set("categories", categories)
    end
    @categories = JSON.load categories
  end
end

Lần đầu tiên, chúng ta sẽ kiểm tra xem dữ liệu đã được caching trong memory hay chưa. Nếu chưa thì chúng ta thực hiện truy cập vào database, lấy ra dữ liệu sau đó lưu vào cache. Chú ý là cần chuyển về json rồi mới caching. Cuối cùng là dùng JSON.load để lấy dữ liệu từ cache ra.

Tuy nhiên, đồng nghĩa với việc này chúng ra cần gọi dữ liệu thông qua dạng json object ở ngoài view:

app/views/category/index.html.haml

%h1
  Category Listing
%ul#categories
  - @categories.each do |cat|
    %li
      %h3
        = cat["name"]
      %p
        = cat["desc"]

Bây gio, quay lại brower và xem sự khác biệt so với trước đó. Sau lần đầu tiên, dữ liệu được gọi từ cache, việc này tiết kiệm nhiều tài nguyên cho chúng ta với một thay đổi đơn gian. enter image description here

2.3 Quản lý cache

Gỉa sử như chúng ta nhận thấy có một sai xót trong qúa trình tạo category, sau đó chúng ta sửa lại như sau:

$ rails c

c = Category.find_by :name => "Famly and Frends"
c.name = "Family and Friends"
c.save

Làm mới lại trình duyệt chúng ta sẽ thấy kết qủa: enter image description here

Chuyện gì đang xảy ra vậy, thay đổi này không hiện thị lên view của chúng ta. Điều này là bởi vì chúng ta đang không truy xuất vào database, tất cả các dữ liệu trả về vẫn đang được lấy từ cache. Để xử lý việc này, chúng ta thêm điều kiện sau:

app/helpers/category_helper.rb

module CategoryHelper
  def fetch_categories
    categories =  $redis.get("categories")
    if categories.nil?
      categories = Category.all.to_json
      $redis.set("categories", categories)
      # Expire the cache, every 3 hours
      $redis.expire("categories",3.hour.to_i)
    end
    @categories = JSON.load categories
  end
end

Cache sẽ bị hết hạn sau 3 giờ. Tuy nhiên, việc này cũng chỉ hoạt đọng trong một số trường hợp. Và chúng ta cần xử lý triệt để hơn. Chúng ta sử dụng call_back trong model:

app/models/category.rb

class Category
  #...........
  after_save :clear_cache

  def clear_cache
    $redis.del "categories"
  end
  #...........
end

Mỗi khi có thay đổi category object, thì Redis sẽ xóa toàn bộ cache. Điều này đảm bảo là cache luôn luôn update.

3. Tổng kết

Caching ở mức thấp thì đơn gianr và có thể làm được, nó thực sự có ích. Nó có thể giúp hệ thống của bạn tăng hiệu năng một cách đáng kể với một sự thay đổi nhỏ. Hi vọng bài viết sẽ giúp các bạn có cái nhìn về caching

Bài viết được dịch từ: http://www.sitepoint.com/rails-model-caching-redis/