這個(gè)時(shí)候,比較sql1和sql2和sql3的效率就會(huì)大大提高,雖然sql1 和 sql2兩個(gè)數(shù)組的長度相等,還是要一個(gè)元素一個(gè)元素的比較,但由于里面大量用到了String常量,相同的String常量具有相同的reference,所以5步下來,就可以判斷出sql1和sql2數(shù)組的元素是完全相等的;4步下來,加上第一個(gè)字符的比較,就可以判斷sql1和sql3的第4個(gè)元素是不相等的。
我們看到,做法1和做法2,能夠100%的提高SQL的比較效率,大部分情況下,也許比parameters的比較還快。
三、定長預(yù)取
多用戶訪問同一頁面的可能性比較大的情況下,比如,論壇的某些熱門話題,很可能被多人同時(shí)翻閱。這時(shí)候,如果把根據(jù)范圍取出的數(shù)據(jù)對(duì)象List也按照QueryKey存入緩存中,那么就可以大大提高響應(yīng)速度,減輕數(shù)據(jù)服務(wù)器負(fù)擔(dān),當(dāng)然,你的Web Server的內(nèi)存負(fù)擔(dān)也大大增加了。
我們進(jìn)一步考慮下面兩種情況:
1. 用戶自定義頁面記錄數(shù)
一般來說,用戶可以自定義自己的每頁顯示記錄個(gè)數(shù),比如,有些用戶喜歡每頁20條,有的喜歡每頁10條。
假設(shè)用戶A翻到一個(gè)論壇的第一頁,顯示1 – 20條信息;用戶B翻到同一個(gè)論壇的第一頁,顯示1 – 10條信息。這個(gè)時(shí)候,緩存的命中率是很低的。用戶A和用戶B無法共享緩存信息。因?yàn)樗麄兊膔ange(的span)總是不同,QueryKey永遠(yuǎn)不可能相同。
2. 記錄很多、每頁記錄數(shù)過少
假設(shè)一個(gè)論壇里面有1000條信息,每頁顯示10條,那么共有100頁。如果用戶一頁一頁的翻動(dòng),每次程序發(fā)出一個(gè)span大小為10的Query請(qǐng)求,取出10條記錄,根據(jù)QueryKey緩存起來。由于頁面記錄數(shù)過少,每次數(shù)據(jù)庫查詢的效率很低,緩存命中率也很低。
為了提高緩存命中率,并且順便實(shí)現(xiàn)數(shù)據(jù)預(yù)取功能,我們可以采取 同一定長Span的方案。比如,還是上面的例子,我們?cè)诔绦蛑性O(shè)定統(tǒng)一Span大小為100。
當(dāng)用戶A請(qǐng)求1 – 10的記錄的時(shí)候,程序判斷這個(gè)落在 1 – 100的范圍內(nèi),那么用range (1, 100)獲取100條記錄,把前面的10條返回給用戶。當(dāng)用戶A翻了一頁,請(qǐng)求11 – 20的記錄的時(shí)候,程序判斷還是落在 1 – 100的范圍內(nèi),而且已經(jīng)存在于緩存中,那么直接把對(duì)應(yīng)的11 – 20條返回給用戶A就可以。
當(dāng)用戶B 請(qǐng)求1 – 20的記錄的時(shí)候,程序判斷這個(gè)落在 1 – 100的范圍內(nèi),而且已經(jīng)存在于緩存中,那么直接把對(duì)應(yīng)的1 – 20條返回給用戶B就可以。
可以看到,這種定長預(yù)取方案能夠大大提高數(shù)據(jù)庫查詢的效率和緩存的命中率。
關(guān)于Cache & QueryKey 部分,偶有1個(gè)問題:你是如何做到cache的自動(dòng)清理和聰明地清理?
舉個(gè)例子
假設(shè)有這樣的查詢語句:select * from message where message_to = ?
執(zhí)行了2次值不同的操作:'buaawhl' 和 'Readonly'
那么就有2個(gè)不同QueryKey對(duì)應(yīng)到Cache里的對(duì)象。
這個(gè)時(shí)候再執(zhí)行一個(gè)write的操作:往message表里面插入了一條message_to等于‘buaawhl’的記錄,那么之前在Cache里QueryKey為'buaawhl'的對(duì)象會(huì)不會(huì)自動(dòng)失效?而QueryKey為'Readonly'的對(duì)象是否還能保持有效呢?
沒有這么智能。我現(xiàn)在無法做到 cache的自動(dòng)清理和聰明地清理。
我現(xiàn)在做的持久層本身是不做cache的清理管理工作,這個(gè)工作交給 調(diào)用程序自己去做。cache也需要用戶自己實(shí)現(xiàn),并且明確提供。
查詢的時(shí)候,需要指定cache
java代碼: |
finder.setRowClass(Message.class); finder.setCache(cache); finder.queryRowsRange(conP, sql, params, offset, span);
|
這個(gè)時(shí)候,finder會(huì)用 QueryKey(sql, params, offset, span) 作為Key,
從指定的cache里面,查找對(duì)應(yīng)的記錄集合。
如果查不到,從conP真正獲取一個(gè)connection,連接并查找數(shù)據(jù)庫,把結(jié)果放到cache里面。
這里有個(gè)優(yōu)化,如果指定了緩存,而且只有當(dāng)緩存中不存在目標(biāo)數(shù)據(jù)的時(shí)候,才真正地從連接池中獲取connection。主要是考慮到連接池的大小總是有限的,如果并發(fā)用戶多的話,這樣就可以節(jié)省連接池里的connection的分配。
---
update, delete, update的時(shí)候,用戶需要手動(dòng)自己清理cache的內(nèi)容。
java代碼: |
persister.insertRow(con, message); cache.clear(); // 或者更智能的操作, 比如, 根據(jù)message的message_to value, // 清理Cache里面的符合下列條件的QueryKey // (sql 包含 message_to = ?, 并且 params[0] = buaawhl)
|
我做的持久層,由于直接使用SQL,在 自動(dòng)智能過濾緩存數(shù)據(jù) 方面,具有先天的缺陷。
因?yàn)镾QL可以寫的很復(fù)雜,比HQL復(fù)雜很多。而且還有各種 Native SQL特性,沒有一個(gè)統(tǒng)一的中間語言(比如HQL), 解析起來也相當(dāng)復(fù)雜。
即使有這么一個(gè)中間語言,解析處理的代價(jià)和難度也相當(dāng)大。相當(dāng)于實(shí)現(xiàn)了一個(gè)小型的HSQL級(jí)別的內(nèi)存數(shù)據(jù)庫。
而對(duì)于用戶來說,定義DAO方法的時(shí)候,很清楚自己SQL的語義,實(shí)現(xiàn)智能緩存處理更容易一些,所以干脆把cache的管理交給用戶自己。
這樣做的另一個(gè)目的是,我想把 對(duì)應(yīng)的頁面緩存也放到同一個(gè)cache中去。
還有一種情況,比如,user, group, group user, 三個(gè)不同的Data Object類,由于相互關(guān)聯(lián),那么用戶可以指定這三個(gè)類使用同一個(gè)cache,簡化管理。
還有一種情況,比如,我想用message類,同時(shí)對(duì)應(yīng) 站內(nèi)短信,和論壇帖子兩個(gè)對(duì)象,我也可以為這兩種不同的情況,指定不同cache。
---
Hibernate如果想實(shí)現(xiàn) 自動(dòng)智能過濾緩存數(shù)據(jù) 方面,那么具有天生的優(yōu)勢(shì)。
因?yàn)楸緛鞨ibernate就是要 解析HQL的,而且Hibernate管理數(shù)據(jù)類本身及其之間的關(guān)聯(lián)。什么信息都有了,做起來也相對(duì)容易很多。
當(dāng)然Hibernate并沒有做這個(gè)工作。Hibernate只提供了一個(gè)evictQueries()方法,不分類型地清理所有的cached query.
Hibernate的QueryKey也是直接使用結(jié)果SQL,而不是HQL。
java代碼: |
// from hibernate 2.7 public class QueryKey implements Serializable { private final String sqlQueryString; private final Type[] types; private final Object[] values; private final Integer firstRow; private final Integer maxRows; private final Map namedParameters; ... public boolean equals(Object other) { QueryKey that = (QueryKey) other; if ( !sqlQueryString.equals(that.sqlQueryString) ) return false; if ( !EqualsHelper.equals(firstRow, that.firstRow) || !EqualsHelper.equals(maxRows, that.maxRows) ) return false; ... return true; }
|
可以看到,hibernate query key 直接比較結(jié)果SQL。而且我們知道,這個(gè)SQL是HQL轉(zhuǎn)換過來的結(jié)果,reference一定不會(huì)相等。
假設(shè)這個(gè)SQL很長的時(shí)候,而兩個(gè)SQL又相同,這個(gè)比較就會(huì)比較消耗時(shí)間。而且,這個(gè)QueryKey占的空間也比較大。
(在我的持久層里面,SQL盡量采用常量字符串、或常量字符串?dāng)?shù)組,在一定程度上解決這個(gè)問題。當(dāng)然,這需要用戶在使用的時(shí)候,有意識(shí)的支持)
我想了一下,為什么hibernate QueryKey直接采用結(jié)果SQL,而不用HQL。
這個(gè)SQL是HQL的解析結(jié)果,直接使用結(jié)果,節(jié)省了解析時(shí)間。比如,F(xiàn)rom A where id = 1 和 from A WHERE ID = '1',兩個(gè)HQL字符串不相同,但語義相同,轉(zhuǎn)換出來的SQL一定相同。
如果Hibernate要加入智能管理QueryCache的功能,需要在QueryKey里面加入更多的信息(比如,HQL的解析結(jié)果的條件過濾部分),這樣QueryKey的占用空間就會(huì)進(jìn)一步加大。
有一個(gè)hibernate cache討論。
http://forum.javaeye.com/viewtopic.php?t=6593
我的另一個(gè)帖子里面也有介紹。
http://forum.javaeye.com/viewtopic.php?t=9706
Hibernate主要的緩存是ID緩存(二級(jí)緩存),而QueryCache緩存的支持非常初級(jí)。
ID緩存的Key非常簡單,就是persistent class + entity identifier。
具體來說,每個(gè)不同的persistent class有獨(dú)立的ID緩存,該獨(dú)立ID緩存的key就是 entity identifier。
ID緩存的管理是不是在 Method Interceptor里面管理的。我從hibernate代碼中看不出這一點(diǎn)。也許在Hibernate自定義的Event中處理。
我大致猜測一下,Hibernate把updated, inserted, deleted Entities(對(duì)于用戶來說,就是PO;對(duì)于Hibernate來說,就是Entity, Proxy)都保存在內(nèi)部的一個(gè)Collection結(jié)構(gòu)里面。在最后Session.flush()的時(shí)候,統(tǒng)一處理,同步更新數(shù)據(jù)庫狀態(tài),和ID緩存狀態(tài)。