讀古今文學網 > MongoDB實戰 > 3.2 驅動是如何工作的 >

3.2 驅動是如何工作的

現在你一定會對通過驅動或MongoDB Shell發出命令後究竟發生了什麼感到好奇。本節中,我們會掀開「簾子」看看驅動是如何序列化數據並將它傳給數據庫的。

所有的MongoDB驅動都有三個主要功能。首先,生成MongoDB對像ID,這是存儲在所有文檔_id字段裡的默認值。其次,驅動會把所有語言特定的文檔表述和BSON互相轉換,BSON是MongoDB使用的二進制數據格式。前面的例子中,驅動將所有Ruby散列都序列化成了BSON,然後再把數據庫返回的BSON反序列化成Ruby散列。

最後一個功能是使用MongoDB的網絡協議通過TCP套接字與數據庫通信。協議的具體內容超出了我們的討論範圍。但套接字通信的風格很重要,尤其是通過套接字寫入時是否要等待響應,本節中我們會探討這個話題。

3.2.1 對像ID生成

每個MongoDB文檔都要求有一個主鍵,它在每個集合中對於所有文檔必須是唯一的,主鍵存放在文檔的_id字段中。開發者可以隨意使用自定義值作為_id,但如果沒有提供該值,就會使用MongoDB對像ID。在向服務器發送文檔前,驅動會檢查是否提供了_id字段,如果沒有則生成一個適當的對象ID,存儲為_id

因為MongoDB對像ID是全局唯一的標識符,所以可以安全地在客戶端為文檔分配ID,不用擔心會有重複ID。現在,你已經看到過真實的對象ID了,但可能沒有注意到它們是由12個字節構成的。如圖3-1所示,這些字節是有特定結構的。

圖3-1 MongoDB對像ID格式

最開頭的4字節是標準的Unix時間戳,編碼了從新紀元開始的秒數。接下來的3字節存儲了機器ID,隨後則是2字節的進程ID。最後3字節存儲了進程局部的計數器,每次生成對像ID計數器都會加1。

使用MongoDB對像ID帶來的好處之一是其中包含了時間戳。大多數驅動都允許方便地提取時間戳,從而提供文檔的創建時間,精度是最接近的一秒鐘。使用Ruby驅動,可以調用對像ID的generation_time方法來獲得ID的創建時間,返回值是Ruby的Time對像:

irb(main):002:0> id = BSON::ObjectId.new
=> BSON::ObjectId(\'4c41e78f238d3b9090000001\'
irb(main):003:0> id.generation_time
=> Sat Jul 17 17:25:35 UTC 2010
  

很自然的,我們還可以使用對像ID根據對象的創建時間進行範圍查詢。舉個例子,如果希望查詢所有在2010年10月至2010年11月之間創建的文檔,可以創建兩個對像ID,將它們的時間戳分別編碼為那兩個時間,然後對_id發起範圍查詢。Ruby提供了從任意Time對像創建對像ID的方法,因此實現這一功能的代碼很簡單:

oct_id = BSON::ObjectId.from_time(Time.utc(2010, 10, 1))
nov_id = BSON::ObjectId.from_time(Time.utc(2010, 11, 1))

@users.find({\'_id\' => {\'$gte\' => oct_id, \'$lt\' => nov_id}})
  

我已經解釋了MongoDB對像ID的基本原理及各個字節背後的含義。剩下的就是瞭解它們是如何編碼的,這是下一節的主題,屆時我們還將討論BSON。

3.2.2 BSON

BSON是MongoDB中用來表示文檔的二進制格式,它既是存儲格式,也是命令格式:所有文檔都以BSON格式存儲在磁盤上,所有查詢和命令都用BSON文檔來指定。因此,所有的MongoDB驅動必須能在語言特定的文檔表述和BSON之間進行轉換。

BSON定義了能在MongoDB中使用的數據類型。知道BSON包含哪些類型,瞭解它們的編碼,這對有效使用MongoDB以及發生性能問題時的診斷都大有好處。

在本書編寫時,BSON規範中包含了19種數據類型。這就是說,文檔中的每個值為了能存儲在MongoDB裡,必須要能轉換為這19種類型中的一種。BSON類型包含了很多我們所期待的類型:UTF-8字符串、32位和64位整數、雙精度浮點數、布爾值、時間戳和UTC 日期時間(datetime)。但是,還有一部分類型是特定於MongoDB的。舉例來說,上一節中描述的對象ID格式就有自己的類型;有針對模糊大字段(opaque blob)的二進制類型;如果語言支持的話,MongoDB裡甚至還提供了符號類型(symbol type)。

圖3-2描述了如何將一個Ruby散列序列化為正確的BSON文檔。Ruby文檔中包含一個對像ID和一個字符串。在轉換為BSON文檔後,頭部的4字節表明了文檔的大小(可以看到此處是38字節)。接下來是兩個鍵值對,每對都由一個表示其類型的字節開頭,隨後是由null結尾的字符串表示鍵名,然後是被存儲的值,最後是一個 null字節表示文檔結束。

圖3-2 從Ruby轉換為BSON

雖然不一定要知道BSON的詳情,但經驗表明瞭解BSON對MongoDB開發者是有好處的。舉個例子,將對像ID表示成字符串或者BSON對像ID這兩種做法都是正確的。因此,以下兩個Shell查詢並不等價:

db.users.find({_id : ObjectId(\'4c41e78f238d3b9090000001\')});
db.users.find({_id : \'4c41e78f238d3b9090000001\'})
  

其中只有一個查詢能匹配_id字段,這完全取決於users集合中的文檔存儲的是BSON對像ID,還是表示ID十六進制值的BSON字符串。1這個例子說明即使只對BSON略知一二,在診斷簡單代碼問題時都很有幫助。

1. 順便說一下,如果要保存MongoDB對像ID,應該使用BSON對像ID,而不是字符串。除了遵循對像ID的存儲慣例,BSON對像ID還能比字符串節省一半以上的空間。

3.2.3 網絡傳輸

除了創建對像ID以及序列化到BSON,MongoDB驅動還有一項核心功能:與數據庫服務器通信。如前文所述,通信是基於TCP套接字的,使用了自定義網絡協議。2這個TCP的工作是相當底層的,大多數應用程序開發者對此也並不關心。此處與開發者相關的是要理解驅動何時會等待服務器的響應,何時又能不必等待響應。

2. 一些驅動還支持Unix 域套接字通信

我已經解釋過查詢是如何工作的,很顯然,查詢必須要有一個響應。回顧一下,當游標對象的next方法被調用後即會發起一次查詢。這時會把查詢發給服務器,其響應是一批文檔。如果這批文檔能滿足查詢,則不必再和服務器進行通信。但如果查詢結果較多,恰好無法全部放進第一個服務器響應中,將會向服務器發送一個所謂的getmore指令獲取下一批查詢結果。隨著游標的迭代,在查詢結束前會連續不斷地調用getmore方法。

上述查詢的網絡行為並沒有什麼好讓人驚訝的,但說到數據庫寫操作(插入、更新及刪除),默認的行為看起來就不怎麼正統了。這是因為在向服務器寫數據時,驅動默認不會等待服務器的響應。因此在插入文檔時,驅動會向套接字寫數據並假設寫入是成功的。讓這種做法能成為現實的一種策略就是客戶端生成對像ID:既然已經有了文檔的主鍵,就沒有必要等待服務器返回該主鍵了。

這種不關心結果的寫策略讓很多用戶如坐針氈;幸運的是,該行為是可配置的。所有的驅動都實現了一個安全寫入模式,對所有的寫操作(插入、更新及刪除)都能開啟該模式。在Ruby中,能像這樣發起一次安全插入:

@users.insert({\"last_name\" => \"james\"}, :safe => true)
  

以安全模式寫入時,驅動會在插入消息後追加一條特殊的getlasterror命令。它將做兩件事。第一,getlasterror是一條命令,因此需要和服務器做一次通信,這保證了寫操作已經送達服務器。第二,該命令驗證了服務器在當前連接中沒有拋出任何錯誤。如果有錯誤被拋出,驅動會發出一個異常,這一異常能被優雅地處理。我們可以使用安全模式來保證應用程序的關鍵寫操作到達服務器,也可以在期待顯式錯誤時使用安全模式。舉例來說,經常要強調值的唯一性。如果正在保存用戶數據,我們會維護一個username字段的唯一性索引。在有重複username時,該唯一性索引會造成文檔插入失敗,但要知道插入失敗的唯一途徑就是使用安全模式。

大多數情況中,慎重的做法是默認開啟安全模式。隨後,針對一些寫入少但要求高吞吐量的應用程序部分可以選擇關閉安全模式。要做這種權衡並不容易,還有更多安全模式選項要考慮。在第8章中我們將就此進行更詳細的討論。

目前為止,你瞭解了驅動是如何工作的,應該感到更舒服了,也許還迫不及待地想要構建一個真實的應用程序。在下一節裡,我們會結合所有的知識,使用Ruby驅動來構建一個基本的Twitter監控應用。