標籤:
mongodb分頁很簡單,本文主要講分頁可能遇到的問題,以及最佳化方案
從傳統web到移動端api,我們都面臨一樣的問題,比如ajax get有大小顯示等,都會強迫你不得不分頁
比如我的項目使用ratchet做h5架構,它的push.js裡就是ajax get載入其他頁面,頁面太大就會報錯。
分頁說明
以典型的列表api來說:下拉重新整理是擷取最新資訊,然後上拉載入下一頁
常見api要寫的2個介面
- get_latest(model,count)
- get_with_page(number,size)
get_latest一般是取最新的資料,比如我們常見的下拉重新整理,一般都是這樣的介面的。由於2次下拉之間,可能非常長的時間間隔,所以取到的資料會把當前列表的資料衝掉。
通常做法
- 如果n(比如n=30s)分鐘內有連續請求,提示最近已更新,沒必要再刷,或者直接返回當前資料
- 如果取到新資料,將當前列表的資料衝掉,保證資料一致性
如果判斷我到最後一頁了
常見的辦法是取出總數,除以pagesize,然後判斷當前頁是否和總頁數-1
n = all_count - 1
量少的時候,毫無感覺,如果量大了,你去查一下count(*)是啥後果呢?
所以比較好的做法是按照id去查,前端根據每次返回的資料條數,如果條數等於pagesize,你就可以取下一頁資料,相反,如果取到的資料小於pagesize,你就知道沒有那麼多資料可以取了,即到了尾頁。此時只要disable擷取下一頁的按鈕即可。
使用 skip() 和 limit() 實現
//Page 1db.users.find().limit (10)//Page 2db.users.find().skip(10).limit(10)//Page 3db.users.find().skip(20).limit(10)........
抽象一下就是:檢索第n頁的代碼應該是這樣的
db.users.find().skip(pagesize*(n-1)).limit(pagesize)
當然,這是假定在你在2次查詢之間沒有任何資料插入或刪除操作,你的系統能嗎?
當然大部分oltp系統無法確定不更新,所以skip只是個玩具,沒太大用
而且skip+limit只適合小量資料,資料一多就卡死,哪怕你再怎麼加索引,最佳化,它的缺陷都那麼明顯。
如果你要處理大量資料集,你需要考慮別的方案的。
使用 find() 和 limit() 實現
之前用skip()方法沒辦法更好的處理大規模資料,所以我們得找一個skip的替代方案。
為此我們想平衡查詢,就考慮根據文檔裡有的時間戳記或者id
在這個例子中,我們會通過‘_id’來處理(用時間戳記也一樣,看你設計的時候有沒有類似created_at這樣的欄位)。
‘_id’是mongodb ObjectID類型的,ObjectID 使用12 位元組的儲存空間,每個位元組兩位十六進位數字,是一個24 位的字串,包括timestamp, machined, processid, counter 等。下面會有一節單獨講它是怎麼構成的,為啥它是唯一的。
使用_id實現分頁的大致思路如下
- 在當前頁內查出最後1條記錄的_id,記為last_id
- 把記下來的last_id,作為查詢條件,查出大於last_id的記錄作為下一頁的內容
這樣來說,是不是很簡單?
代碼如下
//Page 1db.users.find().limit(pageSize);//Find the id of the last document in this pagelast_id = ...//Page 2users = db.users.find({‘_id‘> last_id}). limit(10);//Update the last id with the id of the last document in this pagelast_id = ...
這隻是示範代碼,我們來看一下在Robomongo 0.8.4用戶端裡如何寫
db.usermodels.find({‘_id‘ :{ "$gt" :ObjectId("55940ae59c39572851075bfd")} }).limit(20).sort({_id:-1})
根據上面介面說明,我們仍然要實現2個介面
- get_latest(model,count)
- get_next_page_with_last_id(last_id, size)
為了讓大家更好的瞭解根據‘_id’分頁原理,我們有必要去瞭解ObjectID的組成。
關於 ObjectID組成
前面說了:‘_id’是mongodb ObjectID類型的,它由12位結構組成,包括timestamp, machined, processid, counter 等。

TimeStamp
前 4位是一個unix的時間戳記,是一個int類別,我們將上面的例子中的objectid的前4位進行提取“4df2dcec”,然後再將他們安裝十六進位 專為十進位:“1307761900”,這個數字就是一個時間戳記,為了讓效果更佳明顯,我們將這個時間戳記轉換成我們習慣的時間格式
$ date -d ‘1970-01-01 UTC 1307761900 sec’ -u
2011年 06月 11日 星期六 03:11:40 UTC
前 4個位元組其實隱藏了文檔建立的時間,並且時間戳記處在於字元的最前面,這就意味著ObjectId大致會按照插入進行排序,這對於某些方面起到很大作用,如 作為索引提高搜尋效率等等。使用時間戳還有一個好處是,某些用戶端驅動可以通過ObjectId解析出該記錄是何時插入的,這也解答了我們平時快速連續創 建多個Objectid時,會發現前幾位元字很少發現變化的現實,因為使用的是目前時間,很多使用者擔心要對伺服器進行時間同步,其實這個時間戳記的真實值並 不重要,只要其總不停增加就好。
Machine
接下來的三個位元組,就是 2cdcd2 ,這三個位元組是所在主機的唯一識別碼,一般是機器主機名稱的散列值,這樣就確保了不同主機產生不同的機器hash值,確保在分布式中不造成衝突,這也就是在同一台機器產生的objectid中間的字串都是一模一樣的原因。
pid
上面的Machine是為了確保在不同機器產生的objectid不衝突,而pid就是為了在同一台機器不同的mongodb進程產生了objectid不衝突,接下來的0936兩位就是產生objectid的進程標識符。
increment
前面的九個位元組是保證了一秒內不同機器不同進程產生objectid不衝突,這後面的三個位元組a8b817,是一個自動增加的計數器,用來確保在同一秒內產生的objectid也不會發現衝突,允許256的3次方等於16777216條記錄的唯一性。
用戶端產生
mongodb產生objectid還有一個更大的優勢,就是mongodb可以通過自身的服務來產生objectid,也可以通過用戶端的驅動程式來產生,如果你仔細看文檔你會感歎,mongodb的設計無處不在的使
用空間換時間的思想,比較objectid是輕量級,但服務端產生也必須開銷時間,所以能從伺服器轉移到用戶端驅動程式完成的就盡量的轉移,必須將事務扔給用戶端來完成,減低服務端的開銷,另還有一點原因就是擴充應用程式層比擴充資料庫層要變數得多。
總結
mongodb的ObejctId生產思想在很多方面挺值得我們借鑒的,特別是在大型分布式的開發,如何構建輕量級的生產,如何將生產的負載進行轉移,如何以空間換取時間提高生產的最大最佳化等等。
說這麼多的目的就是告訴你:mongodb的_id為啥是唯一的,單機如何唯一,叢集中如何唯一,理解了這個就可以了。
效能最佳化 索引
按照自己的業務需求即可,參見官方文檔 http://docs.mongodb.org/manual/core/indexes/
關於explain
rdbms裡的執行計畫,如果你不瞭解,那麼mongo的explain估計你也不太熟,簡單說幾句
explain是mongodb提供的一個命令,用來查看查詢的過程,以便進行效能最佳化。
http://docs.mongodb.org/manual/reference/method/cursor.explain/
db.usermodels.find({‘_id‘ :{ "$gt" :ObjectId("55940ae59c39572851075bfd")} }).explain()/* 0 */{ "queryPlanner" : { "plannerVersion" : 1, "namespace" : "xbm-wechat-api.usermodels", "indexFilterSet" : false, "parsedQuery" : { "_id" : { "$gt" : ObjectId("55940ae59c39572851075bfd") } }, "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "_id" : 1 }, "indexName" : "_id_", "isMultiKey" : false, "direction" : "forward", "indexBounds" : { "_id" : [ "(ObjectId(‘55940ae59c39572851075bfd‘), ObjectId(‘ffffffffffffffffffffffff‘)]" ] } } }, "rejectedPlans" : [] }, "executionStats" : { "executionSuccess" : true, "nReturned" : 5, "executionTimeMillis" : 0, "totalKeysExamined" : 5, "totalDocsExamined" : 5, "executionStages" : { "stage" : "FETCH", "nReturned" : 5, "executionTimeMillisEstimate" : 0, "works" : 6, "advanced" : 5, "needTime" : 0, "needFetch" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "invalidates" : 0, "docsExamined" : 5, "alreadyHasObj" : 0, "inputStage" : { "stage" : "IXSCAN", "nReturned" : 5, "executionTimeMillisEstimate" : 0, "works" : 5, "advanced" : 5, "needTime" : 0, "needFetch" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "invalidates" : 0, "keyPattern" : { "_id" : 1 }, "indexName" : "_id_", "isMultiKey" : false, "direction" : "forward", "indexBounds" : { "_id" : [ "(ObjectId(‘55940ae59c39572851075bfd‘), ObjectId(‘ffffffffffffffffffffffff‘)]" ] }, "keysExamined" : 5, "dupsTested" : 0, "dupsDropped" : 0, "seenInvalidated" : 0, "matchTested" : 0 } }, "allPlansExecution" : [] }, "serverInfo" : { "host" : "iZ251uvtr2b", "port" : 27017, "version" : "3.0.3", "gitVersion" : "b40106b36eecd1b4407eb1ad1af6bc60593c6105" }}
欄位說明:
queryPlanner.winningPlan.inputStage.stage列顯示查詢策略
- IXSCAN表示使用Index 查詢
- COLLSCAN表示使用列查詢,也就是一個一個對比過去
cursor中的索引名稱移動到了queryPlanner.winningPlan.inputStage.indexName
3.0中使用executionStats.totalDocsExamined來顯示總共需要檢查的文檔數,用以取而代之2.6裡的nscanned,即掃描document的行數。
- nReturned:返回的文檔行數
- needTime:耗時(毫秒)
- indexBounds:所用的索引
Profiling
另外還有一個Profiling功能
db.setProfilingLevel(2, 20)
profile層級有三種:
- 0:不開啟
- 1:記錄慢命令,預設為大於100ms
- 2:記錄所有命令
- 3、查詢profiling記錄
預設記錄在system.profile中
db[‘system.profile‘].find()
總結一下
- explain在寫代碼階段就可以做效能分析,開發階段用
- profile檢測效能慢的語句,便於線上產品問題定位
無論哪種你定位出來問題,解決辦法
有了上面這些知識,相信大家能夠自己去給分頁語句測試效能了。
全文完
歡迎關注我的公眾號【node全棧】
mongodb分頁最佳化