MongoDB建模

MongoDB數據文件內部結構


MongoDB
  • MongoDB在數據存儲上按命名空間劃分,一個collection是一個命名空間,一個索引也是一個命名空間。
  • 同一個命名空間的數據被分成很多個Extent,Extent之間使用雙向鏈表連接。
  • 在每個Extent中保存了具體每行數據,這些數據也是通過雙向鏈表連接的。
  • 每行數據存儲空間不僅包括數據占用空間,還可能包含一部分附加空間,這使得數據 update 變大后可不移動位置。
  • 索引以 BTree 結構實現
MongoDB數據文件內部結構

MongoDB實現事務

MongoDB僅支持對單行記錄的原子性修改,并不支持對多行數據的原子操作。

MongoDB實現事務
  • 第1步
    先記錄一條事務記錄,將要修改的多行記錄的修改值寫入其中,并設置其狀態為 init,若此時操作中斷那么在重啟時會判斷是否處于 init 狀態,從而將其保存的多行修改操作應用到具體的行上。
  • 第2步
    更新具體要修改的行,將剛才寫的事務記錄的標識寫到它的 tran 字段中。
  • 第3步
    將事務記錄的狀態從 init 轉變為 pending,若此時操作中斷,重啟時會判斷到它的狀態是 pending,此時查看其所對應的多條要修改的記錄,若它的tran有值就向下進行第4步,若無值則說明第4步已執行,直接將其狀態從pending 轉變為 committed 即可。
  • 第4步
    將需要修改的多條記錄的相應值修改,并unset掉之前的tran字段。
  • 第5步
    將事務記錄那一條的狀態從 pending 轉變為 committed,事務完成。

在支持事務的RDBMS中,其事務原子性提交的保證大多與上面類似,其實事務記錄的 tran 那條記錄類似于 DBMS 中的 redolog 一樣。


MongoDB 數據同步

MongoDB采用Relica Sets模式的同步流程
  • 紅色箭頭 寫操作寫到 Primary上,然后異步同步到多個Secondary上。
  • 藍色箭頭 讀操作可從Primary或 Secondary任意一個上讀取。
  • 各 Primary 與 Secondary 之間一直保持心跳同步檢測,用于判斷 Replica Sets 的狀態。
Replication

數據同步與讀寫分離

Replication & Read/Write Splitting

MongoDB分片機制

Chunk size & Shard key
  • MongoDB的分片指定一個分片key來進行,數據按范圍分為不同的chunk,每個chunk大小有限制。
  • 多個分片節點保存這些chunk,每個節點保存一部分的chunk
  • 每個分片節點都是一個 Replica Sets, 這樣保證數據的安全性。
  • 當一個chunk超過限制的最大體積時,會分裂為兩個小的chunk
  • chunk在分片節點中分布不均衡時,會引發chunk遷移操作。
MongoDB分片機制
Sharding

分片時幾種節點角色

  • 客戶端訪問路由節點 mongos 來進行數據讀寫
  • config服務器保存了兩個映射關系,一個是 key 值得區間對應哪個chunk,一個是chunk存在哪一個分片節點。
  • 路由節點通過 config 服務器獲取數據信息,通過這些信息,找到真正存放數據的分片節點進行對應操作。
  • 路由節點還會在寫操作時判斷當前 chunk 是否超出限定大小,若超出就分裂成兩個 chunk 。
  • 對于按分片 key 進行的查詢和 update 操作來說,路由節點會查找具體的 chunk 然后再進行相關工作。
  • 對于不按分片 key 進行的查詢 和 update 操作來說, mongos 會對所有下屬節點發送請求然后再對返回結果進行合并。
分片節點角色

MongoDB數據建模與表結構設計

  • 優先考慮內嵌,除非有什么迫不得已的原因。
  • 若需單獨訪問一個對象,那它就適合被內嵌到其他對象中。
  • 數組不應該無限制增長
  • 不要太過擔心應用層級別的JOIN
  • 在進行反范式設計時先認真考量業務邏輯

MongoDB支持內嵌對象和數組類型,其建模方式有兩種

  • 內嵌(Embed)
    • 子文檔較小
    • 數據不會定期更改
    • 最終數據一致即可
    • 文檔數據小額增加
    • 數據通常需要執行二次查詢
    • 快速讀取
  • 連接(Link)
    • 子文檔較大
    • 數據經常變化
    • 中間階段數據也必須一致
    • 文檔數據大幅增加
    • 數據通常不包含在查詢結果中
    • 快速寫入

什么時候使用內嵌,什么時候用連接呢?那得看兩個實體之間的關系是什么類型的。

1. 內嵌建模

將相關的數據包括在一個單個的結構或文檔下,此模式也叫做非規范化模式,它充分利用了MongoDB的靈活文檔格式功能。

內嵌式一種反范式化的設計,指的是將每個文檔所需的數據都嵌入到文檔內部。例如:用戶和賬戶的關系,在驅動領域設計中,用戶是一個聚合根,每個用戶對應一個賬戶,是一對一的關系,在關系型數據庫設計中,大部分會將此兩者嚴格區分。但在MongoDB中,可直接選擇將用戶需要的賬戶數據內嵌到用戶文檔中,以便于增刪改查。

> db.userinfo.insert({
    username:"junchow",
    contact:{
        phone:"15523423212",
        email:"junchow520@gmail.com"
    },
    access:{
        level:3,
        group:"dev"
    }
})

內嵌數據可讓應用把相關的數據保存在同一條數據庫記錄中,應用即可發送較少的請求非MongoDB來完成常用查詢及更新請求。

一般而言下列情況建議使用內嵌數據

  • 數據對象之間有 “contains” 包含關系
  • 數據之間存在一對多的關系,多個或子文檔會經常和父文檔一起被顯示和查看。

內嵌數據會對讀操作有比較好的性能提高,也可使用應用在一個單個操作就可以完成對數據的讀取。同時內嵌數據也對更新相關數據提供了一個原子性的寫操作。

內嵌數據到同一個文檔的缺陷是會導致文檔的增長,文檔增長會影響寫性能并導致數據碎片化問題。MongoDB文檔大小必須小于16M,超過此大小可考慮使用GridFS。

  • 優點 僅需一次查詢即可獲取數據
  • 缺點 數據重復、不可作為單獨對象、修改、大小
內嵌 VS 連接

2. 連接建模

連接建模即規范化數據建模,是指通過使用引用來表達對象之間關系。

一般而言,下列情況下可使用規范化建模

  • 當內嵌數據導致很多數據的重復,并且讀性能的優勢又不足以蓋過數據重復的弊端時。
  • 需表達比較復雜的多對多的關系
  • 大型多層次結構數據集

引用比內嵌要更加靈活,但客戶端應用必須使用二次查詢來解析文檔內包含的引用。換言之,對同樣的操作而言,規范化模式會導致更多的網絡請求發送到數據庫服務端。

  • 優點 可作為單獨的對象
  • 缺點 需二次查詢
外鍵

文檔結構建模

當設計一個MongoDB數據庫結構時,你需要問下自己一個在使用關系型數據庫不會考慮的問題:

這個關系中集合的大小是什么樣的規模呢?
你需要意識到一對很少、一對許多、一對非常多這些細微的區別,不同的情況下建模也將不同。

1. 一對較少(Basics: Modeling One-to-Few,內嵌)

針對個人需要保存多個地址進行建模的場景使用內嵌文檔是很合適的。

場景:個人地址

> db.persons.insert({
    name:"junchow",
    ssn:"123-456-789",
    addr:{
        privince:"Hubei",
        city:"Wuhan"
    }
})

> db.persons.find()
/* 1 */
{
    "_id" : ObjectId("5a09b8cf61bf6b35a74b0faa"),
    "name" : "junchow",
    "ssn" : "123-456-789",
    "addr" : {
        "privince" : "Hubei",
        "city" : "Wuhan"
    }
}

場景:考試成績

> db.scores.insert({
    name:"junchow",
    grades:[
        {project:"english",grade:90},
        {project:"math", grade:90}
    ]
})

> db.scores.find()
/* 1 */
{
    "_id" : ObjectId("5a09b95461bf6b35a74b0fab"),
    "name" : "junchow",
    "grades" : [ 
        {
            "project" : "english",
            "grade" : 90.0
        }, 
        {
            "project" : "math",
            "grade" : 90.0
        }
    ]
}
  • 優點
    無需單獨執行一條語句去獲取內嵌內容
  • 缺點
    無法把內嵌文檔當做單獨的實體去訪問

例如:對一個任務跟蹤系統進行建模,每個用戶將會分配若干任務,內嵌這些任務到用戶文檔,在遇到“查詢昨天所有的任務”時將非常困難。

2. 一對較多(Basics:One-to-Many,子引用)

場景1:產品零件訂貨系統
每個商品有數百個可替換的零件,但不會超過數千個。
此用例很適合使用間接引用 - 將零件的objectId作為數組存放在商品文檔中,每個零件都將有它們自己的文檔對象。

> db.parts.insert({
    partno:'123-aff-23a',
    name:'#4 grommet',
    qty:90,
    cost:0.98,
    price:3.99
})
> db.parts.find()
/* 1 */
{
    "_id" : ObjectId("5a09bb7461bf6b35a74b0fac"),
    "partno" : "123-aff-23a",
    "name" : "#4 grommet",
    "qty" : 90.0,
    "cost" : 0.98,
    "price" : 3.99
}

每個產品的文檔對象中 parts 數組將會存放多個零件的ObjectId

> db.products.insert({
    name:'left-handed smoke shifter',
    manufacturer:'Acme Corp',
    catalog_number:121,
    parts:[ObjectId('5a09bb7461bf6b35a74b0fac')]
})
> db.products.find()
/* 1 */
{
    "_id" : ObjectId("5a09bc2561bf6b35a74b0fad"),
    "name" : "left-handed smoke shifter",
    "manufacturer" : "Acme Corp",
    "catalog_number" : 121.0,
    "parts" : [ 
        ObjectId("5a09bb7461bf6b35a74b0fac")
    ]
}

在獲取特定產品中所有零件,需一個應用層級別的join。
為了能快速的執行查詢,必須確保 products.catalog_number 有索引,由于零件中 parts._id 一定是由索引的,所以這樣會很高效。

此種引用的方式是對內嵌優缺點的補充,每個零件是單獨的文檔,很容易獨立去搜索和更新。需一條單獨的語句去獲取零件的具體內容是使用此種建模方式需要考慮的一個問題。

此種建模方式中的零件部分可被多個產品使用,所以在多對多時無需一張單獨的連接表。

場景2:游戲庫中每個人所獲卡牌可能有幾百個,將卡牌ID作為數組存在用戶信息中,此外每個卡牌又有自身的文檔對象。

游戲卡牌
反范式前

反范式在實際應用中,大多與玩家相關。例如卡牌等級,覺醒系數等。加入反范式后的結構看看!

反范式后

3. 一對很多(Basics:One-to-Squillions,父引用)

場景1:機器日志
由于每個MongoDB文檔有16M的大小限制,所以即使你存儲ObjectId也不夠的。可使用經典的處理方式“父級引用”,即用一個文檔存儲主機,在每個日志文檔中保存主機的ObjectId。

> db.hosts.insert({
    name:"goofy.example.com",
    ipaddr:"127.88.12.12"
})
> db.hosts.find()
/* 1 */
{
    "_id" : ObjectId("5a09be3161bf6b35a74b0fae"),
    "name" : "goofy.example.com",
    "ipaddr" : "127.88.12.12"
}
> db.logs.insert({
    created_at:Date(),
    host:ObjectId('5a09be3161bf6b35a74b0fae'),
    message:'cpu is on fire'
})
> db.logs.find()
/* 1 */
{
    "_id" : ObjectId("5a09be9261bf6b35a74b0faf"),
    "created_at" : "Mon Nov 13 2017 23:47:30 GMT+0800",
    "host" : ObjectId("5a09be3161bf6b35a74b0fae"),
    "message" : "cpu is on fire"
}

查找某臺主機最近5000條日志

> host = db.hosts.findOne({ipaddr:'127.88.12.12'})

> messages = db.logs.find({host:host._id}).sort({created_at: -1}).limit(5000).toArray()

注意MongoDB和RDBMS建模不同之處在于

Will the entities on the 'N' side of One-to-N ever need to stand alone?
一對多中的多是否需要一個單獨的實體呢?

What is the cardinality of the relationshio : is it one-to-few; one-to-mand; or one-to-squillions?
關系中集合的規模是一對很少,一對很多,還是非常多呢?

Based on these factors, you can pick one of the three basic One-to-N schema designs.
基于以上因素來決定采取三種建模方式

  • 一對很少且無需單獨訪問內嵌內容的情況下可使用內嵌多的一方
  • 一對多且多的一段內容因各種理由需單獨存在的情況,可通過數組方式引用多的一方。
  • 一對非常多的情況,請將一的那端引用嵌入進多的一端對象中。

場景:日志收集系統存放游戲各服務器的游戲日志,使用父級引用更為合理。查詢時僅需取出某臺機器的_id然后去查詢。

日志

高級主題

1. 雙向關聯

讓引用的 one 端 和 many 端,同時保存對象的引用。

場景:任務跟蹤系統
有persons和tasks兩個集合,one-to-n的關系是從persons端到tasks端,在需要獲取persons所有的tasks場景下需要在persons這個對象中保存tasks的id數組。


場景:游戲任務系統,擁有人物person和任務task兩個集合。

人物任務

若任務系統有共享人物就會涉及人物所有者,可在task任務集合中增加一個所有者owner。

共享任務

樹形結構建模

樹形結構建模
父文檔引用
子文檔引用
祖先數組
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容