2007-12-10
关于实现一个rails smart cache 的思路草稿。
最近研究了一下 rails的cache设计,发现其中一些不尽如人意的地方:
* cache expiry 编写繁琐
* 分页缓存的清除,现有cache实现的支持都不是很完善
* 在一次清除大量缓存的时候,脏数据读的问题。
我查阅了一些blog以及相关的文章,从他们的抱怨和设计中得到一些启发,我觉得cache可以做得更好,更智能,更能够减少开发人员的工作量。 下面是我设计思路的一些草稿,如果深入分析,觉得可行的话,就可以动手做他:
* 支持 cache 分组。
* 支持简化的expiry rules
* 适当的设计减少一次清除大量缓存时脏数据读的概率和时间窗
* 仅考虑memcache的支持。
* 尽可能在现有框架的基础上做简单的扩展,减少开发量。
支持cache 分组可以同时带来很多好处,第一,支持分页缓存的一次expire。同时也有利于“减少脏数据读的时间窗”,cache分组的设计是整个设计的关键思路之一。下面来谈谈cache分组的设计:
* 内存开辟一个hash。key是group ID。value是一个特有的Group对象,这个Group对象的设计目的是尽可能简便并且尽快地 清除同一组的所有cache key。
* 要expire 同组所有脏缓存数据,有两种策略: 1) 按照cacheKey 逐一清楚 2) 不清除,而是加版本号
第一种策略意味着需要保存Group和 CacheKey的一对多关系,这个保存可能需要通过数据库来保存,带来的复杂和性能开销似乎不是上选。
第二种策略不用清除缓存,也不用查询数据库。但这种策略也需要做一些工作,在加入缓存的时候,我们需要修改原有的cacheKey,在后面加上Version=1这样版本标识。get cache的时候
也需要做同样的工作。当expire脏Group的时候,只需要将Group对象上的version属性递增一。原有的脏数据通过memcache的自动清理策略来自动清除。
从这个分析来看,我觉得选择第二种策略更好。
整个smart_cache的设计实现应该在现有的cache实现的基础上,通过alias_method(rails的AOP)加入我们自己的中间逻辑。这样开发量最少。
设计中的另外一个关键点是 expiry rules的设计,目前rails cache的expire的编写已经比较简单了,我们只需要更进一步,初步
设计如下,在config目录下增加一个 cache_expiry_rules.rb,示例代码如下:
上面代码中的关键部分:
:expire_rules=>[cud_rule(User,Department,Room),rule(User.update*)])
其中,cud表示某个model对象上的create,update,delete方法完成后。
后面的rule(user.update*) 表示UserModel上的符合规则的某些方法被调用了之后。
原有的缓存的方法 cache 需要最后增加一个属性 :group=>"your group",比如 cache fragment 的编写
参考文章:
* web agile devel....
* http://www.railsenvy.com/2007/2/28/rails-caching-tutorial
* http://blog.leetsoft.com/2007/5/22/the-secret-to-memcached
* http://blog.craigambrose.com/past/2007/11/13/caching_makes_your_brain_explode/
* http://cfis.savagexi.com/articles/2007/09/05/rails-unusual-architecture
.............
* cache expiry 编写繁琐
* 分页缓存的清除,现有cache实现的支持都不是很完善
* 在一次清除大量缓存的时候,脏数据读的问题。
我查阅了一些blog以及相关的文章,从他们的抱怨和设计中得到一些启发,我觉得cache可以做得更好,更智能,更能够减少开发人员的工作量。 下面是我设计思路的一些草稿,如果深入分析,觉得可行的话,就可以动手做他:
* 支持 cache 分组。
* 支持简化的expiry rules
* 适当的设计减少一次清除大量缓存时脏数据读的概率和时间窗
* 仅考虑memcache的支持。
* 尽可能在现有框架的基础上做简单的扩展,减少开发量。
支持cache 分组可以同时带来很多好处,第一,支持分页缓存的一次expire。同时也有利于“减少脏数据读的时间窗”,cache分组的设计是整个设计的关键思路之一。下面来谈谈cache分组的设计:
* 内存开辟一个hash。key是group ID。value是一个特有的Group对象,这个Group对象的设计目的是尽可能简便并且尽快地 清除同一组的所有cache key。
* 要expire 同组所有脏缓存数据,有两种策略: 1) 按照cacheKey 逐一清楚 2) 不清除,而是加版本号
第一种策略意味着需要保存Group和 CacheKey的一对多关系,这个保存可能需要通过数据库来保存,带来的复杂和性能开销似乎不是上选。
第二种策略不用清除缓存,也不用查询数据库。但这种策略也需要做一些工作,在加入缓存的时候,我们需要修改原有的cacheKey,在后面加上Version=1这样版本标识。get cache的时候
也需要做同样的工作。当expire脏Group的时候,只需要将Group对象上的version属性递增一。原有的脏数据通过memcache的自动清理策略来自动清除。
从这个分析来看,我觉得选择第二种策略更好。
整个smart_cache的设计实现应该在现有的cache实现的基础上,通过alias_method(rails的AOP)加入我们自己的中间逻辑。这样开发量最少。
设计中的另外一个关键点是 expiry rules的设计,目前rails cache的expire的编写已经比较简单了,我们只需要更进一步,初步
设计如下,在config目录下增加一个 cache_expiry_rules.rb,示例代码如下:
SmartCache::Rules do |config| config.add_rule(:group=>"user list",:expire_rules=>[cud_rule(User,Department,Room),rule(User.update*)]) config.add_rule(:group=>"department list",:expire_rules=>[cud_rule(Department,Room),rule(Department.update*)]) end
上面代码中的关键部分:
:expire_rules=>[cud_rule(User,Department,Room),rule(User.update*)])
其中,cud表示某个model对象上的create,update,delete方法完成后。
后面的rule(user.update*) 表示UserModel上的符合规则的某些方法被调用了之后。
原有的缓存的方法 cache 需要最后增加一个属性 :group=>"your group",比如 cache fragment 的编写
<% cache :controller="user",:action=>"list",:page=>"params[:page]",:group=>"user list" do %> //rhtml code here to renderring view <% end %> caches_action :list, :show,:group="test group"
参考文章:
* web agile devel....
* http://www.railsenvy.com/2007/2/28/rails-caching-tutorial
* http://blog.leetsoft.com/2007/5/22/the-secret-to-memcached
* http://blog.craigambrose.com/past/2007/11/13/caching_makes_your_brain_explode/
* http://cfis.savagexi.com/articles/2007/09/05/rails-unusual-architecture
.............
评论
firebody
2008-01-06
nihongye 写道
firebody 写道
LRU频繁的话,性能应该会很差
不知道这个猜测是从哪里来的?用java里面的LinkedList实现LRU,只需要一个指针移动就可以了
嗯,我那个结论有点草率。 回头我仔细看看LRU以及memcache的相关设计才能得出准确一些的结论,等以后发布的时候,做一些真正的测试也才能模拟出来。 不过真正的高并发测试以及相应的性能测死确实不是难么容易做。 需要考虑的东西比较多。
nihongye
2008-01-03
firebody 写道
LRU频繁的话,性能应该会很差
不知道这个猜测是从哪里来的?用java里面的LinkedList实现LRU,只需要一个指针移动就可以了
firebody
2007-12-30
最近在忙着一个小项目,赚取一些生活费,所以这边的工作我落下了两个星期,很抱歉 。
这两个星期里面,我抽空想了一下原先的设计,LRU频繁的话,性能应该会很差,如果做精巧一些参数调整的话 ,性能估计会好很多,我没做过相关的研究,我想提供多一种选择。所以后面的话,主要的设计架构会调整,加多一个管理策略: 用 memory的hash来记录 groupId和所属的 cacheKey的关系。
同时也支持 不使用 group而直接使用cacheKey的缓存管理策略。
内存可能会因为使用memory来记录会占用多一些,但是这个占用应该不会很大,因为并不是所有缓存都使用group,大多数还是直接使用cacheKey的。
我做这个插件最希望的就是简化引入mem cache需要做的工作 。实现无缝的引入,并且支持group cache管理的效果 。
这两个星期里面,我抽空想了一下原先的设计,LRU频繁的话,性能应该会很差,如果做精巧一些参数调整的话 ,性能估计会好很多,我没做过相关的研究,我想提供多一种选择。所以后面的话,主要的设计架构会调整,加多一个管理策略: 用 memory的hash来记录 groupId和所属的 cacheKey的关系。
同时也支持 不使用 group而直接使用cacheKey的缓存管理策略。
内存可能会因为使用memory来记录会占用多一些,但是这个占用应该不会很大,因为并不是所有缓存都使用group,大多数还是直接使用cacheKey的。
我做这个插件最希望的就是简化引入mem cache需要做的工作 。实现无缝的引入,并且支持group cache管理的效果 。
casephoen
2007-12-18
测试的结果呢?不贴出来怎么有说服力阿
supermy
2007-12-15
用memcache自己定制cache策略。me在hibernate上就是根据业务的情况划分cache的大小,应用memcached的
取得不错的效果。
取得不错的效果。
firebody
2007-12-14
正如同第一个帖子提到的 ,目前缓存expiry的策略是 唯一递增 group version.
这样的策略 好处是 不用花费较多的逻辑来记录 group和 cache key的一对多关系 ,也不用再expire group的时候一一清除 cache item .
缺点是 因为没有 delete stale cached item,会导致memcache的缓存最终被频繁的LRU.这样的性能损耗 对比与 频繁的 remove 脏数据 性能下降多少 。 如果LRU的参数做一些为微妙的调整可能会对smart_memcache的缓存清除策略带来很大的效果 。
考虑到性能调整的选择,目前也考虑在后续的开发中,支持自动清除过期缓存条目的功能,避免频繁的LRU,不过因为没有得到实际的性能统计,所以也不好立即开展这份工作哦,等后续版本再说。
这样的策略 好处是 不用花费较多的逻辑来记录 group和 cache key的一对多关系 ,也不用再expire group的时候一一清除 cache item .
缺点是 因为没有 delete stale cached item,会导致memcache的缓存最终被频繁的LRU.这样的性能损耗 对比与 频繁的 remove 脏数据 性能下降多少 。 如果LRU的参数做一些为微妙的调整可能会对smart_memcache的缓存清除策略带来很大的效果 。
考虑到性能调整的选择,目前也考虑在后续的开发中,支持自动清除过期缓存条目的功能,避免频繁的LRU,不过因为没有得到实际的性能统计,所以也不好立即开展这份工作哦,等后续版本再说。
firebody
2007-12-14
已经在rubyforge申请了 ,可以匿名访问 下载源代码
Anonymous Subversion Access
This project's SVN repository can be checked out through anonymous access with the following command(s).
svn checkout http://smartmemcache.rubyforge.org/svn/
or
svn checkout svn://rubyforge.org/var/svn/smartmemcache
因为是作为rails的插件使用的,所以项目整体采用了一个简单的rails项目作为基础,真正的plugin代码和测试在vendor/plugins/smart_cache下面。
目录结构说明 :
Anonymous Subversion Access
This project's SVN repository can be checked out through anonymous access with the following command(s).
svn checkout http://smartmemcache.rubyforge.org/svn/
or
svn checkout svn://rubyforge.org/var/svn/smartmemcache
因为是作为rails的插件使用的,所以项目整体采用了一个简单的rails项目作为基础,真正的plugin代码和测试在vendor/plugins/smart_cache下面。
目录结构说明 :
\vendor\plugins\smart_cache\lib smart_memcache核心代码 \vendor\plugins\smart_cache\lib\aop smart_memcache内部的一个 AOP框架 \vendor\plugins\smart_cache\lib\rails smart_memcache对于rails得setup和扩展 \vendor\plugins\smart_cache\spec 对smart_memcache核心代码以及aop框架的spec测试 \vendor\plugins\smart_cache\tasks\rspec.rake 运行整个spec测试的 rake任务 \vendor\plugins\smart_cache\init.rb 插件安装文件 ,由 rails自动调用 \spec\controllers 针对rails集成环境的smart_memcache集成测试。 由rspec对rails的插件支持来运行测试 \config\smart_cache_config.rb smart_memcache在rails中的配置文件,使用smart_memcache主要的工作就是配置此文件 。
firebody
2007-12-14
TODO List:
* 支持查询缓存 ,更简洁,更透明的查询缓存配置。 在 smart_memcache_config.rb中作如下配置:
用户不需要修改任何已有的model代码。 smartmemcache会根据配置自动增强原来的代码。
* aop支持 arround interceptor
* 支持重新启动 rails后能够读取最后的group version,避免读取旧有的脏数据 。
* 支持查询缓存 ,更简洁,更透明的查询缓存配置。 在 smart_memcache_config.rb中作如下配置:
config.cache("User").on(:user_list,:find_all,:find_by_id,:find_by_name).with_group(:user_query_cache)。with_options(:ttl=>60*30)
config.expire_group(:user_query_cache).after("User","Department").fired(:update_all,:do_logic,:del_department_by_user)
用户不需要修改任何已有的model代码。 smartmemcache会根据配置自动增强原来的代码。
* aop支持 arround interceptor
* 支持重新启动 rails后能够读取最后的group version,避免读取旧有的脏数据 。
firebody
2007-12-14
值得再推荐的是,其中做的那个ruby aop小框架,大家看看具体的使用的例子:
describe AOP::RubyAop," after interceptor " do
before(:each) do
@ruby_aop = AOP::RubyAop.new
@aop_inf = nil
reload_blah
Blah.m_call = nil
end
it "should interceptor class method:update_all use methods declaration" do
block1_executed = false
orig_block_executed = false
args = nil
@ruby_aop.interceptor(:classes=>["Blah"],:methods=>[:update_all],:interceptor_type=>:after) { |aop_info,*a| @aop_inf = aop_info;block1_executed = true ;args=a;Blah.m_call.should eql("self.update_all") }
@ruby_aop.enhance_intercepted_classes
Blah.update_all(1,2) { orig_block_executed = true }
orig_block_executed.should be_true
block1_executed.should be_true
@aop_inf[:intercepted_class].should eql("Blah")
@aop_inf[:intercepted_method].should eql(:update_all)
@aop_inf[:intercepted_method_is_class_method].should be_true
args[0].should == 1
args[1].should == 2
end
end
firebody
2007-12-13
现在已经出来一个支持 fragment cache的版本了。 目前测试的底层cache机制 是 cache_fu插件。
当然,设计的目的不局限于 cache_fu。
使用介绍:
* cache_fu的配置不变,该咋办还是咋办
* 将 smart_cache包解压到 vendor/plugins目录下
* 在config目录编写配置文件 smart_memcache_config.rb
例子如下 :
config.expire_group("user_page_list").after_models(:user).fired(:after_create,:after_update,:after_destroy,:after_save)
表示当userModel 的 :after_create,:after_update,:after_destroy,:after_save时间发生后,过期属于 user_page_list 组的所有缓存条目 。
值得说明的是 这种方式的监听 是通过 ModelObserver来实现的 。所以 fired里面的event时间都是大家熟悉的Observer的方法名。
config.expire_group("user_page_list").after("User").fired(:destroy_all,:update_all)
这行配置表示 当User 类执行 :destroy_all,:update_all 方法后 ,过期 user_page_list 组缓存的所有条目 。
这种监听方式的实现通过smart memcache内部实现的ruby aop框架来做的 ,smartmem cache的ruby aop做的比较简单,大家可以单独抽取出来作为一个通用的 aop框架 。
config.delegate_store = ::ActionController::Base.fragment_cache_store
config.use_cache_fu = true
这个配置表明 smartmemcache不单独造一个底层cache的轮子,而是直接拿初始化好的 cache_store,如果使用cache_fu插件的话,务必保证cache_fu先于smartmemcache初始化(默认是按照字母顺序初始化插件,所以默认顺序是对的)
rhtml fragment
主要的代码:
cache({}, {:group=>:user_page_list})
其中 options选项多了 :group=>:user_page_list
当然,设计的目的不局限于 cache_fu。
使用介绍:
* cache_fu的配置不变,该咋办还是咋办
* 将 smart_cache包解压到 vendor/plugins目录下
* 在config目录编写配置文件 smart_memcache_config.rb
例子如下 :
SmartMemcache::Config.init do |config|
config.expire_group("user_page_list").after_models(:user).fired(:after_create,:after_update,:after_destroy,:after_save)
config.expire_group("user_page_list").after("User").fired(:destroy_all,:update_all)
config.delegate_store = ::ActionController::Base.fragment_cache_store
config.use_cache_fu = true
end
config.expire_group("user_page_list").after_models(:user).fired(:after_create,:after_update,:after_destroy,:after_save)
表示当userModel 的 :after_create,:after_update,:after_destroy,:after_save时间发生后,过期属于 user_page_list 组的所有缓存条目 。
值得说明的是 这种方式的监听 是通过 ModelObserver来实现的 。所以 fired里面的event时间都是大家熟悉的Observer的方法名。
config.expire_group("user_page_list").after("User").fired(:destroy_all,:update_all)
这行配置表示 当User 类执行 :destroy_all,:update_all 方法后 ,过期 user_page_list 组缓存的所有条目 。
这种监听方式的实现通过smart memcache内部实现的ruby aop框架来做的 ,smartmem cache的ruby aop做的比较简单,大家可以单独抽取出来作为一个通用的 aop框架 。
config.delegate_store = ::ActionController::Base.fragment_cache_store
config.use_cache_fu = true
这个配置表明 smartmemcache不单独造一个底层cache的轮子,而是直接拿初始化好的 cache_store,如果使用cache_fu插件的话,务必保证cache_fu先于smartmemcache初始化(默认是按照字母顺序初始化插件,所以默认顺序是对的)
rhtml fragment
<% cache({}, {:group=>:user_page_list}) do %>
<%
@users = User.list_all_users(params[:page])
%>
<% for user in @users -%>
<tr align="center">
<td><%=user.id %></td>
<td><%=user.name %></td>
<td><%= link_to "Edit", :action => "edit_user",:id=>user %> ><%= link_to "Delete", { :action => 'delete_user', :id => user}, { :method => :post, :confirm => "Really delete #{user.name}?" } %></td>
</tr>
<% end %>
<tr align="center">
<td colspan="5" align="right"></td>
<td><%= link_to "Add User", :action => "add_user" %></td>
</tr>
<tr>
<td colspan="6" align="center"><%= will_paginate(@users) %></td>
</tr>
</tbody>
<% end %>
主要的代码:
cache({}, {:group=>:user_page_list})
其中 options选项多了 :group=>:user_page_list
firebody
2007-12-10
倪宏业 说: 怎么说呢,觉得你这篇好像分为两大部分,smart cache核心,smart cache的aop 1.核心部分,从缓存读取cache数据的时候,smartcache如何访问memcache。 2.如何expire等吧 firebody(啊翔) 说: 嗯,是的 firebody(啊翔) 说: 底层的缓存还是用原有已经写好的 倪宏业 说: 喔 firebody(啊翔) 说: 我就是中间,加多我的逻辑,相当于一个拦截,再调用底层的方法 firebody(啊翔) 说: memcache就是一个hashtable firebody(啊翔) 说: 我就是修改它们的key,在后面加多一个version firebody(啊翔) 说: put/get配对的改 ,group对象保留当前 的version 倪宏业 说: 喔,原来是这样,我刚才就是version这个没看明白 firebody(啊翔) 说: 同样也需要修改原有的外围调用cache的方法,需要在最后加多一个group对象,这样,我才知道put、get调用的时候,传入的cache key属于哪个group的 倪宏业 说: memcache本身是没组的概念,而是你在原有key上增加group的hashcode? firebody(啊翔) 说: 不是,在key上增加对应group的version。 firebody(啊翔) 说: 要增加group的hashcode也行,呵呵 firebody(啊翔) 说: 主要通过group的version来 废弃原有的脏数据 倪宏业 说: 喔,明白了,vesion唯一递增? firebody(啊翔) 说: 嗯 倪宏业 说: 文章写得明白,重新写过,哈哈 firebody(啊翔) 说: 晕倒,我把你的这个讨论加上去就行了
firebody
2007-12-10
希望有过使用 rails cache的XDJM们踊跃提出自己的看法。
不知道这样的方案是否能够符合实际开发的需要以及其中有什么改进的地方。

不知道这样的方案是否能够符合实际开发的需要以及其中有什么改进的地方。
发表评论
提醒: 该博客已发表在公共论坛,博客所有留言会成为论坛回贴,留言请注意遵守论坛发贴规则
- 浏览: 22312 次
- 性别:


- 详细资料
搜索本博客
最近加入圈子
最新评论
-
谈谈应用ORM框架针对遗留 ...
BaseService extends HibernateDAOSupport? ...
-- by sslaowan -
谁了解Paulo提出的String ...
可以用google scholar
-- by tiantian911 -
关于实现一个rails smart ...
nihongye 写道firebody 写道LRU频繁的话,性能应该会很差 不知 ...
-- by firebody -
关于实现一个rails smart ...
firebody 写道LRU频繁的话,性能应该会很差 不知道这个猜测是从哪里来的 ...
-- by nihongye -
谈谈应用ORM框架针对遗留 ...
我也经常会为了少写一些代码而使用继承,而不是用工具类,这样会在心里上有一种更直观 ...
-- by downpour






评论排行榜