mongodb分頁最佳化

來源:互聯網
上載者:User

標籤:

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. 在當前頁內查出最後1條記錄的_id,記為last_id
  2. 把記下來的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 等。

![](http://images.blogjava.net/blogjava_net/dongbule/46046/o_111.PNG)

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檢測效能慢的語句,便於線上產品問題定位

無論哪種你定位出來問題,解決辦法

  • 根據業務,調整schema結構
  • 最佳化索引

有了上面這些知識,相信大家能夠自己去給分頁語句測試效能了。

全文完

歡迎關注我的公眾號【node全棧】

mongodb分頁最佳化

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.