讀寫鎖
Mongodb使用讀寫鎖來來控制併發操做:css
當進行讀操做的時候會加讀鎖,這個時候其餘讀操做能夠也得到讀鎖。可是不能或者寫鎖。html
當進行寫操做的時候會加寫鎖,這個時候不能進行其餘的讀操做和寫操做。mongodb
因此按照這個道理,是不會出現同時修改同一個文檔(如執行++操做)致使數據出錯的狀況。數據庫
並且按照這個道理,由於寫操做會阻塞讀操做,因此是不會出現髒讀的。安全
可是mongodb在分片和複製集的時候會產生髒讀,後面在研究。網絡
讀寫鎖的粒度:
在2.2以前的版本,一個mongodb實例一個寫鎖,多個讀鎖,在2.2-3.0的版本,一個數據庫一個寫鎖,多個讀鎖,在3.0以後的版本,WiredTiger提供了文檔(不是集合)級別的鎖。併發
findAndModify
db.collection.findAndModify({ app
query: <document>, ide
sort: <document>, 高併發
remove: <boolean>,
update: <document>,
new: <boolean>,
fields: <document>,
upsert: <boolean>,
bypassDocumentValidation: <boolean>,
writeConcern: <document>,
collation: <document>
});
:documentsort:
。可選的。以此參數指定的排序順序修改第一個文檔。
:document::
document。可選的。 要返回的字段的子集。 如:fields: {<field1>: 1, <field2>: 1, ... }
:
book = {
_id: 123456789,
title: "MongoDB: The Definitive Guide",
author: [ "Kristina Chodorow", "Mike Dirolf" ],
published_date: ISODate("2010-09-24"),
pages: 216,
language: "English",
publisher_id: "oreilly",
available: 3,
checkout: [ { by: "joe", date: ISODate("2012-10-15") } ]
}
你可使用 db.collection.findAndModify() 方法來判斷書籍是否可結算並更新新的結算信息。
在同一個文檔中嵌入的 available 和 checkout 字段來確保這些字段是同步更新的:
db.books.findAndModify ( {
query: {
_id: 123456789,
available: { $gt: 0 }
},
update: {
$inc: { available: -1 },
$push: { checkout: { by: "abc", date: new Date() } }
}
} )
執行多個寫入操做
首先,原則上說Mongdb沒有事務的概念。
事務有ACID的概念,好比原子性,一個事務要麼所有成功,要麼所有失敗。
如,考慮一個轉帳的業務,從A轉帳100到B,將分爲兩步:
A = A - 100;
B = B + 100;
在Mongdb中,若是A = A - 100;執行完,將會直接入庫生效,沒有回滾段的概念,因此若是此時B = B + 100;出現了問題,是不能回滾上一步A的操做的。
Mongdb在執行多個更新的時候是沒有原子性的。
一個寫入操做更新了多個文檔:
當單個寫入操做修改多個文檔時,每一個文檔的修改是原子的,但整個操做不是原子的,而其餘操做可能會交錯。 可是,您可使用$ isolation操做符隔離影響多個文檔的單個寫入操做。
當Mongodb執行影響多個文檔的寫入操做的時候,若是在中間某一個文檔出現了錯誤,那麼不會回滾以前的提交。以前的提交已經入庫了。
MongoDB不隔離多文檔寫入操做,具備如下特色:
非時間點讀操做。其中一假設讀取操做在時間t1開始,並開始讀取文檔。寫操做而後在稍後的時間t2向個文檔提交更新。讀操做可能會看到寫操做的更新版本,所以讀取操做沒有時間點的概念。
讀取可能會丟失在讀取操做過程當中更新的匹配文檔。
使用$ isolation來保證隔離性:
使用$isolated操做符能夠保證單個寫入操做修改多個文檔的時候不被交錯。
$isolated實際上是在整個數據庫(Mongodb的手冊對這點說明不清楚,也多是在集合層面加獨佔鎖,可是有一點文檔中是說明的,不論在哪一個層面加獨佔鎖,都會致使真個數據庫單線程化)加獨佔鎖(即便是對於WiredTiger存儲引擎也是),在這期間不能進行其餘任何的讀寫操做。因此若是$isolated的操做執行的時間過長,會大大的影響系統的併發性能。
例子:
db.foo.update(
{ status : "A" , $isolated : 1 },
{ $inc : { count : 1 } },
{ multi: true }
)
注:上面說的影響不是說能夠保證多個文檔更新的原子性,$ isolation隔離操做符不爲寫入操做提供"all-or-nothing"原子性(原子性的定義是要麼所有成功,要麼所有失敗,$isolation不能保證出錯回滾)。沒有$isolation運算符,多更新將容許其餘操做與此更新交錯。 若是這些交錯操做包含寫入,則更新操做可能會產生意外的結果。 經過指定$ isolated,您能夠保證整個多重更新的隔離。
總結以下:
- $ isolation不保證多個文檔操做的原子性。
- $ isolation保證多個文檔操做不會被跟其餘操做交錯。
- $ isolation保證此操做在進行到某一個文檔的更新的時候,在不提交或者回滾以前,不會被客戶端看到。也就是說不會致使這個文檔的查詢產生髒讀。(這一段是個人理解 不必定對)
$isolated使用的場景很苛刻。
因爲單個文檔能夠包含多個嵌入文檔,單個文檔的原子性對於許多實際使用狀況是足夠的。 對於一系列寫入操做必須在單個事務中操做的狀況,您能夠在應用程序中實現兩階段提交。
可是,兩階段的提交只能提供相似事務的語義。 使用兩階段提交確保數據一致性,可是在兩階段提交或回滾期間,應用程序能夠返回中間數據。
副本集中使用readConcern:
在使用副本集的時候,寫入操做只寫入到master節點,slaver節點從master節點同步數據,因此讀操做可能讀取到沒有同步到其餘slaver的數據。
readConcern:讀隔離(
readConcern選項可用於如下操做:
-
find command
-
aggregate command and the db.collection.aggregate() method
-
distinct command
-
count command
-
parallelCollectionScan command
-
geoNear command
-
geoSearch command
用於副本集和副本集分片的readConcern查詢選項肯定從查詢返回哪些數據。
readConcern級別:
"local":默認。該查詢返回實例的最新數據。不保證數據已寫入大多數副本集成員(便可以回滾)。
"majority":該查詢會將實例的最新數據確認爲已寫入副本集中的大多數成員。要使用majority級別,您必須使用--enableMajorityReadConcern命令行選項啓動mongod實例(若是使用配置文件,則將replication.enableMajorityReadConcern設置爲true)。
"linearizable"(add in version3.4):該查詢返回反映全部成功寫入的數據。
這麼說若是配置了linearizable 那麼針對一個集合的查詢就能夠避免髒讀了。由於Mongdb沒有事務,因此也就不存在幻讀和不可重複讀的定義了。不過這個功能是在當前最新的3.4版本纔有的。
readConcern 解決什麼問題?
的初衷在於解決『髒讀』的問題,好比用戶從 MongoDB 的 primary 上讀取了某一條數據,但這條數據並無同步到大多數節點,而後 primary 就故障了,從新恢復後 這個primary 節點會將未同步到大多數節點的數據回滾掉,致使用戶讀到了『髒數據』。
當指定 readConcern 級別爲 majority 時,能保證用戶讀到的數據『已經寫入到大多數節點』,而這樣的數據確定不會發生回滾,避免了髒讀的問題()readConcern 能保證讀到的數據『不會發生回滾』,但並不能保證讀到的數據是最新的,這個官網上也有說明:
在使用副本集的時候,不管讀取關注級別如何,節點上的最新數據可能不會反映系統中最新版本的數據。
有用戶誤覺得,指定爲 majority 時,客戶端會從大多數的節點讀取數據,而後返回最新的數據。
實際上並非這樣,不管何種級別的 注意事項
-
目前 主要用於跟 mongos 與 config server 的交互上,參考MongoDB Sharded Cluster 路由策略
-
使用 須要配置write concern
writeConcern:{ w: <value>, j: <boolean>, wtimeout: <number> }
write concern:0(Unacknowledged)
write concern:1 & journal:true(Jounaled)
write concern:2(Replica Acknowledged)
隔離級別:
Read uncommitted是默認隔離級別,適用於mongod獨立實例以及複製集和分片集羣。
咱們上面看到經過讀寫鎖能夠保證單個實例不會看到髒讀的數據,爲何這裏說在單個實例上的隔離級別也是爲提交讀呢?看看Mongodb官方文檔的解釋:
單個文檔的寫入操做是原子的; 即若是寫入正在更新文檔中的多個字段,則讀取器將永遠不會看到只更新了一些字段的文檔。
並行處理的控制容許多個應用同時運行而不會形成數據的不一致或者衝突。
一個方法是在字段上建立一個惟一性的索引。這樣就能夠阻止插入或者更新重複的數據。在多個字段上建立惟一性索引將保證多個字段組合的惟一性。
另一種方法是經過在寫操做中使用查詢斷言來指按期望的字段當前值。兩階段提交模式除了提供查詢斷言之外還額外能夠指按期望的數據寫的狀態。
兩階段提交模式
儘管當文檔原子操做很強大,可是仍然有須要多文檔事務的狀況。當執行一個由連續操做組成的事務時,某些問題出現了,好比:
- 原子性:若是一個操做失敗,事務內的以前的操做必須 " 回滾 "到以前的狀態(就是 "all or nothing" 裏面的 "nothing")。
- 一致性:若是一個嚴重的故障(好比網絡或者硬件)打斷了事務,數據庫必須能夠恢復到一致的狀態。
mongodb
概述
假設一個情景,你想從帳戶 A 轉錢到帳戶 B 。在關係型數據庫系統裏,你能夠在一個多語句事務內從 A帳戶上減去錢而且爲 B 帳戶添加上錢。在MongoDB裏,你能夠模仿兩階段提交以達到一個相似的結果。
這個教程裏的例子使用下面的兩個集合:
- 命名爲 accounts 的集合存儲帳戶信息。
- 命名爲 transactions 的集合存儲有關轉帳事務的信息。
初始化源帳戶和目的帳戶
db.accounts.insert(
[
{ _id: "A", balance: 1000, pendingTransactions: [] },
{ _id: "B", balance: 1000, pendingTransactions: [] }
]
)
初始化轉帳記錄
db.transactions.insert(
{ _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() }
)
-
檢索事務開始:
var t = db.transactions.findOne( { state: "initial" , source: "A", destination: "B"} )
-
Update transaction state to pending:
db.transactions.update(
{ _id: t._id, state: "initial" },
{
$set: { state: "pending" },
$currentDate: { lastModified: true }
}
)
WriteResult nnModified1
nMatchednModified0
-
Apply the transaction to both accounts.
db.accounts.update(
{ _id: t.source, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)
db.accounts.update(
{ _id: t.destination, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)
-
Update transaction state to applied
db.transactions.update(
{ _id: t._id, state: "pending" },
{
$set: { state: "applied" },
$currentDate: { lastModified: true }
}
)
-
remove both accounts' list of pending transactions
db.accounts.update(
{ _id: t.source, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)
db.accounts.update(
{ _id: t.destination, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)
-
Update transaction state to done.
db.transactions.update(
{ _id: t._id, state: "applied" },
{
$set: { state: "done" },
$currentDate: { lastModified: true }
}
)