咱们常见的OLTP类型的web应用,性能瓶颈每每是数据库查询,由于应用服务器层面能够水平扩展,可是数据库是单点的,很难水平扩展,当数据库服务器发生磁盘IO,每每没法有效提升性能,所以如何有效下降数据库查询频率,减轻数据库磁盘IO压力,是web应用性能问题的根源。web
对象缓存是全部缓存技术当中适用场景最普遍的,任何OLTP应用,即便实时性要求很高,你也可使用对象缓存,并且好的ORM实现,对象缓存是彻底透明的,彻底不须要你的程序代码进行硬编码。算法
用不用对象缓存,怎么用对象缓存,不是一个简单的代码调优技巧,而是整个应用的架构问题。在你开发一个应用以前,你就要想清楚,这个应用最终的场景是什么?会有多大的用户量和数据量。你将采用什么方式来架构这个应用:数据库
也许你偏好对SQL语句级别的优化,数据库设计当大表有不少冗余字段,会尽可能消除大表之间的关联关系,当数据量很大之后,选择分库分表的优化方式,这是目前业界常规作法。可是也能够选择使用ORM的对象缓存优化方式:数据库设计避免出现大表,比较多的表关联关系,经过ORM以对象化方式操做,利用对象缓存提高性能。举个例子:编程
论坛的列表页面,须要显示topic的分页列表,topic做者的名字,topic最后回复帖子的做者,常规作法:浏览器
select ... from topic leftjoinuserleftjoin post .....
你须要经过join user表来取得topic做者的名字,而后你还须要join post表取得最后回复的帖子,post再join user表取得最后回贴做者名字。也许你说,我能够设计表冗余,在topic里面增长username,在post里面增长username,因此经过大表冗余字段,消除了复杂的表关联:缓存
select ... from topic leftjoin post...
OK,且不说冗余字段的维护问题,如今仍然是两张大表的关联查询。而后让咱们看看ORM怎么作?服务器
select * from topic where ... --分页条件
就这么一条SQL搞定,比上面的关联查询对数据库的压力小多了。也许你说,不对阿,做者信息呢?回贴做者信息呢?这些难道不会发送SQL吗?若是发送SQL,这不就是臭名昭著的n+1条问题吗?你说的对,最坏状况下,会有不少条SQL:网络
select * from user where id = topic_id...; .... select * from user where id = topic_id...; select * from post where id = last_topic_id...; .... select * from post where id = last_topic_id...; select * from user where id = post_id...; .... select * from user where id = post_id...;
事实上何止n+1,根本就是3n+1条SQL了。那你怎么还说ORM性能高呢?由于对象缓存在起做用,你能够观察到后面的3n条SQL语句所有都是基于主键的单表查询,这3n条语句在理想情况下(比较繁忙的web网站的热点数据),所有均可以命中缓存。因此事实上只有一条SQL,就是:架构
select * from topic where ...--分页条件
这条单表的条件查询和直接使用join查询SQL经过字段冗余简化事后的大表关联查询相比,当数据量大到必定程度之后对数据库磁盘IO的压力很小,这就是对象缓存的真正威力!less
更进一步分析,使用ORM,咱们不考虑缓存的状况,那么就是3n+1条SQL。可是这3n+1条SQL的执行速度必定比SQL的大表关联查询慢吗?不必定!由于使用ORM的状况下,第一条SQL是单表的条件查询,在有索引的状况下,速度很快,后面的3n条SQL都是单表的主键查询,在繁忙的数据库系统当中,3n条SQL几乎能够所有命中数据库的data buffer。可是使用SQL的大表关联查询,极可能会形成大范围的表扫描,形成频繁的数据库服务器磁盘IO,性能有多是很是差的。
所以,即便不使用对象缓存,ORM的n+1条SQL性能仍然颇有可能超过SQL的大表关联查询,并且对数据库磁盘IO形成的压力要小不少。这个结论貌似使人难以置信,但通过个人实践证实,就是事实。前提是数据量和访问量都要比较大,不然看不出来这种效果。
是OLTP仍是OLAP应用,即便是OLTP,也要看访问的频度,一个极少被访问到的缓存等于没有什么效果。通常来讲,互联网网站是很是适合缓存应用的场景。
毫无疑问,缓存的粒度越小,命中率就越高,对象缓存是目前缓存粒度最小的,所以被命中的概率更高。举个例子来讲吧:你访问当前这个页面,浏览帖子,那么对于ORM来讲,须要发送n条SQL,取各自帖子user的对象。很显然,若是这个user在其余帖子里面也跟贴了,那么在访问那个帖子的时候,就能够直接从缓存里面取这个user对象了。
架构的设计对于缓存命中率也有相当重要的影响。例如你应该如何去尽可能避免缓存失效的问题,如何尽可能提供频繁访问数据的缓存问题,这些都是考验架构师水平的地方。再举个例子来讲,对于论坛,须要记录每一个topic的浏览次数,因此每次有人访问这个topic,那么topic表就要update一次,这意味着什么呢?对于topic的对象缓存是无效的,每次访问都要更新缓存。那么能够想一些办法,例如增长一个中间变量记录点击次数,每累计必定的点击,才更新一次数据库,从而减低缓存失效的频率。
缓存过小,形成频繁的LRU,也会下降命中率,缓存的有效期过短也会形成缓存命中率降低。
因此缓存命中率问题不能一律而论,必定说命中率很低或者命中率很高。可是若是你对于缓存的掌握很精通,有意识的去调整应用的架构,去分解缓存的粒度,老是会带来很高的命中率的。
咱们都知道浏览器会缓存访问过网站的网页,浏览器经过URL地址访问一个网页,显示网页内容的同时会在电脑上面缓存网页内容。若是网页没有更新的话,浏览器再次访问这个URL地址的时候,就不会再次下载网页,而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新,浏览器才会再次下载网页。
对于浏览器的这种网页缓存机制你们已经耳熟能详了,举个例子来讲,JavaEye的新闻订阅地址:http://www.iteye.com/rss/news,当浏览器或者订阅程序访问这个URL地址的时候,JavaEye的服务器在response的header里面会发送给浏览器以下状态标识:
Etag "427fe7b6442f2096dff4f92339305444" Last-ModifiedFri, 04 Sep 2009 05:55:43GMT
这就是告诉浏览器,新闻订阅这个网络资源的最后修改时间和Etag。因而浏览器把这两个状态信息连同网页内容在本地进行缓存,当浏览器再次访问JavaEye新闻订阅地址的时候,浏览器会发送以下两个状态标识给JavaEye服务器:
If-None-Match "427fe7b6442f2096dff4f92339305444" If-Modified-SinceFri, 04 Sep 2009 05:55:43GMT
就是告诉服务器,我本地缓存的网页最后修改时间和Etag是什么,请问你服务器的资源有没有在我上次访问以后有更新啊?因而JavaEye服务器会核对一下,若是该用户上次访问以后没有更新过新闻,那么根本就没必要生成这个RSS了,直接告诉浏览器:“没什么新东西,你仍是看本身缓存的网页吧”,因而服务器就发送一个304 Not Modified的消息,其余什么都不用干了。
这就是HTTP层的Cache,使用这种基于资源的缓存机制,不但大大节省服务器程序资源,并且还减小了网页下载次数,节约了不少网络带宽。
咱们一般的动态网站编程,服务器端程序根本就不去处理浏览器发送过来的If-None-Match和If-Modified-Since状态标识,只要有请求就生成网页发送给浏览器。对于通常状况来讲,用户不会老是没完没了刷新一个页面,因此你们并不认为这种基于资源的缓存有什么太大的做用,但实际状况并不是如此:
比方说Google天天爬JavaEye网站大概15万次左右,但实际上JavaEye天天有更新的内容不会超过1万个网页。由于不少内容更新比较快,所以Google就会反复不停的爬取,这样自己就形成了不少资源的浪费。若是咱们使用HTTP Cache,那么只有当网页内容发生改变的时候,才会真正进行爬取,其余时候咱们直接告诉Google的爬虫304 Not Modified就能够了。这样不但下降了服务器自己的负载和爬虫形成的网络带宽消耗,实际上也大大提升了Google爬虫的工做效率,岂不是皆大欢喜?
比方说一些历史讨论帖子,已通过去了几个月了,这些帖子内容不多更新。用户可能经过搜索,收藏连接,文章关联等方式时不时访问到这个页面。那么只要用户访问过一次之后,后续全部访问服务器直接发送304 Not Modified就能够了,不用真正生成页面。
比方说JavaEye的论坛帖子列表页面,分页到20页后面的帖子已经不多有人直接访问了,可是从服务器日志去看,天天仍然有大量爬虫反复爬取这些分页到很后面的页面。这些页面因为用户不多去点击,因此基本上没有被应用程序的memcached缓存住,每次访问都会形成很高的资源消耗,爬虫隔一段时间就爬一次,对服务器是很大的负担。若是使用了HTTP Cache,那么只要爬虫爬过一次之后,之后不管爬虫爬多少次,均可以直接返回304Not Modified了,极大的节省了服务器的负载。
若是咱们要在本身的程序里面实现HTTP Cache,是件很是简单的事情,特别是对Rails来讲只须要添加一点点代码,以上面的JavaEye新闻订阅来讲,只要添加一行代码:
defnews fresh_when(:last_modified => News.last.created_at, :etag => News.last) end
用最新新闻文章做为Etag,该文章最后修改时间做为资源的最后修改时间,这样就OK了。若是浏览器发送过来的标识和服务器标识一致,说明内容没有更新,直接发送304Not Modified;若是不一致,说明内容更新,浏览器本地的缓存太古老了,那么就须要服务器真正生成页面了。
以上只是一个最简单的例子,若是咱们须要根据状态作一些更多的工做也是很容易的。比方说JavaEye博客的RSS订阅地址:http://robbin.iteye.com/rss
@blogs = @blog_owner.last_blogs @hash = @blogs.collect{|b| {b.id => b.post.modified_at.to_i + b.posts_count}}.hash ifstale?(:last_modified => (@blog_owner.last_blog.post.modified_at || @blog_owner.last_blog.post.created_at), :etag => @hash) render:template => "rss/blog" end
这个实现稍微复杂一些。咱们须要判断博客订阅全部的输出文章是否有更新,因此咱们用博客文章内容最后修改时间和博客的评论数量作一个hash,而后用这个hash值做为资源的Etag,那么只要这些博客文章当中任何文章内容被修改,或者有新评论,都会改变Etag值,从而通知浏览器内容有更新了。
除了RSS订阅以外,JavaEye网站还有不少地方适合使用HTTP Cache,比方说JavaEye论坛的版面列表页面,一些常常喜欢泡论坛的用户,可能时不时会上来刷新一下版面,看看有没有新的帖子,那么咱们就没必要每次用户请求的时候都去执行程序,生成页面给他。咱们判断一下若是没有新帖子的话,直接告诉他304 Not Modified就能够了,在没有使用HTTP Cache以前的版面Action代码:
defboard @topics = @forum.topics.paginate... @announcements = (params[:page] || 1).to_i == 1 ? Topic.find:all, :conditions => ... render:action => 'show' end
添加HTTP Cache之后,代码以下:
对于登陆用户,不使用HTTP Cache,这是由于登陆用户须要实时接收站内短信通知和订阅通知,所以咱们只能对匿名用户使用HTTP Cache,而后咱们使用当前全部帖子id和回帖数构造hash做Etag,这样只要当前分页列表页面有任何帖子发生改变或者有了新回帖,就更新页面,不然就没必要从新生成页面。
论坛帖子页面实际上也可使用HTTP Cache,只不过Etag的hash算法稍微复杂一些,须要保证帖子的任何改动都要引发hash值的改变,示例代码以下:
defshow
defboard @topics = @forum.topics.paginate... iflogged_in? || stale?(:last_modified => @topics[0].last_post.created_at, :etag => @topics.collect{|t| {t.id => t.posts_count}}.hash) @announcements = (params[:page] || 1).to_i == 1 ? Topic.find:all, :conditions... render:action => 'show' end end
defboard @topics = @forum.topics.paginate... iflogged_in? || stale?(:last_modified => @topics[0].last_post.created_at, :etag => @topics.collect{|t| {t.id => t.posts_count}}.hash) @announcements = (params[:page] || 1).to_i == 1 ? Topic.find:all, :conditions... render:action => 'show' end end
分别根据主题贴,该分页的全部回帖和帖子页面的广告内容进行hash,计算出来一个惟一的Etag值,保证任何改动都会生成新的Etag,这样就搞定了,是否是很简单!这种帖子的缓存很是有效,能够避免Rails去render页面和下载页面,极大的减轻了服务器负载和带宽。
再举一个需求比较特殊的例子:对于知识库搜索相关文章的推荐页面,比方说:http://www.iteye.com/wiki/topic/462476也就是本文的相关文章推荐内容,咱们并不但愿用户和爬虫每次访问这个页面都实际执行一遍全文检索,而后构造页面内容,在一个相对不长的时间范围内,这篇文章的相关推荐文章改变的几率不大,所以咱们但愿比方说5天以内,用户重复访问该页面,就直接返回304 Not Modified,那么Rails没有直接的设施给咱们使用,须要咱们稍微了解一些Rails的机制,本身编写,代码示例以下:
deftopic @topic = Topic.find(params[:id]) unlesslogged_in? ifrequest.not_modified?(5.days.ago) head:not_modified else response.last_modified = Time.now end end end
每次用户请求,咱们判断用户是否5天以内访问过该页面,若是访问过,直接返回304 Not Modified,若是没有访问过,或者上次访问已经超过了5天,那么设置最近修改时间为当前时间,而后生成页面给用户。是否是很简单?