使用“俄罗斯套娃”缓存技术构建Rails应用程序,处理具有has_many关系的数据结构

23

在学习了DHH及其他博客文章有关基于键的缓存过期和俄罗斯套娃缓存的内容后,我仍然不确定如何处理一种关系类型,具体来说是has_many关系。

我将分享我在一个样本应用程序上进行研究的结果。这是稍微讲述故事,所以请耐心等待。假设我们有以下ActiveRecord模型。我们所关心的只是正确更改模型的cache_key,对吧?

class Article < ActiveRecord::Base
  attr_accessible :author_id, :body, :title
  has_many :comments
  belongs_to :author
end

class Comment < ActiveRecord::Base
  attr_accessible :article_id, :author_id, :body
  belongs_to :author
  belongs_to :article, touch: true
end

class Author < ActiveRecord::Base
 attr_accessible :name
  has_many :articles
  has_many :comments
end

我们已经有一篇文章,一个评论,两者由不同的作者撰写。目标是在以下情况下更改文章的cache_key

  1. 文章的正文或标题更改
  2. 其评论的正文更改
  3. 文章的作者姓名更改
  4. 文章的评论作者姓名更改

因此,默认情况下,我们已经满足1和2两种情况。

1.9.3-p194 :034 > article.cache_key
 => "articles/1-20130412185804"
1.9.3-p194 :035 > article.comments.first.update_attribute('body', 'First Post!')
1.9.3-p194 :038 > article.cache_key
 => "articles/1-20130412185913"

但对于第三种情况则不是这样。

1.9.3-p194 :040 > article.author.update_attribute('name', 'Adam A.')
1.9.3-p194 :041 > article.cache_key
 => "articles/1-20130412185913"

让我们为Article定义一个复合的cache_key方法。

class Article < ActiveRecord::Base
  attr_accessible :author_id, :body, :title
  has_many :comments
  belongs_to :author

  def cache_key
    [super, author.cache_key].join('/')
  end
end

1.9.3-p194 :007 > article.cache_key
 => "articles/1-20130412185913/authors/1-20130412190438"
1.9.3-p194 :008 > article.author.update_attribute('name', 'Adam B.')
1.9.3-p194 :009 > article.cache_key
 => "articles/1-20130412185913/authors/1-20130412190849"

胜利!但是对于第四种情况,这种方法无效。

1.9.3-p194 :012 > article.comments.first.author.update_attribute('name', 'Bernard A.')
1.9.3-p194 :013 > article.cache_key
 => "articles/1-20130412185913/authors/1-20130412190849"

那么现在还有什么选择?我们可以考虑在 Author 模型上使用 has_many 关联,但是 has_many 并不支持 {touch: true} 选项,这也可能有其原因。我猜实现起来大概会是以下的方式。

class Author < ActiveRecord::Base
  attr_accessible :name
  has_many :articles
  has_many :comments

  before_save do
    articles.each { |record| record.touch }
    comments.each { |record| record.touch }
  end
end

article.comments.first.author.update_attribute('name', 'Bernard B.')
article.cache_key
  => "articles/1-20130412192036"

虽然这个方法确实可以生效,但会对性能产生巨大影响,因为需要一篇篇文章和评论的加载、实例化和更新,一个接一个地进行。我认为这不是一个合适的解决方案,那么有什么更好的方法呢?

当然,37signals的用例/示例可能不同:项目 -> 待办事项列表 -> 待办事项。但我想象一个单独的待办事项也属于一个用户。

有什么方法可以解决这个缓存问题吗?


这并没有特别解决缓存问题,但实现 touch 并不需要那么复杂。你可以简单地执行 articles.update_all(updated_at: Time.now) 这样的操作,这将为文章(和评论)产生一个操作。 - numbers1311407
@numbers1311407 那种方法的问题在于 update_all 只执行 SQL,不会执行回调函数,因此后续的 touch 操作不会发生,并且内存中对象的 cache_key 不会重新生成。 - Graham Conzett
@numbers1311407 两个问题中较大的问题是链接touch调用。如果用户更新了他的姓名并且我们在他的评论上调用update_all,则评论中的belongs_to:article,touch:true不会触发,并且文章的片段缓存将不会过期。至少这就是我看到的,请纠正我如果我错了。您可以手动过期用户有评论的所有文章,但随着树的增长,这将变得难以维护。不幸的是,我目前没有看到其他选择。 - Graham Conzett
1
不,你说得对,“update_all”不会运行回调。我想你可以在作者中添加“has_many:commented_articles through: comments”的关联类型,并在回调中也触发它们。但我理解你的观点,当你在纯层次结构之外呈现内容时(比如作者的用户名),俄罗斯套娃很快就会变得混乱。奇怪的是,在俄罗斯套娃缓存的写作中,比如37Signals提供的链接,从未提到这些解决方案。 - numbers1311407
1
不确定这是否是一个好的解决方案,但你读过这篇文章了吗?(http://mark.stratmann.me/content_items/rails-caching-strategy-using-key-based-approach)这个人是如何解决的?即使在这篇DHH博客文章中(http://37signals.com/svn/posts/3112-how-basecamp-next-got-to-be-so-damn-fast-without-using-much-client-side-ui),他在该项目缓存中列出了用户名(第一张截图)。当其中一个名称更改时,他们如何处理清除缓存?我也想得到一个更好的答案。这似乎是一个重要的问题。 - Slickrick12
显示剩余4条评论
2个回答

8

我发现一种处理方法是通过缓存键来处理。为评论者和文章添加一个 has_many_through 关系:

class Article < ActiveRecord::Base
  attr_accessible :author_id, :body, :title
  has_many :comments
  has_many :commenters, through: :comments, source: :author
  belongs_to :author
end

然后在文章/展示页面中,我们将构建缓存键,如下所示:
<% cache [@article, @article.commenters, @article.author] do %>
  <h2><%= @article.title %></h2>
  <p>Posted By: <%= @article.author.name %></p>
  <p><%= @article.body %></p>
  <ul><%= render @article.comments %></ul>
<% end %>

这个技巧的关键在于,从“commenters”关联生成的缓存键会在添加、删除或更新评论时发生变化。虽然这确实需要额外的SQL查询来生成缓存键,但它与Rails的低级缓存很好地配合,并且添加类似identity_cache gem的东西可以轻松解决这个问题。 我想看看其他人是否有更简洁的解决方案。

这种技术适用于单个记录的显示操作,但对于多个记录的索引操作的顶层(外层)俄罗斯套娃缓存来说不可行 -- 每个记录的缓存都存在本文所描述的问题。 - Chris Beck
在这种情况下,您可以使用自定义缓存键辅助函数,详细信息请参阅Rails指南:http://edgeguides.rubyonrails.org/caching_with_rails.html#highlighter_32543 - Graham Conzett
明白,就像原始帖子中使用的 def cache_key [super, author.cache_key].join('/') end 一样。 - Chris Beck

0
如此建议在这里https://rails.lighthouseapp.com/projects/8994/tickets/4392-add-touch-option-to-has_many-associations,在我的情况下,我只是创建了一个after_save回调来更新相关对象的时间戳。
  def touch_line_items_and_tactics
    self.line_item_advertisements.all.map(&:touch)
  end

顺便提一下,我们的Rails应用程序是构建在一个遗留数据库上的,该数据库具有last_modified_time作为列名,并且其语义是“当用户最后修改它时”。因此,由于语义不同,我们无法直接使用:touch选项。我不得不像这样https://gist.github.com/tispratik/9276110 按照monkeypatch缓存键和触摸方法,以便将更新的时间戳存储在memcached中,而不是数据库的updated_at列中。

另外,请注意我无法使用Rails提供的默认cache_timestamp_format,因为它仅提供时间戳到秒。我感到需要更细粒度的时间戳,因此我选择了:nsec(纳秒)。

使用cache_timestamp_format的时间戳:20140227181414
使用nsec的时间戳:20140227181414671756000


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接