Hôm nay chúng ta sẽ đi sâu vào Russian Doll Caching như một tactic để cải thiện caching hơn nữa so với fragment caching built-in của Rails.
Fragment Caching
Khi dùng fragment caching built-in của Rails, phần của rendered view được lưu làm view fragment và tái sử dụng nếu được request lại. Các cached fragment này được tái dùng cho đến khi chúng trở nên stale (cũ), nghĩa là outdated vì data hiển thị đã thay đổi kể từ khi tạo fragment. Nếu muốn đọc thêm, bài trước chúng tôi giải thích fragment caching sâu hơn.
👋 Nếu bạn thích đọc về hiệu suất rộng hơn caching, xem checklist giám sát hiệu suất Ruby của chúng tôi.
Mặc dù mang来不错 speed boost, đặc biệt cho view phức tạp hoặc nhiều rendered partial, ta có thể cải thiện thêm bằng approach tên là Russian doll caching.
Khi dùng caching approach này, view fragment được đặt lồng vào nhau, giống như những chiếc búp bê "Matryoshka" mà chiến lược này đặt tên theo. Bằng cách chia nhỏ cached fragment thành các mảnh nhỏ hơn, outer cache có thể render nhanh hơn khi chỉ một nested fragment thay đổi.
Ví Dụ Russian Doll Caching
Làm ví dụ, hãy dùng store bán sản phẩm. Mỗi sản phẩm có thể có nhiều variants, cho phép bán nhiều màu của một item. Trên index, ta hiển thị từng sản phẩm available for sale, cũng như tất cả variants của nó.
Trên product index, ta bọc mỗi product partial trong block cache. Ta dùng object product để build cache key, dùng để invalidate cached fragment. Key bao gồm id của object, ngày updated_at, và digest của template tree, nên tự động coi là stale nếu object thay đổi, hoặc nội dung template thay đổi.
# app/views/products/index.html.erb <h1>Products</h1> <% @products.each do |product| %> <% cache product do %> <%= render product %> <% end %> <% end %>Mẹo: Ta viết toàn bộ block cho rõ ràng, nhưng bạn có thể render từng product trong cache block bằng <%= render partial: 'products/product', collection: @products, cached: true %>.
Trong products partial, ta render một row cho mỗi variant của sản phẩm.
# app/views/products/_product.html.erb <article> <h1><%= product.title %></h1> <ul> <% product.variants.each do |variant| %> <%= render variant %> <% end %> </ul> </article>Cache Invalidation
Mặc dù cache key trong fragment caching của Rails làm cache invalidation dễ dàng hơn, bạn không bao giờ hoàn toàn thoát khỏi lo lắng về cache validation (một trong hai việc khó nổi tiếng trong khoa học máy tính).
Trong ví dụ này, ta cache products partial, chứa danh sách variants của sản phẩm. Vì cache key không bao gồm thông tin về variants, variants mới thêm vào sẽ không hiển thị trừ khi chính sản phẩm cũng thay đổi.
Cách fix là đảm bảo sản phẩm có thay đổi khi bất cứ thứ gì thay đổi trong variants. Để làm vậy, ta update attribute updated_at của sản phẩm khi đó xảy ra. Vì rất phổ biến, có argument cho belongs_to (và các relation method khác của ActiveModel), tên :touch, tự động update updated_at của parent object.
class Variant < ApplicationRecord belongs_to :product, touch: true endNested Fragments
Bây giờ đã đảm bảo product fragment update khi variants thay đổi, đến lúc cache variants. Giống như trước, ta thêm block cache quanh mỗi variant.
<article> <h1><%= product.title %></h1> <ul> <% product.variants.each do |variant| %> <% cache(variant) do %> <%= render variant %> <% end %> <% end %> </ul> </article>
Mẹo: Ta viết toàn bộ block cho rõ ràng, nhưng bạn có thể render từng variant trong cache block bằng <%= render partial: 'variants/variant', collection: product.variants, cached: true %>.
Trên cold cache (có thể clear cache bằng rake tmp:cache:clear), request đầu tiên sẽ render từng product partial.
Khi request trang này (đừng quên bật caching trong development bằng rails dev:cache), mỗi product partial sẽ được cached as partial, và request thứ hai trả về cached fragment.
Started GET "/products" for 127.0.0.1 at 2018-03-30 14:51:38 +0200 Processing by ProductsController#index as HTML Rendering products/index.html.erb within layouts/application Product Load (0.2ms) SELECT "products".* FROM "products" Variant Load (0.9ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51) Rendered variants/_variant.html.erb (0.5ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.0ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered products/_product.html.erb (44.8ms) [cache miss] ... Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered variants/_variant.html.erb (0.1ms) Rendered products/_product.html.erb (46.2ms) [cache miss] Rendered products/index.html.erb within layouts/application (1378.6ms) Completed 200 OK in 1414ms (Views: 1410.5ms | ActiveRecord: 1.1ms) Started GET "/products" for 127.0.0.1 at 2018-03-30 14:51:41 +0200 Processing by ProductsController#index as HTML Rendering products/index.html.erb within layouts/application Product Load (0.3ms) SELECT "products".* FROM "products" Variant Load (12.7ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51) Rendered products/index.html.erb within layouts/application (48.1ms) Completed 200 OK in 76ms (Views: 59.0ms | ActiveRecord: 13.0ms)
Voila: Ma Thuật Russian Doll
Ma thuật của Russian doll caching hiện ra khi thay đổi một variant. Khi request index lại sau khi một variant thay đổi, cached product fragment được render lại vì attribute updated_at thay đổi.
Product partial bao gồm các variant của sản phẩm. Cached fragment cho variant vừa thay đổi đã stale, cần regenerate, nhưng các variant khác không thay đổi, nên cached fragment của chúng được tái dùng. Trong log, ta thấy cả variant và product partial được render một lần.
Started GET "/products" for 127.0.0.1 at 2018-03-30 14:52:04 +0200 Processing by ProductsController#index as HTML Rendering products/index.html.erb within layouts/application Product Load (0.3ms) SELECT "products".* FROM "products" Variant Load (1.2ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51) Rendered variants/_variant.html.erb (0.5ms) Rendered products/_product.html.erb (13.3ms) [cache miss] Rendered products/index.html.erb within layouts/application (45.9ms) Completed 200 OK in 78ms (Views: 73.5ms | ActiveRecord: 1.5ms)
Kết Quả
Bằng cách lồng cache fragment như này, view gần như không bao giờ render hoàn toàn, trừ khi cache hoàn toàn trống. Ngay khi data thay đổi, hầu hết rendered page được serve thẳng từ cache.
Hy vọng bài này giúp bạn có insight mới về hiệu suất app. Đó là lý do chúng tôi ở đây. Nếu thích bài về caching, và muốn thêm, những "snack" bổ sung là bài về ActiveRecord Counter Cache, bài về Cache Stores in Rails, bài về collection caching và bài về fragment caching in Rails đã nhắc ở trên.