Homeland 缓存问题:话题相关页面 可能 半个小时内没有热门节点

zhangyuan · November 20, 2011 · Last by jinleileiking replied at February 21, 2012 · 3546 hits

大家好,刚才看了一下网站源码,发现一个关于缓存失效的问题,导致半个小时内没有热门节点。

不知道说得对不对,如有错误,请见谅哈:

生成话题边栏时, https://github.com/huacnlee/ruby-china/blob/master/app/controllers/topics_controller.rb 判断片段缓存是否存在

def init_list_sidebar 
 if !fragment_exist? "topic/init_list_sidebar/hot_nodes"
    @hot_nodes = Node.hots.limit(10)
  end
  set_seo_meta("社区")
end

然后在 views 里生成缓存 https://github.com/huacnlee/ruby-china/blob/master/app/views/topics/_sidebar.html.erb

<%= cache("topic/init_list_sidebar/hot_nodes",:expires_in => 30.minutes) do %>
  <% if not @hot_nodes.blank? %>
  <div class="hot_nodes nodes box">
    <h2>热门节点</h2>
    <ul>
      <% @hot_nodes.each do |node| %>
        <li><a href="<%= node_topics_path(:id => node.id) %>"><%= node.name %></a></li>
      <% end %>
    </ul>
  </div>
  <% end %>
<% end %>

在一些临界时刻,可能出现下面问题:

  1. 请求到达 controller 时,如果缓存“topic/init_list_sidebar/hot_nodes”还在,就不会有实例变量 @hot_nodes
  2. 在渲染视图之前,30 分钟的时间到了,缓存失效
  3. 由于@hot_nodes 为 nil,然后半个小时以内,就没有热门节点了

一般情况下,在 controller 里查数据库,在视图里渲染。但是和缓存相关时,这样做可能就有问题。

这个问题之前我都没太注意,不知 Rails 内部机制是否又对这种情况做过处理,翻看了一下 Rails 源代码,没有发现有关的处理,但是从之前的一些实际项目情况来看,这种写法是可以的 不知有人是否知道具体情况

如果是 ActiveRecord,@hot_nodes = Node.hots.limit(10) 返回的是 ActiveRelation,不会去查数据库,只有在用到的时候才去查,所以没必要在 controller 检查 cache key 是否存在。但是 MongoId 就不知道了。

这里 controller 的片段检测应该去掉

#2 楼 @zhangyuan 别陷入这个场景啊,有些场景在 Controller 里面是已经取得数据了的

这部分代码该改由 cells 实现

别跑题嘛,说那个临界点的问题啊

插播一句 if !fragment_exist? "topic/init_list_sidebar/hot_nodes" 改成: unless fragment_exist?('topic/init_list_sidebar/hot_nodes') 比较舒服。

#7 楼 @xds2000 不喜欢用 unless 容易逻辑混乱 if ! 比较符合语言说法,“如果,如果没有”

楼主,解决方法我给一个

  1. 去掉 controller 层的 fragment_exist? 2.去掉 views 层的<% if not @hot_nodes.blank? %>

就搞定

@huacnlee if not 更 ruby。

#10 楼 @bony 那应该是 unless 更 ruby 了,呵呵

都不要争啦。。 unless @hot_nodes最 Ruby! if @hot_nodes.present?最 Rails! if defined?(@hot_nodes) 最 Professional!

其实我觉得即使不用 cell,这种组件也应该是自包含的。就是不该依赖 controller 里的变量。

<%= cache("topic/init_list_sidebar/hot_nodes",:expires_in => 30.minutes) do %>
 <div class="hot_nodes nodes box">
    <h2>热门节点</h2>
    <ul>
      <% Node.hots.limit(10).each do |node| %>
        <li><a href="<%= node_topics_path(:id => node.id) %>"><%= node.name %></a></li>
      <% end %>
    </ul>
  </div>
<% end %>

优雅一些可以在 Helper 里:

def hot_notes
  Node.hots.limit(10)
end

view:

<%= cache("topic/init_list_sidebar/hot_nodes",:expires_in => 30.minutes) do %>
 <div class="hot_nodes nodes box">
    <h2>热门节点</h2>
    <ul>
      <% hot_nodes.each do |node| %>
        <li><a href="<%= node_topics_path(:id => node.id) %>"><%= node.name %></a></li>
      <% end %>
    </ul>
  </div>
<% end %>

再 DRY 一下,可以做成局部模板:

<%= cache("topic/init_list_sidebar/hot_nodes",:expires_in => 30.minutes) do %>
  <%= render :hot_nodes %>
<% end %>

这种通用组件依赖 controller 里的实例变量的缺点是,如果有多个页面需要调用,必须在 controller 里都加这么一句:

@hot_nodes = Node.hots.limit(10)

还有就是楼主描述的缓存时候会出现的问题

# @hot_nodes = Node.hots.limit(10) 如果这一句查询超过了 1 秒,则很可能出现这个情况。

#边界条件互相依赖 缓存内容依赖一个实例变量 这个实例变量依赖缓存的存在 这样互相依赖的情况,边界点,又没有事务控制

# 做成自包含,的确是个不错的条约

自包含不一定就是在 v 层 在 c 层也是可以的,就是直接去取缓存的流,有则渲染,只不过 c 层丑点儿。

做成自包含其实是把互相依赖放在了一起

但是如果没有直接去取,而是先判断,再取,这又会有“原子”问题。只不过这个时候程序执行很快,极少发生。

另外还有缓存放在哪里了,谁来清除,异步的实现方式等

#8 楼 @huacnlee unless 用惯了就行了。。。话说用了 ruby 才知道 unless = if not...

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