如何將關系型數(shù)據(jù)導入MongoDB?
準備工作
關系型數(shù)據(jù)庫已經(jīng)統(tǒng)治數(shù)據(jù)存儲長達三十幾年的時間,即便在 2000 年以后誕生了 NoSQL 數(shù)據(jù)庫,但他的出現(xiàn)并沒有改變關系型數(shù)據(jù)的統(tǒng)治地位。隨著最近幾年互聯(lián)網(wǎng)應用的快速崛起,以及互聯(lián)網(wǎng)用戶的不斷增加,數(shù)據(jù)來源越來越復雜多樣,傳統(tǒng)關系型數(shù)據(jù)存儲面臨了很大的挑戰(zhàn)。這種挑戰(zhàn)體現(xiàn)在數(shù)據(jù)格式死板,改動困難,存儲不夠靈活,難于擴展等方面。因此,很多企業(yè)、公司都先后把數(shù)據(jù)從關系型遷移到 NoSQL 上來,其中 MongoDB 又是使用相對較廣泛的數(shù)據(jù)庫實現(xiàn)。本文就為大家分享一下關系型數(shù)據(jù)導入進 MongoDB 中應當遵循的步驟和注意的問題。
在考慮將關系型數(shù)據(jù)導入到 NoSQL 中時,首先需要確認的幾點是:這個導入過程不會是全自動的,并不是像備份數(shù)據(jù),遷移數(shù)據(jù),記住幾個命令那么簡單;其次,這個過程不是一個純技術問題,在制定具體方案時,項目經(jīng)理,業(yè)務分析人員,開發(fā)人員,數(shù)據(jù)庫管理員都應當參與到方案的討論中。遷移的計劃、技術方案、各個項目負責人的職責應當在全體人員在場的情況下制定清楚;***,應當考慮到遷移失敗以后的恢復方案,根據(jù)應用數(shù)據(jù)的復雜程度不同,遷移的工作量也不會完全一樣。
上圖列出了一個項目經(jīng)過關系型數(shù)據(jù)向 NoSQL 中遷移的大致步驟,當然這絕對不是一個唯一的標準。只是通常情況下的做法,可能會根據(jù)不同項目的特別需求有一些調(diào)整。下面我們來詳細分析每一個階段的具體工作內(nèi)容。
數(shù)據(jù)模型定義
有可能你會覺得奇怪,MongoDB 不是結(jié)構無關的 NoSQL 數(shù)據(jù)庫嗎?為什么我們要提到數(shù)據(jù)庫表結(jié)構定義。實際上,NoSQL 中的結(jié)構是指從技術層面來講,數(shù)據(jù)庫對表結(jié)構沒有強約束,任何格式的 JSON 都可以插入進 MongoDB 表中。但是,我們在做項目時不能為所欲為的在數(shù)據(jù)庫中插入數(shù)據(jù),一定要遵循我們自己定義的一套規(guī)則來進行,否則程序根本無法管理數(shù)據(jù)層面的業(yè)務邏輯。在討論表結(jié)構之前,先來看一下 MongoDB 中的一些術語和關系型數(shù)據(jù)庫的對應關系。
看起來很好理解,在 MongoDB 中我們把表稱做Collection,表中每一行的數(shù)據(jù)稱作Document。其他的基本沿用關系型數(shù)據(jù)庫的命名。在 MongoDB 中,所有的數(shù)據(jù)格式都是以 JSON 為數(shù)據(jù)庫類型,它能夠比較靈活的存儲各種數(shù)據(jù)庫關系。這也是為什么 MongoDB 能夠在一個 Collection 中存儲各種不同結(jié)構的數(shù)據(jù)。比如,你可以插入這樣一個 JSON 到 MongoDB 中:{"user": {"name": "Zhang San", }},另外再插入這個 JSON{"product": {"id": "00001"}}??梢钥吹竭@兩個 JSON 沒有任何關系,也沒有任何相同的屬性,但在 MongoDB 中都是合法的數(shù)據(jù),他們可以同時存在于一個 Collection 中。當然,我們并不鼓勵大家這樣做,因為這樣很難維護你的數(shù)據(jù)庫表格,而且對于查詢索引來說也很麻煩,會產(chǎn)生很多不必要的索引存儲。我們所說的結(jié)構靈活指的是在一個結(jié)構框架基礎上,可以靈活擴充、添加新的數(shù)據(jù)而不用重新定義數(shù)據(jù) Schema。因此,我們在進行數(shù)據(jù)庫遷移之前需要討論如何定義 Collection 的結(jié)構。
MongoDB 將 JSON 存儲成一個叫BSON的數(shù)據(jù)結(jié)構中,BSON指的是Binary JSON,二進制 JSON,并在 JSON 的基礎上添加了一些新的數(shù)據(jù)類型,int,float,long。JSON 格式可以靈活的存儲嵌入式數(shù)據(jù)結(jié)構,以及數(shù)組,要是在關系型數(shù)據(jù)庫中實現(xiàn)其難度是很難想象的。在定義Collection 結(jié)構時,需要根據(jù)應用程序?qū)嶋H需求找出數(shù)據(jù)模型的定義,***程度的利用 MongDB 的存儲靈活性。例如,下面是一個典型的兩張一對多的數(shù)據(jù)庫表格。
學生表:
成績表:
其中,***張表是學生表,第二張是學生成績表,一個學生可以有多門課程的成績,因此他們之間是一對多的關系,其中studnet_id在學生表中是主鍵,對應成績表中的外鍵。在關系型數(shù)據(jù)庫中這種表示方法***并正確,但是到了 MongoDB 中也許就是另外一種存儲樣式了。為了充分利用 JSON 格式的內(nèi)嵌式存儲,我們通常會把這種關系存儲到 Collection 中的一條記錄(Document),如下所示:
上面是對學生 Zhang Scan 的記錄存儲,可以看出我們把學生成績當作是學生表的內(nèi)嵌字段,由于是一對多的關系,我們把他存儲成一個數(shù)組的形式。這種基于 JSON 文檔的存儲結(jié)構有一下幾點優(yōu)勢:
- 數(shù)據(jù)一目了然,當你從數(shù)據(jù)庫中取出一條學生記錄后,關于學生的基本信息全部顯示出來。方便大家閱讀瀏覽。
- 避免了多次數(shù)據(jù)庫表連接操作。在關系型數(shù)據(jù)庫中存在著多種表之間的鏈接操作,比如左右連接,內(nèi)連,外連等等。為了找到關于一個學生的全部信息,我們也許需要進行若干張表的連接才能拿到想要的數(shù)據(jù)。除了需要寫更復雜的 SQL 語句以外,數(shù)據(jù)庫的性能也會受到影響。當數(shù)據(jù)庫進行一次連接操作時,內(nèi)部可能是需要從磁盤不同位置讀取數(shù)據(jù),加大了 IO 操作。反觀 MongoDB,一次查詢只需要讀取一次磁盤,大大提高的查詢效率。
- 刪除、修改操作簡單方便。如果所有相關學生的信息都存儲在一張 Collection 中,那么對學生信息的刪除和修改只需要在一張表中操作就可以。試想一下在關系型數(shù)據(jù)庫中,如果需要刪除一個學生紀錄,有可能需要操作學生表、成績表、宿舍表、等等與學生關聯(lián)的所有表,這樣的設計是困擾關系型數(shù)據(jù)庫開發(fā)人員的一大難題。搞不好數(shù)據(jù)庫中就會存儲大量過時、失效的數(shù)據(jù),而這些數(shù)據(jù)可能成為永遠也不會被訪問的死角。
- 所有 Document 都是自我描述的,這方便大家進行數(shù)據(jù)庫的水平擴展。在 MongoDB Shard 中,我們可以將一個 Collection 切分到不同的 Shard 集群中,這種切分方法在不需要進行 JOIN 的操作前提下變得十分簡單。因為,DBA 再不用擔心需要進行夸節(jié)點的 JOIN 操作。(關于 MongoDB 水平擴展的內(nèi)容,情參考另外一篇文章MongoDB 的水平擴展,你做對了嗎?。
內(nèi)嵌還是引用
上面是一個將一對多關系的兩張表整合到一個 Document 中,實際上我們的數(shù)據(jù)表結(jié)構會復雜很多,一個企業(yè)級應用動輒就要設計幾百甚至上千張表,表之間會有一對一,一對多,多對多種關聯(lián)關系。對于如此復雜的場景目前我們還沒有一個準確的可以使用任何情況的解決方案?;旧隙夹枰槍I(yè)務數(shù)據(jù)具體分析,從而得出新的數(shù)據(jù)結(jié)構。這里,我可以給大家列出一些基本的原則以及處理不同關系的基本方法,根據(jù)這些基本原則方法我想大家總可以根據(jù)自己的業(yè)務歸納出一個行之有效的解決方案。具體到 MongoDB,有內(nèi)嵌和飲用兩種方式來進行關聯(lián),下面我們分布看一下它們應用的場景。
內(nèi)嵌
就像上面舉的例子那樣,將關系型數(shù)據(jù)中表的一行內(nèi)嵌到與他相關聯(lián)的表中使之在新的 Collection 中成為一個 Document。這種內(nèi)嵌的方法適用于兩種情況:
- 當表關系是一對一時,或者
- 當表關系是一對多時
在上面兩種關系下,如果關系表不經(jīng)常單獨進行查詢,它只是依附在主表查詢的基礎上進行,那么我們可以考慮使用內(nèi)嵌的方法。以產(chǎn)品和產(chǎn)品價格為例說明一下,在紀錄產(chǎn)品價格時,價格是會隨著時間的變化而取不同的值。一款新上線的產(chǎn)品價格相對較高,隨著時間的推移其價格也會隨之下降。在一些類似雙十一節(jié)假日期間,價格也會臨時調(diào)整。在分析產(chǎn)品銷售狀況的時候,我們還要考慮到在什么樣的價格下產(chǎn)品銷量高,所以不能簡單的把產(chǎn)品和價格放到一張表中,必然會存在一張與產(chǎn)品相關聯(lián)的價格表,它紀錄了產(chǎn)品當前價格以及歷史價格。那么,我們在統(tǒng)計產(chǎn)品的銷量報表時,這張價格表不會單獨存在,它必然會依附在產(chǎn)品表之下。此時,將產(chǎn)品價格內(nèi)嵌到產(chǎn)品表中就是一個比較可行的方案。查詢語句可以通過一個 Collection 找出所有產(chǎn)品相關價格從而避免了表之間的 JOIN 操作。
但是并不是所有的一對一和一對多的關系都適合使用內(nèi)嵌的方式。在一下情況下應當慎重使用內(nèi)嵌數(shù)據(jù)結(jié)構:
- 如果一個 Document 的大小超過了 MongoDB 的限制(16M),此時不應考慮嵌入數(shù)據(jù)結(jié)構。當你的數(shù)據(jù)表關系很復雜,可能將所有相關的數(shù)據(jù)內(nèi)嵌到一個 Document 中會超過 16M 的限制。
- 如果一個 Document 需要經(jīng)常被訪問,而其中的一個內(nèi)嵌 Document 很少被訪問到,這時不太適合使用內(nèi)嵌;因為這會使 MongoDB 在檢索數(shù)據(jù)時增加內(nèi)存的消耗。
- 如果一個 Document 中的一個內(nèi)嵌 Document 需要經(jīng)常修改,或者大小經(jīng)常發(fā)生變化,而另一個內(nèi)嵌 Document 相對靜態(tài),這是也不要考慮使用內(nèi)嵌結(jié)構。
- 由于內(nèi)嵌 Document 的增加和減少會導致整個 Document 大小發(fā)生變化,當變化超過了分配給 Document 的磁盤空間時會導致數(shù)據(jù)庫從新為 Document 分配空間。
引用
除了內(nèi)嵌之外還可以使用引用的方式來關聯(lián)數(shù)據(jù)。引用的方式和關系型數(shù)據(jù)庫表的主外鍵很想。你可以把主表和外鍵表分別存儲成一個 Collection,然后用他們的_id進行關聯(lián),_id是 MongoDB 文檔中一個比較特殊的字段,他會被 MongoDB 自動生成并且唯一存在在一個 Collection 中。但是,在使用引用的時候需要注意一下幾點:
在一些復雜的多對多關系表中,不要嘗試引用,因為這會加大應用程序邏輯上的開發(fā)和維護。
當使用內(nèi)嵌結(jié)構產(chǎn)生過多重復數(shù)據(jù)的時候,可以考慮使用引用。
雖然 MongoDB 不支持 JOIN 操作,但是可以通過 Aggregation 中的$lookup指令來完成連接多表的操作請求。
應用集成
有了數(shù)據(jù)模型的定義,我們就可以開始進行應用集成。集成的方法可以使用 MongoDB 的 Driver,它支持了幾乎常用的各種計算機語言。使用簡單和開發(fā)效率高是 MongoDB 的兩大特點。于 SQL 語句不同的是,MongoDB 采用了 API 的方法提供接口,開發(fā)人員可以選擇支持自己熟悉語言的 Driver,DBA 可以直接使用 Mongo Shell 腳本。幸運的是,MongoDB 提供了 API 和 SQL 語句的對照表供大家參考,SQL to MongoDB Mapping Chart。
另一個強大的功能不能不提的是 Aggregation Framework(聚合)。并不是所有 NoSQL 數(shù)據(jù)庫都支持 Aggregation,簡單理解 Aggregation 可以把它當成是 Hadoop 里面的 Map Reduce,或者 SQL 里面的 Left Join。在沒有 Aggregation 的情況下,開發(fā)人員進行數(shù)據(jù)遷移不得不進行如下操作:
- 在應用程序?qū)娱_發(fā)類似 Aggregation 的功能,將數(shù)據(jù)聚合在一起并寫進數(shù)據(jù)庫。這樣做加大了應用程序的復雜度,并且很難適應各種不同數(shù)據(jù)的組合情況。沒遇到一個新的需求都需要進行一定量的開發(fā)工作。
- 有些人會把數(shù)據(jù)到如今 Hadoop,然后在上面運行 MapReduce 生成結(jié)果,之后將結(jié)果倒進 NoSQL 中。這是一個折中的方法,但是他并不支持實時數(shù)據(jù)遷移,只能進行線下操作。
MongoDB 支持原生 Aggregation 操作,你可以把需要遷移的數(shù)據(jù)進行聚合操作,每一次操作可以想象成一個流水線上的環(huán)節(jié),將所有的操作連接起來可以構成一條 Aggregation Pipeline。在 Pipeline 上面的每一個節(jié)點都有自己的輸入輸出,前一個節(jié)點的輸出是下一個節(jié)點的輸入。有興趣的同學可以在這個連接上找到更多的關于 Aggregation 操作,它列出了每一個 Aggregation 命令和 SQL 語句的對應關系,SQL to Aggregation Mapping Chart
數(shù)據(jù)完整性
在關系型數(shù)據(jù)庫中,有很多支持 ACID 事務操作的方法和應用,DBA 并不希望在數(shù)據(jù)遷移的過程中有任何閃失,例如損失數(shù)據(jù)完整性。MongoDB 在這方面具有不同形式的支持。在 3.0 以上版本中,MongoDB 支持了 WiredTiger Storage Engine,他支持了 Document 級別上的鎖操作。也就是說,在進行數(shù)據(jù)庫寫操作時,MongoDB 可以保證針對一個 Document 操作的原子性,這個操作可以和其他操作完全分隔開來。除了對單個 Dcoument 的原子操作支持外,MongoDB 還支持多 Document 的事務,比如,findAndModify方法允許你在進行多個文檔操作的時保持事務完整。再比如,可以通過Perform Two Phase Commits實現(xiàn)更新多個文檔的原子操作,更多信息請訪問 Perform Two Phase Commits。
數(shù)據(jù)一致性
在數(shù)據(jù)一致性方面,MongoDB 通過 Read Preference 來調(diào)節(jié)一致性的程度。默認情況下,在一個 MongoDB Replica Set 中,所有的數(shù)據(jù)庫讀操作都會發(fā)到 Primary 服務器上,Replica Set 中的所有 Secondary 保證數(shù)據(jù)最終一致性。同時,MongoDB 提供了修改這種一致性的行為方式。數(shù)據(jù)庫管理員可以通過修改 Read Preference 參數(shù)達到對一致性不同要求的場景。數(shù)據(jù)一致性可以有下面集中方案:
- primary: 默認模式,所有請求都會發(fā)送到 Primary 上。
- primaryPreferred:大部分讀請求都會發(fā)送到 Primary,但是當 Primary 無法訪問時,改請求會被轉(zhuǎn)發(fā)到 Secondary 上。
- secondary: 所有請求都會發(fā)送到 Secondary 上。
- secondaryPreferred: 大部分情況下,讀請求被發(fā)送到 Secondary 中,但是如果 Replica 中沒有 Secondary,請求會發(fā)送到 Primary 上。
- nearest: 請求會被發(fā)送到網(wǎng)絡最近的服務器上。該模式在多數(shù)據(jù)中心上非常有效。
數(shù)據(jù)遷移
進行完上面的設計和思考以后,數(shù)據(jù)遷移就會變得想對容易。將數(shù)據(jù)導入進 MongoDB 有幾個不同的選擇,可以使用 mongoimport 將 JSON 數(shù)據(jù)進行導入,也可以通過 ETL(Extract Transform Load) 工具完成。很多項目允許在當前應用程序運行的情況下并行遷移關系型數(shù)據(jù)庫中的數(shù)據(jù),并且支持增量更新,具體操作如下:
- 當一條記錄從關系型數(shù)據(jù)庫讀出后,應用程序會將這條記錄按照先前定義的 JONS 格式插入到 MongoDB 中。
- 一致性檢查,可以通過 MD5 等方法進行數(shù)據(jù)一致性檢查。
- 新的插入操作和數(shù)據(jù)修改操作全部轉(zhuǎn)到 MongoDB 中完成。
小結(jié)
按照本文提供的方法和步驟,項目團隊可以在數(shù)據(jù)遷移中減少不必要的時間和錯誤的操作。當然,數(shù)據(jù)永遠是應用系統(tǒng)中的核心內(nèi)容,任何數(shù)據(jù)遷移都需要支持錯誤恢復,如果失敗也要能夠快速恢復到以前的版本上。在這方面,MongoDB 做到了更靈活的支持,具體內(nèi)容可以參考
MongoDB Webnar。
參考文獻
[Data Modeling]
(https://docs.mongodb.com/manual/core/data-modeling-introduction/)
[SQL to MongoDB Mapping Chart]
(https://docs.mongodb.com/manual/reference/sql-comparison/)
[SQL to Aggregation Mapping Chart]
(https://docs.mongodb.com/manual/reference/sql-aggregation-comparison/)
[WiredTiger Storage Engine]
(https://docs.mongodb.com/manual/core/wiredtiger/)
[Perform Two Phase Commits]
(https://docs.mongodb.com/manual/tutorial/perform-two-phase-commits/)