最近给博客添加了缓存,感觉速度提升了不少,在这段时间里,看了一些关于缓存及 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 中被移除了, 用的可能性也比较小, 因此就不说了
更多资料
注: 看完正文可以看看回复, 里面不乏各种姿势