1. 程式人生 > 其它 >【翻譯】MongoDB指南/CRUD操作(三)

【翻譯】MongoDB指南/CRUD操作(三)

【原文地址】https://docs.mongodb.com/manual/

CRUD操作(三)

主要內容:

原子性和事務(Atomicity and Transactions),讀隔離、一致性和新近性,分散式查詢(Distributed Queries),分散式寫操作,模擬兩階段任務提交,在副本集中執行配額讀取

1 原子性和事務(Atomicity and Transactions)

在MongoDB中,寫操作在單文件級別具有原子性,即使修改一個文件中的多個嵌入式文件也是如此。

當一個寫操作修改多個文件時,對每一個文件的修改都是原子的,但是整體操作卻不是原子的,並且其他的操作可能是交替進行的。然而,使用

$isolated 操作符可以隔離影響多個文件的寫操作。

$isolated操作符

使用$isolated操作符,一旦一個操作多個文件的寫操作修改了第一個文件,交替地修改多個文件的行為將被阻止。這確保直到寫操作完成或者有錯誤丟擲時,客戶端才會看到變化。

$isolated不能用於分片叢集。

一個隔離寫操作不能提供“要麼全有要麼全無的”的原子性。這是因為,寫操作執行過程中產生錯誤時不能回滾到錯誤之前的狀態。

注:

$isolated 操作符使寫操作獲得一個集合的排他鎖,即使對於文件級鎖儲存引擎WiredTiger也是如此。因為,$isolated 操作符會使WiredTiger 在執行操作期間以單執行緒的方式執行。

$isolated不能用於分片叢集。

例如更新操作,刪除操作都可使用$isolated操作符。

同事務語義

因為一個文件可以包含多個嵌入式文件,單文件原子性可滿足許多實際用例。對於那些執行一系列操作的情況,可在你的應用程式中實施兩階段任務提交策略(two-phase commit)。

然而,兩階段任務提交策略(two-phase commit)僅是對事務的模擬。使用兩階段任務提交策略(two-phase commit)可以確保資料一致性,但對於應用程式來說,可能返回兩階段事務提交或回滾執行過程中的資料。

併發控制

併發控制機制可保證多個應用程式並行執行時不會引起資料不一致或存在衝突。

一種方法是在具有唯一值的欄位上建立唯一索引。這樣可防止插入操作或更新操作產生重複資料。在多個欄位上建立唯一索引時,強制要求多個欄位值的組合具有唯一性。

另一種方法是,對於寫操作來說,在查詢謂詞中指定一個欄位期望的當前值。兩階段任務提交模式提供一個變異的版本:在寫操作中,查詢謂詞包含應用標識以及資料的期望狀態。

2 讀隔離、一致性和新近性

2.1隔離保障

未提交讀

在MongoDB中,客戶端可以看到資料持久化之前的寫入結果。

  • 如果沒有寫關注,某一客戶端執行寫操作確認之前,其他的客戶端使用"local" (預設情況下) readConcern的時能夠看到寫操作的結果。
  • 使用"local" (預設情況下) readConcern的客戶端能夠讀取稍候可能會回滾的資料。

未提交讀是預設的隔離級別並被應用於獨立的mongod 例項以及副本集和分片叢集。

未提交讀和單文件原子性

寫操作具有單文件級別原子性;例如,一個寫操作更新一個文件中的多個欄位,不會發生只更新了其中某些欄位的情況。

對於一個獨立的mongod 例項,對一個文件的一系列讀寫操作是序列化的。對於一個副本集,僅在資料沒有回滾的情況下對一個文件的一系列讀寫操作是序列化的。

然而,儘管讀者不會看到部分更新的文件,未確認讀意味著在變化持續階段執行併發訪問的讀者可能會看到已更新的那部分文件。

未確認讀和多文件寫操作

當一個寫操作修改多個文件時,對每個文件的修改都是原子的,但整個操作不是原子的並且對每個文件的寫操作可能交替執行。但是可以使用

$isolated操作符隔離影響多個文件的一個寫操作。

如果沒有隔離多文件寫操作,MongoDB 會表現出如下行為:

1.非時間點讀操作。假設讀操作起始於時刻t1 並且開始讀取多個文件。然後在隨後的t2時刻寫操作更新了其中一個文件。讀者可能會看到那個文件的新版本,並且因此不會看到某一時間點的資料快照。

2.非序列化操作。假設在t1 時刻讀操作讀取文件d1 並且在隨後的t3時刻寫操作更新了d1。這產生了讀寫依賴:如果操作不是序列化的,讀操作必須先於寫操作被執行。但同樣假設寫操作在 t2 時刻更新文件d2並且讀操作在隨後的t4時刻讀取d2 ,這引入了寫讀依賴,寫讀依賴要求寫操作先於讀操作以序列化的方式執行。存在迴圈依賴關係使得序列化不可得。

3.讀操作匹配到某一文件,讀取的同時此文件被更新,這時讀操作可能會漏掉此文件。

使用$isolated操作符,一旦寫操作修改了第一個文件,影響多個文件的寫操作能夠阻止操作交替進行。這能夠保證沒有客戶端會看到變化直到寫操作完成或者有錯誤丟擲。

$isolated操作符不能用於分片叢集。

一個隔離的寫操作不能提供“要麼全有要麼全無的”的原子性。這是因為,寫操作執行過程中產生錯誤時不能回滾到錯誤之前的狀態。

$isolated操作符使得寫操作獲得集合上的排他鎖,即使對於文件級鎖儲存引擎WiredTiger也是如此。因為,$isolated 操作符會使WiredTiger 在執行操作期間以單執行緒的方式執行。

遊標快照

某些情況下,MongoDB 遊標不止一次地返回同一文件。當遊標返回一些文件時,伴隨著查詢操作的其他操作可能交替進行。如果上述操作中的某些操作是使文件移動的更新操作(例如使用MMAPv1儲存引擎,文件增大時)或者改變了所查詢欄位的索引,遊標會返回相同文件不止一次。

在非常特殊的情況下,你可以使用cursor.snapshot() 方法阻止遊標多次返回同一文件。snapshot()確保查詢返回每個文件最多一次。

警告:

  • snapshot() 不能保證查詢返回的資料對應一瞬間的資料,也不能保證將刪除和插入操作隔離。
  • snapshot() 不能用於分片集合。
  • snapshot() 不能和遊標方法sort() or hint()一起使用。

一個替代的解決方案是,如果你的集合中有一個或多個欄位從不被修改,你可以在這個欄位或這些欄位上建立唯一索引,達到和snapshot()同樣的效果。查詢操作使用hint() 以明確強制查詢使用哪些索引。

2.2 一致性保障

單調讀

MongoDB 提供了從獨立的mongod 例項單調讀取的功能。假設一個應用執行一系列的操作,這些操作中包含了讀操作R1 ,緊跟 R1 後面的操作是讀操作 R2。如果應用在獨立的mongod 例項上執行這一系列操作,那麼 R2的返回結果所反應的狀態不會比R1 早。例如R2返回的資料多於R1 所返回的資料。

3.2版本中的變化:對於副本集和分片叢集,如果讀操作指定 Read Concern為"majority" 並且優先讀主成員,那麼MongoDB 提供單調讀操作。

在以前的版本中MongoDB 不能保證單調讀副本集和分片叢集。

單調寫

對於mongod 例項,副本集和分片叢集,MongoDB 提供單調寫。

假設一個應用執行一系列的操作,這些操作中包含了寫操作W1 ,緊跟 W1 後面的操作是寫操作 W2。MongoDB 保證W1 在W2之前執行。

2.3 新近性

在MongoDB中,一個副本集有一個主成員[1]。

  • readConcern為"local",在不發生故障轉移的情況下,從主成員讀取的資料為最近寫入的資料。
  • readConcern為"majority",讀主成員或第二成員的操作具有最終一致性。

註釋[1]:

在某些情況下,副本集中的兩個成員可能被持續片刻地認為都是主成員。但至多他們中的一個會執行{ w: "majority" } 的寫操作。可以完成

{ w: "majority" }寫操作的成員是當前的主成員,另一個成員是之前的主成員,只不過它還沒有意識到它已被降級,例如由於網路引起的。這種情況發生時,儘管已經請求優先讀取主成員資料,但連線之前主成員的客戶端可能看到的是舊的資料,並且對於之前的主成員的新的寫操作最終會回滾。

3 分散式查詢(Distributed Queries)

分片叢集的讀操作

分片叢集允許以一種對應用幾乎透明的方式將資料分佈到mongod 例項叢集中。

對於分片叢集,應用發出對叢集中的一個mongod 例項的操作。

當定位到分片叢集中一個指定的分片時,讀操作是最高效。查詢分片集合應該包含集合的片鍵。當查詢包含片鍵時,mongos 能夠使用

config database中的叢集元資料路由到片鍵。

如果一個查詢不包含片鍵,mongos 必須查詢所有的分片。這種分散聚集查詢是低效的。在一個巨大的叢集上,對於常規操作來講,分散聚集查詢是不可行的。

對於副本集分片,查詢副本集的第二分片可能不會反映主成員的當前狀態。讀優先設定指定讀不同伺服器,這可能會導致非單調的讀。

副本集的讀操作

預設地,客戶端讀副本集的主成員。客戶端能夠設定讀優先配置(read preference)來指定讀其他的副本整合員。例如,客戶端能夠配置讀優先

(read preference)以讀取第二成員或者最近的成員:

  • 減少多資料中心排程延遲。
  • 通過分散高讀取量來改進查詢吞吐量。
  • 執行備份操作。
  • 允許讀操作直到新的主成員被選出。

查詢副本集的第二分片可能不會反映主成員的當前狀態。讀優先設定指定讀不同伺服器,這可能會導致非單調的讀。

4 分散式寫操作

分片叢集上的寫操作

對於分片叢集上的分片集合,mongos 指定來自應用的寫操作給分片,這些分片上儲存了資料集的指定部分。mongos 使用來自

config database 的叢集元資料將寫操作路由到適當的分片上。

一個分片集合上的分割槽資料分佈範圍取決於分片鍵值。MongoDB 將這些塊分佈到片上。片鍵決定了塊的分佈。這會影響叢集寫操作的效能。

重點:

作用於一個文件的更新操作必須包含片鍵或_id欄位。如果使用片鍵,作用於多個文件的更新操作在某些情況下更高效,但這種操作會廣播到所有分片。

如果每次執行插入操作片鍵的值會增加或者減小,那麼所有的插入操作都是針對同一個分片。結果,一個分片的容量限制就成了整個分片叢集的容量限制。

副本集的寫操作

在副本集中,所有的寫操作都是針對主成員的。主成員用於寫操作並且在主成員操作日誌或oplog中記錄操作行為。oplog是一系列可重用資料集操作。第二成員不斷地複製oplog 並以非同步的方式執行那些操作。

5 模擬兩階段任務提交

5.1簡介

本文提供了一種使用兩階段任務提交方法完成多文件更新或執行多文件事務的方式。另外你可以擴充套件這個處理過程來模擬回滾功能。

5.2背景

對於MongoDB來說,單文件操作總是具有原子性的。對多文件操作不具有原子性,這種操作常常涉及到多文件事務。因為文件結構可以比較複雜並且可以包含巢狀的文件,所以對許多實際的用例來講,單文件原子性提供了足夠的支援。

儘管單文件原子性足夠有力,還是有一些用例需要多文件事務。當執行有多個操作構成的事務時,問題便顯現出來:

  • 原子性:如果一個操作失敗,事務中的之前的操作必須回滾到以前的狀態(例如,要麼全做要麼全不做)。
  • 一致性:如果一個錯誤中斷了事務,那麼資料庫必須使所有資料保持一致的狀態。

對於需要多文件事務的情形,可以在你的應用中實現兩階段任務提交以支援這種需要多文件更新的情形。使用兩階段任務提交確保資料一致性,並且一旦發生錯誤,會回滾到之前的狀態。然而,在處理的過程中,文件能夠表示待定的資料和狀態。

注:

MongoDB中僅對單文件操作具有原子性,兩階段任務提交僅模擬了事務。在兩階段任務提交或回滾的過程中,應用能夠返回中間事務。

5.3模式

概述

假設你要將A賬戶中的資金轉入B賬戶。在關係資料庫系統中,你可以使用多語句事務減去A賬戶的資金加到B賬戶上。在MongoDB中,你可以模仿兩階段任務提交模式來達到相當的效果。

本例中使用下面兩個集合:

accounts 集合儲存賬戶資訊。

transactions 集合儲存資金轉移資訊。

初始化資料來源和目標賬戶

向accounts 集合中插入一個文件表示賬戶A和另一個文件表示賬戶B。

db.accounts.insert(
   [
     { _id: "A", balance: 1000, pendingTransactions: [] },
     { _id: "B", balance: 1000, pendingTransactions: [] }
   ]
)

操作返回含有操作執行狀態的BulkWriteResult() 物件。當成功插入時,BulkWriteResult()中的nInserted 被設定為2。

初始化轉移記錄

對於每次資金轉移,將含有轉移資訊的文件插入transactions 集合中。文件包含下面的欄位:

  • source 和destination,他們引用了accounts 集合_id欄位值。
  • value 欄位,指定了從原賬戶到目標賬戶轉移的金額。
  • state 欄位,反映了金額轉移的狀態。state 的值為initial, pending, applied, done, canceling, 和canceled。
  • lastModified 欄位反映了資料最後被修改的日期。

初始化transactions集合,將賬戶A轉移到B的金額設定為100,state 設定為“initial”,lastModified 欄位值設定為當前日期,向集合中插入文件:

db.transactions.insert(

    { _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() })

返回結果為WriteResult()物件,nInserted 值為1。

使用兩階段任務提交模式執行轉賬

1 )檢索transaction文件

從transactions 集合中找到state值為“initial ”的一個事務文件。當前的transactions集合僅有一個文件,即在初始化轉移記錄那步中新增的文件。如果集合中包含了額外的文件,那麼除非使用額外檢索條件才會返回state為initial的事物文件。

var t = db.transactions.findOne( { state: "initial" } )

在mongo shell中變數t的內容將會被列印輸出。除了時間為你執行插入操作的時間外,打印出來的文件應該和下面的文件類似:

{ "_id" : 1, "source" : "A", "destination" : "B", "value" : 100, "state" : "initial", "lastModified" : ISODate("2014-07-11T20:39:26.345Z") }

2 )更新事務狀態為pending

將事務狀態由initial 改為pending 並且$currentDate 操作符將lastModified 欄位值設定為當前時間。

db.transactions.update(
    { _id: t._id, state: "initial" },
    {
      $set: { state: "pending" },
      $currentDate: { lastModified: true }
}
)

操作返回WriteResult() 物件,更新成功後,nMatched 和nModified 被設定為1。

在更新宣告中,state:欄位值為"initial" 能夠確保沒有其他的操作對記錄進行修改過。如果nMatched 和nModified返回值為0的話,返回第一步,找到一個不同的事務文件並且從新開始此過程。

3)將事務用於兩個賬戶

如果事務還沒有用於兩個賬戶,那麼使用update() 方法將事務t應用於兩個賬戶。在更新條件中,為了避免此步驟執行多次而引起重複應用事務,更新條件包括pendingTransactions: { $ne: t._id }。

為了將事務用於兩個賬戶,同時更新欄位balance 和pendingTransactions 。

更新源賬戶,從賬戶中減去事務文件中value欄位值,並將事務文件的_id插入自身陣列pendingTransactions 中。

db.accounts.update(
   { _id: t.source, pendingTransactions: { $ne: t._id } },
   { $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } })

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

更新目標賬戶,將事務文件value欄位值加到賬戶中並且將事務文件的_id插入自身陣列pendingTransactions 中。

db.accounts.update(
   { _id: t.destination, pendingTransactions: { $ne: t._id } },
   { $inc: { balance: t.value }, $push: { pendingTransactions: t._id } })

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

4 )更新事務文件state欄位值為applied

使用update()方法將事務文件state欄位值由pending更新為applied並將lastModified 欄位值設定為當前時間。

db.transactions.update(
   { _id: t._id, state: "pending" },
   {
     $set: { state: "applied" },
     $currentDate: { lastModified: true }
   }
)

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

5 )更新兩個賬戶的pendingTransactions陣列

將兩個賬戶pendingTransactions 陣列中的已應用的事務文件_id 值移除。

更新源賬戶

db.accounts.update(
   { _id: t.source, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } }
)

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

更新目標賬戶

db.accounts.update(
   { _id: t.destination, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } })

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

6 )更新事務文件state欄位值為done

通過設定事務文件state欄位值為done 來表示事務結束並將lastModified 欄位值設定為當前時間。

db.transactions.update(
   { _id: t._id, state: "applied" },
   {
     $set: { state: "done" },
     $currentDate: { lastModified: true }
   })

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

從失敗的場景中恢復

事務處理最重要的部分並不是上面給出的設計原型,而是當事務並沒有完全成功時,可以從各種失敗的場景中恢復。這節給出了可能失敗場景的概覽和針對這些場景恢復資料的步驟。

恢復操作

通過執行下面的一系列操作,兩階段任務提交模式允許應用重新開始事務並達到資料的一致性。

在應用啟動後定期地執行恢復操作來捕獲任何未完成的事務。達到資料一致性的時間取決於應用恢復每一個事務所需的時間。

下面的恢復過程使用lastModified 作為標誌,指示是否狀態為pending 的事務需要恢復。特別地,如果狀態為pending 或applied的事務在30分鐘內沒有更新,程式判定這些事務是需要恢復的。你可以依據不同的條件做出判斷。

事務處於Pending 狀態

錯誤發生在將事務狀態更新為pending之後與將事務狀態更新為applied之前時,為了從錯誤中恢復,在transactions 集合中檢索狀態為

pending 的事務文件並將其恢復:

var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
var t = db.transactions.findOne( { state: "pending", lastModified: { $lt: dateThreshold } } );

並從步驟“將事務應用到兩個賬戶”重新開始。

事務處於Applied 狀態

錯誤發生在將事務狀態更新為applied之後與將事務狀態更新為done之前時,為了從錯誤中恢復,在transactions 集合中檢索狀態為applied 的事務文件並將其恢復:

var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
var t = db.transactions.findOne( { state: "applied", lastModified: { $lt: dateThreshold } } );

並從步驟“更新兩個賬戶的pendingTransactions陣列”重新開始

回滾操作

在某些情況下,你可能需要回滾或者撤銷事務。例如,如果應用需要撤銷事務或一個賬戶不存在或賬戶已停用。

事務處於Applied 狀態

執行完步驟“更新事務狀態為Applied ”後,不應該回滾。而要通過改變源賬戶和目的賬戶的value欄位值的方式來完成事務並建立一個新的事務文件來換掉已有的事務文件。

事務處於Pending 狀態

更新事務文件為“pending”狀態完成之後,但在更新事務文件為“applied”狀態之前,可依照下面的步驟執行回滾:

1 )更新事務狀態為“canceling”

db.transactions.update(
   { _id: t._id, state: "pending" },
   {
     $set: { state: "canceling" },
     $currentDate: { lastModified: true }
   }
)

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

2 )取消兩個賬戶的事務

為了取消兩個賬戶的事務,查詢事務t是否已被使用。在更新條件中包含pendingTransactions: t._id 來更新文件,僅當pending 事務已被使用時。

更新目標賬戶,從賬戶中減去事務文件balance 欄位值並將事務文件_id值從源賬戶陣列pendingTransactions 中移除。

db.accounts.update(
   { _id: t.destination, pendingTransactions: t._id },
   {
     $inc: { balance: -t.value },
     $pull: { pendingTransactions: t._id }
   })

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

如果pending 狀態的事務沒有用於兩個賬戶,那麼匹配不到任何文件並且nMatched 和nModified 的值為0。

更新源賬戶,將事務文件中balance 欄位值加到源賬戶上將事務文件_id值從源賬戶陣列pendingTransactions 中移除。

db.accounts.update(
   { _id: t.source, pendingTransactions: t._id },
   {
     $inc: { balance: t.value},
     $pull: { pendingTransactions: t._id }
   })

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

如果pending 狀態的事務沒有用於兩個賬戶,那麼匹配不到任何文件並且nMatched 和nModified 的值為0。

3 )更新事務狀態為canceled

db.transactions.update(
   { _id: t._id, state: "canceling" },
   {
     $set: { state: "cancelled" },
     $currentDate: { lastModified: true }
   })

更新成功後返回WriteResult() 物件,其中nMatched 和nModified 的值為1。

多個應用

某種程度上,有了事務,多個應用才能連續不斷地建立並執行操作而不會引起資料不一致或相互衝突。在我們的處理過程中為了更新或查詢事務文件,更新條件中包含state 欄位來阻止多個應用程式重複地應用事務。

例如,應用App1和App2獲取了相同的事務,此時事務的狀態為initial。App1 在App2啟動之前使用了整個事務。當App2 試著執行“更新應用狀態為pending”這步時,使用的更新條件包括state: "initial",此時不會匹配到任何文件且返回物件WriteResult()中的nMatched 和nModified 的值為0。這指示App2 應該退回到第一步,使用不同的事務文件重新開始。

當多個應用程式執行時,在任意一個時間點上,只有一個應用程式能夠控制指定的事務是關鍵。像這樣,除了在更新條件中包含預期的事務狀態,你也可以在事務文件中建立一個標誌來指明那個應用程式在使用這個事務文件。

使用findAndModify() 方法修改事務文件並且一步完成。

t = db.transactions.findAndModify(
       {
         query: { state: "initial", application: { $exists: false } },
         update:
           {
             $set: { state: "pending", application: "App1" },
             $currentDate: { lastModified: true }
           },
         new: true
       }
    )

修改事務操作能夠確保只有匹配了application 欄位值的應用使用這個事務文件。

如果在事務執行過程中App1 失敗了,你可以使用恢復步驟,但是應用要確保在使用事務文件之前已經擁有事務文件。例如找到並重新開始待定的工作:

var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
db.transactions.find(
   {
     application: "App1",
     state: "pending",
     lastModified: { $lt: dateThreshold }
   })

在生產應用中使用兩階段任務提交模式

上面的例子被刻意簡化了。例如,當一個賬戶的金額是負值時可能進行回滾操作。

產品的實現可能更復雜,通常賬戶需要關於賬戶餘額,待處理存款,待處理借款的資訊。

對於所有的事務文件使用適當的write concern 級別。

6 在副本集中執行配額讀取

簡介

當從副本集主成員中讀取資料時,讀取的資料可能不是最新的或者不是持久化的資料,這取決於所使用的read concern。read concern級別為

“local”時,客戶端讀取的資料是持久化之前的資料;因此,在他們被傳送到足夠的副本整合員之前避免回滾。read concern級別為“majority”時,能夠保證讀取的資料為持久化的資料,但是讀取的資料可能不是最新的,資料已經被其他的寫操作重寫了。

本文介紹了使用db.collection.findAndModify() 讀取的資料可能不是最新的並且不能回滾。為了能夠這樣做,使用findAndModify() 和

write concern來修改文件中的啞變數。特別地,這個過程需要:

  • db.collection.findAndModify()方法使用精確的查詢條件,唯一索引一定要存在以滿足檢索的需要。
  • findAndModify()必須真的修改文件,例如引起文件的改變。
  • findAndModify()必須使用write concern { w: "majority" }

重要的:

使用 read concern of "majority" 執行“配額讀”的開銷很大,因為這會引起寫延遲而不是讀延遲。這項技術僅應用在資料過期是無法容忍的情形下。

先決條件

本文例項讀取集合products。使用下面的操作初始化集合:

db.products.insert( [
   {
     _id: 1,
     sku: "xyz123",
     description: "hats",
     available: [ { quantity: 25, size: "S" }, { quantity: 50, size: "M" } ],
     _dummy_field: 0
   },
   {
     _id: 2,
     sku: "abc123",
     description: "socks",
     available: [ { quantity: 10, size: "L" } ],
     _dummy_field: 0
   },
   {
     _id: 3,
     sku: "ijk123",
     description: "t-shirts",
     available: [ { quantity: 30, size: "M" }, { quantity: 5, size: "L" } ],
     _dummy_field: 0
   }] )

在這個集合的文件中包含了名為_dummy_field的啞變數,使用db.collection.findAndModify()方法使啞變數遞增。如果這個欄位不存在,那麼db.collection.findAndModify()方法會新增這個欄位。新增這個欄位的目的是確保db.collection.findAndModify()修改了文件。

過程

1)建立唯一索引

建立唯一索引在一些欄位上,這些欄位將被用於為db.collection.findAndModify()方法指定精確測查詢條件。

本教程使用sku 欄位來指定精確的匹配條件。建立唯一索引在sku 欄位上。

db.products.createIndex( { sku: 1 }, { unique: true } )

2)使用findAndModify 讀取提交的資料

使用db.collection.findAndModify()方法更新你要讀的文件並返回已修改的文件。{ w: "majority" }的write concern 是必須的。為了指定要讀取的文件,你必須使用被索引支援的精確查詢條件。

下面使用findAndModify() 方法,指定關於具有唯一索引的欄位sku 的精確查詢條件並使匹配文件中_dummy_field欄位的值加1。對於這個命令來說,write concern中包含值為5000 毫秒的wtimeout 欄位,如果寫操作沒有傳播到被選中成員的多數成員,那麼這樣設定將會防止寫操作永遠阻塞應用,而這樣設定不是必須的。

var updatedDocument = db.products.findAndModify(
   {
     query: { sku: "abc123" },
     update: { $inc: { _dummy_field: 1 } },
     new: true,
     writeConcern: { w: "majority", wtimeout: 5000 }
   });

即使在副本集中有兩個成員被認為是主成員的情況下,也僅會有一個成員完成w: "majority"的寫操作。這樣使用了 write concern 為"majority"的findAndModify() 方法僅當客戶端連線到真正的主成員時執行才會成功。

因為配額讀的過程僅是在文件中增加了dummy 欄位而已,因此可以安全地反覆呼叫findAndModify()方法,必要時調整wtimeout 的值。

-----------------------------------------------------------------------------------------

時間倉促,水平有限,如有不當之處,歡迎指正。