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