- 求職 : Linux運維
- 論壇徽章:
- 203
|
分享
12月12日上午,TJ在開源中國的年終盛典會上分享了文檔模型設計的進階技巧,就讓我們來回顧一下吧: —————————————————————————————————————————————————————————-
從很久以前,我就開始接觸開源產(chǎn)品:從最開始的使用、受益者到后來的貢獻者,到現(xiàn)在的熱情推廣者。現(xiàn)在,我是MongoDB的技術(shù)顧問。我的職責是為MongoDB的客戶和用戶提供MongoDB使用的一些最佳實踐,包括模式設計、性能優(yōu)化和集群部署方案等方面。
MongoDB 模式設計進階案例_頁面_01
今天的話題是進階模式,所以我假設在坐各位至少是已經(jīng)對MongoDB有了一些基本的了解。 不過每次總有一些同學以為這里有水果吃才坐進來的,所以在這里我簡單介紹一下:MongoDB 不是芒果(mango),它在拉丁文中的原意是巨大的意思。如果用一句話來概括的話,mongo是一個高可用、分布式、無模式的文檔數(shù)據(jù)庫。等一下,這里我故意用錯了一個詞: 不是無模式,而是“靈活模式”。 如果真的是無模式,今天我就不用站在這里了。沒有模式何來模式設計之說。在你開始用mongo做一些 prototype的時候,確實不用考慮太多的模式。MongoDB內(nèi)存數(shù)據(jù)庫的一些特性,讓你在前期不會遇到什么問題。但是一旦涉及到幾千萬幾十億的數(shù)據(jù)量,或者是數(shù)千數(shù)萬的并發(fā)量,模式設計就是個你必須提前面對的問題。 MongoDB 模式設計進階案例_頁面_02
在我們談mongo的模式設計之前,我們很有必要來了解一下MongoDB的數(shù)據(jù)模型。大家都知道,無論你從哪個角度來看,MongoDB都是目前NoSQL,或者說非關系型的數(shù)據(jù)庫中的領頭羊。那么,mongo和傳統(tǒng)關系數(shù)據(jù)庫的最本質(zhì)的區(qū)別在那里呢?我們說是它的文檔模型。
MongoDB 模式設計進階案例_頁面_03
關系模型和文檔模型的區(qū)別在哪里?
關系模型需要你把一個數(shù)據(jù)對象,拆分成零部件,然后存到各個相應的表里,需要的是最后把它拼起來。舉例子來說,假設我們要做一個CRM應用,那么要管理客戶的基本信息,包括客戶名字、地址、電話等。由于每個客戶可能有多個電話,那么按照第三范式,我們會把電話號碼用單獨的一個表來存儲,并在顯示客戶信息的時候通過關聯(lián)把需要的信息取回來。
而MongoDB的文檔模式,與這個模式大不相同。由于我們的存儲單位是一個文檔,可以支持數(shù)組和嵌套文檔,所以很多時候你直接用一個這樣的文檔就可以涵蓋這個客戶相關的所有個人信息。關系型數(shù)據(jù)庫的關聯(lián)功能不一定就是它的優(yōu)勢,而是它能夠工作的必要條件。 而在MongoDB里面,利用富文檔的性質(zhì),很多時候,關聯(lián)是個偽需求,可以通過合理建模來避免做關聯(lián)。
雖然MongoDB的模型和關系型截然不同,但是關系型數(shù)據(jù)庫的一些必不可少的功能如動態(tài)查詢、二級索引、聚合等在MongoDB中也有非常完善的支持。 MongoDB 模式設計進階案例_頁面_04
這里我介紹一下文檔模型的優(yōu)點:
讀寫效率高-由于文檔模型把相關數(shù)據(jù)集中在一塊,在普通機械盤上讀數(shù)據(jù)的時候不用花太多時間去定位磁頭,因此在IO性能上有先天獨厚的優(yōu)勢;
可擴展能力強-關系型數(shù)據(jù)庫很難做分布式的原因就是多節(jié)點海量數(shù)據(jù)關聯(lián)有巨大的性能問題。如果不考慮關聯(lián),數(shù)據(jù)分區(qū)分庫,水平擴展就比較簡單;
動態(tài)模式-文檔模型支持可變的數(shù)據(jù)模式,不要求每個文檔都具有完全相同的結(jié)構(gòu)。對很多異構(gòu)數(shù)據(jù)場景支持非常好;
模型自然-文檔模型最接近于我們熟悉的對象模型。從內(nèi)存到存儲,無需經(jīng)過ORM的雙向轉(zhuǎn)換,性能上和理解上都很自然易懂。
MongoDB 模式設計進階案例_頁面_05
那么我們?nèi)绾慰紤]MongoDB 文檔模式設計的基本策略呢?
其實很簡單,我們一般建議的是先考慮內(nèi)嵌, 直接按照你的對象模型來設計你的數(shù)據(jù)模型。如果你的對象模型數(shù)量不多,關系不是很復雜,那么恭喜你,可能直接一種對象對應一個集合就可以了。
內(nèi)嵌是文檔模型的特色,可以充分利用MongoDB的富文檔功能來享受我們剛才談到的一些文檔模型的性能和擴展性等特性。一般的一對一、一對多關系,比如說一個人多個地址多個電話等等都可以放在一個文檔里用內(nèi)嵌來完成。
但是有一些時候,使用引用則難以避免。比如說, 一個明星的博客可能有幾十萬或者幾百萬的回復,這個時候如果把comments放到一個數(shù)組里,可能會超出16M的限制。這個時候你可以考慮使用引用的方式,在主表里存儲一個id值,指向另一個表中的 id 值。使用引用要注意的就是:從性能上講,一般我們可能需要兩次以上才能把需要的數(shù)據(jù)取回來。更加重要的是:需要把數(shù)據(jù)存放到兩個集合里,但是目前為止MongoDB并不支持跨表的事務性,所以對于強事務的應用場景要謹慎使用。
MongoDB 模式設計進階案例_頁面_06
很多時候我們并不能很好地回答自己的問題,包括剛才的內(nèi)嵌還是引用的問題。那么這個時候有必要了解一下,MongoDB模式設計的終極原則。MongoDB的模式設計和關系型大不相同,我們說MongoDB是為應用程序設計的,而不是為了存儲優(yōu)化的。如果可以達到最高性能的話,我們甚至可以做一些反范式的東西。 接下來我們來看幾個比較具體的設計案例,了解一下MongoDB的模式設計思路:
MongoDB 模式設計進階案例_頁面_07
我這里準備了4個比較經(jīng)典的MongoDB案例,從CMS 內(nèi)容管理到電商,社交到物聯(lián)網(wǎng)。 由于時間原因我就從第二個開始。
MongoDB 模式設計進階案例_頁面_08
在電商方面MongoDB的應用場景其實蠻多,比如說,大名鼎鼎的京東用mongo來存儲過億的商品信息,另外有一家著名的境外電商從頭到尾用的都是MongoDB,包括訂單管理等。這里我們就來看一下購物車這個場景。購物車的特點就是單個購物車數(shù)據(jù)項不會太大,一般來說不會超過100項目。雙十一的時候淘寶的購物車里最多就只能放99件商品。在這里我要謝謝我的太太,是她讓我知道了這個限制。另外一點就是購物車的數(shù)據(jù)可能需要過期刪除。
MongoDB 模式設計進階案例_頁面_09
我們說文檔模型在這種場景會是個很好的選擇:
MongoDB 模式設計進階案例_頁面_10
大家看一下下面的參考數(shù)據(jù)模型,第一點注意我們可以使用MongoDB的TTL 索引來自動清理過期數(shù)據(jù)。TTL索引可以建立在任意一個時間字段上,在建立索引的時候可以指定文檔在過多少時間后會被自動清理掉。 第二個大家注意的是什么呢?在這里我們把商品的一些主要信息放到購物車里了,比如說 name,price, quantity,為什么? 讀一次所有信息都拿到了:價格、數(shù)量等等,不需要再去查另一張表。這是一種比較常見的優(yōu)化手段,用冗余的方式來提供讀取性能。
MongoDB 模式設計進階案例_頁面_11
接下來我們看一下使用這種模式的時候如何進行一些購物車的操作。比如說,如果我們想要往購物車里增加一個價值2元的面包,我們可以用下面的update語句。注意$push的用法。$push 類似于javascript的操作符,意思是往數(shù)組尾部增加一個元素。MongoDB 模式設計進階案例_頁面_12
如果需要更新購物車中某個產(chǎn)品的數(shù)量,你可以用update語句直接操作數(shù)組的某一個元素。在這里我們需要做的是更新item 4567的數(shù)量為5。 注意 items.$.quanity的使用,這里的$ 表示在查詢條件里匹配上的數(shù)組元素的序數(shù)。
MongoDB 模式設計進階案例_頁面_13
如果需要統(tǒng)計一下在購物車內(nèi)某個商品的總數(shù),可以使用MongoDB的聚合功能。聚合運算在MongoDB里面是對數(shù)據(jù)輸入源進行一系列的運算。在這里我們做的就是幾個步驟是:
$match: 在所有購物車中過濾掉其他商品,只選出id是8910的商品
$unwind: 把items 數(shù)組展開,每個數(shù)組元素變成一個文檔
$group: 用聚合運算 $sum 把每一件商品的數(shù)量相加獲得總和
MongoDB 模式設計進階案例_頁面_14
下面我們來看一個社交網(wǎng)絡的例子。社交app最關鍵的一些場景就是維護朋友關系以及朋友圈或微博墻等。
MongoDB 模式設計進階案例_頁面_15
對于關系描述,使用文檔模型的內(nèi)嵌數(shù)組特性,我們可以很容易地把我關注的用戶(following)和關注我的用戶表示出來。下例表示TJ我的關注的用戶是mandy和bert,而oscar和mandy則在關注我。這種模式是文檔模型中最經(jīng)典的。但是有一個潛在問題就是如果TJ我是一個明星,他們關注我的人可能有千萬。一個千萬級的數(shù)組會有兩個問題:1) 有可能超出一個文檔最大16M的硬性限制 2) MongoDB數(shù)組太大會嚴重影響性能。MongoDB 模式設計進階案例_頁面_16
怎么辦?我們可以建立一個專門的集合來描述關注關系。這里就是一個內(nèi)嵌和引用的經(jīng)典選擇。我們希望用內(nèi)嵌,但是如果數(shù)組維度太大,就需要考慮用另外一個集合的方式來表示一對多的關系(用戶 1–N 關注者)MongoDB 模式設計進階案例_頁面_17
另外一個要注意的是 關注數(shù),我們在顯示關注和粉絲數(shù)量的時候,不希望去跑一次count 查詢再顯示。因為count操作一般來說會比較占資源。通常的做法可以再用戶對象里面加兩個字段,一個是關注數(shù)一個是粉絲數(shù)。每次有人關注或者關注別人時候就更新一下。
MongoDB 模式設計進階案例_頁面_18
下面我們來看看比較有趣的微博墻,或者微信朋友圈的實現(xiàn)有什么考量。
MongoDB 模式設計進階案例_頁面_19
在實現(xiàn)微博墻的時候,有兩種方式可以考慮: 扇出讀 或者是 扇出寫。
MongoDB 模式設計進階案例_頁面_20
扇出讀、扇出寫的說法是基于社交網(wǎng)絡的海量用戶、海量數(shù)據(jù)的應用特征。這些大量的數(shù)據(jù)往往分布在各個分片服務器上。扇出讀是一種比較常規(guī)的做法,就是當你需要去獲得所有你關注用戶的最新更新的時候,你就去到每一個你關注用戶的數(shù)據(jù)區(qū),把最新的一些數(shù)據(jù)取回來。因為需要去到不同的分片服務器去取,所以叫做扇出讀。大家可以想象,這種扇出讀的效率不會太高,基本上是最慢的那個服務器的響應時間決定了總體的響應時間。 當然,這種方式是比較簡單的,不需要特殊處理。
MongoDB 模式設計進階案例_頁面_21
扇出寫,我稱之為土豪玩法。具體來說就是當發(fā)布的時候,一條數(shù)據(jù)會寫多次,直接寫到每一個關注你的粉絲的墻上。這樣做的好處是當你的粉絲讀他自己的微博墻的時候,他只需要去一個地方就可以把所有最新的更新連續(xù)取回來。由于一個用戶的數(shù)據(jù)可一般可以存儲在同一臺服務器上的同一個區(qū)域,通過這種方式可以實現(xiàn)快速的讀取微博墻數(shù)據(jù)。 代價當然也是很明顯: 你的寫入需求會被放大幾十幾百倍,存儲也是相應的擴大幾十幾百倍。這個絕對不是關系型數(shù)據(jù)庫的玩法,但是在MongoD 模式設計,這個很正常。只要保證性能,什么事情都做得出來。
MongoDB 模式設計進階案例_頁面_22
下面這個例子,首先是mandy在發(fā)消息的時候會寫(push)到我的墻上(timeline)來。如果mandy有50個關注者,那么這個寫就會有50次,每個關注者一次。
第二條語句就是我打開微博的時候,一條語句,一個地方就可以找到所有我朋友發(fā)的狀態(tài)更新。注意:這里還使用了bucket,這是另外一個控制文檔內(nèi)數(shù)組元素個數(shù)的有效方法。比如說我們定義bucket 大小是1000的話,超過1000 就把新的數(shù)據(jù)插入到下一個文檔并對bucket 序數(shù)遞增。
MongoDB 模式設計進階案例_頁面_24
好了,最后我們來看一下物聯(lián)網(wǎng)的應用場景:
MongoDB 模式設計進階案例_頁面_25
各位還有多少人仍然記得MH370,去年在印度洋消失的客機?在該事故之后,許多人都在疑惑:在當今的技術(shù)水平下,為什么我們不能跟蹤如此龐大的一個東西?
MongoDB 模式設計進階案例_頁面_26
讓我們來看看如果要監(jiān)控飛機數(shù)據(jù)有什么樣的挑戰(zhàn)。飛機上面的數(shù)據(jù)源眾多,光收集位置信息,就需要多個系統(tǒng)協(xié)作完成, 如ADS-C, EUROCONTROL等等。此外,收集的數(shù)據(jù)也是各種各樣:位置是2D、速度是數(shù)值、引擎參數(shù)則是多維度的。 MongoDB 模式設計進階案例_頁面_27
另一個挑戰(zhàn)就是海量數(shù)據(jù)。一個三小時的航班,每分鐘采集一次,少說點,每次100條數(shù)據(jù),那就是每秒1萬8千個數(shù)據(jù)點。按每天100,000航班,一天的數(shù)據(jù)算下來有18億條,1.8TB 左右的數(shù)據(jù), 21,000 的QPS。 從哪個角度來看,這都是個經(jīng)典的大數(shù)據(jù)問題。
MongoDB 模式設計進階案例_頁面_28
這個問題在關系型數(shù)據(jù)庫解決的話,比較幼稚的方法就是設計一個超寬的表。所有需要采集的每一個值就是一個列。這種設計的問題比較明顯:
1. 容易造成空白浪費,不是每一條記錄都包含所有字段值
2. 可能會經(jīng)常需要改數(shù)據(jù)庫模式。對于海量數(shù)據(jù),改一次模式代價巨大。
MongoDB 模式設計進階案例_頁面_29
另一種改良方案是用EAV 設計模式。就是采用一個主表和一個屬性值表。在屬性值表里存放所有的參數(shù)鍵值對。這樣做的好處自然是靈活性:增加新的參數(shù)時無需修改模式。但是問題同樣存在:用來存儲值的那列 METRIC_VALUE的字節(jié)大小必須定義成所有值的最大值 才可以放下所有的參數(shù)值。這個可能帶來空間浪費,但是更嚴重的問題是:將不太可能在此字段上建索引,進而影響一些場景的使用。 MongoDB 模式設計進階案例_頁面_30
下面我們來看看文檔模型怎么做: 這里對于location 、speed 等不同數(shù)據(jù)類型的字段,在文檔模型下可以直接支持。下面的兩個文檔,第一個文檔和第二個文檔可以同屬一個集合,但是可以有完全不同的字段。 MongoDB對異構(gòu)數(shù)據(jù)的支持在這樣的場景下有得天獨厚的優(yōu)勢。如果我們希望對某一個metric如location建立索引,我們也可以使用mongoDB的稀疏索引 (Sparse Index)僅對有l(wèi)ocation字段的文檔建索引,在不造成索引空間浪費的前提下提高檢索效率。當需要增加新的字段的時候,也不需要對模式做任何修改,可以直接就在應用中的JSON模型里添加需要的字段(elevation)。MongoDB 模式設計進階案例_頁面_31
在IOT這個場景里,我們可以使用一個叫做分桶的設計方式來進行幾十倍的性能增長。 具體來說就是把采集的數(shù)據(jù)按小時為一個桶,把每小時的數(shù)據(jù)聚合到一個文檔里。如下面所示,每分鐘的值用子文檔的一個字段來表示。這樣做的好處就是大量減少文檔的數(shù)量,相應的索引數(shù)量也會減少,總體寫入IO將會大幅度降低并得到性能提升。
MongoDB 模式設計進階案例_頁面_33
使用這種方式我們還可以把一些統(tǒng)計需要的數(shù)值,如每小時的平均值預先就作為一個字段存進去,需要的時候不用現(xiàn)場計算,只要從文檔里讀出來即可。
MongoDB 模式設計進階案例_頁面_34
小結(jié)一下,冗余、扇出寫、分桶,這些都是mongodb 的一些常用優(yōu)化手段。 大家可以看到,通過減少額外查詢或者關聯(lián)的需求,通過使用冗余、額外存儲的非常規(guī)方式,我們希望做到的是性能上的最高提升。
MongoDB 模式設計進階案例_頁面_35
MongoDB 中國團隊正在擴張中。希望和一流的、創(chuàng)新的數(shù)據(jù)庫團隊一起工作嗎?加入我們吧,我們在尋找有開發(fā)架構(gòu)或者數(shù)據(jù)庫相關經(jīng)驗的大牛們加入我們的技術(shù)顧問陣營。有興趣?加微信 tjtang826 私聊吧!
MongoDB 模式設計進階案例_頁面_36 |
|