MongoDB整理筆記のjava MongoDB分頁優化


    最近項目在做網站用戶數據新訪客統計,數據存儲在MongoDB中,統計的數據其實也並不是很大,1000W上下,但是公司只配給我4G內存的電腦,讓我程序跑起來氣喘吁吁...很是疲憊不堪。

    最常見的問題莫過於查詢MongoDB內存溢出,沒辦法只能分頁查詢。這種思想大家可能都會想到,但是如何分頁,確實多有門道!

    網上用的最多的,也是最常見的分頁采用的是skip+limit這種組合方式,這種方式對付小數據倒也可以,但是對付上幾百上千萬的大數據,卻只能望而興嘆...

    經過網上各種查找資料,尋師問道的,發現了一種速度足以把skip+limit組合分頁甩出幾條街的方法。

    思路: 條件查詢+排序+限制返回記錄。邊查詢,邊排序,排序之后,抽取第一次分頁中的最后一條記錄,作為第二次分頁的條件,進行條件查詢,以此類推....

    先上代碼: 

 /**
     * 小於指定日期的所有根據UUID分組的訪問記錄
     * @param 指定日期
     * @return 所有訪問記錄的MAP
     */
    public static Multimap<String, Map<String, String>> getOldVisitors(String date){
        
        //每次查詢的記錄數
        int pagesize = 100000;
        
        //mongodb中的"_id"
        String objectId = "";
        
        //方法的返回值類型,此處用的google guava
        Multimap<String, Map<String, String>> mapless = null;
        
        //查詢的條件
        BasicDBObject queryless = new BasicDBObject(),fields = new BasicDBObject(),field = new BasicDBObject();
        
        //初始化返回的mongodb集合操作對象,大家可以寫個數據連接池
        dbCol = init();
        
        //查詢指定字段,字段越少,查詢越快,當然都是一些不必要字段
        field.put("uuid",1);
        
        fields.put("uuid", 1);
        
        fields.put("initTime", 1);
        
        //小於指定日期的條件
        String conditionless = TimeCond.getTimeCondless(date);
        
        queryless.put("$where", conditionless);
        
        DBCursor cursorless = dbCol.find(queryless,field);
        
        //MongoDB在小於指定日期條件下,集合總大小
        int countless = cursorless.count();
        
        //查詢遍歷的次數 circleCountless+1
        int circleCountless = countless/pagesize;
        
        //取模,這是最后一次循環遍歷的次數
        int modless = countless%pagesize;
        
        //開始遍歷查詢
        for (int i = 1; i <=circleCountless+1; i++) {
            
            //文檔對象
            DBObject obj = null;
            
            //將游標中返回的結果記錄到list集合中,為什么放到list集合中?這是為后面guava 分組做准備
            List<Map<String, String>> listOfMaps = new ArrayList();
            
            //如果條件不為空,則加上此條件,構成多條件查詢,這一步是分頁的關鍵
            if (!"".equals(objectId)) {
                
                  //我們通過文檔對象obj.get("_id")返回的是不帶ObjectId(),所以要求此步驟
                               ObjectId id = new ObjectId(objectId);
                
                   queryless.append("_id", new BasicDBObject("$gt",id));
                
            }
            
            if (i<circleCountless+1) {
                
            cursorless = dbCol.find(queryless,fields).sort(new BasicDBObject("_id", 1)).limit(pagesize);
                
            }else if(i==circleCountless+1){//最后一次循環
                
                cursorless = dbCol.find(queryless,fields).limit(modless);
            }
            
                    //將游標中返回的結果記錄到list集合中,為什么放到list集合中?這是為后面guava 分組做准備
                while (cursorless.hasNext()) {
                    
                    obj = cursorless.next();
                    
                    listOfMaps.add((Map<String, String>) obj);
                    
                }
                //獲取一次分頁中最后一條記錄的"_id",然后作為條件傳入到下一個循環中
                if (null!=obj) {
                    
                     objectId = obj.get("_id").toString();
                     
                    }
            //第一次分組,根據uuid分組,分組除今天之外的歷史數據
        mapless = Multimaps.index(
                      listOfMaps,new Function<Map<String, String>, String>() {
                          public String apply(final Map<String, String> from) {
                             
                                  return from.get("uuid");    
                      }
                 });
              
          }    
    
        return mapless;
    }
View Code

    這里為什么要用"_id"這個字段作為分頁的條件?其實,我也用過其他字段,比如時間字段,時間字符串也是可以比大小的,但它的效率遠不如"_id"高。

    關於MongoDB中的"_id",以前一直忽略它的作用,直接結果是讓我耗了很多時間和精力,繞了大半圈,又回到了原點,有一種眾里尋他千百度,驀然回首,那人卻在燈火闌珊處的感覺...

    MongoDB ObjectId

    “4e7020cb7cac81af7136236b”這個24位的字符串,雖然看起來很長,也很難理解,但實際上它是由一組十六進制的字符構成,每個字節兩位的十六進制數字,總共用了12字節的存儲空間。相比MYSQLint類型的4個字節,MongoDB確實多出了很多字節。不過按照現在的存儲設備,多出來的字節應該不會成為什么瓶頸。不過MongoDB的這種設計,體現着空間換時間的思想。官網中對ObjectId的規范,如圖所示:

     

    1)Time

    時間戳。將剛才生成的objectid的前4位進行提取“4e7020cb”,然后按照十六進制轉為十進制,變為“1315971275”,這個數字就是一個時間戳。通過時間戳的轉換,就成了易看清的時間格式。

    2)Machine

機器。接下來的三個字節就是“7cac81”,這三個字節是所在主機的唯一標識符,一般是機器主機名的散列值,這樣就確保了不同主機生成不同的機器hash值,確保在分布式中不造成沖突,這也就是在同一台機器生成的objectId中間的字符串都是一模一樣的原因。

    3)PID

    進程ID。上面的Machine是為了確保在不同機器產生的objectId不沖突,而pid就是為了在同一台機器不同的mongodb進程產生了objectId不沖突,接下來的“af71”兩位就是產生objectId的進程標識符。

    4)INC

    自增計數器。前面的九個字節是保證了一秒內不同機器不同進程生成objectId不沖突,這后面的三個字節“36236b”是一個自動增加的計數器,用來確保在同一秒內產生的objectId也不會發現沖突,允許256的3次方等於16777216條記錄的唯一性。

    總的來看,objectId的前4個字節時間戳,記錄了文檔創建的時間;接下來3個字節代表了所在主機的唯一標識符,確定了不同主機間產生不同的objectId;后2個字節的進程id,決定了在同一台機器下,不同mongodb進程產生不同的objectId;最后通過3個字節的自增計數器,確保同一秒內產生objectId的唯一性。ObjectId的這個主鍵生成策略,很好地解決了在分布式環境下高並發情況主鍵唯一性問題,值得學習借鑒。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM