Mongodb的锁 原子性 隔离性 一致性
讀寫鎖
Mongodb使用讀寫鎖來來控制并發(fā)操作:
當(dāng)進行讀操作的時候會加讀鎖,這個時候其他讀操作可以也獲得讀鎖。但是不能或者寫鎖。
當(dāng)進行寫操作的時候會加寫鎖,這個時候不能進行其他的讀操作和寫操作。
所以按照這個道理,是不會出現(xiàn)同時修改同一個文檔(如執(zhí)行++操作)導(dǎo)致數(shù)據(jù)出錯的情況。
而且按照這個道理,因為寫操作會阻塞讀操作,所以是不會出現(xiàn)臟讀的。
但是mongodb在分片和復(fù)制集的時候會產(chǎn)生臟讀,后面在研究。
讀寫鎖的粒度:
在2.2之前的版本,一個mongodb實例一個寫鎖,多個讀鎖,在2.2-3.0的版本,一個數(shù)據(jù)庫一個寫鎖,多個讀鎖,在3.0之后的版本,WiredTiger提供了文檔(不是集合)級別的鎖。
?
findAndModify
?
findAndModify可以保證修改+返回結(jié)果(修改前或者修改后都可以)這兩個步驟的原子性。
修改并返回單個文檔。 默認情況下,返回的文檔不包括對更新所做的修改。
db.collection.findAndModify({
query: <document>,
sort: <document>,
remove: <boolean>,
update: <document>,
new: <boolean>,
fields: <document>,
upsert: <boolean>,
bypassDocumentValidation: <boolean>,
writeConcern: <document>,
collation: <document>
});
query:
document。可選的。 使用這個查詢來定位需要修改的記錄。 雖然查詢可能匹配多個文檔,但findAndModify()只會選擇一個要修改的文檔。
sort:
document。可選的。以此參數(shù)指定的排序順序修改第一個文檔。
remove:
boolean。標(biāo)識刪除操作。update和remove必須選其一。
update:
document。更新操作。update和remove必須選其一。
new:
boolean。可選的。 當(dāng)為true時,返回修改后的文檔而不是原始文件。刪除的時候,設(shè)置為true沒有意義。
fields:
document。可選的。 要返回的字段的子集。?如:fields:?{<field1>:?1,?<field2>:?1,?...?}
upsert:
boolean。適用于update,當(dāng)沒有query匹配的時候,是否插入。
writeConcern:
參考writeConcern的說明。
update和findAndModify
默認情況下,update()方法更新單個文檔。 設(shè)置多參數(shù)以更新與查詢條件匹配的所有文檔。
update可以更新多個文檔,但是Mongodb只保證單個文檔的寫入是原子性的。
update()方法返回一個包含操作狀態(tài)的WriteResult對象。要返回更新的文檔,請使用find()方法。但是,其他更新可能已經(jīng)在更新和文檔檢索之間修改了文檔。此外,如果更新僅修改了單個文檔,但是多個文檔匹配,則需要使用其他邏輯來標(biāo)識更新的文檔。
findAndModify可能引起的原子性問題:
當(dāng)findAndModify()包含upsert:true選項,并且查詢字段不是唯一索引時,該方法可能會在某些情況下多次插入文檔。
如下:
db.people.findAndModify({
query: { name: "Andy" },
sort: { rating: 1 },
update: { $inc: { score: 1 } },
upsert: true
})
當(dāng)多個客戶端同時發(fā)出了這個指令,然后在服務(wù)端并行執(zhí)行,而都沒有找到query的匹配,可能同時執(zhí)行了多個upsert操作。導(dǎo)致數(shù)據(jù)重復(fù)。
如果不使用upsert,就沒有這種問題。
findAndModify在分片集群中的要求:
在分片環(huán)境中使用findAndModify時,查詢必須包含分片鍵。
findAndModify示例:
實例說明了在一個相同的文檔中如何確保嵌入字段關(guān)聯(lián)原子操作(update:更新)的字段是同步的。
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()?方法來判斷書籍是否可結(jié)算并更新新的結(jié)算信息。
在同一個文檔中嵌入的?available?和?checkout?字段來確保這些字段是同步更新的:
db.books.findAndModify?(?{
query:?{
_id:?123456789,
available:?{?$gt:?0?}
},
update:?{
$inc:?{?available:?-1?},
$push:?{?checkout:?{?by:?"abc",?date:?new?Date()?}?}
}
}?)
?
執(zhí)行多個寫入操作
?
首先,原則上說Mongdb沒有事務(wù)的概念。
事務(wù)有ACID的概念,比如原子性,一個事務(wù)要么全部成功,要么全部失敗。
如,考慮一個轉(zhuǎn)賬的業(yè)務(wù),從A轉(zhuǎn)賬100到B,將分為兩步:
A = A - 100;
B = B + 100;
在Mongdb中,如果A = A - 100;執(zhí)行完,將會直接入庫生效,沒有回滾段的概念,所以如果此時B = B + 100;出現(xiàn)了問題,是不能回滾上一步A的操作的。
Mongdb在執(zhí)行多個更新的時候是沒有原子性的。
?
一個寫入操作更新了多個文檔:
當(dāng)單個寫入操作修改多個文檔時,每個文檔的修改是原子的,但整個操作不是原子的,而其他操作可能會交錯。 但是,您可以使用$ isolation操作符隔離影響多個文檔的單個寫入操作。
當(dāng)Mongodb執(zhí)行影響多個文檔的寫入操作的時候,如果在中間某一個文檔出現(xiàn)了錯誤,那么不會回滾之前的提交。之前的提交已經(jīng)入庫了。
MongoDB不隔離多文檔寫入操作,具有以下特點:
非時間點讀操作。其中一假設(shè)讀取操作在時間t1開始,并開始讀取文檔。寫操作然后在稍后的時間t2向個文檔提交更新。讀操作可能會看到寫操作的更新版本,因此讀取操作沒有時間點的概念。
讀取可能會丟失在讀取操作過程中更新的匹配文檔。
使用$ isolation來保證隔離性:
使用$isolated操作符可以保證單個寫入操作修改多個文檔的時候不被交錯。
$isolated其實是在整個數(shù)據(jù)庫(Mongodb的手冊對這點說明不清楚,也可能是在集合層面加獨占鎖,但是有一點文檔中是說明的,不論在哪個層面加獨占鎖,都會導(dǎo)致真?zhèn)€數(shù)據(jù)庫單線程化)加獨占鎖(即使是對于WiredTiger存儲引擎也是),在這期間不能進行其他任何的讀寫操作。所以如果$isolated的操作執(zhí)行的時間過長,會大大的影響系統(tǒng)的并發(fā)性能。
例子:
db.foo.update( { status :"A" , $isolated : 1 }, { $inc : { count : 1 } }, { multi:true } )注:上面說的影響不是說可以保證多個文檔更新的原子性,$ isolation隔離操作符不為寫入操作提供"all-or-nothing"原子性(原子性的定義是要么全部成功,要么全部失敗,$isolation不能保證出錯回滾)。沒有$isolation運算符,多更新將允許其他操作與此更新交錯。 如果這些交錯操作包含寫入,則更新操作可能會產(chǎn)生意外的結(jié)果。 通過指定$ isolated,您可以保證整個多重更新的隔離。
總結(jié)如下:
- $ isolation不保證多個文檔操作的原子性。
- $ isolation保證多個文檔操作不會被跟其他操作交錯。
- $ isolation保證此操作在進行到某一個文檔的更新的時候,在不提交或者回滾之前,不會被客戶端看到。也就是說不會導(dǎo)致這個文檔的查詢產(chǎn)生臟讀。(這一段是我的理解 不一定對)
? ?
$isolated使用的場景很苛刻。
由于單個文檔可以包含多個嵌入文檔,單個文檔的原子性對于許多實際使用情況是足夠的。 對于一系列寫入操作必須在單個事務(wù)中操作的情況,您可以在應(yīng)用程序中實現(xiàn)兩階段提交。
但是,兩階段的提交只能提供類似事務(wù)的語義。 使用兩階段提交確保數(shù)據(jù)一致性,但是在兩階段提交或回滾期間,應(yīng)用程序可以返回中間數(shù)據(jù)。
?
?
副本集中使用readConcern:
?
在使用副本集的時候,寫入操作只寫入到master節(jié)點,slaver節(jié)點從master節(jié)點同步數(shù)據(jù),所以讀操作可能讀取到?jīng)]有同步到其他slaver的數(shù)據(jù)。
readConcern:讀隔離(New in version 3.2.):
readConcern:?{ level:?<"majority"|"local"|"linearizable">?}
readConcern選項可用于以下操作:
- find?command
- aggregate?command and the?db.collection.aggregate()?method
- distinct?command
- count?command
- parallelCollectionScan?command
- geoNear?command
- geoSearch?command
用于副本集和副本集分片的readConcern查詢選項確定從查詢返回哪些數(shù)據(jù)。
readConcern級別:
"local":默認。該查詢返回實例的最新數(shù)據(jù)。不保證數(shù)據(jù)已寫入大多數(shù)副本集成員(即可以回滾)。
"majority":該查詢會將實例的最新數(shù)據(jù)確認為已寫入副本集中的大多數(shù)成員。要使用majority級別,您必須使用--enableMajorityReadConcern命令行選項啟動mongod實例(如果使用配置文件,則將replication.enableMajorityReadConcern設(shè)置為true)。
"linearizable"(add in version3.4):該查詢返回反映所有成功寫入的數(shù)據(jù)。
? ?
這么說如果配置了linearizable?那么針對一個集合的查詢就可以避免臟讀了。因為Mongdb沒有事務(wù),所以也就不存在幻讀和不可重復(fù)讀的定義了。不過這個功能是在當(dāng)前最新的3.4版本才有的。
?
readConcern 解決什么問題?
readConcern?的初衷在于解決『臟讀』的問題,比如用戶從 MongoDB 的 primary 上讀取了某一條數(shù)據(jù),但這條數(shù)據(jù)并沒有同步到大多數(shù)節(jié)點,然后 primary 就故障了,重新恢復(fù)后 這個primary 節(jié)點會將未同步到大多數(shù)節(jié)點的數(shù)據(jù)回滾掉,導(dǎo)致用戶讀到了『臟數(shù)據(jù)』。
當(dāng)指定 readConcern 級別為 majority 時,能保證用戶讀到的數(shù)據(jù)『已經(jīng)寫入到大多數(shù)節(jié)點』,而這樣的數(shù)據(jù)肯定不會發(fā)生回滾,避免了臟讀的問題(這段話不是來自官方文檔,是阿里的一篇文章,這句話我不太認可,即使寫入了大部分的節(jié)點,也不代表不會回滾,因為可能選舉出來新節(jié)點正好沒有同步這部分?jǐn)?shù)據(jù))。
需要注意的是,readConcern?能保證讀到的數(shù)據(jù)『不會發(fā)生回滾』,但并不能保證讀到的數(shù)據(jù)是最新的,這個官網(wǎng)上也有說明:
在使用副本集的時候,無論讀取關(guān)注級別如何,節(jié)點上的最新數(shù)據(jù)可能不會反映系統(tǒng)中最新版本的數(shù)據(jù)。
有用戶誤以為,readConcern?指定為 majority 時,客戶端會從大多數(shù)的節(jié)點讀取數(shù)據(jù),然后返回最新的數(shù)據(jù)。
實際上并不是這樣,無論何種級別的?readConcern,客戶端都只會從『某一個確定的節(jié)點』(具體是哪個節(jié)點由 readPreference 決定)讀取數(shù)據(jù),該節(jié)點根據(jù)自己看到的同步狀態(tài)視圖,只會返回已經(jīng)同步到大多數(shù)節(jié)點的數(shù)據(jù)。
?
注意事項
- 目前?readConcern?主要用于跟 mongos 與 config server 的交互上,參考MongoDB Sharded Cluster 路由策略
- 使用?readConcern?需要配置replication.enableMajorityReadConcern選項
- 只有支持 readCommited 隔離級別的存儲引擎才能支持?readConcern,比如 wiredtiger 引擎,而 mmapv1引擎則不能支持。
write concern
寫關(guān)注。
在寫操作中加入:
writeConcern:{ w: <value>, j: <boolean>, wtimeout: <number> }
所有的mongodb driver,在執(zhí)行一個寫操作(insert、update、delete)之后,都會立刻調(diào)用db.getLastError()方法。這樣才有機會知道剛才的寫操作是否成功,如果捕獲到錯誤,就可以進行相應(yīng)的處理。處理邏輯也是完全由client決定的,比如寫入日志、拋出錯誤、等待一段時間再次嘗試寫入等。作為mongodb server并不關(guān)心,server只負責(zé)通知client發(fā)生了錯誤
對寫操作的保證,級別越高,可靠性越高但是性能越低? ?
write concern:0(Unacknowledged)
driver調(diào)用了getLastError()之后,mongod立刻返回結(jié)果,然后才實際進行寫操作。所以getLastError()的返回值一定是null,即使之后的Apply發(fā)生了錯誤,driver也不知道。使用這個級別的write concern,driver的寫入調(diào)用立刻返回,所以性能是最好的,但是可靠性是最差的,因此并不推薦使用。
其實還有一個w:-1的級別,是error ignored,基本上和w:0差不多。區(qū)別在于,w:-1不會捕獲任何錯誤,而w:0可以捕獲network errorwrite concern:1(acknowledged)
和Unacknowledged的區(qū)別是,現(xiàn)在mongod只有在Apply(實際寫入操作)完成之后,才會返回getLastError()的響應(yīng)。所以如果寫入時發(fā)生錯誤,driver就能捕獲到,并進行處理。這個級別的write concern具備基本可靠性,也是目前mongodb的默認設(shè)置級別write concern:1 & journal:true(Jounaled)
mongodb的Apply操作,是將數(shù)據(jù)寫入內(nèi)存,定期通過fsync寫入硬盤。如果在Apply之后,fsync之前mongod掛了,那持久化實際上是失敗的。但是在w:1的級別下,driver無法捕獲到這種情況下的error(因為response在apply之后就已經(jīng)返回到driver)
使用Journal機制:寫操作在寫入內(nèi)存之后,還會寫到journal文件中,實實在在的把journal落盤以后才會返回。
MongoDB并不會對每一個操作都立即刷盤,而是會等最多30ms,把30ms內(nèi)的寫操作集中到一起,采用順序追加的方式寫入到盤里。在這30ms內(nèi)客戶端線程會處于等待狀態(tài)。這樣對于單個操作的總體響應(yīng)時間將有所延長,但對于高并發(fā)的場景,綜合下來平均吞吐能力和響應(yīng)時間不會有太大的影響write concern:2(Replica Acknowledged)
這個級別只在replica set的部署模式下生效
這個級別下,只有secondary從primary完成了復(fù)制之后,getLastError()的結(jié)果才會返回。也可以同時設(shè)置journal:true或j:true,則還要等journal寫入也成功后才會返回。但是注意,只要primary的journal寫入就會返回,而不需要等待secondary的journal也寫入。類似的也可以設(shè)置w:3,表示至少要有3個節(jié)點有數(shù)據(jù);或者w:majority,表示>1/2的節(jié)點有數(shù)據(jù)注意
wtimeout: 寫入超時時間,僅w的值大于1時有效。
當(dāng)指定{w: }時,數(shù)據(jù)需要成功寫入number個節(jié)點才算成功,如果寫入過程中有節(jié)點故障,可能導(dǎo)致這個條件一直不能滿足,從而一直不能向客戶端發(fā)送確認結(jié)果,針對這種情況,客戶端可設(shè)置wtimeout選項來指定超時時間,當(dāng)寫入過程持續(xù)超過該時間仍未結(jié)束,則認為寫入失敗。journal無論如何都是建議打開的,設(shè)置j:true,只是說driver調(diào)用getLastError()之后是否要等待journal寫入完成再返回。并不是說不設(shè)置j:true就關(guān)閉了server端的journal
一般來說,MongoDB建議在集群中使用 {w: "majority"} 設(shè)置。在一個集群是健壯的部署的情況下(如:足夠網(wǎng)絡(luò)帶寬,機器沒有滿負荷),這個可以滿足絕大部分?jǐn)?shù)據(jù)安全的要求,因為MongoDB的復(fù)制在正常情況下是毫秒級別的,往往在Journal刷盤之前已經(jīng)復(fù)制到從節(jié)點了。如果你追求完美,那么可以再進一步使用{j:1}
隔離級別:
Read uncommitted是默認隔離級別,適用于mongod獨立實例以及復(fù)制集和分片集群。
我們上面看到通過讀寫鎖可以保證單個實例不會看到臟讀的數(shù)據(jù),為什么這里說在單個實例上的隔離級別也是為提交讀呢?看看Mongodb官方文檔的解釋:
單個文檔的寫入操作是原子的; 即如果寫入正在更新文檔中的多個字段,則讀取器將永遠不會看到只更新了一些字段的文檔。
然而,雖然度去操作可能看不到部分更新的文檔,但讀取未提交意味著并發(fā)讀取可能仍然會看到更新后的文檔,但是這些文檔還沒有持久化。
如果是副本集,那么就不能保證已提交讀,因為主節(jié)點發(fā)生故障后,其他節(jié)點接替它作為主節(jié)點,接替它的節(jié)點可能還沒有同步上一個主節(jié)點的所有數(shù)據(jù),這部分沒有同步的數(shù)據(jù)就成了臟數(shù)據(jù)。
?
并行處理的控制
并行處理的控制允許多個應(yīng)用同時運行而不會造成數(shù)據(jù)的不一致或者沖突。
一個方法是在字段上創(chuàng)建一個唯一性的索引。這樣就可以阻止插入或者更新重復(fù)的數(shù)據(jù)。在多個字段上創(chuàng)建唯一性索引將保證多個字段組合的唯一性。
另外一種方法是通過在寫操作中使用查詢斷言來指定期望的字段當(dāng)前值。兩階段提交模式除了提供查詢斷言以外還額外可以指定期望的數(shù)據(jù)寫的狀態(tài)。
兩階段提交模式
盡管當(dāng)文檔原子操作很強大,但是仍然有需要多文檔事務(wù)的情況。當(dāng)執(zhí)行一個由連續(xù)操作組成的事務(wù)時,某些問題出現(xiàn)了,比如:
- 原子性:如果一個操作失敗,事務(wù)內(nèi)的之前的操作必須 " 回滾 "到之前的狀態(tài)(就是 "all or nothing" 里面的 "nothing")。
- 一致性:如果一個嚴(yán)重的故障(比如網(wǎng)絡(luò)或者硬件)打斷了事務(wù),數(shù)據(jù)庫必須可以恢復(fù)到一致的狀態(tài)。
對于需要多文檔事務(wù)的情景,你可以在你的應(yīng)用里實現(xiàn)兩階段提交以提供這些多文檔更新的支持。使用兩階段提交保證數(shù)據(jù)是一致的,并且在發(fā)生錯誤的情況下,執(zhí)行事務(wù)之前的狀態(tài)是?recoverable (可恢復(fù)的)?。然而,在執(zhí)行過程中,文檔可以展示未確定的(事務(wù)提交之前的)數(shù)據(jù)和狀態(tài)。
以下例子來自mongodb文檔:
概述
假設(shè)一個情景,你想從賬戶?A?轉(zhuǎn)錢到賬戶?B?。在關(guān)系型數(shù)據(jù)庫系統(tǒng)里,你可以在一個多語句事務(wù)內(nèi)從?A賬戶上減去錢并且為?B?賬戶添加上錢。在MongoDB里,你可以模仿兩階段提交以達到一個類似的結(jié)果。
這個教程里的例子使用下面的兩個集合:
- 命名為?accounts?的集合存儲賬戶信息。
- 命名為?transactions?的集合存儲有關(guān)轉(zhuǎn)賬事務(wù)的信息。
初始化源賬戶和目的賬戶
db.accounts.insert(
[
{ _id: "A", balance: 1000, pendingTransactions: [] },
{ _id: "B", balance: 1000, pendingTransactions: [] }
]
)
初始化轉(zhuǎn)賬記錄
db.transactions.insert(
{ _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() }
)
注:我的理解:初始化操作應(yīng)該是在第一次做事務(wù)的時候插入的,為了防止出現(xiàn)重復(fù)數(shù)據(jù),那么需要在相應(yīng)的列上加上唯一索引。如,這里應(yīng)該是source和destination作為唯一索引。而且value不應(yīng)該出現(xiàn)在transactions表中才對。
檢索事務(wù)開始:
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 }
}
)
該操作返回具有操作狀態(tài)的WriteResult()對象。 成功更新后,n次和nModified顯示1。
在更新語句中,狀態(tài):"初始"條件確保沒有其他進程已更新此記錄。 如果nMatched和nModified為0,則返回到第一步以獲取其他事務(wù)并重新啟動該過程。
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 }
}
)
轉(zhuǎn)載于:https://www.cnblogs.com/xiaolang8762400/p/7255878.html
總結(jié)
以上是生活随笔為你收集整理的Mongodb的锁 原子性 隔离性 一致性的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 初入前端,面对一个项目应注意哪些?
- 下一篇: CSDN编程挑战——《交替字符串》