緩存是有很多層次的,有web server前端緩存,有動態頁面靜態化,有頁面片斷緩存,有查詢緩存,也有對象緩存。不同層面的緩存適用于不同的應用場景,作用也各自不同,如果可以,你全部一起用上,他們不矛盾,但這個話題比較大,現在不展開談。
針對OLTP類型的web應用,只要代碼寫的質量沒有問題,最終的性能瓶頸毫無疑問還是數據庫查詢。應用服務器層面可以水平擴展,但是數據庫是單點的,很難水平擴展,所以如何有效降低數據庫查詢頻率,減輕數據庫壓力,是web應用性能問題的根源。
以上所有的緩存方式都可以直接或者間接的降低數據庫訪問,但緩存是有應用場景的,雖然新聞網站非常適合使用動態頁面靜態化技術,但是例如電子商務網站就不適合動態頁面靜態化,而頁面緩存和查詢緩存可以使用的場景也不多。但是對象緩存是所有緩存技術當中適用場景最廣泛的,任何OLTP應用,即使實時性要求很高,你也可以使用對象緩存,而且好的ORM實現,對象緩存是完全透明的,不需要你的程序代碼進行硬編碼。
用不用對象緩存,怎么用對象緩存,不是一個調優的技巧問題,而是整個應用的架構問題。在你開發一個應用之前,你就要想清楚,這個應用最終的場景是什么?會有多大的用戶量和數據量。你將采用什么方式來架構這個應用:
OK,也許你偏愛SQL,那么你選擇iBATIS,數據庫設計當中大表有很多冗余字段,會盡量消除大表之間的關聯關系,最終用戶量和訪問量很高以后,你會選擇使用Oracle,雇傭資深的DBA,進行數據庫調優和SQL調優,這是大多數公司走的路。
但是我告訴你,你還有另外一條路可以走。你可以選擇ORM(不見得一定是Hibernate),數據庫設計當中避免出現大表,比較多的表關聯關系,通過ORM以對象化方式操作。當用戶量和訪問量很高以后,除了數據庫端本身的優化,你還有對象緩存這條途徑。對象緩存是怎樣提高性能的呢?隨便舉個例子:
論壇的列表頁面,需要顯示topic的分頁列表,topic作者的名字,topic最后回復帖子的作者,如果是iBATIS,你準備怎么做?
- select ... from topic left join user left join post .....
select ... from topic left join user left join post .....
你需要通過join user表來取得topic作者的名字,然后你還需要join post表取得最后回復的帖子,post再join user表取得最后回貼作者名字。
也許你說,我可以設計表冗余,在topic里面增加username,在post里面增加username,所以通過大表冗余字段,消除了復雜的表關聯:
- select ... from topic left join post...
select ... from topic left join post...
OK,且不說冗余字段的維護問題,現在仍然是兩張大表的關聯查詢。然后讓我們看看ORM怎么做?
- select * from topic where ...
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...;
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 ...
select * from topic where ...--分頁條件
這條單表的條件查詢和iBATIS通過字段冗余簡化過后的大表關聯查詢相比,當數據量大到一定程度以后(十幾萬條),查詢的速度會差至少一個數量級,而且對數據庫的壓力很小,這就是對象緩存的真正威力!
更進一步分析,使用ORM,我們不考慮緩存的情況,那么就是3n+1條SQL。但是這3n+1條SQL的執行速度一定比iBATIS的大表關聯查詢慢嗎?不一定!因為使用ORM的情況下,第一條SQL是單表的條件查詢,在有索引的情況下,速度很快,后面的3n條SQL都是單表的主鍵查詢,在繁忙的數據庫系統當中,3n條SQL幾乎可以全部命中數據庫的data buffer。但是使用iBATIS的大表關聯查詢,很可能會造成全表掃描,這樣性能是非常差的。
所以結論就是:即使不使用對象緩存,ORM的n+1條SQL性能仍然很有可能超過iBATIS的大表關聯查詢,而且對數據庫造成的壓力要小很多。這個結論貌似令人難以置信,但經過我的實踐證明,就是事實。前提是數據量和訪問量都要比較大,否則看不出來這種效果
還是拿上面這個例子的應用場景來說,由于JavaEye網站用RoR的ActiveRecord,所以這個場景事實上就會發送3n+1條SQL語句。我從log里面看到這密密麻麻的SQL,著實非常擔憂性能,所以嘗試使用了find的:include選項去eager fetch,迫使ActiveRecord發送單條復雜的關聯查詢。但非常不幸的是,在網站服務器的production.log里面經過前后對比,發現使用:include以后,單條復雜關聯查詢耗時更多,數據庫壓力更大。
在使用memcached之后,比3n+1條的性能進一步明顯提升。所以性能對比就是這樣的:
ORM + Cache > ORM n+1 > iBATIS 關聯查詢
那為什么應用Cache可以進一步提高性能,是因為訪問Cache的開銷比訪問數據庫小的得多造成的。
應用程序根據主鍵key去Cache Server取value,是非常簡單的算法,開銷極小。
而發送一條主鍵查詢的SQL到數據庫,要經過非常復雜的過程,有SQL的解析,執行計劃的優化,占位符參數的代入,只讀事務的保護和隔離等等,最終雖然也命中了數據庫的data buffer,但是開銷確實很大。
BerkeleyDB就是一個極好的證明,它號稱其查詢速度是Oracle的1000倍,不是因為它做的比Oracle牛,而是因為它本質上就是一個大Cache,查詢沒有額外的開銷。