1. 程式人生 > >java 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;
	}
    本人初來乍到,寫出的程式碼自然不是那麼優雅難懂,況且註釋寫的自認為還是比較清楚,如果大家有疑問的,或者有更優雅的辦法可以留言評論...

    這裡為什麼要用"_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的這個主鍵生成策略,很好地解決了在分散式環境下高併發情況主鍵唯一性問題,值得學習借鑑。