Fragment Caching In Rails

Fragment Caching là cách lưu trữ một phần views vào trong cache. Thường thì sẽ caching partials hoặc một phần lớn HTML, và những nội dung được caching sẽ riêng biệt, không liên quan tới những phần khác. Ví dụ như: Một danh sách employees, hoặc những nội dung tương tự như vậy.

1. Hoạt động?

Rails cung cấp cache view helper cho Fragment Caching. Đơn giản nhất là không cần một tham số nào bên trong block cache. Bất cứ gì được render trong block sẽ được lưu vào cache. Nguyên tắc cơ bản bên trong Fragment Caching là làm sao để tốn ít thời gian render lại HTML từ cache. Nếu như chúng ta không chú ý thì việc đó sẽ gây lãng phí tài nguyên rất lớn. Vậy nên chúng ta cần triển khai caching đúng cách.

Để bắt đầu, chúng ta cần hiểu quá trình tạo và đọc nội dung từ cache.

Ví dụ:

$ rails g scaffold post title:string content:text author:string

Tạo ra cache như sau:

<%= cache "post-#{@post.id}" do %>
  <p>
    <b>Title:</b>
    <%= @post.title %>
  </p>

  <p>
    <b>Content:</b>
    <%= @post.content %>
  </p>
<% end %>

Lần đầu tiên khi chúng ta thực hiện request, chúng ta nhận được:

Exist fragment? views/post-2 (1.6ms)
Write fragment views/post-2 (0.9ms)

Chúng ta có thể thấy được, Rails kiểm tra để biết được cache tồn tại với cache key hay không. Từ đó nó sẽ thực hiện lấy cache content hay tạo ra cache. Trường hợp này, không có cache content, do đó cache content sẽ được tạo ra. Bây giờ, chúng ta thực hiện một request tương tự khác và cùng xem kết quả:

Exist fragment? views/post-2 (0.6ms)
Read fragment views/post-2 (0.0ms)

Lần này, nội dung được lấy ra từ cache thay vì render.

Tuy nhiên, khi nội dung của post bị thay đổi thì sao? Khi post thay đổi thì việc đọc từ cache content và hiện thị lên không còn đúng nữa. Chúng ta đều biết, cache được build ra theo key-value. Vậy nên, để xử lý vấn đề trên, chỉ cần tạo thay đổi cache key mỗi khi post thay đổi là được. Có một trường bên trong mỗi object luôn thay đổi khi object thay đổi là updated_at. Do đó, việc sử dụng updated_at để làm cache key là một lựa chọn đơn giản và hiểu quả. Cách này sẽ thay đổi cache key mỗi khi mà nội dung thay đổi, đồng nghĩa với việc chúng ta sẽ không cần phải can thiệp tới những cache hết hạn bằng cách thông thường.

Thay đổi cahe key như sau:

<% cache "post-#{@post.id}", @post.updated_at.to_i do %>

Thử request lại và xem rails log chúng ta sẽ thấy:

Exist fragment? views/post-2/1304291241 (0.5ms)
Write fragment views/post-2/1304291241 (0.4ms)

OK, cache key đã thay đổi 😃.

Tiếp theo, chúng ta tạo ra model Comment có relation với Post theo mối quan hệ:

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

View của chúng ta thay đổi như sau:

<% cache "post-#{@post.id}", @post.updated_at.to_i do %>
  <p>
    <b>Title:</b>
    <%= @post.title %>
  </p>

  <p>
    <b>Content:</b>
    <%= @post.content %>
  </p>

  <%= render @post.comments %>
<% end %>

Có một vấn đề mà chúng ta sẽ gặp phải đối với việc hiện thị comments của post như sau:

  • Khi thêm comment vào post
  • Khi sửa comment của post
  • Khi xóa comment của post

Rõ ràng, cache key sẽ không thay đổi vì post object không thay đổi. Và để hiện thị được comment đúng như dữ liệu đang có bắt buộc phải thay đổi cache key khi rơi vào những trường hợp vừa nêu trên.

Rails đã có giải pháp cho chúng ta, sử dụng touch option khi khai báo relation trong model Comment:

class Comment < ActiveRecord::Base
  belongs_to :post, touch: true
end

Bây giờ, tất cả comments sẽ touch post và change updated_at timestamp.

Post.find(1).touch

Exist fragment? views/post-2/1304292445 (0.4ms)
Write fragment views/post-2/1304292445 (0.4ms)

Khái niệm này được biết đến như là: auto expiring cache keys. Chúng ta tạo ra một hỗn hợp cache key với normal keytime stamp. Sau đây là một ví dụ: Chúng ta có fragmen, và nó được cache. Sau đó chúng ta thực hiện update post. Ngay sau đó, chúng ta sẽ có 2 version của fragment cache. Nếu như có 10 lần update, tất nhiên chúng ta sẽ có 10 version cache khác nhau. Dường như chúng ta đang lãng phí bộ nhớ để lưu trữ những cache content đã cũ và không cần thiết. Tuy nhiên, thực sự không phải, Memcached sử dụng cơ chế thay thế LRU (Least Recently Used). Điều này có nghĩa là những cache key không được sử dụng trong một thời gian dài sẽ bị thay thế bằng nội dung mới khi cần thiết.

Ví dụ, giả sử cache chỉ có thể giữ được 10 posts cache version. Lần update tiếp theo sẽ tạo ra một cache key mới và nội dung mới. Lúc đó version 0 sẽ bị xóa và version 11 sẽ được lưu trữ vào cache.

2. Cache key?

Chúng ta sử dụng tham số đầu vào để tạo ra cache key cho content mà chúng ta muốn cache.

Giống như ví dụ trên, chúng ta sử dụng post-#{@post.id} để tạo ra một cache key.

Một cache key sẽ có dạng sau:

#   views/posts/123-20120806214154/7a1156131a6928cb0026877f8b749ac9

Trong đó:

  • posts là class
  • 123 là id của object
  • 20120806214154 là updated_at
  • 7a1156131a6928cb0026877f8b749ac9 là template tree digest

Template digest được thêm vào cache key bằng cách lấy md5 của nội dung toàn bộ template file. Việc này đảm bảo rằng cache sẽ tự động hết hiệu lực khi mà chúng ta thay đổi nội dung trong tương lai.

Dưới đây là dòng code tạo ra template digest được lấy ra từ cache helper

digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies

Tùy vào đầu vào mà cache key sẽ được tạo ra khác nhau:

[rails c]

include ActionController::Caching::Fragments
fragment_cache_key [User.first, "hello", :monkey, false]
=> "views/users/1-20151226165702000000000/hello/monkey/false"

fragment_cache_key User.first
=> "views/users/1-20151226165702000000000"

Tài liệu tham khảo: