MongoDB模式設計

1. 開篇

今天的話題是進階模式,所以我假設在坐各位至少是已經對MongoDB有了一些基本的瞭解。 不過每次總有一些同學以爲這裏有水果吃才坐進來的,所以在這裏我簡單介紹一下:MongoDB 不是芒果(mango),它在拉丁文中的原意是巨大的意思。如果用一句話來概括的話,mongo是一個高可用、分佈式、無模式的文檔數據庫。等一下,這裏我故意用錯了一個詞: 不是無模式,而是“靈活模式”。 如果真的是無模式,今天我就不用站在這裏了。沒有模式何來模式設計之說。在你開始用mongo做一些 prototype的時候,確實不用考慮太多的模式。MongoDB內存數據庫的一些特性,讓你在前期不會遇到什麼問題。但是一旦涉及到幾千萬幾十億的數據量,或者是數千數萬的併發量,模式設計就是個你必須提前面對的問題。

MongoDB的定位:

  • 高可用
  • 分佈式
  • 靈活模式
  • 文檔數據庫

2. MongoDB文檔模型

在我們談mongo的模式設計之前,我們很有必要來了解一下MongoDB的數據模型。大家都知道,無論你從哪個角度來看,MongoDB都是目前NoSQL,或者說非關係型的數據庫中的領頭羊。那麼,mongo和傳統關係數據庫的最本質的區別在那裏呢?我們說是它的文檔模型。

關係模型和文檔模型的區別:

  • 關係模型需要你把一個數據對象,拆分成零部件,然後存到各個相應的表裏,需要的是最後把它拼起來。舉例子來說,假設我們要做一個CRM應用,那麼要管理客戶的基本信息,包括客戶名字、地址、電話等。由於每個客戶可能有多個電話,那麼按照第三範式,我們會把電話號碼用單獨的一個表來存儲,並在顯示客戶信息的時候通過關聯把需要的信息取回來。
  • 而MongoDB的文檔模式,與這個模式大不相同。由於我們的存儲單位是一個文檔,可以支持數組和嵌套文檔,所以很多時候你直接用一個這樣的文檔就可以涵蓋這個客戶相關的所有個人信息。關係型數據庫的關聯功能不一定就是它的優勢,而是它能夠工作的必要條件。 而在MongoDB裏面,利用富文檔的性質,很多時候,關聯是個僞需求,可以通過合理建模來避免做關聯。

雖然MongoDB的模型和關係型截然不同,但是關係型數據庫的一些必不可少的功能如動態查詢、二級索引、聚合等在MongoDB中也有非常完善的支持。

MongoDB 模式設計進階案例_頁面_04

這裏我介紹一下文檔模型的優點:

  • 讀寫效率高-由於文檔模型把相關數據集中在一塊,在普通機械盤上讀數據的時候不用花太多時間去定位磁頭,因此在IO性能上有先天獨厚的優勢;
  • 可擴展能力強-關係型數據庫很難做分佈式的原因就是多節點海量數據關聯有巨大的性能問題。如果不考慮關聯,數據分區分庫,水平擴展就比較簡單;
  • 動態模式-文檔模型支持可變的數據模式,不要求每個文檔都具有完全相同的結構。對很多異構數據場景支持非常好;
  • 模型自然-文檔模型最接近於我們熟悉的對象模型。從內存到存儲,無需經過ORM的雙向轉換,性能上和理解上都很自然易懂。

那麼我們如何考慮MongoDB 文檔模式設計的基本策略呢?

  • 其實很簡單,我們一般建議的是先考慮內嵌, 直接按照你的對象模型來設計你的數據模型。如果你的對象模型數量不多,關係不是很複雜,那麼恭喜你,可能直接一種對象對應一個集合就可以了。
  • 內嵌是文檔模型的特色,可以充分利用MongoDB的富文檔功能來享受我們剛纔談到的一些文檔模型的性能和擴展性等特性。一般的一對一、一對多關係,比如說一個人多個地址多個電話等等都可以放在一個文檔裏用內嵌來完成。
  • 但是有一些時候,使用引用則難以避免。比如說, 一個明星的博客可能有幾十萬或者幾百萬的回覆,這個時候如果把comments放到一個數組裏,可能會超出16M的限制。這個時候你可以考慮使用引用的方式,在主表裏存儲一個id值,指向另一個表中的 id 值。使用引用要注意的就是:從性能上講,一般我們可能需要兩次以上才能把需要的數據取回來。更加重要的是:需要把數據存放到兩個集合裏,但是目前爲止MongoDB並不支持跨表的事務性,所以對於強事務的應用場景要謹慎使用。

很多時候我們並不能很好地回答自己的問題,包括剛纔的內嵌還是引用的問題。那麼這個時候有必要了解一下,MongoDB模式設計的終極原則。MongoDB的模式設計和關係型大不相同,我們說MongoDB是爲應用程序設計的,而不是爲了存儲優化的。如果可以達到最高性能的話,我們甚至可以做一些反範式的東西。

MongoDB的模式設計思路:

  • 爲應用程序服務,而不是爲了存儲優化
  • 爲實現最佳性能而設計

3. 模式設計案例

3.1 電商案例

在電商方面MongoDB的應用場景其實蠻多,比如說,大名鼎鼎的京東用mongo來存儲過億的商品信息,另外有一家著名的境外電商從頭到尾用的都是MongoDB,包括訂單管理等。這裏我們就來看一下購物車這個場景。

購物車的特點:

  • 一個cart數據項不會太大,一般項數是少於100
  • 數據自動過期(15~30分鐘無交互)

文檔模型在這種場景下就是個很好的選擇:

  • 一個文檔、一個購物車
  • 動態模式可以支持車內不同商品的分類描述
  • 易於水平擴展
  • TTL索引自動刪除過期數據

大家看一下下面的參考數據模型,第一點注意我們可以使用MongoDB的TTL 索引來自動清理過期數據。TTL索引可以建立在任意一個時間字段上,在建立索引的時候可以指定文檔在過多少時間後會被自動清理掉。第二個大家注意的是什麼呢?在這裏我們把商品的一些主要信息放到購物車裏了,比如說 name,price, quantity,爲什麼? 讀一次所有信息都拿到了:價格、數量等等,不需要再去查另一張表。這是一種比較常見的優化手段,用冗餘的方式來提供讀取性能。

MongoDB 模式設計進階案例_頁面_11接下來我們看一下使用這種模式的時候如何進行一些購物車的操作。比如說,如果我們想要往購物車裏增加一個價值2元的麪包,我們可以用下面的update語句。注意$push的用法。$push 類似於javascript的操作符,意思是往數組尾部增加一個元素。

MongoDB 模式設計進階案例_頁面_12

如果需要更新購物車中某個產品的數量,你可以用update語句直接操作數組的某一個元素。在這裏我們需要做的是更新item 4567的數量爲5。 注意 items.$.quanity的使用,這裏的$ 表示在查詢條件裏匹配上的數組元素的序數。

MongoDB 模式設計進階案例_頁面_13

如果需要統計一下在購物車內某個商品的總數,可以使用MongoDB的聚合功能。聚合運算在MongoDB裏面是對數據輸入源進行一系列的運算。在這裏我們做的就是幾個步驟是:

  1. $match: 在所有購物車中過濾掉其他商品,只選出id是8910的商品
  2. $unwind: 把items 數組展開,每個數組元素變成一個文檔
  3. $group: 用聚合運算 $sum 把每一件商品的數量相加獲得總和

MongoDB 模式設計進階案例_頁面_14

3.2 社交案例

下面我們來看一個社交網絡的例子。社交app最關鍵的一些場景就是維護朋友關係以及朋友圈或微博牆等。

MongoDB 模式設計進階案例_頁面_15

對於關係描述,使用文檔模型的內嵌數組特性,我們可以很容易地把我關注的用戶(following)和關注我的用戶表示出來。下例表示TJ我的關注的用戶是mandy和bert,而oscar和mandy則在關注我。這種模式是文檔模型中最經典的。但是有一個潛在問題就是如果TJ我是一個明星,他們關注我的人可能有千萬。一個千萬級的數組會有兩個問題:1) 有可能超出一個文檔最大16M的硬性限制; 2) MongoDB數組太大會嚴重影響性能。

MongoDB 模式設計進階案例_頁面_16

怎麼辦?我們可以建立一個專門的集合來描述關注關係。這裏就是一個內嵌和引用的經典選擇。我們希望用內嵌,但是如果數組維度太大,就需要考慮用另外一個集合的方式來表示一對多的關係(用戶 1–N 關注者)

MongoDB 模式設計進階案例_頁面_17

另外一個要注意的是關注數,我們在顯示關注和粉絲數量的時候,不希望去跑一次count 查詢再顯示。因爲count操作一般來說會比較佔資源。通常的做法可以再用戶對象裏面加兩個字段,一個是關注數一個是粉絲數。每次有人關注或者關注別人時候就更新一下。

MongoDB 模式設計進階案例_頁面_18

下面我們來看看比較有趣的微博牆,或者微信朋友圈的實現有什麼考量。

MongoDB 模式設計進階案例_頁面_19

在實現微博牆的時候,有兩種方式可以考慮:扇出讀 或者是扇出寫

MongoDB 模式設計進階案例_頁面_20

扇出讀、扇出寫的說法是基於社交網絡的海量用戶、海量數據的應用特徵。這些大量的數據往往分佈在各個分片服務器上。扇出讀是一種比較常規的做法,就是當你需要去獲得所有你關注用戶的最新更新的時候,你就去到每一個你關注用戶的數據區,把最新的一些數據取回來。因爲需要去到不同的分片服務器去取,所以叫做扇出讀。大家可以想象,這種扇出讀的效率不會太高,基本上是最慢的那個服務器的響應時間決定了總體的響應時間。 當然,這種方式是比較簡單的,不需要特殊處理。

MongoDB 模式設計進階案例_頁面_21

扇出寫,我稱之爲土豪玩法。具體來說就是當發佈的時候,一條數據會寫多次,直接寫到每一個關注你的粉絲的牆上。這樣做的好處是當你的粉絲讀他自己的微博牆的時候,他只需要去一個地方就可以把所有最新的更新連續取回來。由於一個用戶的數據可一般可以存儲在同一臺服務器上的同一個區域,通過這種方式可以實現快速的讀取微博牆數據。 代價當然也是很明顯: 你的寫入需求會被放大幾十幾百倍,存儲也是相應的擴大幾十幾百倍。這個絕對不是關係型數據庫的玩法,但是在MongoD 模式設計,這個很正常。只要保證性能,什麼事情都做得出來。

MongoDB 模式設計進階案例_頁面_22

MongoDB 模式設計進階案例_頁面_23

下面這個例子,首先是mandy在發消息的時候會寫(push)到我的牆上(timeline)來。如果mandy有50個關注者,那麼這個寫就會有50次,每個關注者一次。

第二條語句就是我打開微博的時候,一條語句,一個地方就可以找到所有我朋友發的狀態更新。注意:這裏還使用了bucket,這是另外一個控制文檔內數組元素個數的有效方法。比如說我們定義bucket 大小是1000的話,超過1000 就把新的數據插入到下一個文檔並對bucket 序數遞增。

MongoDB 模式設計進階案例_頁面_24

3.3 物聯網案例

好了,最後我們來看一下物聯網的應用場景:

MongoDB 模式設計進階案例_頁面_25

各位還有多少人仍然記得MH370,去年在印度洋消失的客機?在該事故之後,許多人都在疑惑:在當今的技術水平下,爲什麼我們不能跟蹤如此龐大的一個東西?

讓我們來看看如果要監控飛機數據有什麼樣的挑戰。飛機上面的數據源衆多,光收集位置信息,就需要多個系統協作完成, 如ADS-C, EUROCONTROL等等。此外,收集的數據也是各種各樣:位置是2D、速度是數值、引擎參數則是多維度的。

 

MongoDB 模式設計進階案例_頁面_27

另一個挑戰就是海量數據。一個三小時的航班,每分鐘採集一次,少說點,每次100條數據,那就是每秒1萬8千個數據點。按每天100,000航班,一天的數據算下來有18億條,1.8TB 左右的數據, 21,000 的QPS。 從哪個角度來看,這都是個經典的大數據問題。

MongoDB 模式設計進階案例_頁面_28

這個問題在關係型數據庫解決的話,比較幼稚的方法就是設計一個超寬的表。所有需要採集的每一個值就是一個列。這種設計的問題比較明顯:

  1. 容易造成空白浪費,不是每一條記錄都包含所有字段值
  2. 可能會經常需要改數據庫模式。對於海量數據,改一次模式代價巨大。

MongoDB 模式設計進階案例_頁面_29

另一種改良方案是用EAV 設計模式。就是採用一個主表和一個屬性值表。在屬性值表裏存放所有的參數鍵值對。這樣做的好處自然是靈活性:增加新的參數時無需修改模式。但是問題同樣存在:用來存儲值的那列METRIC_VALUE的字節大小必須定義成所有值的最大值 纔可以放下所有的參數值。這個可能帶來空間浪費,但是更嚴重的問題是:將不太可能在此字段上建索引,進而影響一些場景的使用。

MongoDB 模式設計進階案例_頁面_30

下面我們來看看文檔模型怎麼做: 這裏對於location 、speed 等不同數據類型的字段,在文檔模型下可以直接支持。下面的兩個文檔,第一個文檔和第二個文檔可以同屬一個集合,但是可以有完全不同的字段。 MongoDB對異構數據的支持在這樣的場景下有得天獨厚的優勢。如果我們希望對某一個metric如location建立索引,我們也可以使用mongoDB的稀疏索引 (Sparse Index)僅對有location字段的文檔建索引,在不造成索引空間浪費的前提下提高檢索效率。當需要增加新的字段的時候,也不需要對模式做任何修改,可以直接就在應用中的JSON模型裏添加需要的字段(elevation)。

MongoDB 模式設計進階案例_頁面_31

在IOT這個場景裏,我們可以使用一個叫做分桶的設計方式來進行幾十倍的性能增長。具體來說就是把採集的數據按小時爲一個桶,把每小時的數據聚合到一個文檔裏。如下面所示,每分鐘的值用子文檔的一個字段來表示。這樣做的好處就是大量減少文檔的數量,相應的索引數量也會減少,總體寫入IO將會大幅度降低並得到性能提升。

MongoDB 模式設計進階案例_頁面_32

使用這種方式我們還可以把一些統計需要的數值,如每小時的平均值預先就作爲一個字段存進去,需要的時候不用現場計算,只要從文檔裏讀出來即可。

MongoDB 模式設計進階案例_頁面_33

MongoDB 模式設計進階案例_頁面_34

4. 小結

冗餘、扇出寫、分桶,這些都是mongodb 的一些常用優化手段。 大家可以看到,通過減少額外查詢或者關聯的需求,通過使用冗餘、額外存儲的非常規方式,我們希望做到的是性能上的最高提升。

MongoDB 模式設計進階案例_頁面_35

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章