Web 缓存及在 Rails 中的使用

最近给博客添加了缓存,感觉速度提升了不少,在这段时间里,看了一些关于缓存及 Rails 中使用缓存的资料,把自己学到的一些姿势总结一下。

HTTP 缓存

又可以称为客户端缓存。当用户第一次访问某个页面时,服务端按正常方式渲染页面,并在 Response Header 中添加 ETag 或 Last-Modified 或两者,当用户再次访问那个页面时,Request Header 中会有 If-Modified-Since 和 If-None-Match ,然后服务端会根据这两项判断此页面自从上次访问过后是否被修改过,从而决定服务端是正常渲染页面还是返回 304 Not Modified。

Rails 中用来处理 HTTP 缓存有几个方法 fresh_when , stale? , expires_in , expires_now

fresh_when

fresh_when 在 Response Header 中加入 ETag 或者 Last-Modified 或者两者,之后与 Request Header 进行比较,如果无需重新渲染页面则在响应中添加 304 状态。

于是对于一个简单的文章页面 posts#show 页面,可以使用以下缓存

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])

    fresh_when(@post, last_modified: @post.updated_at)
  end
end

对于多个变量可能导致页面发生变化时, 只需要把会导致页面发生变化的变量加入 ETag 就行了

比如对于有若干回复列表的文章页面, 新增的回复也会引起页面的变化, 因此可以加入 ETag 计算

def show
  @post    = Post.find(params[:id])
  @replies = @post.replies

  fresh_when([@post, @replies])
end

注意, 上面只使用了 ETag , 而并没有使用 Last-Modified , 因为最后修改时间不一定是文章的更新时间, 而要解决这个问题, 可以在 Model 中解决

Touch

Post 和 Reply 的关系是 1-N 关系,模型中声明如下

class Post
  has_many :replies
end
class Reply
  belongs_to :post
end

只需要修改为

class Reply
  belongs_to :post, touch: true
end

就可以保证每次有新回复时, 都会 touch 它所 belongs_to 的 post 一下, 即是更新 post 的 updated_at 项, 这样就能保证 Post#updated_at 始终是该页面的最后修改时间,于是上面的 controller 可以改为

def show
  @post    = Post.find(params[:id])
  @replies = @post.replies

  fresh_when(@post, last_modified: @post.updated_at)
end

效果

下面是第一次和再次访问首页时服务端用时:

  • 第一次: X-Runtime:1.127763 , 由于要把文章源码渲染为 HTML,,所以很耗时
  • 第二次: X-Runtime:0.052087 , 只需要取出一些数据计算 ETag,无需渲染,所以很快

stale?

stale? 的参数跟 fresh_when 是一样的, 实际上 stale? 调用了 fresh_when 方法

# File actionpack/lib/action_controller/metal/conditional_get.rb, line 136
def stale?(record_or_options, additional_options = {})
  fresh_when(record_or_options, additional_options)
  !request.fresh?(response)
end

stale? 返回一个布尔值, 如果此次请求需要正常渲染, 则返回 true , 如果是 304 无需渲染页面, 则返回 false

因此, 对于上面的文章页面, 可以更改为

def show
  @post = Post.find(params[:id])

  if stale?(@post, last_modified: @post.updated_at)
    @replies = @post.replies
  end
end

只需要在页面需要正常渲染时才需要读取回复, 否则就无需进行数据库操作了

expires_in

用来设置 Respond Header 中的 Cache-Control

# Cache-Control: max-age=36000
expires_in 10.hours

# Cache-Control: max-age=3600, public
expires_in 1.hour, public: true

# Cache-Control: max-age=86400, public, must-revalidate
expires_in 1.day, public: true, must_revalidate: true

expires_now

设置 Cache-Control 为 no-cache 以禁止缓存

片段缓存

对于复杂的页面, HTTP 缓存应用的机会不是很大, 而片段缓存是应用最广泛的, 它可一针对不同的 HTML 片段设置不同的缓存机制

比如本博客带有侧边栏的文章页面, 侧边栏可能会新增一条友情链接, 而整个页面的ETag 和 Last-Modified 就改变了, 因此 HTTP 缓存就失效了, 但是仅仅因为添加了一条友情链接就重新渲染整个页面显然是不合适的, 这时候就可以使用片段缓存对页面进行分区块缓存, 下次只要渲染缓存失效的那部分片段并与缓存未失效的片段拼接起来组成响应正文就行了。

.main
  - cache @post do
    .post
      .title = link_to post, post_path(post)
      .body == post.html_body

.sidebar
  - cache @sidebar_posts do
    .sidebar-posts
      - @sidebar_posts.each do |post|
        = link_to post, post_path(post)

  - cache @sidebar_categories do
    .sidebar-friends
      - @sidebar_categories do |category|
        = link_to category, category_path(category)

  - cache @sidebar_friends do
    .sidebar-friends
      - @sidebar_friends.each do |friend|
        = link_to friend, friend.website

采用上面的片段缓存, 当添加一条友情链接之后, @sidebar_friends 会发生变化, 因此 .sidebar-friends 的缓存失效了, 会重新渲染 .sidebar-friends , 但是其它三个缓存并没有失效, 因此会继续从缓存中获取 HTML 片段, 之后再与新渲染的友情链接拼接成响应正文。

套娃机制

片段缓存不仅能像上面一样分区块使用, 而且可以嵌套使用

对于文章列表, 可以像这样嵌套使用片段缓存

- cache @posts do
  .posts
    - @posts.each do |post|
      - cache post do
        .post
          .title = post.title
          .body == post.body
  • 当所有文章都没有改变时, cache @posts 会直接从缓存中读取, 而不必使用内层缓存,
  • 而当其中一片文章发生更新时, @posts 会发生变化, 此时外层缓存失效, 需要重新渲染文章列表, 而没有更新的文章的内层片段缓存并没有失效,所以可以直接从缓存中都缺,而只需要重新渲染那些实际被修改的文章的内层片段

更多有关套娃的姿势参考 说说 Rails 的套娃缓存机制

底层缓存

Rails 提供了完全手动操作缓存的方法

方法 含义
read 读取缓存
write 写入缓存
exist? 判断缓存是否存在
fetch 读取缓存
clear 清空所有缓存

更多这些函数的详细参数请访问 http://api.rubyonrails.org 搜索 ActiveSupport::Cache::Store

应用

比如对于我的博客, 所有文章都是用 Markdown 或者 Org 写的, 因此需要先渲染成 HTML, 这样每有一次访问就需要渲染一次, 十分消耗资源和时间, 因此可以考虑将渲染后的 HTML 保存进缓存

class Post
  #
  # 在文章保存之前,如果正文修改了,就写入缓存
  #
  before_save do
    write_html_body_to_cache if body_changed?
  end

  #
  # 直接从缓存中读取数据
  #
  def html_body
    fetch_html_body_from_cache
  end

  private

  #
  # 尝试从缓存中读取数据, 如果没有则先写入缓存, 之后再读取缓存
  #
  def fetch_html_body_from_cache
    Rails.cache.fetch(cache_key_with_html_body) do
      # 如果缓存中没有找到,就写入缓存,并返回它
      write_html_body_to_cache
    end
  end

  #
  # 写入缓存
  #
  def write_html_body_to_cache
    html = genarate_html_body
    Rails.cache.write(cache_key_with_html_body, html)
    html
  end

  #
  # 为每个 html_body 缓存设置一个 key
  #
  def cache_key_with_html_body
    "#{cache_key}-html-body"
  end

  def genarate_html_body
    # 这里进行实际的将 Markdown 或者 Org 渲染为 HTML 工作
  end
end

效果

下面是访问首页时产生的日志的一部分

Cache read: posts/5487ea6e6c6f631568010000-20141213130142-html-body
Cache fetch_hit: posts/5487ea6e6c6f631568010000-20141213130142-html-body
Cache read: posts/54768fb46c6f630ca3000000-20141215060253-html-body
Cache fetch_hit: posts/54768fb46c6f630ca3000000-20141215060253-html-body
Cache read: posts/546c7a796c6f630ff9000000-20141213124729-html-body
Cache fetch_hit: posts/546c7a796c6f630ff9000000-20141213124729-html-body
Cache read: posts/546322cb6c6f633ba3000000-20141210110159-html-body
Cache fetch_hit: posts/546322cb6c6f633ba3000000-20141210110159-html-body

页面缓存

这个缓存机制在 Rails 4 中被移除了, 用的可能性也比较小, 因此就不说了

更多资料

  1. Caching with Rails: An overview
  2. 总结 web 应用中常用的各种 cache
  3. Cache 在 Ruby China 里面的应用

注: 看完正文可以看看回复, 里面不乏各种姿势

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值