分享 说说 Rails 的套娃缓存机制

zchar · September 11, 2014 · Last by zzz6519003 replied at December 17, 2019 · 18887 hits
Topic has been selected as the excellent topic by the admin.

Rails 4.0 以后,开始推广一种称为「俄罗斯套娃」的缓存机制,这是一种使用 Fragment Caching(http://guides.rubyonrails.org/caching_with_rails.html#fragment-caching)技术的缓存机制,在数据库做完查询以后,如果记录没有变化,那么对应的页面不会被 Rails 重新渲染,而是直接从缓存里取出,拼装好以后,返回给客户。

Tower 正是借鉴了这套缓存机制,给那些访问 Tower 的用户提供流畅的使用体验,今天跟大家分享一下我们的经验,和一些需要注意的坑。

1. 套娃是怎么套的

拿 Tower 的项目详情页为例,我们可以把这个页面明显的分成一个个的 Section,比如「讨论区」、「任务清单区」、「文件区」、「文档区」、「日历事件区」,我们可以把每一个列表区域设置为一个独立的缓存,这样,如果列表的数据没有更新的话,在渲染项目详情页的时候,就可以直接从缓存里读取之前生成好的数据。

那么,所谓的套娃在哪儿呢?实际上,除了可以把上面的 Section 放到缓存里,我们也可以把整个项目详情页放入缓存,这样,如果某一个项目里的数据没有任何更新,访问这个详情页就可以直接读取详情页的缓存,连下面的 Section 缓存都不用碰了。

同样,对于某一个列表缓存,比如「讨论区」,我们可以看到里面会放三条讨论 item,这里其实每一个 item 也可以对应放入一个独立的缓存,这样如果只有其中一条 item 有更新的话,其它两条数据是不会被重新渲染,而是直接从缓存区读取的。这样分拆下去,我们大概可以把整个项目详情页的缓存弄成下面这个模样:

这样不就一层一层的套起来了么?

接下来我们看看,如果这个页面的某一条数据,比如「任务 A-1」的内容被改变了,会发生什么。

首先,这条任务自己对应的 L4 Todo Item 缓存失效了,所以在拼装外面的 L3 级「任务清单 A」缓存的时候,会从缓存里获取任务 A-2、A-3 的缓存,速度嗖嗖快,快到可以忽略不计,然后对任务 A-1 重新渲染一次,放入缓存,这样「任务清单 A」通过直接从缓存里读取两条任务(A-2 和 A-3),以及渲染一条新的(A-1)生成了整个 L3 Todolist Item 的页面片段。剩下的「任务清单 B」和「任务清单 C」,都没有变化,因此由在生成「任务清单」Section 缓存的时候,直接拼装即可。

其它几个 Section 片段因为和任务没有任何关系,所有缓存都不会过期,因此这几个 Section 的页面片段都是直接从缓存里捞出来,同样嗖嗖快。

最后,整个项目详情页把这几个 Section 拼装起来,返回给客户。从上面的过程可以看出,只有「任务 A-1」这个片段的页面被重新渲染了。

所以,这种套娃式的缓存,能够保证页面缓存利用率的最大化,任何数据的更新,只会导致某一个片段的缓存失效,这样在组装完整页面的时候,由于大量的页面片段都是直接从缓存里读取,所以页面生成的时间开销就很小。

那么,套娃是如何在缓存中存取页面片段的呢?主要是靠一个叫做 cache_key 的东西来决定的。

2. cache_key

我们在页面上可以使用一个叫做 cache 的方法,把一坨 HTML 代码片段放在一个 Fragment Cache 里,以项目详情页为例,我们的代码可能是这个样子的:

可以看到,在整个页面的最外面,有个最大的「套娃」:<% cache @project %>,这个 cache 使用 @project 作为方法参数,在 cache 方法内部,会把这个对象进行一番处理,最后生成一个字符串,大概是这个样子:

views/projects/1-20140906112338

这就是所谓的 cache_key,Rails 会使用这串字符串作为 key,对应的页面片段作为内容,存储进缓存系统里。每次渲染页面的时候,Rails 会根据 cache 里的元素计算出对应的 cache_key,然后拿着这个 cache_key 到缓存里去找对应的内容,如果有,则直接从缓存里取出,如果没有,则渲染 cache 里的 HTML 代码片段,并且把内容存储进缓存里。

对于一个具体的 Model 对象,cache_key 的生成机制简单来说,就是:对象对应的模型名称/对象数据库 ID-对象的最后更新时间。

这里我们能够很容易分析出,一个缓存判断最后是否过期,其实很大程度上只和数据最后更新时间有关,因为在系统里,数据对象对应的模型名称是不变的,对象在数据库里的 ID 一般也是不变的,唯一可能变化的就是最后更新时间。Rails 在创建模型数据表的时候,一般会创建两个默认的 datetime 类型字段,一个是 created_at,一个是 updated_at,而后者正是用来生成 cache_key 的最后更新时间。而且这个时间一般来说不需要我们手动更新,我们都知道如果对一个模型对象调用 save 方法,Rails 会自动帮我们更新这个 updated_at 字段,这样,如果我修改了项目名称,项目的 updated_at 会发生变化,自然的,页面上项目对应的 cache_key 也就会发生变化,因此我们的 <%cache @project %> 也就自动过期了。

继续项目详情页里的示例代码,接下来我们看看第二级套娃:各个 Section 缓存。

拿这个 <% cache @top3_topics.max(&:updated_at) %> 为例,它比 <% cache @project %> 稍微复杂了点。我们首先应该知道的是,@top3_topics 存储的是对应项目里最新创建的三条讨论,这里比较奇怪的是,我们为什么要用一个 max(&:updated_at) 方法呢?

如果我们直接把 @top3_topics 对象作为 cache 的参数 <% cache @top3_topics %>,得到的 cache_key 实际上会是这样的形式:

views/topics/3-20140906112338/topics/2-20140906102338/topics/3-20140906092338

看的出来,是每个对象的 cache_key 的组合,我们并不太希望 cache_key 变得这么复杂,特别是当列表元素超过 3 个,比如说有 20 条记录的时候,所以最简单的办法,是取这组数据里最新一个被更新的数据的 updated_at 时间戳,这样生成的 cache_key 就是下面的样子了:

views/20140906112338

但是注意,这里有一个问题,就是假如 @top3_topics 一条数据都没有,会出现什么情况?比如我新建的项目,里面理所当然的一条讨论都没有,这个时候,实际上 cache 的是一个空的 relation,对这个空对象调用 max(&:updated) 方法,返回的值永远都是 nil,所以实际上我们是对 nil 进行 cache,不幸的是,所有 nil 的 cache_key 都一模一样,导致这样的缓存片根本不可用,你不知道究竟是对什么数据进行的缓存。另外,加入任务清单 Section 和讨论 Section 最后更新的那条数据的 updated_at 时间戳恰好一样,也会造成两个缓存片混淆的问题。

而解决这个问题的方法很简单,就是给 cache 参数里增加一个特定的字符串标识,比如把 <% cache @top3_topics.max(&:updated_at) %> 改成 <% cache [:topics, @top3_topics.max(&:updated_at)] %>,这样一来,如果 @top3_topics 里一条数据都没有,生成的 cache_key 是这样的:

views/topics/20140906112338

带上了「topics」自己的标识,这样就能和其它 nil 类型的缓存区分开了。修改后的项目详情页代码片段如下:

3. Touch!

我们回过头来再看看套娃缓存的读取机制,访问项目详情页的时候,首先读取最外层的大套娃 <% cache @project %> ,如果这个缓存片对应的 cache_key 在缓存里能找到,则直接取出来并且返回,如果缓存过期,则读取第二级套娃 — 几个列表 Section 缓存,这些缓存根据列表里最新一条数据的更新时间生成 cache_key,如果最新一条数据的更新时间没有变化,则缓存不过期,直接取出来供页面拼装用,如果缓存过期,则继续读取各自的第三级套娃。

等等,这里有个问题,如果我改变了一条任务的内容,也就是作废了任务 partial 自己的缓存,但是包裹任务的任务清单,以及包裹任务清单的项目都没有变化,这样当页面加载的时候,读取到的第一个大套娃 -- <% cache @project %> 都没有更新,会直接返回被缓存了的整个项目详情页,所以根本不会走到渲染更新的任务 partial 那里去。对于这个问题的解决方案,是 Rails 模型层的 touch 机制。

简单的说,我们需要让里面的子套娃在数据更新了以后,touch 一下处在外面的套娃,告诉它,嘿,我更新了,你也得更新才行。我们直接看看这个代码片段:

在这里,我们使用 Rails model 的 belongs_to 来声明模型的从属关系,比如一个 Todo 属于一个 Todolist,一个 Todolist 属于一个 Project,而在 belongs_to 后面,我们还传入了一个 touch: true 的参数,这样,当一条 Todo 更新的时候,会自动更新它对应的 Todolist 对象的 updated_at 字段,然后又因为 Todolist 和 Project 之间也有 touch 机制,所以对应 Project 对象的 updated_at 字段也会被更新。放到我们的套娃缓存片里面看的话,就是当一条任务更新以后,「包裹」它的任务清单的缓存片也会被更新,因为对应的 Todolist 对象的 updated_at 时间改变了,而「包裹」这个任务清单的任务清单列表 Section 的缓存片也会失效,因为 @todolists.max(:updated_at) 改变了,接着是「包裹」列表 Section 的项目缓存片过期,因为 @project 对应的 updated_at 也被更新了。

就是通过这么重重 touch 的机制,我们能确保子元素在更新以后,它的父容器的缓存也能过期,整个套娃机制才能正常运作。下面是整个 Tower 里面,各个模型层的 Touch 结构图:

4. 那些踩过的坑

经过上面的介绍,大家应该已经明白了套娃的实际使用方式,看上去很完美不是么?但在我们的实际使用过程中,套娃缓存还是有一些坑需要注意的,这里跟大家分享一下。

我们在开发过程中经常遇到的一个问题,是缓存模板里如果存在「父」元素的情况。我们把 Project 定义为 Todolist 的父元素,把 Todolist 定义为 Todo 的父元素,因为 touch 机制是自底向上的,从子 touch 到父,但是如果我们的模板是下面这个样子:

在任务清单模板里,我们需要显示一下项目的名称,也就是一个子元素的模板里,包含了父元素,这个时候如果缓存是 <% cache [:todolists, @todolists.max(&:updated_at) %> 的话,当我们把项目名称修改了,这个缓存片是不会过期的,因此任务清单列表里的项目名也不会改变。

解决这个问题的办法,一是修改任务清单的缓存的 cache_key,改成:

这样修改项目名称,就能导致缓存片过期,这也是一个普遍的手段,就是把缓存里面存在的所有模型对象统一纳入 cache_key 里面,但是这样存在一个问题,就是因为项目本身是经常被 touch 的,修改任务也会、创建评论也会,所以导致这个任务清单的缓存片会随时失效,缓存命中率降低,所以使用这种方法的时候要仔细考虑,引入父元素作为 cache_key 的一部分,是否会导致这个问题。

另一个办法是,使用实际需要的模型字段来做缓存,比如上面的例子,我们实际上只是需要项目名称,所以可以把缓存改为:

这样只会在项目名称发生改变的时候,更新缓存片,这个方法可能性价比最高,不过如果一个缓存里出现多个模型字段的时候,就要写一串这样的 cache_key,和我们「只对一个具体资源缓存」的原则有些差距,所以一般来说,缓存的具体字段最好不要超过一个。

还有一个处理方法是,在 HTML 结构上做调整,基于我们上面所说的「只对一个具体资源缓存」的原则,这里我们如果针对的是 @todolists 做缓存,那么就应该把其它无关的资源从 HTML 结构里提取出来,比如放到一个外层的 hidden input 里面:

这样可以通过 JS 读取这个属性,再重新注入到模板相应的元素里面。选择这种方案,需要提前根据设计做好规划,把那些需要提取出来的元素放在缓存以外。

最后还有一个方法,就是不理会它。如果你相信任务清单不会长期不变,而项目名称不会经常变化的话,那么缓存里的项目名称不会随时都是最新版本,就是一个可以被接受的事实了,这需要在产品层面上考虑,我们建议如果遇到这样的问题,不妨先用这种最简单的方式处理,看看用户反馈再决定是否进行调整。

——————————————— 我是分割线 —————————————————

我们遇到的第二个问题比第一个问题更加让人头疼,这个问题发生在我们为 Tower 引入一个叫做「访客锁」的新功能的时候。在 Tower 里,用户被分为普通成员、管理员和访客三种,在一个项目里,有些资源比如一条任务清单,是可以设置对访客不可见的,这个在模型层处理起来很简单,只需要增加一个字段来标识一个资源是否是对访客不可见即可,但是一旦和 Fragment Caching 结合的时候,就有问题了。在引入访客锁功能之前,任务清单列表的 Cache 是这样的:

这里 @todolists 是从项目里取出来的所有未完成的任务清单,然后使用 max(&:updated_at) 时间戳来作为 cache_key,这样在一条任务清单更新以后,这个最后更新时间会变化,cache_key 也就变化了。但是在引入访客锁以后,这就会有潜在问题了。假如我们现在有如下图所示的三条任务清单:

我们首先将「任务清单 B」加锁,然后再去修改一下「任务清单 A」的名称,这个时候整个清单列表的 max(&:updated_at) 时间就是「任务清单 A」的 updated_at 时间,如果一个普通成员先打开项目详情页,根据这个更新时间,会缓存一个含有三条任务清单的页面,接着一个访客再打开同一个项目详情页,会出现什么情况呢?这个访客会看到三条同样的任务清单,「任务清单 B」加锁是无效的!这是因为对于访客来说,虽然在控制器里查询出来的任务清单只有 A 和 C 两条,但是对于这两条任务清单,最后更新的是 A 的 updated_at 时间戳,这个和能看到三条清单的普通成员以及管理员是一样的,因此他们的任务清单列表的 cache_key 是一样的,取出来的缓存片也一样。

关于这个问题我们考虑了很久,最后发现只有两种解决方案,要么是彻底放弃对这种列表类型的片段做缓存,要么就是遍历列表里的所有子元素,把各自元素的 cache_key 组合起来再求一个 MD5 值,最后我们选择了后者,具体的做法是在有列表缓存需要的 Model 里,引入一个 Concern:

这样,在需要对列表进行缓存的时候,我们的写法就不再是 <% cache [:todolists, @todolists.max(&:updated_at)] %>,而是这样:

这种办法是目前我们能想到的最佳解决方案,不知道有没有更好的处理方式。

绕过这个最大的坑以后,还剩下最后一个地方需要修改,就是我们最外层的那个套娃,我们使用的是 <% cache @project %> 来对整个项目详情页做缓存的,但是因为引入了访客锁,所以访客看到的页面,和普通成员以及管理员看到的页面,是不一样的,如果都用 @project 作为 cache_key,会导致和上面列表模式一样的问题,好在这个地方的解决方法比较简单,把缓存改成 <% cache [@project, current_user.visitor?] %> 即可,只是对于同一个项目详情页,需要存储两份缓存了。

5. 小结

以上就是我们在 Tower 里使用套娃缓存的一些经验,除了 Fragment Caching 之外,我们也没有额外再使用 Page Caching 或者 Action Caching 之类的技术,37signals 在这篇 Blog 里(https://signalvnoise.com/posts/3690-the-performance-impact-of-russian-doll-caching)统计过他们使用套娃后的缓存命中率,这个值是 67%,而 Tower 目前 8G 的 Memcache 的缓存命中率是 45%,相比之下还有差距,不过整站使用体验上,速度并不是一个显著的短板,如果能把 Fragment Cache 的细粒度继续做下去,应该会有更好的效果。

综上,套娃缓存机制还是蛮适合于小团队用来加速自己的网站(实际上 Tower 的 Hybird 模式的移动客户端也是用这种方式来做加速的),只要在模板设计的时候,尽量按照资源做好规划,后面逐步增加套娃的数量和层级,由于只涉及到模板部分的更改,总体来说是一个性价比很高的方案。

PS:特别感谢 @hayeah 以及 @quakewang ,很多技术问题经常骚扰二位 :D

请问个问题 不知道 updated_at 作为 cache key 在更复杂一点的情况下 比如很多时候只修改了 model 部分字段 其实不需要刷新 cache 这样用什么作为 key 更合适?

#12 楼 @zj0713001 这个实际上应该用 update_column 方法来更新这些字段,这样就不会自动更新 updated_at 了

#12 楼 @zj0713001 这种情况我觉得可以忽略,如果确实要这么做,你可以自己维护一个字段,例如 cache update at 字段就好了,想什么时候更新自己搞。

great job!

Thanks for sharing ^_^

话说可不可以把 cache_key 换成 role_name 呢?这样不同角色就可以 hit 不同缓存了

def role_name
 .... # admin, member, public
end
<% cache [:todolists, role_name, @todolists.max(&:updated_at) ] %>
...
<% end %>

#21 楼 @saiga 这样其实也是有问题的,还是三条任务清单 A、B、C,如果管理员把 B 设置成加锁,对于访客来说,@todolists = [A, C]@todolists.max(&:updated_at) 还是 A,没有变化,所以缓存片不更新,看到的还是 [A, B, C] 三条

像最后一种情况,记得以前看 Railscasts 里面也有提到过,就是所有用户还是共用一套缓存,然后通过 JS 在前端来做权限控制呢?

喜欢 tower 感谢 分享

来自实战经验,接地气,图文并茂,逻辑清晰,叙事有条不紊,可以看出作者写这篇博文非常用心,心态非常平和,点赞收藏。

太棒了 顶一个

#25 楼 @teddy_1004 这个其实也不好,因为不安全,加锁的数据仍然从服务器上取回了,所以实际上访客关掉 JS 就能看到,对 Tower 这种企业应用来说是不行的。

:plus1: 赞内容,更赞译名,套娃……

其实最好的是被嵌套的 cache 调用可以自动向外层的 cache 调用传递依赖的 key,形成一个依赖链,这个需要框架来实现了,可以去提一个 feature request

Unknow user #33 September 12, 2014

:thumbsup:

@femto 思路很赞 ~

关于第一个坑,有没有可能把整个 @project 也当成 key 的一部分呢?这样的话就不用考虑要用多少 @project 的 attributes 了。我对 cache 方法不大了解,仅仅一个例子:

<% cache [:todolists, @project, @todolists.max(&:updated_at)] do %>
<% end %>

关于第二个坑,缓存对不同类型的用户怎么处理确实是个头疼的问题。有的用户可以看见所有内容,有的用户只能看见他有权限看见的。DHH 的 How Basecamp Next got to be so damn fast without using much client-side UI 讲的方法是仍然 cache 所有人都可见的版本,然后用 JavaScript 去掉当前用户无权看到的部分。也不失为一个简单的方法。做 cache 反而简单了。不需要考虑把当前用户的 role 加入 cache key 或者查询时筛选掉用户看不到的数据之类的。

长知识! 还有个问题请教: @zchar 以单页面应用来说,往往采取前后端分离的方式,比如用 Angular。我理解的是应该可以采取 Action 缓存来响应数据。请问这两种方式差别多大?

长知识,赞一个!!

看完了,很赞,thx

#35 楼 @darkbaby123 第一个在 cache 里加 @project 的写法我有分析过,如果每个列表都加,因为 project 会被频繁 touch,所以会导致缓存片失效得非常快,我更新一条任务会导致文档列表缓存的失效,所以不好。

第二个还是安全问题,如果只是取所有成员的昵称回来,也许还可以接受,但是把本来加锁的内容也取回来再由 JS 处理,这就很危险了哈。

#37 楼 @loveltyoic 如果是用前端 MVC 框架,那缓存就应该是在模型层去做了,后端只是 RESTFul 的 API,但是这种架构我们从来没做过,所以也不知道性能上有什么不一样,我自己预计是不会差太多的。

@zchar @loveltyoic 原来如此,如果缓存经常失效就失去意义了。这点来看后端缓存非常依赖页面如何展现。如果前后端分离的方式缓存可能影响比较小。后端只操心缓存单条数据,不用去 touch parent,前端也可以按需去加载页面中的数据,而不用一次请求整个页面。

#32 楼 @femto

DHH 的 cache digest 就算是向外传递 cache key 的思路吧。不过只针对的是页面源码。

还有另一种思路是 Tag Based Caching: https://github.com/ahawkins/cashier

在写缓存的时候,通过声明 tag,把一块儿的 cache key 用一个 tag 标记出来,失效的时候通过 tag 找到底下的所有 cache,然后 clear 掉。

rails 可能是这个世界上缓存解决方案最全的框架了

#45 楼 @rainchen 很赞!I18n 的问题也被考虑到了,非常感谢~ 😄

@hooopo cache digest 是针对源码的把?类似于 assets precompile 完了在 js,css 文件后面增添不同的 digest,是用来区分不同源代码版本不会混用的把?没有看过,能够简单说说,怎么用? Tag Based Caching,回头我去看看。

请问,类似

<% cache [:todolists, @todolists.max(&:updated_at)] do %>
  <%= render partial:'todolists/todolist', collections: @todolists
<% end %>

@todolists中的某个 todolist 被删除时,应该不会影响到@todolits.max(&:updated_at)的结果吧? 这样缓存应该不会失效,还是我对@todolists.max(&:updated_at)的返回值理解有误?

#48 楼 @infinityBlue 抱歉,忘了说了,Tower 里面所有资源都是「软删除」的,也就是并不是真正从数据表里删除,所以当一个资源被删除的时候,实际上它的 updated_at 时间戳也会被更新,另外,我们也确实在 cache_key 里加上了当前资源的 count,所以实际上之前完整的缓存 key 是 [:todolists, @todolists.max(&:updated_at), @todolists.count] , 关于这个问题可以看这篇文章:http://zhuanlan.zhihu.com/mycolorway/19724081 ,谢谢你的问题 :)

@zchar 懂了,非常感谢解答!

深入浅出!

我有一点疑问:

浏览器请求 => 服务器渲染页面时拿着 cache_key => 进缓存块查找 => 命中返回

那么,服务器是在什么时候知道资源更新了呢?

1、资源更新 => 自动通知到缓存块,使缓存块失效 2、资源更新 => 自动通知到页面的 cache 块,修改其 cache_key ( 计算 cache_key 时自动查询资源出来) 3、服务器拿着 cache_key 进缓存块查找 => 命中资源 => 查询资源是否有更新过 => 再决定是否重新查找 or 直接返回

求解答疑惑@zchar @leekelby

从使用的角度出发,我归纳了几条片段缓存相关的规则,特别是嵌套的情况:

  1. 缓存由动态内容和静态内容两部分构成。

  2. 动态内容的 cache_key 由我们指定;

    1. 没有嵌套的情况下,如果动态内容不指定 cache_key,则自己的动态内容永远不会更新 (例外见最后);
    2. 有嵌套的情况下,如果动态内容不指定 cache_key,则自己的动态内容 & 孩子的动态内容永远不会更新 (例外见最后);
  3. 没有嵌套的情况下,有且只有自己的 cache_key 更新,动态内容才更新;

  4. 有嵌套的情况下,有且只有自己的 cache_key 更新 & 父亲的 cache_key 更新,动态内容才更新;

  5. 动态内容的更新,不影响静态内容的部分;

  6. (各动态内容的 cache_key 是独立的,自己及其父亲、兄弟、孩子的 cache_key 没有依赖关系)

  7. 无论哪的静态内容更改,有且只有重启后更新,不存在 (也不用考虑) 嵌套的问题;

  8. 静态内容的更新,不影响动态内容的部分 (例外见最后);

  9. 例外:动态内容没有指定 cache_key,只有静态内容同时更新,并且重启,动态内容才会更新。

有遗漏/不对的地方,请楼主&其他人员...补充/指正。

好文章,看的酣畅淋漓。对于"touch"有一些小 comment。

@rainchen “这个更多是一个 view 层面 render 时要顾虑的东西” (要 decouple View 和 Model),引申一下这个观点,我们使用 russian-roll caching 的时候,我觉得需要了解 touch 机制的局限,并且尽量把 view 和 model 解耦。

  1. 数据库写操作的 Scalability 被'touch'的表将有可能变成 bottleneck,比如 project 这个 root node,它的读写操作会是子孙 node 的总和(由于 leaf node 会 populate 整个 touch chain,所以不单单是 child node 了)所以,有越多的 leaf node 加进来,ancestor node 就一直被 touch,写操作比较难 scalable 了。当然,读操作的效率很高,因为如果 root node 没变,就不用 touch child node 了。

  2. Coupling 数据库 table 之间的 reference 关系和 view 的层次被 couple 到一起了。一旦 view 之间的相互关系变了,touch 也要相应的改变。而且,数据库里要存 foreign key 来形成这个 touch chain,这些 key 也许从 data modeling 的角度都是冗余信息,只是为了 view 的嵌套方便。

所以 touch 适用的范围是,外层大套娃的 table 轻量,view 的结构基本不变,和 model 有一致的层次关系(view 的对应的套娃和 model 的套娃的套法是相近的)。

也许楼主的 use case 就是如此,所以做了那样的 design。

#52 楼 @linjunzhugg 我不能直接回答你的问题,下面是我的一些理解,希望能你有帮助。

说一说 Cache Key

1) record 的 cache_key

2) helper 方法 cache(name = {}, options = nil, &block) 这里的 name

3) 内存数据库 key

不严格区分的话,它们都可以叫做"Cache Key"

但,你可以把它们区分开来:

post.cache_key
=> "posts/1-20140921032815201680000”
<% cache [ 'a_post', post ] do %>
  … ...
<% end %>
views/a_post/posts/1-20140921032815201680000/9746fd05c8428f7999681aa804071e9a
(路径    helper方法cache的name部分    静态内容md5)

特点:

1 由 record 的"id + updated_at 时间戳"组成,所以 record 更新,cache_key 会更新 (update_column 等情况不讨论)

2 由我们指定,所以理论上可以随便写。没有嵌套缓存的情况下,一般可以直接使用 1;有嵌套的情况下可以用 touch 更新父亲资源或者使用一个组合,如:cache [current_user, post]

3 根据 2 生成 (但不等于 2),要求唯一 (要不然就没意义了)

2 每次请求都要计算

2 计算之后和 3 对比是否匹配。不匹配则生成新的内容,渲染页面;匹配则返回,渲染页面

2 不匹配,那么意味着之前的内容过期了,过期的内容还是继续存在的数据库里的。(这里的过期不等于被删除)

2 过期的内容可以由我们手动删除,如:Rails.cache.clear;或让数据库自动删除,如 config.cache_store = :redis_store, 'redis://localhost:6379/5/cache', { expires_in: 90.minutes } 这里数据最多只能在 redis 里保存 90 分钟,到期的缓存数据会被删除。(这里的到期等于被删除)

#55 楼 @leekelby

THX

It's helpful for me

57 Floor has deleted

“另外,加入任务清单 Section 和讨论 Section 最后更新的那条数据的 updated_at 时间戳恰好一样,也会造成两个缓存片混淆的问题。”这里如果时间戳恰好相同一定会造成 cache_key 相同吗?

好文章,收藏了。

这个只是在渲染页面的时候做了一层 cache,但还是要做数据库查询。 能否直接 cache 到直接不做数据库查询了?

获益匪浅,感谢啊!

有两个小问题请教: 1.

把 <% cache @top3_topics.max(&:updated_at) %> 改成 <% cache [:topics, @top3_topics.max(&:updated_at)] %>,这样一来,如果 @top3_topics 里一条数据都没有,生成的 cache_key 是这样的:

views/topics/20140906112338

cache [:topics, nil] 生成的 cache_key 中怎么会包含时间信息呀?

2. 关于 max(&:updated_at),我知道&加上一个 symbol 是将一个 method 转换为一个 block,但我所知道 max 方法后面的 block 是要有 2 个参数的,比如 {|a,b| a.size <=> b.size}. 我自己在 rails console 里尝试类似的语句 User.all.max(&:updated_at) 会出错。你这里的 max 是有重新定义吗?

#58 楼 @shangrenzhidao 如果能够在缓存中找到命中缓存,cache_key 应该是采用可规则性方法来产生,所以应该是会产生相同的,不过概率很小。

我想在 45 楼回答的基础上,将 user_role 作为变化因子加入到 cache_key 算法中,或许可以比较好的解决问题 2;也就是说,某些部分,可以设置为 user_role 敏感部分,比如 todolist 等等,而公开性的内容则不需要。

64 Floor has deleted

涨知识了

:plus1: 原来touch: true是这么用的。

第一个坑:在缓存中设置一个 key 假如叫做 project-id-name,里面的值就是所有涉及到 project name 的其它缓存的 key。当 project name 改变的时候,取出这些 keys,依次删除或更新这些 keys 的内容。

第二个坑:参考 #45 的做法,在缓存 key 中引入 role。

使用了一段时间,感觉速度提升上面很不明显。

因为每次重新生成 cache_key 的时候,都会去 DB 中查出 objects, 然后计算 cache_Key。如果我们仅仅单纯的搜出某个 model 的 前 30 条记录,那么使用 cache_key 反而耗费的时间更长

#68 楼 @linjunzhugg rails 渲染页面慢只是因为几个 help 方法,如果 view 是纯 html 并且没有其他的 ruby 代码,加上片段缓存和不加效果是差不多的。

每次看都有新收获

这个 cache 只是页面生成不需要重新计算,直接从缓存中获取,但是页面还是需要重新传输?

踩到地雷,前来拜读下

最外层的套娃

<%cache @project do%>

没有使用 updated_at,如何实现的缓存更新呢? 这个 Project 必须是基于 ActiveRecord 的 model 吗?

alsotang in Web 开发后端缓存思路 mention this topic. 16 Jun 11:23

非常感谢楼主分享的文章,我个人觉得第二个问题是加入了锁导致的问题,是不是可以从不给模型加这个字段来实现这个锁的功能来解决这个缓存问题。等我实现了告诉楼主

涨知识了。

ruby_xi in Rails 缓存,你应该知道的几件事 mention this topic. 16 Mar 09:44

学习了,不过现在应该有更好的解决方案了,我想。https://github.com/rails/rails/pull/20884/files

论坛新人值得一看的,Ruby China 上的经典讨论贴子

You need to Sign in before reply, if you don't have an account, please Sign up first.