MongoDB數據文件內部結構
- MongoDB在數據存儲上按命名空間劃分,一個
collection
是一個命名空間,一個索引也是一個命名空間。 - 同一個命名空間的數據被分成很多個Extent,Extent之間使用雙向鏈表連接。
- 在每個Extent中保存了具體每行數據,這些數據也是通過雙向鏈表連接的。
- 每行數據存儲空間不僅包括數據占用空間,還可能包含一部分附加空間,這使得數據
update
變大后可不移動位置。 - 索引以 BTree 結構實現
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 數據同步
- 紅色箭頭 寫操作寫到 Primary上,然后異步同步到多個Secondary上。
- 藍色箭頭 讀操作可從Primary或 Secondary任意一個上讀取。
- 各 Primary 與 Secondary 之間一直保持心跳同步檢測,用于判斷 Replica Sets 的狀態。
數據同步與讀寫分離
MongoDB分片機制
- MongoDB的分片指定一個分片
key
來進行,數據按范圍分為不同的chunk
,每個chunk
大小有限制。 - 多個分片節點保存這些
chunk
,每個節點保存一部分的chunk
。 - 每個分片節點都是一個 Replica Sets, 這樣保證數據的安全性。
- 當一個
chunk
超過限制的最大體積時,會分裂為兩個小的chunk
。 - 當
chunk
在分片節點中分布不均衡時,會引發chunk
遷移操作。
分片時幾種節點角色
- 客戶端訪問路由節點 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。
- 優點 僅需一次查詢即可獲取數據
- 缺點 數據重復、不可作為單獨對象、修改、大小
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。