Bộ nhớ đệm là một thuật ngữ chung có nghĩa là lưu trữ kết quả của một số mã để chúng ta có thể nhanh chóng truy xuất nó sau này. Điều này cho phép chúng tôi, ví dụ, tránh đánh đi đánh lại cơ sở dữ liệu để lấy dữ liệu hiếm khi thay đổi. Mặc dù khái niệm chung là giống nhau đối với tất cả các loại bộ nhớ đệm, Rails cung cấp cho chúng tôi các hỗ trợ khác nhau tùy thuộc vào những gì chúng tôi đang cố gắng lưu vào bộ nhớ đệm.
Đối với các nhà phát triển Rails, các dạng bộ nhớ đệm phổ biến bao gồm ghi nhớ, bộ nhớ đệm cấp thấp (cả hai đều được đề cập trong các phần trước của loạt bài viết này) và xem bộ nhớ đệm, chúng tôi sẽ đề cập ở đây.
Cách Ruby on Rails hiển thị các lượt xem
Đầu tiên, chúng ta hãy đề cập đến một số thuật ngữ hơi khó hiểu. Cái mà cộng đồng Rails gọi là "lượt xem" là các tệp nằm bên trong app/views
của bạn danh mục. Thông thường, đây là .html.erb
các tệp, mặc dù có các tùy chọn khác (tức là .html
thuần túy , .js.erb
hoặc các tệp sử dụng các bộ tiền xử lý khác, chẳng hạn như slim và haml). Trong nhiều khung công tác web khác, các tệp này được gọi là "mẫu", tôi nghĩ mô tả tốt hơn việc sử dụng chúng.
Khi ứng dụng Rails nhận được GET
yêu cầu, nó được chuyển đến một hành động bộ điều khiển cụ thể, ví dụ:UsersController#index
. Hành động sau đó chịu trách nhiệm thu thập bất kỳ thông tin cần thiết nào từ cơ sở dữ liệu và chuyển nó để sử dụng trong việc hiển thị tệp dạng xem / mẫu. Tại thời điểm này, chúng ta đang vào "lớp xem".
Thông thường, chế độ xem (hoặc mẫu) của bạn sẽ là hỗn hợp của đánh dấu HTML được mã hóa cứng và mã Ruby động:
#app/views/users/index.html.erb
<div class='user-list'>
<% @users.each do |user| %>
<div class='user-name'><%= user.name %></div>
<% end %>
</div>
Mã Ruby trong tệp cần được thực thi để hiển thị chế độ xem (đối với erb
đó là bất cứ thứ gì trong <% %>
thẻ). Làm mới trang 100 lần và @users.each...
sẽ được thực hiện 100 lần. Điều này cũng đúng đối với bất kỳ phần tử nào được bao gồm; bộ xử lý cần tải một phần html.erb
, thực thi tất cả mã Ruby bên trong nó và kết hợp các kết quả thành một tệp HTML duy nhất để gửi lại cho người yêu cầu.
Nguyên nhân khiến Lượt xem chậm
Bạn có thể nhận thấy rằng khi bạn xem một trang trong quá trình phát triển, Rails in ra rất nhiều thông tin nhật ký, trông giống như sau:
Processing by PagesController#home as HTML
Rendering layouts/application.html.erb
Rendering pages/home.html.erb within layouts/application
Rendered pages/home.html.erb within layouts/application (Duration: 4.0ms | Allocations: 1169)
Rendered layouts/application.html.erb (Duration: 35.9ms | Allocations: 8587)
Completed 200 OK in 68ms (Views: 40.0ms | ActiveRecord: 15.7ms | Allocations: 14307)
Dòng cuối cùng là hữu ích nhất đối với chúng tôi ở giai đoạn này. Bằng cách theo dõi thời gian từ trái sang phải, chúng tôi thấy rằng tổng thời gian mà Rails thực hiện để trả lại phản hồi cho trình duyệt là 68 mili giây, trong đó 40 mili giây được sử dụng để hiển thị erb
và 15,7ms khi xử lý các truy vấn ActiveRecord.
Mặc dù đó là một ví dụ nhỏ, nó cũng cho thấy lý do tại sao chúng ta có thể muốn xem xét bộ nhớ đệm của lớp chế độ xem. Ngay cả khi chúng tôi có thể thực hiện một cách kỳ diệu các truy vấn ActiveRecord xảy ra ngay lập tức, chúng tôi đang dành nhiều hơn hai lần thời gian để hiển thị erb
.
Có một vài lý do khiến việc hiển thị chế độ xem của chúng tôi có thể bị chậm; ví dụ:chúng tôi có thể đang gọi các truy vấn DB đắt tiền trong các khung nhìn hoặc thực hiện nhiều công việc trong các vòng lặp. Một trong những tình huống phổ biến nhất mà tôi đã thấy chỉ đơn giản là hiển thị rất nhiều phần tử, có lẽ với nhiều cấp độ lồng nhau.
Hãy tưởng tượng một hộp thư đến email, nơi chúng ta có thể có một phần xử lý một hàng riêng lẻ:
# app/views/emails/_email.html.erb
<li class="email-line">
<div class="email-sender">
<%= email.from_address %>
</div>
<div class="email-subject">
<%= email.subject %>
</div>
</div>
Và, trong trang hộp thư đến chính của chúng tôi, chúng tôi hiển thị một phần cho mỗi email:
# app/views/emails/index.html.erb
...
<% @emails.each do |email| %>
<%= render email %>
<% end %>
Nếu hộp thư đến của chúng tôi có 100 thư, thì chúng tôi đang hiển thị _email.html.erb
tiệc tùng 100 lần. Với ví dụ tầm thường của chúng tôi, điều này không đáng lo ngại lắm. Trên máy của tôi, một phần chỉ mất 15ms để hiển thị toàn bộ chỉ mục. Tất nhiên, các ví dụ trong thế giới thực sẽ phức tạp hơn và thậm chí có thể bao gồm các phần khác bên trong chúng; không khó để thời gian hiển thị tăng lên. Ngay cả khi chỉ mất 1-2 mili giây để hiển thị _email
của chúng tôi một phần, sẽ mất 100-200 mili giây để thực hiện toàn bộ bộ sưu tập.
May mắn thay, Rails có một số chức năng tích hợp để giúp chúng ta dễ dàng thêm bộ nhớ đệm để giải quyết vấn đề này, cho dù chúng ta chỉ muốn lưu vào bộ đệm ẩn __email
một phần, index
hoặc cả hai.
View Caching là gì
Xem bộ nhớ đệm trong Ruby on Rails đang sử dụng HTML mà một chế độ xem tạo ra và lưu trữ nó cho sau này. Mặc dù Rails có hỗ trợ ghi chúng vào hệ thống tệp hoặc giữ chúng trong bộ nhớ, để sử dụng trong sản xuất, bạn gần như chắc chắn muốn có một máy chủ lưu trữ bộ nhớ đệm độc lập, chẳng hạn như Memcached hoặc Redis. Rails 'memory_store
hữu ích cho việc phát triển nhưng không thể được chia sẻ trên các quy trình (ví dụ:nhiều máy chủ / máy chủ dynos hoặc máy chủ phân nhánh, chẳng hạn như kỳ lân). Tương tự, file_store
là cục bộ của máy chủ. Do đó, nó không thể được chia sẻ trên nhiều hộp và nó sẽ không tự động xóa các mục đã hết hạn, vì vậy bạn sẽ cần gọi Rails.cache.clear
theo định kỳ để ngăn đĩa máy chủ của bạn bị đầy.
Việc bật lưu trữ bộ nhớ đệm có thể được thực hiện trong tệp cấu hình môi trường của bạn (ví dụ:config/environments/production.rb
):
# memory store is handy for testing
# during development but not advisable
# for production
config.cache_store = :memory_store
Trong cài đặt mặc định, development.rb
của bạn sẽ có một số cấu hình được thực hiện cho bạn để cho phép dễ dàng chuyển đổi bộ nhớ đệm trên máy của bạn. Chỉ cần chạy rails dev:cache
để bật và tắt bộ nhớ đệm.
Lưu vào bộ nhớ đệm một chế độ xem trong Rails được cho là đơn giản, vì vậy để minh họa sự khác biệt về hiệu suất, tôi sẽ chỉ sử dụng sleep(5)
để tạo độ trễ giả tạo:
<% cache do %>
<div>
<p>Hi <%= @user.name %>
<% sleep(5) %>
</div>
<% end %>
Hiển thị chế độ xem này lần đầu tiên mất 5 giây, như mong đợi. Tuy nhiên, việc tải nó lên lần thứ hai chỉ mất vài mili giây vì mọi thứ bên trong cache do
khối được tìm nạp từ bộ nhớ cache.
Thêm chế độ xem vào bộ nhớ đệm theo ví dụ
Hãy xem một ví dụ nhỏ và xem qua các tùy chọn của chúng tôi để lưu vào bộ nhớ đệm. Chúng tôi sẽ giả định rằng chế độ xem này thực sự gây ra một số vấn đề về hiệu suất:
# app/views/user/show.html.erb
<div>
Hi <%= @user.name %>!
<div>
<div>
Here's your list of posts,
you've written
<%= @user.posts.count %> so far
<% @user.posts.each do |post|
<div><%= post.body %></div>
<% end %>
</div>
<% sleep(5) #artificial delay %>
Điều này cung cấp cho chúng tôi một khung cơ bản để làm việc cùng với độ trễ 5 giây nhân tạo của chúng tôi. Đầu tiên, chúng ta có thể kết hợp toàn bộ show.html.erb
tệp trong cache do
như đã mô tả trước đó. Bây giờ, khi bộ nhớ cache đã ấm, chúng ta sẽ có thời gian hiển thị nhanh và đẹp. Tuy nhiên, không mất nhiều thời gian để bắt đầu thấy các vấn đề với kế hoạch này.
Đầu tiên, điều gì sẽ xảy ra nếu người dùng thay đổi tên của họ? Chúng tôi chưa thông báo cho Rails khi nào hết hạn trang đã lưu trong bộ nhớ cache của chúng tôi, vì vậy người dùng có thể không bao giờ thấy phiên bản cập nhật. Một giải pháp dễ dàng là chỉ cần chuyển @user
đối tượng với cache
phương pháp:
<% cache(@user) do %>
<div>
Hi <%= @user.name %>!
</div>
...
<% sleep(5) #artificial delay %>
<% end %>
Bài viết trước trong loạt bài này về bộ nhớ đệm cấp thấp đã trình bày chi tiết về các phím bộ nhớ đệm, vì vậy tôi sẽ không trình bày lại ở đây. Bây giờ, đủ để biết rằng nếu chúng ta chuyển một mô hình vào cache()
, nó sẽ sử dụng updated_at
của mô hình đó để tạo khóa để tra cứu trong bộ nhớ đệm. Nói cách khác, bất cứ khi nào @user
được cập nhật, trang được lưu trong bộ nhớ cache này sẽ hết hạn và Rails sẽ hiển thị lại HTML.
Chúng tôi đã đề phòng trường hợp người dùng thay đổi tên của họ, nhưng còn bài viết của họ thì sao? Thay đổi bài đăng hiện có hoặc tạo bài đăng mới sẽ không thay đổi updated_at
dấu thời gian của User
, vì vậy trang đã lưu trong bộ nhớ cache của chúng tôi sẽ không hết hạn. Ngoài ra, nếu người dùng thay đổi tên của họ, chúng tôi sẽ hiển thị lại tất cả trong số các bài đăng của họ, ngay cả khi bài đăng của họ không thay đổi. Để giải quyết cả hai vấn đề này; chúng ta có thể sử dụng "bộ nhớ đệm cho búp bê Nga" (tức là bộ nhớ đệm trong bộ nhớ đệm):
<% cache(@user) do %>
<div>
Hi <%= @user.name %>!
<div>
<div>
Here's your list of posts,
you've written
<%= @user.posts.count %> so far<br>
<% @user.posts.each do |post| %>
<% cache(post) do %>
<div><%= post.body %></div>
<% end %>
<% end %>
</div>
<% sleep(5) #artificial delay %>
<% end %>
Chúng tôi hiện đang lưu vào bộ nhớ đệm từng post
được hiển thị riêng lẻ (trong thế giới thực, điều này có thể là một phần). Do đó, ngay cả khi @user
được cập nhật, chúng tôi không phải kết xuất lại bài đăng; chúng ta chỉ có thể sử dụng giá trị được lưu trong bộ nhớ cache. Tuy nhiên, chúng tôi vẫn còn một vấn đề nữa. Nếu một post
bị thay đổi, chúng tôi vẫn sẽ không hiển thị bản cập nhật vì @user.update_at
không thay đổi, vì vậy khối bên trong cache(@user) do
sẽ không thực thi.
Để khắc phục sự cố này, chúng tôi cần thêm touch: true
tới Post
của chúng tôi mô hình:
class Post < ApplicationRecord
belongs_to :user, touch: true
end
Bằng cách thêm touch: true
ở đây, chúng tôi đang nói với ActiveRecord rằng mọi lúc một bài đăng được cập nhật, chúng tôi muốn updated_at
dấu thời gian của người dùng mà nó "thuộc về" cũng sẽ được cập nhật.
Tôi cũng nên nói thêm rằng Rails cung cấp một trình trợ giúp cụ thể để hiển thị một bộ sưu tập các phần tử, với mức độ phổ biến của nó:
<%= render partial: 'posts/post',
collection: @posts, cached: true %>
Tương đương về mặt chức năng như sau:
<% @posts.each do |post| %>
<% cache(post) do %>
<%= render post %>
<% end %>
<% end %>
render partial: ... cached: true
hình thức ít dài dòng hơn, nó cũng mang lại cho bạn một số hiệu quả hơn vì Rails có thể phát hành một đa mục tiêu cho kho lưu trữ bộ nhớ cache (tức là đọc nhiều cặp khóa / giá trị trong một chuyến đi vòng lại) thay vì truy cập lưu trữ bộ nhớ cache của bạn cho từng mục trong bộ sưu tập.
Nội dung trang động
Một số trang thường bao gồm một số nội dung 'động' thay đổi với tốc độ nhanh hơn nhiều so với phần còn lại của trang xung quanh nó. Điều này đặc biệt đúng trên các trang chủ hoặc trang tổng quan, nơi bạn có thể có nguồn cấp dữ liệu hoạt động / tin tức. Việc bao gồm những điều này trong trang đã lưu trong bộ nhớ cache của chúng tôi có thể có nghĩa là bộ nhớ cache của chúng tôi cần phải bị vô hiệu hóa thường xuyên, điều này hạn chế lợi ích mà chúng tôi nhận được từ bộ nhớ đệm ngay từ đầu.
Ví dụ đơn giản, hãy thêm ngày hiện tại vào chế độ xem của chúng tôi:
<% cache(@user) do %>
<div>
Hi <%= @user.name %>,
hope you're having a great
<%= Date.today.strftime("%A") %>!
<div>
...
<% end %>
Chúng tôi có thể làm mất hiệu lực bộ nhớ cache mỗi ngày, nhưng điều đó không thực tế lắm vì những lý do rõ ràng. Một tùy chọn là sử dụng giá trị trình giữ chỗ (hoặc thậm chí chỉ là <span>
trống ) và điền nó bằng javascript. Loại tiếp cận này thường được gọi là "javascript rắc" và là một cách tiếp cận được Basecamp phần lớn ưa chuộng, nơi rất nhiều mã lõi của Rails được phát triển. Kết quả sẽ như thế này:
<% cache(@user) do %>
<div>
Hi <%= @user.name %>,
hope you're having a great
<span id='greeting-day-name'>Day</span>!
<div>
...
<% end %>
<script>
// assuming you're using vanilla JS with turbolinks
document.addEventListener(
"turbolinks:load", function() {
weekdays = new Array('Sunday', 'Monday',
'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday');
today = weekdays[new Date().getDay()];
document.getElementById("greeting-day-name").textContent=today;
});
</script>
Một cách tiếp cận khác là chỉ lưu vào bộ nhớ cache một số phần của khung nhìn. Trong ví dụ của chúng tôi, lời chào nằm ở đầu trang, vì vậy, khá đơn giản nếu chỉ lưu vào bộ nhớ cache những gì sau:
<div>
Hi <%= @user.name %>,
hope you're having a great
<%= Date.today.strftime("%A") %>!
<div>
<% cache(@user) do %>
...
<% end %>
Rõ ràng, điều này thường không đơn giản với các bố cục bạn tìm thấy trong thế giới thực, vì vậy bạn sẽ phải cân nhắc về vị trí và cách bạn áp dụng bộ nhớ đệm.
Lời cảnh báo
Có thể dễ dàng coi bộ nhớ đệm như một liều thuốc chữa bách bệnh nhanh chóng và dễ dàng cho các vấn đề về hiệu suất. Thật vậy, Rails làm cho nó trở nên vô cùng dễ dàng lưu các chế độ xem và phần chia vào bộ nhớ cache, ngay cả khi chúng được lồng sâu vào nhau. Trong bài viết đầu tiên của loạt bài này, tôi đã trình bày các vấn đề có thể phát sinh khi bạn thêm bộ nhớ đệm vào hệ thống của mình, nhưng tôi nghĩ điều này đặc biệt đúng với bộ nhớ đệm ở cấp độ xem.
Lý do là các khung nhìn, về bản chất của chúng, có xu hướng tương tác nhiều hơn với dữ liệu cơ bản của một hệ thống. Khi bạn áp dụng tính năng ghi nhớ hoặc bộ nhớ đệm cấp thấp trong Rails, bạn thường không cần phải nhìn ra bên ngoài tệp mà bạn đang ở để xác định thời điểm và lý do nên làm mới giá trị đã lưu trong bộ nhớ cache. Mặt khác, một chế độ xem có thể có nhiều mô hình khác nhau được gọi và nếu không lập kế hoạch có chủ ý, có thể khó biết được mô hình nào sẽ khiến phần nào của chế độ xem được hiển thị lại tại thời điểm đó.
Cũng như với bộ nhớ đệm cấp thấp, lời khuyên tốt nhất là nên có chiến lược về vị trí và thời điểm bạn sử dụng nó. Sử dụng ít bộ nhớ đệm nhất có thể, ở ít nơi nhất có thể, để đạt được mức hiệu suất có thể chấp nhận được.
Rails 'Cache theo mặc định
Cho đến nay trong loạt bài về bộ nhớ đệm này, chúng tôi đã đề cập đến các cách lưu trữ mọi thứ theo cách thủ công nhưng ngay cả khi không có bất kỳ cấu hình thủ công nào, ActiveRecord đã thực hiện một số bộ nhớ đệm ẩn để tăng tốc các truy vấn (hoặc bỏ qua chúng hoàn toàn). Trong bài viết tiếp theo của loạt bài về bộ nhớ đệm này, chúng ta sẽ xem xét ActiveRecord đang lưu vào bộ nhớ đệm gì đối với chúng ta và làm thế nào, với một số lượng công việc nhỏ, chúng ta có thể đặt nó giữ một "bộ nhớ đệm bộ đếm" để các dòng như thing.children.size
hoàn toàn không phải nhấn vào cơ sở dữ liệu để có được số lượng cập nhật.