1. 程式人生 > >MongoDB 進階模式設計

MongoDB 進階模式設計

12月12日上午,TJ在開源中國的年終盛典會上分享了文件模型設計的進階技巧,就讓我們來回顧一下吧:原文網址 —————————————————————————————————————————————————————————-

從很久以前,我就開始接觸開源產品:從最開始的使用、受益者到後來的貢獻者,到現在的熱情推廣者。現在,我是MongoDB的技術顧問。我的職責是為MongoDB的客戶和使用者提供MongoDB使用的一些最佳實踐,包括模式設計、效能優化和叢集部署方案等方面。

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

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

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

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

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

關係模型和文件模型的區別在哪裡?

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

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

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

這裡我介紹一下文件模型的優點:

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

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

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

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

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

很多時候我們並不能很好地回答自己的問題,包括剛才的內嵌還是引用的問題。那麼這個時候有必要了解一下,MongoDB模式設計的終極原則。MongoDB的模式設計和關係型大不相同,我們說MongoDB是為應用程式設計的,而不是為了儲存優化的。如果可以達到最高效能的話,我們甚至可以做一些反正規化的東西。 接下來我們來看幾個比較具體的設計案例,瞭解一下MongoDB的模式設計思路:

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

我這裡準備了4個比較經典的MongoDB案例,從CMS 內容管理到電商,社交到物聯網。 由於時間原因我就從第二個開始。

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

在電商方面MongoDB的應用場景其實蠻多,比如說,大名鼎鼎的京東用mongo來儲存過億的商品資訊,另外有一家著名的境外電商從頭到尾用的都是MongoDB,包括訂單管理等。這裡我們就來看一下購物車這個場景。購物車的特點就是單個購物車資料項不會太大,一般來說不會超過100專案。雙十一的時候淘寶的購物車裡最多就只能放99件商品。在這裡我要謝謝我的太太,是她讓我知道了這個限制。另外一點就是購物車的資料可能需要過期刪除。

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

我們說文件模型在這種場景會是個很好的選擇:

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

大家看一下下面的參考資料模型,第一點注意我們可以使用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

下面我們來看一個社交網路的例子。社交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

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

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

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

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

讓我們來看看如果要監控飛機資料有什麼樣的挑戰。飛機上面的資料來源眾多,光收集位置資訊,就需要多個系統協作完成, 如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

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

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

MongoDB 中國團隊正在擴張中。希望和一流的、創新的資料庫團隊一起工作嗎?加入我們吧,我們在尋找有開發架構或者資料庫相關經驗的大牛們加入我們的技術顧問陣營。有興趣?加微信 tjtang826 私聊吧!