iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0
Modern Web

Rails,我要進來囉系列 第 26

第二十六天:Rails 的 Caching

  • 分享至 

  • xImage
  •  

開場白

鼬~~哩賀,我是寫程式的山姆老弟,昨天跟大家一起看了 Rails 的 Rack,今天來看 RailsGuide 的 Caching 篇,看 Rails 是怎麼處理 暫存(Cache)的,夠夠~

https://raw.githubusercontent.com/shrimp509/my-img-host/master/relacs-studio/Rails%E6%88%91%E8%A6%81%E9%80%B2%E4%BE%86%E5%9B%89/day26-1.png

Cache 的種類

Cache 可以用在各種場景,像是同樣或類似的 request,不需要每一次都重新計算、重新去資料庫拿一樣的資料、重新把一樣的資料放到一樣的頁面上,這樣的情況就很適合直接暫存起來,下次遇到一樣的 request,就直接從暫存中拿出來即可,省了 CPU 成本、資料庫成本,還加快了反應時間

  1. Page Caching
  2. Action Caching
  3. Fragment Caching
  4. Russian Doll Caching
  5. Shared Partial Caching
  6. Dependency Caching
  7. Low-Level Caching
  8. SQL Caching

Cache 的好處

  • 加快整體網頁反應速度
  • 節省各種運算成本

Cache 的壞處

  • Cache invalidation 很難實作 (怎麼知道暫存的資料已經過期?怎麼決定要暫存多久?)

*There are only two hard things in Computer Science: cache invalidation and naming things

  • Phil Karlton (1947-1997, Software Developer)*

Rails 怎麼處理這些 Cache?

Page Caching

  • Cache 整個頁面的內容
  • 啟用方式:
    • 要額外安裝 actionpack-page_caching 這個 gem
    • 設定: config.action_controller.perform_caching = true or $ rails dev:cache
  • 不適用:需要 authentication 的頁面(通常會用 before_action :authenticate_user)
  • 過期時機:?

Action Caching

  • 補足 Page Caching 不足,可以在使用 Page Caching 的同時,又能讓 before filters(e.g. before_action) 正常運作
  • 啟用方式:
    • 要額外安裝 actionpack-action_caching 這個 gem
    • 設定:config.action_controller.perform_caching = true or $ rails dev:cache
  • 過期時機:?

ps. Page CachingAction Caching 從 Rails 4 開始,就被拿掉了,變成需要額外安裝的 gem

Fragment Caching

  • Cache Partial

  • 使用方式: e.g. 產生 Product scaffold 的時候,會有 _product.html.erb 的 partial

    • 方法一:cache block

      <% @products.each do |product| %>
        <% cache product do %>
          <%= render product %>
        <% end %>
      <% end %>
      
    • 方法二:cache_if, cache_unless

      <% cache_if admin?, product do %>
        <%= render product %>
      <% end %>
      
      <% cache_unless admin?, product do %>
        <%= render product %>
      <% end %>
      
  • 過期時機:partial 內的 HTML 內容改變時

Russian Doll Caching

  • Cache 巢狀(nest) fragments,也就是 partial 包 partial,可能會有很多層

  • 使用方法:

    Product 包 Games,Cache Product 的同時

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

    也 Cache 每一個 Game

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

    這時如果 Game 改變,Product 並不會知道需要更新 Cache,這時候需要在 dependency 加上 touch,讓 Game 改變的時候,也去更新 Product 的 updated_at,這樣 Product 的 Cache 也會被更新

    class Product < ApplicationRecord
      has_many :games
    end
    
    class Game < ApplicationRecord
      belongs_to :product, touch: true
    end
    

Shared Partial Caching

  • Cache 跨 MEME 的頁面
  • 使用方法:
    • 方法一:不管 respond_to 是 HTML 或 Javascript,都可以吃得到 cache

      render(partial: 'hotels/hotel', collection: @hotels, cached: true)
      
    • 方法二:強制指定,不管 respond_to 是什麼 format,就是直接回傳 html 格式的

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

Dependency Caching

這段我看不太懂,大家看看就好 ?

  • 隱性(Implicit) dependencies:

    • ActionView::Digestor 會自動判斷以下要 render 的是一樣的

      render partial: 'comments/comment'
      render 'comments/comment'
      render "comments/comment"
      render('comments/comment')
      
  • 顯性(Explicit) dependencies

    有時候沒辦法直接指定 partial

    <%= render_sortable_todolists @project.todolists %>
    

    這時候就要手動加這個特殊的註解格式 (?

    <%# Template Dependency: todolists/todolist %>
    <%= render_sortable_todolists @project.todolists %>
    
  • 外部(External) dependencies

    這邊直接放棄,我看不懂QQ

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

Low-Level Caching

  • 有時候只是要 cache 特定的值或是某個 query 的結果,而不是要 cache 整個頁面
    • 其實這也是比較常使用的方式,讓開發者自己去決定要暫存哪些內容
  • 使用方法:
    • 方法一:帶入值

      Rails.cache.fetch('some_key', 'your_value')
      
    • 方法二:帶入 Block,會取 Block 最後 return 的 value 出來暫存(以這個例子,暫存就不會每呼叫一次就發一次 API)

      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 Stores 章節

  • 建議:
    • 不要 Cache ActiveRecord instance,改存 ActiveRecord 的 id

      # 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  # 事後再 SQL 拿出來
      

SQL Caching

  • 重複的 SQL query 會被 cache

  • 設定方法:不需特別設定

  • 使用方法:

    class ProductsController < ApplicationController
    
      def index
        # Run a find query
        @products = Product.all
    
        # ...
    
        # Run the same query again
        @products = Product.all
      end
    
    end
    
  • Caching 的時機:起始點是從 Controller action 開始,當 action 結束,cache 就會被清除

    • 如果想要在以上時機之外的時間暫存的話,那就使用 low-level caching

Cache Stores

Rails 提供 Cache Stores 這個暫存的「統一介面」,讓我們可以抽換掉最底層的暫存機制,只需要用統一的介面就可以使用暫存,跟 ActiveStorageActiveJob 的概念很像,Rails 就是提供同一介面,可抽換底層

  • 設定方法:

    可選擇的暫存方式有:

    • memory_store:暫存在記憶體裡,不能跨 process 取用

      # config/application.rb or config/environemnts/*.rb
      config.cache_store = :memory_store, { size: 64.megabytes }
      
    • file_store:暫存在檔案裡

      # config/application.rb or config/environemnts/*.rb
      config.cache_store = :file_store, "/path/to/cache/directory"
      
    • mem_cache_store:使用 MemCached 暫存,預設使用 dalli gem,還可以透過 MEMCACHE_SERVERS 環境變數來指定 Cache Server 的 host,如果都沒有指定,那就預設會找 127.0.0.111211 port

    # config/application.rb or config/environemnts/*.rb
    config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
    
    • redis_cache_store:使用 Redis 暫存,使用 redis gem 或 hiredis gem

      # config/application.rb or config/environemnts/*.rb
      config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
      
      # Production 的設定會長得像這樣
      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,  # Defaults to 20 seconds
        read_timeout:       0.2, # Defaults to 1 second
        write_timeout:      0.2, # Defaults to 1 second
        reconnect_attempts: 1,   # Defaults to 0
      
        error_handler: -> (method:, returning:, exception:) {
          # Report errors to Sentry as warnings
          Raven.capture_exception exception, level: 'warning',
            tags: { method: method, returning: returning }
        }
      }
      
    • null_store:只有在每個 request 之前暫存,request 結束後就會清除,適合用d evelopment 和 test 環境

      # config/application.rb or config/environemnts/*.rb
      config.cache_store = :null_store
      
    • 其他 custom store:只要繼承 ActiveSupport::Cache::Store 即可

      # config/application.rb or config/environemnts/*.rb
      config.cache_store = MyCacheStore.new
      
  • 使用方法:

    • Rails.cache.read(key)
    • Rails.cache.write(key, value)
    • Rails.cache.delete(key)
    • Rails.cache.exist?(key)
    • Rails.cache.fetch(key, value) = write + read

總結

Cache 真的是一門很深的學問,這篇看下來,會好奇前面的 Page Caching, Action CachingFragment Caching 是不是過時了?因為現在的 Rails 前端,如果改接 React 或 Vue,這樣的 Caching 機制就失效了

不過 Rails 就是提供一些工具,給開發者自己去決定怎麼使用,我們還是可以針對一些重大的效能瓶頸去使用 low-level caching,那今天就先這樣囉,我們明天見~


上一篇
第二十五天:Rack 到底是什麼?跟 Rails 有什麼關係?那 Rake 又是什麼?
下一篇
第二十七天:Rails 的 API-only mode,只做 API Server 需要用到 Rails 嗎?
系列文
Rails,我要進來囉30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言