咱們常見的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天,那麼設置最近修改時間爲當前時間,而後生成頁面給用戶。是否是很簡單?