天天看點

對象緩存和n+1問題分析

我們常見的web應用,性能瓶頸往往是資料庫查詢,因為應用伺服器層面可以水準擴充,但是資料庫是單點的,很難水準擴充,當資料庫伺服器發生磁盤io,往往無法有效提高性能,是以如何有效降低資料庫查詢頻率,減輕資料庫磁盤io壓力,是web應用性能問題的根源。

對象緩存是所有緩存技術當中适用場景最廣泛的,任何web應用,即使實時性要求很高,你也可以使用對象緩存,而且好的orm實作,對象緩存是完全透明的,完全不需要你的程式代碼進行寫死。

用不用對象緩存,怎麼用對象緩存,不是一個簡單的代碼調優技巧,而是整個應用的架構問題。在你開發一個應用之前,你就要想清楚,這個應用最終的場景是什麼?會有多大的使用者量和資料量。你将采用什麼方式來架構這個應用:

也許你偏好對sql語句級别的優化,資料庫設計當大表有很多備援字段,會盡量消除大表之間的關聯關系,當資料量很大以後,選擇分庫分表的優化方式,這是目前業界正常做法。但是也可以選擇使用orm的對象緩存優化方式:資料庫設計避免出現大表,比較多的表關聯關系,通過orm以對象化方式操作,利用對象緩存提升性能。舉個例子:

論壇的清單頁面,需要顯示topic的分頁清單,topic作者的名字,topic最後回複文章的作者,正常做法:

你需要通過join user表來取得topic作者的名字,然後你還需要join post表取得最後回複的文章,post再join user表取得最後回貼作者名字。也許你說,我可以設計表備援,在topic裡面增加username,在post裡面增加username,是以通過大表備援字段,消除了複雜的表關聯:

ok,且不說備援字段的維護問題,現在仍然是兩張大表的關聯查詢。然後讓我們看看orm怎麼做?

select * from topic where ... --分頁條件

就這麼一條sql搞定,比上面的關聯查詢對資料庫的壓力小多了。 也許你說,不對阿,作者資訊呢?回貼作者資訊呢?這些難道不會發送sql嗎?如果發送sql,這不就是臭名昭著的n+1條問題嗎? 你說的對,最壞情況下,會有很多條sql:

事實上何止n+1,根本就是3n+1條sql了。那你怎麼還說orm性能高呢? 因為對象緩存在起作用,你可以觀察到後面的3n條sql語句全部都是基于主鍵的單表查詢,這3n條語句在理想狀況下(比較繁忙的web網站的熱點資料),全部都可以命中緩存。是以事實上隻有一條sql,就是:

這條單表的條件查詢和直接使用join查詢sql通過字段備援簡化過後的大表關聯查詢相比,當資料量大到一定程度以後對資料庫磁盤io的壓力很小,這就是對象緩存的真正威力!

更進一步分析,使用orm,我們不考慮緩存的情況,那麼就是3n+1條sql。但是這3n+1條sql的執行速度一定比sql的大表關聯查詢慢嗎?不一定!因為使用orm的情況下,第一條sql是單表的條件查詢,在有索引的情況下,速度很快,後面的3n條sql都是單表的主鍵查詢,在繁忙的資料庫系統當中,3n條sql幾乎可以全部命中資料庫的data buffer。但是使用sql的大表關聯查詢,很可能會造成大範圍的表掃描,造成頻繁的資料庫伺服器磁盤io,性能有可能是非常差的。

是以,即使不使用對象緩存,orm的n+1條sql性能仍然很有可能超過sql的大表關聯查詢,而且對資料庫磁盤io造成的壓力要小很多。這個結論貌似令人難以置信,但經過我的實踐證明,就是事實。前提是資料量和通路量都要比較大,否則看不出來這種效果。

即使是web應用,也要看通路的頻度,一個極少被通路到的緩存等于沒有什麼效果。一般來說,網際網路網站是非常适合緩存應用的場景。

毫無疑問,緩存的粒度越小,命中率就越高,對象緩存是目前緩存粒度最小的,是以被命中的幾率更高。舉個例子來說吧:你通路目前這個頁面,浏覽文章,那麼對于orm來說,需要發送n條sql,取各自文章user的對象。很顯然,如果這個user在其他文章裡面也跟貼了,那麼在通路那個文章的時候,就可以直接從緩存裡面取這個user對象了。

架構的設計對于緩存命中率也有至關重要的影響。例如你應該如何去盡量避免緩存失效的問題,如何盡量提供頻繁通路資料的緩存問題,這些都是考驗架構師水準的地方。再舉個例子來說,對于論壇,需要記錄每個topic的浏覽次數,是以每次有人通路這個topic,那麼topic表就要update一次,這意味着什麼呢?對于topic的對象緩存是無效的,每次通路都要更新緩存。那麼可以想一些辦法,例如增加一個中間變量記錄點選次數,每累計一定的點選,才更新一次資料庫,進而減低緩存失效的頻率。

緩存太小,造成頻繁的lru,也會降低命中率,緩存的有效期太短也會造成緩存命中率下降。

是以緩存命中率問題不能一概而論,一定說命中率很低或者命中率很高。但是如果你對于緩存的掌握很精通,有意識的去調整應用的架構,去分解緩存的粒度,總是會帶來很高的命中率的。