edge
เพิ่มเติมที่ rubyonrails.org: เพิ่มเติมเกี่ยวกับ Ruby on Rails

การใช้แคชกับ Rails: ภาพรวม

เอกสารนี้เป็นการแนะนำในการเพิ่มความเร็วให้แอปพลิเคชัน Rails ของคุณด้วยการใช้แคช

แคชหมายถึงการเก็บเนื้อหาที่สร้างขึ้นระหว่างรอบของคำขอและการตอบสนองและนำมาใช้ใหม่เมื่อตอบสนองต่อคำขอที่คล้ายกัน

การใช้แคชเป็นวิธีที่มีประสิทธิภาพที่สุดในการเพิ่มประสิทธิภาพของแอปพลิเคชัน ผ่านการใช้แคชเว็บไซต์ที่ทำงานบนเซิร์ฟเวอร์เดียวกับฐานข้อมูลเดียวสามารถรองรับการใช้งานพร้อมกันของผู้ใช้หลายพันคนได้

Rails มีคุณสมบัติการใช้แคชที่มาพร้อมกัน ในเอกสารนี้จะสอนคุณขอบเขตและวัตถุประสงค์ของแต่ละอย่าง หากคุณเรียนรู้เทคนิคเหล่านี้แล้ว แอปพลิเคชัน Rails ของคุณสามารถให้บริการมุมมองหลายล้านโดยไม่ต้องใช้เวลาตอบสนองมากหรือเสียค่าบริการเซิร์ฟเวอร์มากเกินไป

หลังจากอ่านเอกสารนี้คุณจะรู้:

1 การใช้แคชเบื้องต้น

นี่คือการแนะนำในการใช้เทคนิคแคชสามประเภท: แคชหน้า แคชแอคชัน และแคชฟรากเมนต์ โดยค่าเริ่มต้น Rails จะให้แคชฟรากเมนต์ ในการใช้แคชหน้าและแคชแอคชันคุณจะต้องเพิ่ม actionpack-page_caching และ actionpack-action_caching เข้าไปใน Gemfile ของคุณ

โดยค่าเริ่มต้นแคชจะเปิดใช้งานเฉพาะในสภาพแวดล้อมการพัฒนาของคุณ คุณสามารถทดลองใช้แคชในเครื่องคอมพิวเตอร์ของคุณโดยการเรียกใช้ rails dev:cache หรือโดยการตั้งค่า config.action_controller.perform_caching เป็น true ใน config/environments/development.rb

หมายเหตุ: เปลี่ยนค่าของ config.action_controller.perform_caching จะมีผลเฉพาะกับแคชที่ Action Controller ให้ ตัวอย่างเช่น มันจะไม่มีผลต่อการแคชระดับต่ำที่เรากล่าวถึงด้านล่าง

1.1 แคชหน้า

แคชหน้าเป็นกลไกของ Rails ที่ช่วยให้คำขอสำหรับหน้าที่สร้างขึ้นสามารถได้รับการบริการจากเว็บเซิร์ฟเวอร์ (เช่น Apache หรือ NGINX) โดยไม่ต้องผ่านทั้งสแต็กของ Rails ทั้งหมด แม้ว่าสิ่งนี้จะเร็วมาก แต่ไม่สามารถใช้กับทุกสถานการณ์ได้ (เช่นหน้าที่ต้องการการรับรองตัวตน) นอกจากนี้ เนื่องจากเว็บเซิร์ฟเวอร์กำลังให้บริการไฟล์โดยตรงจากไฟล์ระบบไฟล์คุณจะต้องดำเนินการหมดอายุแคชด้วยตนเอง

ข้อมูล: แคชหน้าถูกลบออกจาก Rails 4 ดู actionpack-page_caching gem

1.2 แคชแอคชัน

แคชหน้าไม่สามารถใช้สำหรับแอคชันที่มีตัวกรองก่อน - ตัวอย่างเช่นหน้าที่ต้องการการรับรองตัวตน นี่คือที่แคชแอคชันเข้ามาช่วย แคชแอคชันทำงานเหมือนกับแคชหน้า ยกเว้นคำขอเว็บที่เข้ามาจะถูกส่งไปยังสแต็กของ Rails เพื่อให้สามารถเรียกใช้ตัวกรองก่อนแคชได้ นี่ช่วยให้สามารถเรียกใช้การรับรองตัวตนและข้อจำกัดอื่น ๆ ได้ในขณะที่ยังคงให้ผลลัพธ์จากการแคช

ข้อมูล: แคชแอคชันถูกลบออกจาก Rails 4 ดู actionpack-action_caching gem ดู ภาพรวมการหมดอายุแคชตามคีย์ของ DHH สำหรับวิธีการที่แนะนำใหม่

1.3 แคชฟรากเมนต์

แอปพลิเคชันเว็บแบบไดนามิกทั่วไปมักจะสร้างหน้าด้วยส่วนประกอบต่าง ๆ ที่มีลักษณะการแคชที่แตกต่างกัน หากต้องการแคชและหมดอายุส่วนประกอบต่าง ๆ ของหน้าแยกกันคุณสามารถใช้แคชฟรากเมนต์ได้

แคชฟรากเมนต์ช่วยให้ส่วนประกอบของตัวตนมุมมองถูกห่อหุ้มในบล็อกแคชและถูกเก็บไว้ในร้านค้าแคชเมื่อคำขอถัดไปมาถึง ตัวอย่างเช่นหากคุณต้องการเก็บแคชสินค้าแต่ละรายการในหน้าเว็บ คุณสามารถใช้โค้ดนี้ได้:

<% @products.each do |product| %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

เมื่อแอปพลิเคชันของคุณได้รับคำขอแรกสำหรับหน้านี้ Rails จะเขียนแคชเข้าสู่ระบบด้วยคีย์ที่ไม่ซ้ำกัน คีย์จะมีลักษณะเช่นนี้:

views/products/index:bea67108094918eeba42cd4a6e786901/products/1

สตริงของตัวอักษรที่อยู่ตรงกลางคือการเข้ารหัสแบบต้นไม้ของเทมเพลต มันเป็นการเข้ารหัสแบบต้นไม้ที่คำนวณขึ้นจากเนื้อหาของเฟรกเมนต์วิวที่คุณกำลังเก็บแคช หากคุณเปลี่ยนเฟรกเมนต์วิว (เช่น HTML เปลี่ยนแปลง) เครื่องหมายการเข้ารหัสจะเปลี่ยนแปลง ทำให้ไฟล์ที่มีอยู่ในแคชหมดอายุ

เวอร์ชันแคชที่ได้มาจากบันทึกสินค้าจะถูกเก็บไว้ในรายการแคช หากสินค้าถูกแตะ (touched) เวอร์ชันแคชจะเปลี่ยนแปลง และเฟรกเมนต์ที่ถูกแคชที่มีเวอร์ชันก่อนหน้านั้นจะถูกละเลย

เคล็ดลับ: ร้านค้าแคชเช่น Memcached จะลบไฟล์แคชเก่าๆอัตโนมัติ

หากคุณต้องการเก็บแคชเฟรกเมนต์ภายใต้เงื่อนไขบางอย่าง คุณสามารถใช้ cache_if หรือ cache_unless:

<% cache_if admin?, product do %>
  <%= render product %>
<% end %>

1.3.1 การเก็บแคชของคอลเลกชัน

เฮลเปอร์ render ยังสามารถเก็บแคชเทมเพลตแต่ละรายการที่ถูกเรนเดอร์สำหรับคอลเลกชันได้ มันยังสามารถทำได้ดีกว่าตัวอย่างก่อนหน้านี้ด้วย each โดยอ่านเทมเพลตแคชทั้งหมดในครั้งเดียวแทนที่จะทำหนึ่งต่อหนึ่ง สามารถทำได้โดยการส่ง cached: true เมื่อเรนเดอร์คอลเลกชัน:

<%= render partial: 'products/product', collection: @products, cached: true %>

เทมเพลตแคชที่เก็บไว้จากการเรนเดอร์ก่อนหน้านี้จะถูกดึงขึ้นทั้งหมดในครั้งเดียวโดยมีความเร็วที่ดีกว่ามาก นอกจากนี้ เทมเพลตที่ยังไม่ได้รับแคชจะถูกเขียนลงในแคชและดึงขึ้นมาพร้อมกันในการเรนเดอร์ครั้งถัดไป

1.4 การเก็บแคชแบบรัสเซียนดอลล์

คุณอาจต้องการซ้อนเฟรกเมนต์แคชภายในเฟรกเมนต์แคชอื่น ซึ่งเรียกว่าการเก็บแคชแบบรัสเซียนดอลล์

ข้อดีของการเก็บแคชแบบรัสเซียนดอลล์คือหากมีการอัปเดตสินค้าเพียงรายการเดียว แฟรกเมนต์ภายในที่เหลืออาจถูกใช้ซ้ำเมื่อสร้างแฟรกเมนต์ภายนอกใหม่

ตามที่อธิบายในส่วนก่อนหน้านี้ ไฟล์แคชจะหมดอายุหากมีการเปลี่ยนแปลงค่าของ updated_at สำหรับเรคคอร์ดที่ไฟล์แคชขึ้นอยู่ อย่างไรก็ตาม สิ่งนี้จะไม่ทำให้แคชที่ฟรากเมนต์ถูกซ้ำในกรณีที่ฟรากเมนต์อยู่ภายใน

ตัวอย่างเช่น ดูวิวต่อไปนี้:

<% cache product do %>
  <%= render product.games %>
<% end %>

ซึ่งในลำดับต่อมาจะเรนเดอร์วิวนี้:

<% cache game do %>
  <%= render game %>
<% end %>

หากมีการเปลี่ยนแปลงคุณสมบัติใดๆของเกม เวลาที่ updated_at จะถูกตั้งค่าเป็นเวลาปัจจุบัน ทำให้แคชหมดอายุ อย่างไรก็ตาม เนื่องจาก updated_at จะไม่เปลี่ยนแปลงสำหรับวัตถุสินค้า แคชนั้นจะไม่หมดอายุและแอปของคุณจะให้บริการข้อมูลที่เก่า ในการแก้ไขปัญหานี้ เราจะผูกโมเดลเข้าด้วยกันด้วยวิธี touch:

class Product < ApplicationRecord
  has_many :games
end

class Game < ApplicationRecord
  belongs_to :product, touch: true
end

เมื่อตั้งค่า touch เป็น true การกระทำใดๆที่เปลี่ยนค่า updated_at สำหรับเรคคอร์ดเกมจะเปลี่ยนค่า updated_at สำหรับสินค้าที่เกี่ยวข้องด้วย ทำให้แคชหมดอายุ

1.5 การแบ่งปันการใช้งานแคชบางส่วน

สามารถแบ่งปันแคชบางส่วนและการแคชที่เกี่ยวข้องระหว่างไฟล์ที่มีประเภท MIME ต่างกันได้ ตัวอย่างเช่น การแบ่งปันแคชบางส่วนช่วยให้ผู้เขียนเทมเพลตสามารถแบ่งปันส่วนบางส่วนระหว่างไฟล์ HTML และไฟล์ JavaScript ได้ โดยเมื่อเก็บรวบรวมเทมเพลตในไฟล์เส้นทางการแก้ปัญหาเทมเพลต มันจะรวมเฉพาะส่วนขยายของภาษาเทมเพลตเท่านั้นและไม่รวมประเภท MIME เนื่องจากเหตุนี้เทมเพลตสามารถใช้สำหรับประเภท MIME หลายประเภท การร้องขอ HTML และ JavaScript จะตอบสนองต่อโค้ดต่อไปนี้:

render(partial: 'hotels/hotel', collection: @hotels, cached: true)

จะโหลดไฟล์ที่ชื่อ hotels/hotel.erb.

ตัวเลือกอื่น ๆ คือการรวมชื่อไฟล์แบบเต็มของส่วนบางส่วนที่จะแสดงผล

render(partial: 'hotels/hotel.html.erb', collection: @hotels, cached: true)

จะโหลดไฟล์ที่ชื่อ hotels/hotel.html.erb ในประเภท MIME ไฟล์ใดก็ได้ เช่นคุณสามารถรวมส่วนนี้ในไฟล์ JavaScript ได้

1.6 การจัดการความขึ้นต่อกันของข้อมูลที่ต้องการ

เพื่อที่จะยกเลิกแคชได้อย่างถูกต้อง คุณต้องกำหนดความขึ้นต่อกันของข้อมูลที่ต้องการอย่างถูกต้อง Rails สามารถจัดการกรณีที่พบบ่อยเองได้เพื่อให้คุณไม่ต้องระบุอะไรเลย อย่างไรก็ตาม บางครั้ง เมื่อคุณมีการใช้งานเฮลเปอร์ที่กำหนดเอง เช่น คุณจะต้องระบุเอง

1.6.1 ความขึ้นต่อกันแบบอัตโนมัติ

ส่วนใหญ่ความขึ้นต่อกันของเทมเพลตสามารถได้รับจากการเรียกใช้ render ในเทมเพลตเอง ตัวอย่างเช่นการเรียกใช้ render ที่ ActionView::Digestor รู้จักแปลงเป็นรหัส:

render partial: "comments/comment", collection: commentable.comments
render "comments/comments"
render 'comments/comments'
render('comments/comments')

render "header" จะแปลงเป็น render("comments/header")

render(@topic)         จะแปลงเป็น render("topics/topic")
render(topics)         จะแปลงเป็น render("topics/topic")
render(message.topics) จะแปลงเป็น render("topics/topic")

อย่างไรก็ตาม บางครั้งคุณต้องเปลี่ยนการเรียกใช้เพื่อให้การแคชทำงานอย่างถูกต้อง ตัวอย่างเช่น หากคุณกำลังส่งค่าคอลเลกชันที่กำหนดเอง คุณจะต้องเปลี่ยนจาก:

render @project.documents.where(published: true)

เป็น:

render partial: "documents/document", collection: @project.documents.where(published: true)

1.6.2 ความขึ้นต่อกันแบบชัดเจน

บางครั้งคุณอาจมีความขึ้นต่อกันของเทมเพลตที่ไม่สามารถได้มาจากการแปลงเอง นี่เป็นกรณีที่พบบ่อยเมื่อการแสดงผลเกิดขึ้นในเฮลเปอร์ เช่น ตัวอย่างนี้:

<%= render_sortable_todolists @project.todolists %>

คุณจะต้องใช้รูปแบบคอมเมนต์พิเศษเพื่อเรียกใช้:

<%# Template Dependency: todolists/todolist %>
<%= render_sortable_todolists @project.todolists %>

ในบางกรณี เช่น การตั้งค่าการสืบทอดตารางเดียว คุณอาจมีความขึ้นต่อกันแบบชัดเจนมากมาย แทนที่จะเขียนเทมเพลตทุกตัวคุณสามารถใช้เครื่องหมายแทนที่เพื่อจับคู่กับเทมเพลตในไดเรกทอรี:

<%# Template Dependency: events/* %>
<%= render_categorizable_events @person.events %>

ในกรณีของการแคชคอลเลกชัน หากเทมเพลตส่วนบางส่วนไม่เริ่มต้นด้วยการเรียกแคชที่สะอาด คุณยังสามารถใช้ประโยคคอมเมนต์พิเศษในที่ใดก็ได้ในเทมเพลต เช่น:

<%# Template Collection: notification %>
<% my_helper_that_calls_cache(some_arg, notification) do %>
  <%= notification.name %>
<% end %>

1.6.3 ความขึ้นต่อกันภายนอก

หากคุณใช้เมธอดเฮลเปอร์ เช่น ภายในบล็อกแคชและคุณอัปเดตเมธอดช่วยเหลือนั้น คุณต้องอัปเดตแคชเช่นกัน วิธีที่คุณทำก็ไม่สำคัญ แต่ MD5 ของไฟล์เทมเพลตต้องเปลี่ยน แนะนำคือให้เป็นชัดเจนในคอมเมนต์ เช่น:

<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
<%= some_helper_method(person) %>

1.7 การเก็บแคชระดับต่ำ

บางครั้งคุณต้องการเก็บแคชค่าหรือผลลัพธ์ของคำสั่งค้นหาแทนการเก็บแคชส่วนของมุมมอง (view fragments) กลไกการเก็บแคชของ Rails ทำงานได้ดีสำหรับการเก็บข้อมูลที่สามารถแปลงเป็นรหัสได้

วิธีที่มีประสิทธิภาพที่สุดในการนำเอาการเก็บแคชระดับต่ำมาใช้คือการใช้เมธอด Rails.cache.fetch วิธีนี้ใช้ทั้งการอ่านและเขียนข้อมูลลงในแคช ถ้าส่งอาร์กิวเมนต์เดียว จะดึงคีย์และคืนค่าจากแคช หากส่งบล็อกมา บล็อกนั้นจะถูกทำงานเมื่อไม่พบข้อมูลในแคช ค่าที่คืนจากบล็อกนั้นจะถูกเขียนลงในแคชใต้คีย์แคชที่กำหนด และค่าที่คืนนั้นจะถูกคืนค่าออกไป ในกรณีที่พบข้อมูลในแคช ค่าที่เก็บในแคชจะถูกคืนค่าโดยไม่ต้องทำงานบล็อก

พิจารณาตัวอย่างต่อไปนี้ แอปพลิเคชันมีโมเดล Product ที่มีเมธอดอินสแตนซ์ที่ใช้ค้นหาราคาสินค้าในเว็บไซต์แข่งขัน ข้อมูลที่คืนค่าจากเมธอดนี้เหมาะกับการเก็บแคชระดับต่ำ:

class Product < ApplicationRecord
  def competing_price
    Rails.cache.fetch("#{cache_key_with_version}/competing_price", expires_in: 12.hours) do
      Competitor::API.find_price(id)
    end
  end
end

หมายเหตุ: สังเกตว่าในตัวอย่างนี้เราใช้เมธอด cache_key_with_version เพื่อให้ได้คีย์แคชเช่น products/233-20140225082222765838000/competing_price ซึ่ง cache_key_with_version จะสร้างสตริงขึ้นมาจากชื่อคลาสของโมเดล แอตทริบิวต์ id และ updated_at นี่เป็นแนวทางที่พบบ่อยและมีประโยชน์ในการทำให้แคชเกิดการตัดสินใจใหม่เมื่อมีการอัปเดตสินค้า โดยทั่วไปเมื่อคุณใช้การเก็บแคชระดับต่ำคุณต้องสร้างคีย์แคช

1.7.1 หลีกเลี่ยงการเก็บแคชอินสแตนซ์ของออบเจกต์ Active Record

พิจารณาตัวอย่างนี้ซึ่งเก็บรายการออบเจกต์ Active Record ที่แทนผู้ดูแลระบบ (superusers) ในแคช:

# super_admins is an expensive SQL query, so don't run it too often
Rails.cache.fetch("super_admin_users", expires_in: 12.hours) do
  User.super_admins.to_a
end

คุณควร หลีกเลี่ยง รูปแบบนี้ เพราะอินสแตนซ์อาจเปลี่ยนแปลง ในโปรดักชันอาจมีแอตทริบิวต์ที่แตกต่างกันหรือบันทึกอาจถูกลบ และในการพัฒนา มันทำงานได้ไม่เสถียรกับร้านค้าแคชที่โหลดโค้ดเมื่อคุณทำการเปลี่ยนแปลง

แทนนั้นให้เก็บรหัสไอดีหรือชนิดข้อมูลพื้นฐานอื่น ๆ ตัวอย่างเช่น:

# super_admins is an expensive SQL query, so don't run it too often
ids = Rails.cache.fetch("super_admin_user_ids", expires_in: 12.hours) do
  User.super_admins.pluck(:id)
end
User.where(id: ids).to_a

1.8 การเก็บแคช SQL

การเก็บแคชคิวรีเป็นคุณสมบัติของ Rails ที่เก็บผลลัพธ์ที่คืนมาจากแต่ละคิวรี หาก Rails พบคิวรีเดียวกันอีกครั้งในคำขอเดียวกัน จะใช้ผลลัพธ์ที่เก็บไว้ในแคชแทนที่จะเรียกใช้คิวรีกับฐานข้อมูลอีกครั้ง

ตัวอย่าง:

class ProductsController < ApplicationController
  def index
    # รันคิวรีค้นหา
    @products = Product.all

    # ...

    # รันคิวรีเดียวกันอีกครั้ง
    @products = Product.all
  end
end

ครั้งที่สองที่คิวรีเดียวกันถูกรันต่อฐานข้อมูลจริง แต่จริง ๆ แล้วไม่ได้เข้าถึงฐานข้อมูล ครั้งแรกผลลัพธ์จะถูกเก็บไว้ในแคชคิวรี (ในหน่วยความจำ) และครั้งที่สองจะถูกดึงมาจากหน่วยความจำ

อย่างไรก็ตาม ควรระวังว่าแคชคิวรีจะถูกสร้างขึ้นตอนเริ่มต้นของแอ็กชันและถูกทำลายเมื่อสิ้นสุดแอ็กชัน ดังนั้นแคชคิวรีจะอยู่ในระยะเวลาเดียวกับแอ็กชันเท่านั้น หากคุณต้องการเก็บผลลัพธ์ของคิวรีในรูปแบบที่มีอยู่ต่อไปได้ คุณสามารถใช้การเก็บแคชระดับต่ำได้

2 การจัดเก็บแคช

Rails มีการจัดเก็บแคชข้อมูลที่แตกต่างกัน (นอกเหนือจาก SQL และการจัดเก็บหน้า).

2.1 การกำหนดค่า

คุณสามารถกำหนดค่าการจัดเก็บแคชเริ่มต้นของแอปพลิเคชันของคุณโดยการตั้งค่าตัวเลือก config.cache_store. คุณสามารถส่งพารามิเตอร์อื่นๆเป็นอาร์กิวเมนต์ให้กับคอนสตรัคเตอร์ของการจัดเก็บแคช:

config.cache_store = :memory_store, { size: 64.megabytes }

หรือคุณสามารถกำหนดค่า ActionController::Base.cache_store นอกบล็อกการกำหนดค่าได้

คุณสามารถเข้าถึงแคชได้โดยเรียกใช้ Rails.cache.

2.1.1 ตัวเลือก Connection Pool

โดยค่าเริ่มต้น :mem_cache_store และ :redis_cache_store ถูกกำหนดค่าให้ใช้ การจัดกลุ่มการเชื่อมต่อ (connection pooling). นั่นหมายความว่าหากคุณกำลังใช้ Puma หรือเซิร์ฟเวอร์ที่มีเธรดหลายเธรด คุณสามารถมีเธรดหลายเธรดที่ดำเนินการคิวรีไปยังการจัดเก็บแคชพร้อมกันได้

หากคุณต้องการปิดการจัดกลุ่มการเชื่อมต่อ ให้ตั้งค่า :pool เป็น false เมื่อกำหนดค่าการจัดเก็บแคช:

config.cache_store = :mem_cache_store, "cache.example.com", pool: false

คุณยังสามารถแทนที่การตั้งค่าการจัดกลุ่มเริ่มต้นโดยการให้ตัวเลือกแต่ละตัวกับตัวเลือก :pool:

config.cache_store = :mem_cache_store, "cache.example.com", pool: { size: 32, timeout: 1 }
  • :size - ตัวเลือกนี้กำหนดจำนวนการเชื่อมต่อต่อกระบวนการ (ค่าเริ่มต้นคือ 5).

  • :timeout - ตัวเลือกนี้กำหนดจำนวนวินาทีที่ต้องรอการเชื่อมต่อ (ค่าเริ่มต้นคือ 5). หากไม่มีการเชื่อมต่อที่พร้อมใช้งานภายในเวลาที่กำหนด จะเกิดข้อผิดพลาด Timeout::Error.

2.2 ActiveSupport::Cache::Store

ActiveSupport::Cache::Store ให้พื้นฐานสำหรับการติดต่อกับแคชใน Rails. นี่เป็นคลาสแบบนามธรรมและคุณไม่สามารถใช้งานได้เอง แต่คุณต้องใช้การประมวลผลที่เชื่อมโยงกับเครื่องมือจัดเก็บ. Rails มาพร้อมกับการประมวลผลหลายอย่างที่เอกสารแสดงด้านล่าง.

เมธอด API หลักคือ read, write, delete, exist?, และ fetch.

ตัวเลือกที่ส่งผ่านไปยังคอนสตรัคเตอร์ของการจัดเก็บแคชจะถูกจัดการเป็นตัวเลือกเริ่มต้นสำหรับเมธอด API ที่เหมาะสม

2.3 ActiveSupport::Cache::MemoryStore

ActiveSupport::Cache::MemoryStore เก็บรายการในหน่วยความจำในกระบวนการ Ruby เดียวกัน. การจัดเก็บแคช มีขนาดที่ถูกกำหนดโดยการส่ง :size เป็นตัวเลือกให้กับ ตัวกำหนดค่า (ค่าเริ่มต้นคือ 32Mb). เมื่อแคชเกินขนาดที่กำหนด การทำความสะอาดจะเกิดขึ้นและรายการที่ไม่ได้ใช้ล่าสุดจะถูกลบออก.

config.cache_store = :memory_store, { size: 64.megabytes }

หากคุณกำลังเรียกใช้กระบวนการเซิร์ฟเวอร์ Ruby on Rails หลายกระบวนการ (ซึ่งเป็นกรณี หากคุณกำลังใช้ Phusion Passenger หรือโหมดการจัดกลุ่มของ puma), กระบวนการเซิร์ฟเวอร์ Rails จะไม่สามารถแบ่งปันข้อมูลแคชกันได้. การจัดเก็บแคชนี้ไม่เหมาะสำหรับการใช้งานแอปพลิเคชันขนาดใหญ่. อย่างไรก็ตาม มันสามารถทำงานได้ดีสำหรับเว็บไซต์ขนาดเล็กที่มีการเข้าชมต่ำและมีเพียงไม่กี่กระบวนการเซิร์ฟเวอร์ รวมถึงสภาพแวดล้อมการพัฒนาและทดสอบ.

โปรเจค Rails ใหม่ถูกกำหนดค่าให้ใช้การประมวลผลนี้ในสภาพแวดล้อมการพัฒนาโดยค่าเริ่มต้น.

หมายเหตุ: เนื่องจากกระบวนการจะไม่แบ่งปันข้อมูลแคชเมื่อใช้ :memory_store, จะไม่สามารถอ่าน, เขียน, หรือลบแคชด้วยตนเองผ่านคอนโซลของ Rails ได้.

2.4 ActiveSupport::Cache::FileStore

ActiveSupport::Cache::FileStore ใช้ระบบไฟล์เพื่อจัดเก็บรายการ. ต้องระบุเส้นทางไปยังไดเรกทอรีที่เก็บไฟล์ของการจัดเก็บเมื่อเริ่มต้นแคช.

config.cache_store = :file_store, "/path/to/cache/directory"

ด้วยการจัดเก็บแคชนี้ กระบวนการเซิร์ฟเวอร์หลายๆกระบวนการบนโฮสต์เดียวกันสามารถแบ่งปันแคชกันได้. การจัดเก็บแคชนี้เหมาะสำหรับเว็บไซต์ที่มีการเข้าชมระดับต่ำถึงปานกลางที่ให้บริการจากหนึ่งหรือสองโฮสต์. กระบวนการเซิร์ฟเวอร์ที่ทำงานบนโฮสต์ที่แตกต่างกันสามารถแบ่งปันแคชได้โดยใช้ระบบไฟล์ที่ใช้ร่วมกัน แต่ไม่แนะนำให้ติดตั้งแบบนั้น. เนื่องจากแคชจะเติบโตจนกระทั่งดิสก์เต็ม แนะนำให้ล้างรายการเก่าออกเป็นระยะๆ

นี่คือการดำเนินการเก็บแคชเริ่มต้น (ที่ "#{root}/tmp/cache/") ถ้าไม่ได้ระบุ config.cache_store โดยเฉพาะ

2.5 ActiveSupport::Cache::MemCacheStore

ActiveSupport::Cache::MemCacheStore ใช้เซิร์ฟเวอร์ memcached ของ Danga เพื่อให้บริการแคชที่เซ็นทรัลสำหรับแอปพลิเคชันของคุณ รู้จักกับ dalli gem ที่ใช้เป็นค่าเริ่มต้น นี่เป็นการเก็บแคชที่ได้รับความนิยมสูงสุดสำหรับเว็บไซต์ในการดำเนินการจริง สามารถใช้เพื่อให้มีแคชเดียวที่ใช้ร่วมกันในคลัสเตอร์แคชที่มีประสิทธิภาพสูงและความเสถียรสูง

เมื่อเริ่มต้นแคชคุณควรระบุที่อยู่สำหรับเซิร์ฟเวอร์ memcached ทั้งหมดในคลัสเตอร์ของคุณ หรือตรวจสอบให้แน่ใจว่าตัวแปรสภาพแวดล้อม MEMCACHE_SERVERS ถูกตั้งค่าอย่างเหมาะสม

config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"

หากไม่ได้ระบุเซิร์ฟเวอร์จะถือว่า memcached กำลังทำงานบนเครื่อง localhost ที่พอร์ตเริ่มต้น (127.0.0.1:11211) แต่นี่ไม่ใช่การตั้งค่าที่เหมาะสำหรับเว็บไซต์ขนาดใหญ่

config.cache_store = :mem_cache_store # จะถูกสลับกลับไปใช้ $MEMCACHE_SERVERS แล้ว 127.0.0.1:11211

ดูเอกสาร Dalli::Client สำหรับประเภทที่รองรับ

เมธอด write (และ fetch) ในแคชนี้ยอมรับตัวเลือกเพิ่มเติมที่ใช้ประโยชน์จากคุณสมบัติที่เฉพาะเจาะจงของ memcached

2.6 ActiveSupport::Cache::RedisCacheStore

ActiveSupport::Cache::RedisCacheStore ใช้ Redis เพื่อรองรับการลบอัตโนมัติเมื่อมีการใช้หน่วยความจำสูงสุด ทำให้มีพฤติกรรมคล้ายกับเซิร์ฟเวอร์แคช Memcached

หมายเหตุการติดตั้ง: Redis ไม่ได้ตั้งค่าให้หมดอายุของคีย์โดยค่าเริ่มต้น ดังนั้นควรใช้เซิร์ฟเวอร์แคช Redis ที่มีการกำหนดเฉพาะ อย่าเต็มพื้นที่เซิร์ฟเวอร์ Redis ถาวรของคุณด้วยข้อมูลแคชที่เปลี่ยนแปลงได้! อ่านคู่มือการติดตั้งเซิร์ฟเวอร์แคช Redis อย่างละเอียด

สำหรับเซิร์ฟเวอร์ Redis ที่ใช้แค่แคชเท่านั้น ตั้งค่า maxmemory-policy เป็นหนึ่งในตัวเลือกของ allkeys Redis 4+ รองรับการลบตามการใช้งานที่น้อยที่สุด (allkeys-lfu) เป็นตัวเลือกที่เหมาะสมที่สุด ส่วน Redis 3 และต่ำกว่าควรใช้การลบตามการใช้งานล่าสุด (allkeys-lru)

ตั้งค่าเวลาที่อ่านแคชและเขียนแคชให้สั้นลง การสร้างค่าแคชใหม่บ่อยกว่าการรอให้เวลาเกิน 1 วินาทีในการเรียกคืน การอ่านและเขียนแคชเริ่มต้นทั้งคู่เป็น 1 วินาที แต่สามารถตั้งค่าให้ต่ำกว่านั้นได้หากเครือข่ายของคุณมีเวลาตอบสนองต่ำตลอดเวลา

โดยค่าเริ่มต้นแคชสโตร์จะไม่พยายามเชื่อมต่อกับ Redis หากการเชื่อมต่อล้มเหลวในระหว่างคำขอ หากคุณพบการตัดการเชื่อมต่อบ่อยคุณอาจต้องเปิดใช้งานการเชื่อมต่ออีกครั้ง

การอ่านและเขียนแคชไม่เกิดข้อผิดพลาด; แทนที่จะเกิดข้อยกเว้นพวกมันจะส่งคืน nil แสดงพฤติกรรมเหมือนว่าไม่มีอะไรอยู่ในแคช หากต้องการประเมินว่าแคชของคุณมีข้อยกเว้นคุณสามารถให้ error_handler รายงานไปยังบริการรวบรวมข้อยกเว้น ต้องรับอาร์กิวเมนต์คีย์เวิร์ดสามตัว: method เมธอดของแคชสโตร์ที่เรียกเริ่มต้น; returning ค่าที่ส่งคืนให้ผู้ใช้ โดยทั่วไปคือ nil; และ exception ข้อยกเว้นที่ถูกแก้ไข

เพื่อเริ่มต้นให้เพิ่ม gem redis ใน Gemfile:

gem 'redis'

ในที่สุดเพิ่มการกำหนดค่าในไฟล์ config/environments/*.rb ที่เกี่ยวข้อง:

config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }

การกำหนดค่าแคช Redis ที่ซับซ้อนมากขึ้นสามารถดูได้ดังนี้:

cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
config.cache_store = :redis_cache_store, { url: cache_servers,

  connect_timeout:    30,  # ค่าเริ่มต้น 20 วินาที
  read_timeout:       0.2, # ค่าเริ่มต้น 1 วินาที
  write_timeout:      0.2, # ค่าเริ่มต้น 1 วินาที
  reconnect_attempts: 1,   # ค่าเริ่มต้น 0

  error_handler: -> (method:, returning:, exception:) {
    # รายงานข้อผิดพลาดไปยัง Sentry เป็นคำเตือน
    Sentry.capture_exception exception, level: 'warning',
      tags: { method: method, returning: returning }
  }
}

2.7 ActiveSupport::Cache::NullStore

ActiveSupport::Cache::NullStore ถูกกำหนดขอบเขตในแต่ละคำขอเว็บ และล้างค่าที่เก็บไว้ที่สิ้นสุดของคำขอ มันถูกออกแบบมาสำหรับใช้ในสภาพแวดล้อมการพัฒนาและทดสอบ มันสามารถเป็นประโยชน์อย่างมากเมื่อคุณมีโค้ดที่มีปฏิสัมพันธ์โดยตรงกับ Rails.cache แต่การใช้แคชขัดขวางการเห็นผลลัพธ์จากการเปลี่ยนแปลงของโค้ด

config.cache_store = :null_store

2.8 การเก็บแคชที่กำหนดเอง

คุณสามารถสร้างเก็บแคชที่กำหนดเองได้โดยการขยาย ActiveSupport::Cache::Store และการดำเนินการที่เหมาะสม นี่คือวิธีที่คุณสามารถสลับเทคโนโลยีการแคชใดก็ได้เข้าสู่แอปพลิเคชัน Rails ของคุณ

ในการใช้เก็บแคชที่กำหนดเอง คุณเพียงแค่ตั้งค่าเก็บแคชเป็นอินสแตนซ์ใหม่ของคลาสที่กำหนดเอง

config.cache_store = MyCacheStore.new

3 คีย์แคช

คีย์ที่ใช้ในแคชสามารถเป็นอ็อบเจ็กต์ใดก็ได้ที่ตอบสนองกับ cache_key หรือ to_param คุณสามารถสร้างเมธอด cache_key บนคลาสของคุณหากคุณต้องการสร้างคีย์ที่กำหนดเอง Active Record จะสร้างคีย์ขึ้นมาจากชื่อคลาสและรหัสบันทึก

คุณสามารถใช้ Hashes และ Arrays ของค่าเป็นคีย์แคชได้

# นี่คือคีย์แคชที่ถูกต้อง
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])

คีย์ที่คุณใช้ใน Rails.cache จะไม่เหมือนกับคีย์ที่ใช้จริงกับเก็บข้อมูล อาจถูกแก้ไขด้วยเนมสเปซหรือถูกแก้ไขเพื่อเข้ากับข้อจำกัดของเทคโนโลยีที่ใช้ นั่นหมายความว่า เช่น คุณไม่สามารถบันทึกค่าด้วย Rails.cache แล้วพยายามดึงออกมาด้วย gem dalli อย่างไรก็ตาม คุณก็ไม่ต้องกังวลเกินไปเกี่ยวกับการเกินขีดจำกัดขนาดของ memcached หรือการละเมิดกฎไวยากรณ์ไว้ใจ

4 การสนับสนุนการรับเงื่อนไข GET

การรับเงื่อนไข GET เป็นคุณสมบัติของข้อกำหนด HTTP ที่ให้วิธีในการบอกเซิร์ฟเวอร์เว็บว่าการตอบสนองกับคำขอ GET ไม่เปลี่ยนแปลงตั้งแต่คำขอล่าสุดและสามารถดึงจากแคชของเบราว์เซอร์ได้อย่างปลอดภัย

มันทำงานโดยใช้ส่วนหัว HTTP_IF_NONE_MATCH และ HTTP_IF_MODIFIED_SINCE เพื่อส่งกลับและส่งผ่านไปมาทั้งตัวแยกแยะเนื้อหาที่ไม่ซ้ำกันและเวลาที่เนื้อหาเปลี่ยนครั้งล่าสุด หากเบราว์เซอร์ทำคำขอที่เนื้อหาแยกแยะ (ETag) หรือเวลาแก้ไขล่าสุดตรงกับเวอร์ชันของเซิร์ฟเวอร์ แล้วเซิร์ฟเวอร์จะต้องส่งกลับคำขอที่ว่างเปล่าพร้อมสถานะไม่ได้เปลี่ยนแปลง

เป็นหน้าที่ของเซิร์ฟเวอร์ (เช่นของเรา) ที่จะค้นหาเวลาแก้ไขล่าสุดและส่วนหัว if-none-match และกำหนดว่าจะส่งคำขอที่เต็มกลับหรือไม่ ด้วยการรับเงื่อนไข GET ใน Rails นี้เป็นงานที่ง่ายพอ:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    # หากคำขอเก่าตามเวลาและค่า etag ที่กำหนดไม่ตรงกับค่าปัจจุบัน (คือต้องทำงานใหม่) ให้ดำเนินการบล็อกนี้
    if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version)
      respond_to do |wants|
        # ... การประมวลผลคำขอปกติ
      end
    end

    # หากคำขอเป็นเวลาใหม่ (คือไม่ได้เปลี่ยนแปลง) คุณไม่ต้องทำอะไรเลย การเรนเดอร์เริ่มต้นตรวจสอบสิ่งนี้โดยใช้พารามิเตอร์
    # ที่ใช้ในการเรียกใช้ stale? ก่อนหน้านี้และจะส่งอัตโนมัติ :not_modified ดังนั้นนั่นคือทั้งหมด คุณเสร็จสิ้นแล้ว
  end
end

แทนที่ options hash คุณยังสามารถส่ง model เข้าไปได้อีกวิธี Rails จะใช้เมธอด updated_at และ cache_key_with_version เพื่อตั้งค่า last_modified และ etag:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    if stale?(@product)
      respond_to do |wants|
        # ... normal response processing
      end
    end
  end
end

หากคุณไม่มีการประมวลผลการตอบกลับที่เฉพาะเจาะจงและใช้กลไกการเรนเดอร์เริ่มต้น (เช่นคุณไม่ได้ใช้ respond_to หรือเรียกใช้ render เอง) คุณสามารถใช้เฮลเปอร์ fresh_when ได้ง่ายๆ:

class ProductsController < ApplicationController
  # นี่จะส่งกลับ :not_modified โดยอัตโนมัติหากคำขอเป็นเวอร์ชันล่าสุด
  # และจะเรนเดอร์เทมเพลตเริ่มต้น (product.*) หากคำขอไม่ได้เป็นเวอร์ชันล่าสุด

  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, etag: @product
  end
end

บางครั้งเราต้องการแคชการตอบกลับ เช่นหน้าสถิติที่ไม่มีวันหมดอายุ ในการทำเช่นนี้ เราสามารถใช้เฮลเปอร์ http_cache_forever และด้วยการทำเช่นนั้น เบราว์เซอร์และพร็อกซีจะแคชไว้ตลอดเวลา

โดยค่าเริ่มต้นการแคชคำตอบจะเป็นแบบส่วนตัว เก็บแคชไว้เฉพาะในเบราว์เซอร์ของผู้ใช้ หากต้องการให้พร็อกซีแคชคำตอบได้ ให้ตั้งค่า public: true เพื่อระบุว่าพร็อกซีสามารถให้คำตอบแคชกับผู้ใช้ทั้งหมดได้

ในการใช้เฮลเปอร์นี้ เราจะตั้งค่า last_modified ให้กับเฮดเดอร์เป็น Time.new(2011, 1, 1).utc และตั้งค่าเฮดเดอร์ expires เป็น 100 ปี

คำเตือน: ใช้เมธอดนี้อย่างระมัดระวัง เนื่องจากเบราว์เซอร์/พร็อกซีจะไม่สามารถยกเลิกการแคชคำตอบได้เว้นแต่จะล้างแคชของเบราว์เซอร์โดยบังคับ

class HomeController < ApplicationController
  def index
    http_cache_forever(public: true) do
      render
    end
  end
end

4.1 ETag แบบแข็งและแบบอ่อน

Rails สร้าง ETag แบบอ่อนโดยค่าเริ่มต้น ETag แบบอ่อนช่วยให้คำตอบที่มีความหมายเท่ากันสามารถมี ETag เดียวกันได้ แม้ว่าเนื้อหาของคำตอบจะไม่ตรงกันอย่างยิ่ง นี้เป็นประโยชน์เมื่อเราไม่ต้องการที่จะสร้างหน้าใหม่สำหรับการเปลี่ยนแปลงเล็กน้อยในเนื้อหาของคำตอบ

ETag แบบอ่อนจะมีตัวอักษร W/ นำหน้าเพื่อแยกจาก ETag แบบแข็ง

W/"618bbc92e2d35ea1945008b42799b0e7" → ETag แบบอ่อน
"618bbc92e2d35ea1945008b42799b0e7" → ETag แบบแข็ง

ในขณะที่ ETag แบบอ่อนไม่เกี่ยวข้องกับเนื้อหาของคำตอบ ETag แบบแข็งแสดงว่าคำตอบควรเหมือนกันและตรงตามตำแหน่งไบต์ที่เหมือนกัน มีประโยชน์เมื่อทำ Range requests ภายในไฟล์วิดีโอหรือไฟล์ PDF ขนาดใหญ่ บาง CDN รองรับเฉพาะ ETag แบบแข็งเท่านั้น เช่น Akamai หากคุณจำเป็นต้องสร้าง ETag แบบแข็ง คุณสามารถทำได้ดังนี้

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, strong_etag: @product
  end
end

คุณยังสามารถตั้งค่า ETag แบบแข็งโดยตรงบนคำตอบได้

response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"

5 การแคชในโหมดการพัฒนา

เป็นสิ่งที่ธรรมดาที่คุณต้องการทดสอบกลยุทธ์การแคชของแอปพลิเคชันของคุณในโหมดการพัฒนา Rails มีคำสั่ง dev:cache เพื่อเปิด/ปิดการแคชได้อย่างง่ายดาย

$ bin/rails dev:cache
Development mode is now being cached.
$ bin/rails dev:cache
Development mode is no longer being cached.

โดยค่าเริ่มต้นเมื่อการแคชในโหมดการพัฒนาถูกปิด Rails จะใช้ :null_store

6 อ้างอิง

ข้อเสนอแนะ

คุณสามารถช่วยปรับปรุงคุณภาพของคู่มือนี้ได้

กรุณาช่วยเพิ่มเติมหากพบข้อผิดพลาดหรือข้อผิดพลาดทางความจริง เพื่อเริ่มต้นคุณสามารถอ่านส่วน การสนับสนุนเอกสาร ของเราได้

คุณอาจพบเนื้อหาที่ไม่สมบูรณ์หรือเนื้อหาที่ไม่ได้อัปเดต กรุณาเพิ่มเอกสารที่ขาดหายไปสำหรับเนื้อหาหลัก โปรดตรวจสอบ Edge Guides ก่อนเพื่อตรวจสอบ ว่าปัญหาได้รับการแก้ไขหรือไม่ในสาขาหลัก ตรวจสอบ คู่มือแนวทาง Ruby on Rails เพื่อดูรูปแบบและกฎเกณฑ์

หากคุณพบข้อผิดพลาดแต่ไม่สามารถแก้ไขได้เอง กรุณา เปิดปัญหา.

และสุดท้าย การสนทนาใด ๆ เกี่ยวกับ Ruby on Rails เอกสารยินดีต้อนรับที่สุดใน เว็บบอร์ดอย่างเป็นทางการของ Ruby on Rails.