海鷗航際

          JAVA站
          posts - 11, comments - 53, trackbacks - 1, articles - 102
          分頁(yè) & QueryKey & 預(yù)取

          數(shù)據(jù)庫(kù)分頁(yè)查詢(xún)一般分為兩步,
          (1)根據(jù)查詢(xún)條件,count 記錄總數(shù)
          (2)根據(jù)當(dāng)前頁(yè)的數(shù)據(jù)范圍(起始位置offset, 每頁(yè)數(shù)據(jù)個(gè)數(shù)span),從符合查詢(xún)條件的記錄集 取出對(duì)應(yīng)范圍的數(shù)據(jù)。

          一、根據(jù)范圍取數(shù)據(jù)的方法
          如果單純用JDBC從ResultSet中取出一個(gè)指定范圍(offset, span)的數(shù)據(jù),可以采用這樣的方法。
          java代碼: 


          ps = con.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)
          ps.setMaxRows(offset + span)
          rs = ps.executeQuery();
          rs.absolute(offset);
          while(rs.next())...



          數(shù)據(jù)量大的時(shí)候,頁(yè)數(shù)很多,offset很大,這種方法不太適合。這時(shí)候,需要使用各數(shù)據(jù)庫(kù)的native SQL特性。
          我們來(lái)看Hibernate dialect package的類(lèi),支持了各種數(shù)據(jù)庫(kù)的getLimitString方法。這里舉Mysql和Oracle的例子。假設(shè)查詢(xún)語(yǔ)句為
          java代碼: 


          Select * from message where forum_id = ? and created_time > ? order by created_time desc



          Mysql 的limit SQL為
          java代碼: 


          Select * from message where forum_id = ? and created_time > ? order by created_time desc
          limit ?, ?


          后面的兩個(gè)limit ?, ? 分別為 offset, span。

          Oracle的limit SQL為
          java代碼: 


          select * from ( select row_.*, rownum rownum_ from (
          Select * from message where forum_id = ? and created_time > ? order by created_time desc
          ) row_ where rownum <= ?) where rownum_ > ?


          后面的兩個(gè)limit ?, ? 分別為 offset + span, offset。

          二、緩存 & QueryKey
          count語(yǔ)句可以根據(jù)查詢(xún)語(yǔ)句自動(dòng)生成,比如
          java代碼: 


          Select count(*) from (
          Select * from message where forum_id = ? and created_time > ? order by created_time desc
          )



          這樣的自動(dòng)count語(yǔ)句有些浪費(fèi),用了子查詢(xún)不說(shuō),還保留了沒(méi)有必要的order by。最好還是另外提供一個(gè)count語(yǔ)句。
          java代碼: 


          Select count(*) from message where forum_id = ? and created_time > ?



          在多頁(yè)翻動(dòng)的情況下,這個(gè)count語(yǔ)句要被反復(fù)執(zhí)行。為了提高效率,我把這個(gè)count結(jié)果保存在全局緩存中,不僅本Session用戶(hù)可以重復(fù)使用,其他用戶(hù)在根據(jù)同樣條件翻找message的時(shí)候,也可以重復(fù)使用這個(gè)結(jié)果。

          我在持久層中使用通用的QueryKey做為緩存鍵值。
          QueryKey分成三個(gè)部分,SQL, Parameters, Range。比如:
          java代碼: 


          Query Key:
          SQL : Select count(*) from message where forum_id = ? and created_time > ?
          Parameters : [buaawhl, time long value]
          Range: (0, 1)



          這個(gè)QueryKey的效率很關(guān)鍵。主要是hashCode和equals兩個(gè)方法的效率。
          我們知道,當(dāng)key放在Map等Hash數(shù)據(jù)結(jié)構(gòu)中,首先hashCode,然后用equals比較hashCode后面的一串key。
          舉個(gè)例子。Key1和key2 的hashCode一樣,都和key3的hashCode不一樣。
          java代碼: 



          [ 101 ] -> key1 -> key2

          [ 666 ] -> key3




          可以看到,hashCode,equals,這兩個(gè)方法都是每次查找緩存都要調(diào)用的方法。尤其是equals方法更是重中之重,很可能需要被調(diào)用多次。
          hashCode的優(yōu)化實(shí)現(xiàn)相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,只要根據(jù)QueryKey中各部分的不同,盡量實(shí)現(xiàn)hashCode取值的擴(kuò)散化,降低hashCode的重復(fù)率就可以了。
          關(guān)鍵是equals的實(shí)現(xiàn)方案。這里有個(gè)原則,越小的結(jié)構(gòu)越先比較,可以提高比較速度。
          QueryKey中的parameters和range比較好辦。每次equals比較的時(shí)候,先比較range,如果不相等,返回false; 如果相等,再比較Parameters,如果有一個(gè)parameter value不相等,返回false。這樣,我們可以用很短的時(shí)間開(kāi)銷(xiāo) 過(guò)濾掉一大批不相等的QueryKey。
          但是parameters和range都相等的時(shí)候,我們還是無(wú)可避免的要比較SQL。String的equals方法如下:
          java代碼: 

          // from jdk src
          //這個(gè)方法沒(méi)有比較hashCode,直接比較長(zhǎng)度和字符
              public boolean equals(Object anObject) {
                  if (this == anObject) {
                      return true;
                  }
                  if (anObject instanceof String) {
                      String anotherString = (String)anObject;
                      int n = count;
                      if (n == anotherString.count) {
                          char v1[] = value;
                          char v2[] = anotherString.value;
                          int i = offset;
                          int j = anotherString.offset;
                          while (n-- != 0) {
                              if (v1[i++] != v2[j++])
                                  return false;
                          }
                          return true;
                      }
                  }
                  return false;
              }


          我們看到,當(dāng)SQL String很長(zhǎng)的時(shí)候,長(zhǎng)度相等,前面大部分字符相同的時(shí)候,(最極端的情況下,兩個(gè)不同reference的String的字符完全相等),這個(gè)比較是相當(dāng)消耗時(shí)間的。比如,
          java代碼: 


          Select * from message where forum_id = ? and created_time > ? order by created_time desc



          java代碼: 


          Select * from message where forum_id = ? and created_time > ? order by updated_time desc


          兩個(gè)String的長(zhǎng)度相等,前面大部分也相等,只有走到cre 和 upd 的時(shí)候,才能比較出不相同。如果兩個(gè)字符串內(nèi)容一樣,那更是要走到頭,才能判斷出兩個(gè)字符串完全一樣了。

          我的第一個(gè)做法就是,盡量使用static final String做為QueryKey的SQL。這樣兩個(gè)SQL的reference如果相等,那么可以迅速判斷出兩個(gè)SQL相同。
          這個(gè)做法只能處理事先定義好的SQL語(yǔ)句,但實(shí)際需求中,存在很多需要?jiǎng)討B(tài)拼接SQL的情況,不可能做到所有相同的SQL具有相同的reference。
          當(dāng)然大部分不同的SQL都具有不同的長(zhǎng)度,即使長(zhǎng)度相同,前面走不了幾個(gè)字符,就可以判斷出不相同。所以做法一已經(jīng)能夠解決95%以上的SQL效率問(wèn)題。

          不過(guò),為了解決這剩下的5%情況,我又采取了第二個(gè)做法:分而治之,把一個(gè)SQL String拆分成多個(gè)SQL常量的數(shù)組;泛化SQL的類(lèi)型,SQL不限制為String類(lèi)型,也可以是String[]類(lèi)型。
          比如。
          java代碼: 


          String[] sql1 = {
          “Select * from message where forum_id = ?”,
          “ and created_time > ?”,
          “ order by ”,
          “created_time”,
          “desc”
          };



          java代碼: 


          String[] sql2 = {
          “Select * from message where forum_id = ?”,
          “ and created_time > ?”,
          “ order by ”,
          “created_time”,
          “desc”
          };



          java代碼: 


          String[] sql3 = {
          “Select * from message where forum_id = ?”,
          “ and created_time > ?”,
          “ order by ”,
          “updated_time”,
          “desc”
          };



          這個(gè)時(shí)候,比較sql1和sql2和sql3的效率就會(huì)大大提高,雖然sql1 和 sql2兩個(gè)數(shù)組的長(zhǎng)度相等,還是要一個(gè)元素一個(gè)元素的比較,但由于里面大量用到了String常量,相同的String常量具有相同的reference,所以5步下來(lái),就可以判斷出sql1和sql2數(shù)組的元素是完全相等的;4步下來(lái),加上第一個(gè)字符的比較,就可以判斷sql1和sql3的第4個(gè)元素是不相等的。

          我們看到,做法1和做法2,能夠100%的提高SQL的比較效率,大部分情況下,也許比parameters的比較還快。

          三、定長(zhǎng)預(yù)取
          多用戶(hù)訪問(wèn)同一頁(yè)面的可能性比較大的情況下,比如,論壇的某些熱門(mén)話(huà)題,很可能被多人同時(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)也大大增加了。Smile
          我們進(jìn)一步考慮下面兩種情況:
          1. 用戶(hù)自定義頁(yè)面記錄數(shù)
          一般來(lái)說(shuō),用戶(hù)可以自定義自己的每頁(yè)顯示記錄個(gè)數(shù),比如,有些用戶(hù)喜歡每頁(yè)20條,有的喜歡每頁(yè)10條。
          假設(shè)用戶(hù)A翻到一個(gè)論壇的第一頁(yè),顯示1 – 20條信息;用戶(hù)B翻到同一個(gè)論壇的第一頁(yè),顯示1 – 10條信息。這個(gè)時(shí)候,緩存的命中率是很低的。用戶(hù)A和用戶(hù)B無(wú)法共享緩存信息。因?yàn)樗麄兊膔ange(的span)總是不同,QueryKey永遠(yuǎn)不可能相同。

          2. 記錄很多、每頁(yè)記錄數(shù)過(guò)少
          假設(shè)一個(gè)論壇里面有1000條信息,每頁(yè)顯示10條,那么共有100頁(yè)。如果用戶(hù)一頁(yè)一頁(yè)的翻動(dòng),每次程序發(fā)出一個(gè)span大小為10的Query請(qǐng)求,取出10條記錄,根據(jù)QueryKey緩存起來(lái)。由于頁(yè)面記錄數(shù)過(guò)少,每次數(shù)據(jù)庫(kù)查詢(xún)的效率很低,緩存命中率也很低。

          為了提高緩存命中率,并且順便實(shí)現(xiàn)數(shù)據(jù)預(yù)取功能,我們可以采取 同一定長(zhǎng)Span的方案。比如,還是上面的例子,我們?cè)诔绦蛑性O(shè)定統(tǒng)一Span大小為100。
          當(dāng)用戶(hù)A請(qǐng)求1 – 10的記錄的時(shí)候,程序判斷這個(gè)落在 1 – 100的范圍內(nèi),那么用range (1, 100)獲取100條記錄,把前面的10條返回給用戶(hù)。當(dāng)用戶(hù)A翻了一頁(yè),請(qǐng)求11 – 20的記錄的時(shí)候,程序判斷還是落在 1 – 100的范圍內(nèi),而且已經(jīng)存在于緩存中,那么直接把對(duì)應(yīng)的11 – 20條返回給用戶(hù)A就可以。
          當(dāng)用戶(hù)B 請(qǐng)求1 – 20的記錄的時(shí)候,程序判斷這個(gè)落在 1 – 100的范圍內(nèi),而且已經(jīng)存在于緩存中,那么直接把對(duì)應(yīng)的1 – 20條返回給用戶(hù)B就可以。

          可以看到,這種定長(zhǎng)預(yù)取方案能夠大大提高數(shù)據(jù)庫(kù)查詢(xún)的效率和緩存的命中率。

           

          關(guān)于Cache & QueryKey 部分,偶有1個(gè)問(wèn)題:你是如何做到cache的自動(dòng)清理和聰明地清理?

          舉個(gè)例子
          假設(shè)有這樣的查詢(xún)語(yǔ)句: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ì)象是否還能保持有效呢?

           

          沒(méi)有這么智能。我現(xiàn)在無(wú)法做到 cache的自動(dòng)清理和聰明地清理。
          我現(xiàn)在做的持久層本身是不做cache的清理管理工作,這個(gè)工作交給 調(diào)用程序自己去做。cache也需要用戶(hù)自己實(shí)現(xiàn),并且明確提供。
          查詢(xú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ù)庫(kù),把結(jié)果放到cache里面。
          這里有個(gè)優(yōu)化,如果指定了緩存,而且只有當(dāng)緩存中不存在目標(biāo)數(shù)據(jù)的時(shí)候,才真正地從連接池中獲取connection。主要是考慮到連接池的大小總是有限的,如果并發(fā)用戶(hù)多的話(huà),這樣就可以節(jié)省連接池里的connection的分配。

          ---
          update, delete, update的時(shí)候,用戶(hù)需要手動(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)智能過(guò)濾緩存數(shù)據(jù) 方面,具有先天的缺陷。
          因?yàn)镾QL可以寫(xiě)的很復(fù)雜,比HQL復(fù)雜很多。而且還有各種 Native SQL特性,沒(méi)有一個(gè)統(tǒng)一的中間語(yǔ)言(比如HQL), 解析起來(lái)也相當(dāng)復(fù)雜。
          即使有這么一個(gè)中間語(yǔ)言,解析處理的代價(jià)和難度也相當(dāng)大。相當(dāng)于實(shí)現(xiàn)了一個(gè)小型的HSQL級(jí)別的內(nèi)存數(shù)據(jù)庫(kù)。

          而對(duì)于用戶(hù)來(lái)說(shuō),定義DAO方法的時(shí)候,很清楚自己SQL的語(yǔ)義,實(shí)現(xiàn)智能緩存處理更容易一些,所以干脆把cache的管理交給用戶(hù)自己。
          這樣做的另一個(gè)目的是,我想把 對(duì)應(yīng)的頁(yè)面緩存也放到同一個(gè)cache中去。
          還有一種情況,比如,user, group, group user, 三個(gè)不同的Data Object類(lèi),由于相互關(guān)聯(lián),那么用戶(hù)可以指定這三個(gè)類(lèi)使用同一個(gè)cache,簡(jiǎn)化管理。
          還有一種情況,比如,我想用message類(lèi),同時(shí)對(duì)應(yīng) 站內(nèi)短信,和論壇帖子兩個(gè)對(duì)象,我也可以為這兩種不同的情況,指定不同cache。

          ---
          Hibernate如果想實(shí)現(xiàn) 自動(dòng)智能過(guò)濾緩存數(shù)據(jù) 方面,那么具有天生的優(yōu)勢(shì)。
          因?yàn)楸緛?lái)Hibernate就是要 解析HQL的,而且Hibernate管理數(shù)據(jù)類(lèi)本身及其之間的關(guān)聯(lián)。什么信息都有了,做起來(lái)也相對(duì)容易很多。
          當(dāng)然Hibernate并沒(méi)有做這個(gè)工作。Hibernate只提供了一個(gè)evictQueries()方法,不分類(lèi)型地清理所有的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)換過(guò)來(lái)的結(jié)果,reference一定不會(huì)相等。
          假設(shè)這個(gè)SQL很長(zhǎng)的時(shí)候,而兩個(gè)SQL又相同,這個(gè)比較就會(huì)比較消耗時(shí)間。而且,這個(gè)QueryKey占的空間也比較大。
          (在我的持久層里面,SQL盡量采用常量字符串、或常量字符串?dāng)?shù)組,在一定程度上解決這個(gè)問(wèn)題。當(dāng)然,這需要用戶(hù)在使用的時(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字符串不相同,但語(yǔ)義相同,轉(zhuǎn)換出來(lái)的SQL一定相同。

          如果Hibernate要加入智能管理QueryCache的功能,需要在QueryKey里面加入更多的信息(比如,HQL的解析結(jié)果的條件過(guò)濾部分),這樣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非常簡(jiǎn)單,就是persistent class + entity identifier。
          具體來(lái)說(shuō),每個(gè)不同的persistent class有獨(dú)立的ID緩存,該獨(dú)立ID緩存的key就是 entity identifier。

          ID緩存的管理是不是在 Method Interceptor里面管理的。我從hibernate代碼中看不出這一點(diǎn)。也許在Hibernate自定義的Event中處理。
          我大致猜測(cè)一下,Hibernate把updated, inserted, deleted Entities(對(duì)于用戶(hù)來(lái)說(shuō),就是PO;對(duì)于Hibernate來(lái)說(shuō),就是Entity, Proxy)都保存在內(nèi)部的一個(gè)Collection結(jié)構(gòu)里面。在最后Session.flush()的時(shí)候,統(tǒng)一處理,同步更新數(shù)據(jù)庫(kù)狀態(tài),和ID緩存狀態(tài)。

           

           

          主站蜘蛛池模板: 子洲县| 岱山县| 织金县| 盐源县| 兴仁县| 鄢陵县| 枣庄市| 明水县| 安乡县| 博兴县| 合山市| 郯城县| 澄迈县| 遂平县| 屯留县| 开鲁县| 随州市| 阿图什市| 比如县| 保康县| 拜泉县| 建平县| 宣恩县| 盐边县| 嵩明县| 南雄市| 巴南区| 平邑县| 鹤岗市| 平顶山市| 梧州市| 柳州市| 即墨市| 广水市| 嘉定区| 逊克县| 新建县| 揭西县| 奉化市| 丰镇市| 北辰区|