最近項目在做網站用戶數據新訪客統計,數據存儲在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的這個主鍵生成策略,很好地解決了在分布式環境下高並發情況主鍵唯一性問題,值得學習借鑒。