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