讀古今文學網 > MongoDB實戰 > 1.2 MongoDB的主要特性 >

1.2 MongoDB的主要特性

數據庫在很大程度上是由其數據模型來定義的。本節中,我們將瞭解文檔數據模型和MongoDB的特性,這些特性讓我們能有效地操作文檔數據模型。我們還會看到與運維相關的內容,重點介紹MongoDB的複製和水平伸縮策略。

1.2.1 文檔數據模型

MongoDB的數據模型是面向文檔的。如果你不熟悉數據庫中文檔的概念,那我們最好先看一個例子。

代碼清單1-1 表示社交新聞網站中一個條目的文檔

代碼清單1-1是一個示例文檔,表示社交新聞網站(比如Digg)上的一篇文章。如你所見,文檔基本上是一組屬性名和屬性值的集合。屬性的值可以是簡單的數據類型,例如字符串、數字和日期。但這些值也可以是數組,甚至是其他文檔➋,這讓文檔可以表示各種富數據結構。在示例文檔中有一個屬性tags➊,其中用數組的形式保存了文章的標籤。更有趣的是comments屬性➌,它是一個評論文檔的數組。

讓我們花點時間把它和標準關係型數據庫中相同數據的表述對比一下。圖1-1是一個對應的關係型數據庫的表述。既然數據表本質上來說是扁平的,那麼要表示多個一對多關係就需要多張表。先從包含每篇文章核心信息的posts表開始,然後創建三張其他的表,每個表都包含一個post_id字段指向原始的文章。這種將對象的數據拆分到多張表裡的技術稱為正規化(normalization)。排除其他因素,正規化的數據集可以保證每個數據單元僅出現在一個地方。

圖1-1 表示社交新聞網站中一個條目的基本關係數據模型

但嚴格的正規化是有代價的,特別是需要一些裝配工作。為了顯示我們剛剛提到的文章,需要在poststags表之間執行聯結操作。還需要單獨查詢評論,或者也把它們放在一個join語句裡。最終,是否需要嚴格正規化要取決於所建模的數據的類型,在第4章我會更深入地討論這個問題。這裡重點說一下,面向文檔的數據模型很容易以聚合的形式來表示數據,讓你能徹底和對像打交道:所有用來表示一篇文章的數據,從評論到標籤,都能放進一個單獨的數據庫對像裡。

你可能已經注意到了,除了提供豐富的結構,文檔無需預先定義Schema。在關係型數據庫中存儲的是數據表中的行,每張表都有嚴格定義的Schema,規定了列和類型。如果表中的某一行需要一個額外的字段,那麼就不得不顯式地修改表結構。MongoDB把文檔組織成集合,這種容器無需任何類型的Schema。理論上,集合中的每個文檔都能擁有完全不同的結構。在實踐中,一個集合裡的文檔相對統一,舉例來說,文章集合裡的文檔都有表示標題、標籤、評論等內容的字段。

這種做法帶來了一定的優勢。首先,是應用程序,而非數據庫在保證數據結構。在Schema頻繁變化的初期開發階段,這能提升應用程序的開發效率。其次,更重要的是無Schema的模型允許用真正的可變屬性來表示數據。舉例來說,假設正在構建一個電子商務產品編目,沒辦法事先知道產品會有什麼屬性,因此應用程序需要處理這種可變性。在固定Schema的數據庫中,傳統的解決方案是使用實體—屬性—值模式(entity-attribute-value pattern1),如圖1-2所示。你所看到的內容選自Magento的數據模型,這是一個開源的電子商務框架。請注意,這些數據表基本上是一樣的,value字段除外,該字段僅根據數據類型變化。該結構允許管理員定義附加的產品類型和屬性,但卻帶來了很大的複雜性。試想打開MySQL Shell檢查或更新一個用這種方式建模的產品,用於裝配該產品的聯結語句是何等複雜。以文檔的方式建模,就不用做聯結,還可以動態地添加新屬性。

1. 參見http://en.wikipedia.org/wiki/Entity-attribute-value_model。

圖1-2 PHP電子商務項目Magento的部分Schema,其中這些表用來輔助動態創建產品屬性

1.2.2 即時查詢

說一個系統支持即時查詢(ad hoc query)的意思就是無需預先定義系統接受的查詢類型。關係型數據庫有這個能力,它們會嚴格遵照指示執行任何完備的SQL查詢,無論有多少條件。如果你僅使用過關係型數據庫,那麼會認為即時查詢是理所應當的。但是,並非所有的數據庫都支持動態查詢。舉例來說,鍵值存儲只能按一個維度來查詢——鍵。和很多其他系統一樣,鍵值存儲犧牲了豐富的查詢能力來換取一個簡單的可伸縮模型。關係型數據庫世界中,查詢能力是再基礎不過的事情,MongoDB的設計目標之一就是盡可能保留這種能力。

要瞭解MongoDB的查詢語句如何工作,讓我們先來看一個簡單的例子,它涉及文章和評論。假設想要找到所有帶politics標籤、投票數大於10的文章,SQL查詢大概會是這樣的:

SELECT * FROM posts
  INNER JOIN posts_tags ON posts.id = posts_tags.post_id
  INNER JOIN tags ON posts_tags.tag_id == tags.id
  WHERE tags.text = \'politics\' AND posts.vote_count > 10;
  

MongoDB中的等效查詢是用文檔來做匹配的,特殊的$gt鍵表示「大於」:

db.posts.find({\'tags\': \'politics\', \'vote_count\': {\'$gt\': 10}});
  

請注意,這兩個查詢採用了不同的數據模型。SQL查詢依賴於嚴格正規化的模型,其中文章和標籤保存在不同的數據表中,而MongoDB的查詢假定標籤是存儲在每個文章的文檔中。兩者都演示了對任意屬性組合執行查詢的能力,這是即時查詢的本質。

正如之前提到的,一些數據庫的數據模型過於簡單,因此不支持即時查詢。舉例來說,你只能根據主鍵在鍵值存儲中進行查詢。對於查詢而言,它並不知道這些鍵所對應的值。要根據第二屬性進行查詢,比如本例中的投票數,唯一的方法是自己寫代碼來構造條目,其中主鍵是指定的投票數,值是一個文檔主鍵的列表,文檔裡包含了鍵中所指定的投票數。如果你在鍵值存儲中使用了這種方法,那麼一定會為此而深感愧疚,雖然這種做法在數據集較小時能管用,把多個索引塞進物理結構是單索引的存儲中,這並不是一個好主意。而且,鍵值存儲中基於散列的索引不支持範圍查詢,而在查詢類似投票數這樣的東西時,範圍查詢可能是必不可少的。

如果你之前是使用關係型數據庫系統的,視即時查詢為常態,那麼應該會發現MongoDB提供了類似的查詢能力。如果正在評估多種不同的數據庫技術,請牢記不是所有的數據庫都支持即時查詢,要是你的確需要這種能力,MongoDB會是一個不錯的選擇。但光有即時查詢是不夠的,一旦數據集膨脹到一定程度,出於查詢效率就必須使用索引。適當的索引能把查詢和排序的速度提升一個數量級,所以支持即時查詢的系統還應該要支持二級索引。

1.2.3 二級索引

理解數據庫索引的最佳方法就是類比:很多書都有索引,把關鍵字和頁碼對應起來。假設你有一本菜譜,想要找到其中要用梨的菜(也許你有很多梨,不想它們壞掉)。最花時間的做法是一頁頁找過去,看每道菜的配料。大多數人都喜歡查書的索引,從中找到梨那一項,其中會指出所有包含梨的菜。數據庫索引就是提供類似服務的數據結構。

MongoDB中的二級索引是用B樹(B-tree)實現的,B樹索引也是大多數關係型數據庫的默認索引,針對多種查詢做了優化,包括範圍掃瞄和帶排序子句的查詢。通過允許使用多個二級索引,MongoDB讓用戶能對大量不同的查詢進行優化。

在MongoDB裡,每個集合最多可以創建64個索引。它支持能在RDBMS中找到的各種索引,升序、降序、唯一性、復合鍵索引,甚至地理空間索引都被支持。因為MongoDB和大多數RDBMS使用相同的索引數據結構,這些系統中有關管理索引的建議都是通用的。下一章裡我們會開始介紹索引,因為瞭解索引對高效操作數據庫至關重要,所以我會用整個第7章來討論這個話題。

1.2.4 複製

MongoDB通過稱為副本集(replica set)的拓撲結構提供了複製功能。副本集將數據分佈在多台機器上以實現冗余,在服務器和網絡故障時能提供自動故障轉移。除此之外,複製功能還能用於擴展數據庫的讀能力。如果有一個讀密集型的應用程序(Web上很常見),可以把數據庫讀操作分散到副本集集群中的各台機器上。

副本集由一個主節點(primary node)和一個或多個從節點(secondary node)構成。與你所熟悉的其他數據庫中的主從複製(master-slave replication)類似,副本集的主節點既能接受讀操作又能接受寫操作,但從節點是只讀的。讓副本集與眾不同的是它能支持自動故障轉移:如果主節點出了問題,集群會選一個從節點自動將它提升為主節點。在先前的主節點恢復之後,它就會變成一個從節點。圖1-3描述了這個過程。

圖1-3 副本集的自動故障轉移

我會在第8章裡詳細討論複製。

1.2.5 速度和持久性

要理解MongoDB實現持久性的方法,需要先理解一些思想。在數據庫系統領域內,寫速度和持久性存在一種相反的關係。寫速度可以理解為在給定時間內數據庫可以處理的插入、更新和刪除操作的數量。持久性則是指數據庫保持這些寫操作結果不變的時間長短。

舉例來說,假設要向數據庫寫100條50 KB的記錄,隨後立即切斷服務器的電源。機器重啟後這些記錄能恢復麼?答案是——有可能,這取決於數據庫系統和托管它的硬件。問題是寫磁盤的速度要比寫內存慢幾個數量級。某些數據庫,例如memcached,只寫內存,這讓它們速度很快,但數據完全易失。另一方面,幾乎沒有數據庫只寫磁盤,因為這樣的操作性能過低,無法接受。因此,數據庫設計者經常需要在速度和持久性中做出權衡,以平衡兩者的關係。

在MongoDB中,用戶可以選擇寫入語義,決定是否開啟Journaling日誌記錄,通過這種方式來控制速度和持久性間的平衡。默認所有的寫操作都是fire-and-forget2的,即寫操作通過TCP套接字發送,不要求數據庫應答。如果用戶需要獲得應答,可以使用特殊的安全模式發起寫操作,所有驅動都提供這個安全模式。該模式強制數據庫作出應答,確保數據庫正確無誤地接收到了寫操作。安全模式是可配置的,還可用於阻塞操作,直到寫操作被複製到特定數量的服務器。對於高容量、低價值的數據(例如點擊流和日誌),fire-and-forget風格的寫操作是很理想的選擇。對於重要的數據,則更傾向於安全模式。

2. 維基百科中解釋為「射後不理」,源自軍事領域,泛指武器發射後無需外界干涉就能自己更新目標或自己坐標的能力。——譯者注

在MongoDB 2.0中,Journaling日誌是默認開啟的。有了這個功能,所有寫操作都會被提交到一個只能追加的日誌裡。即使服務器非正常關閉(比方說電源故障),該日誌也能保證在重啟服務器後MongoDB的數據文件被恢復到一致的狀態。這是運行MongoDB最安全的方式。

事務日誌

MySQL的InnoDB中有一個關於速度和持久性的折中。InnoDB是事務性存儲引擎,根據定義,必須保證持久性。它通過向兩個地方寫入更新來實現這一目標:先寫事務日誌,再寫內存緩衝池。事務日誌會立刻同步到磁盤,而緩衝池則只會由後台線程最終同步。採取這種雙重寫入的原因是一般來講隨機I/O要比順序I/O慢得多。因為向主數據文件的寫操作構成隨機I/O,所以先寫內存會更快,可以後面再同步到磁盤上。但有些寫操作(至磁盤)要保證持久性,保證寫入是連續的這一點很重要,這就是事務日誌的功能。在非正常關閉時,InnoDB能回放事務日誌,並依此來更新主數據文件。這種做法在保證高持久性的同時也提供了能接受的性能。

可以在不記日誌的情況下運行服務器,這樣能提升寫入的性能,但在服務器意外關閉後可能會損壞數據文件。其結果就是那些想要關閉Journaling日誌功能的人必須使用複製功能,最好還能將數據複製到另一個數據中心,以此來增加失敗時還能找回原始數據副本的可能性。

複製和持久性是一個很大的話題,第8章會詳細展開討論的。

1.2.6 數據庫擴展

對大多數數據庫而言,最簡單的擴展方法就是升級硬件。如果應用程序運行在單個節點上,增加磁盤IOPS(Input/Output Operations Per Second,每秒輸入輸出操作)、內存和CPU通常都可以暫時消除數據庫的性能瓶頸。提升單一節點的硬件來進行擴展稱為垂直擴展或向上擴展。垂直擴展的優勢在於簡單、可靠,某種程度上而言還是比較划算的。如果你正在使用虛擬化硬件(比如亞馬遜的EC2)上,可能會找不到足夠大的實例。如果正在使用物理硬件,終會有一天,更強大的服務器的成本會讓你望而卻步。

這時就該考慮水平擴展或向外擴展了。水平擴展不是提升單一節點的性能,而是將數據庫分佈到多台機器上。因為水平擴展架構可以使用普通硬件,所以托管整個數據集的成本會顯著降低。而且,將數據分佈在多台服務器上可以降低故障帶來的影響。有時機器的故障是難以避免的,如果採用的是垂直擴展,在機器發生故障時,你需要處理的就是自己大多數系統所依賴的那台服務器的故障。如果在複製的從服務器上有一份數據副本,問題還不算嚴重,但在單機故障仍需暫停整個系統時,這依然很棘手。水平擴展架構中的故障與之形成鮮明對比,單節點故障不會帶來災難性影響,因為從整體上看,它只代表了很小一部分數據。圖1-4對比了水平擴展和垂直擴展。

圖1-4 水平擴展與垂直擴展

MongoDB的水平擴展非常易於管理,它通過基於範圍的分區機制,即 自動分片(auto-sharding)來實現這一設計目標,自動分片機制會自動管理各個節點之間的數據分佈。分片系統會處理分片節點的增加,幫助進行自動故障轉移。單獨的分片由一個副本集組成,其中包含至少兩個節點3,保證能夠自動恢復,沒有單點失敗。綜上所述,完全不需要編寫應用程序代碼來處理這些事情,應用程序的代碼只要像和單個節點通信一樣來訪問分片集群就可以了。

3. 技術上來看,每個副本集都至少有三個節點,但其中只有兩個需要攜帶數據副本。

我們已經講到了MongoDB中大多數的重要特性,第2章將介紹其中一些特性在實踐中是如何應用的。但此時此刻,讓我們從更實用的角度來看看數據庫。MongoDB的核心服務器自帶了一套工具,下一節我們將介紹怎麼使用這些工具以及一些輸入輸出數據的方式。