1.MongoDB的寫操作事務
寫入策略 writeConcern
語法:db.collection.insert({x: 1}, {writeConcern: {w: 1}})
什么是writeConcern?
writeConcern決定一個寫操作落到多少個節點上才算成功,這決定了MongoDB是否成功寫入數據。writeConcern的取值有以下:
- 0:發起寫入操作,不關心是否成功(適用於性能要求高,但不關注正確性的場景)
- 1-集群最大節點數:寫操作需要被復制到指定節點數才算成功
- majority:寫操作需要被復制到大多數節點上才算成功(適用於對數據安全性要求比較高的場景,該選項會降低寫入性能
) - all:復制到全部節點上才算成功
發起寫操作的程序將阻塞到寫操作到達指定的節點數為止
writeConcern的行為
以3節點復制集為例:不做任何特定設置
上圖表示一個寫操作進入后,直接寫入主節點成功就返回了,后台會異步復制到從節點secondary1和secondary2。但假如數據剛寫入主節點后,從節點還沒有復制數據,主節點就宕機了,此時MongoDB就可能出現丟失數據的問題,那么如何解決呢?
參數 w:"majority"
majority表示數據寫入大多數(超半數)節點后才算成功。此時如果主節點再發生宕機情況,那么從節點secondary1就會被選舉為新的主節點,數據也沒有丟失
參數 w:"all"
all表示確認全部節點寫入成功后才返回。這是一種最安全的寫法,數據絕對不會丟失,但是如果有一個節點故障,那么就會發生阻塞一直等待
參數 j:true
j表示寫入操作的journal持久化后才向客戶端確認,取值有:
- true:寫操作落到 journal 文件中才算成功
- false:寫操作到達內存即算成功
參數 wtimeout: 寫入超時時間,僅w的值大於1時有效
- 當指定{w: }時,數據需要成功寫入n個節點才算成功,如果寫入過程中有節點故障,可能導致這個條件一直不能滿足,從而一直不能向客戶端發送確認結果,針對這種情況,客戶端可設置wtimeout選項來指定超時時間,當寫入過程持續超過該時間仍未結束,則認為寫入失敗
writeConcern測試
以下測試在3個節點環境中
db.test.insert({count:1},{writeConcern:{w:"majority"}})
db.test.insert({count:1},{writeConcern:{w:3}})
db.test.insert({count:1},{writeConcern:{w:4}}) # 報錯:Not enough data-bearing nodes
db.test.insert({count:1},{writeConcern:{w:3,wtimeout:3000}}) # 超過3s未響應則不再等待直接返回
slaveDelay:設置節點延遲時間(單位:s),延遲多久才會同步數據
var conf = rs.conf
conf.members[2].salveDelay = 10 #設置節點3延遲10秒
conf.members[2].priority=0 #設置了延遲的節點不能參與選舉
注意事項
- 雖然多半數的 writeConcern都是安全的,但通常只會設置 majority,因為這是等待寫入延遲時間最短的選擇
- 不要設置 writeConcern 等於總節點數,因為一旦有一個節點故障,所有寫操作都會失敗
- writeConcern 雖然會增加寫操作的延遲時間,但並不會顯著增加集群的壓力,因此無論是否等待,寫操作最終都會復制到所有節點上。但設置 writeConcern只是讓寫操作等待復制后在返回而已
- 應對重要數據(訂單、金融有關的)應用 {w:"majority"},普通數據(日志)可以應用{w:1}以確保最佳性能
2.MongoDB的讀操作事務
在讀取數據的過程中我們需要關注以下問題:
- 從哪里讀?關注數據節點的位置(由readPerference解決)
- 什么樣的數據可以讀?關注數據的隔離性(由readConcern解決)
readPerference:
使用:db.collection.find({}).readPerf("secondary")
readPerference決定使用哪一個節點來滿足正在發起的讀請求,可選值包括:
- primary(默認):只選擇主節點,保證每次讀到的數據都是最新的
- primaryPerferred:優先選擇主節點,如果不可用則選擇從節點
- secondary:只選擇從節點
- secondaryPerferred:優先選擇從節點,如果從節點不可用則選擇主節點
- nearest:選擇最近的節點,針對多區域部署的情況
readPerference使用場景舉例:
- 用戶下單后馬上跳轉到訂單詳情頁--primary/primaryPerferred,因為此時從節點可能還沒復制到新的訂單數據
- 用戶查詢自己下過的訂單--secondary/secondaryPerferred,查詢歷史訂單對時效性通常沒有太高要求
- 生成報表--secondary,報表對時效性要求不高,但資源需求大,可以在從節點單獨處理,避免影響主節點操作
- 將用戶上傳的圖片分發到全世界,讓各地用戶就近讀取--nearest,每個地區的應用選擇最近的節點讀取數據
readPerference與Tag
readPreference只能控制使用一類節點。Tag則可以將節點選擇控制到一個或幾個具體的節點,有以下場景:
- 一個5個節點的復制集
- 3個節點硬件較好,專用於服務線上用戶
- 2個節點硬件較差,專用於生成包報表
可以使用Tag來達到這樣的控制目的:
- 為3個較好的節點打上 {purpose:"online"}
- 為2個較差的節點打上 {purpose:"analyse"}
- 在線應用讀取時指定online,報表讀取時指定analyse
readPerference配置:
- 通過MongoDB的連接字符串:mongodb://host:27017,host2:27017,host3:27017/?replicaSet=rs&readPerferende=secondary
- 通過MongoDB驅動程序API:MongoCollection.withReadPerference(ReadPerference readPerf)
- Mongo Shell:db.collection.find({}).readPerf("secondary")
readPerference注意事項:
- 指定readPerference時也應該注意高可用問題。例如將 readPerference 指定primary,則發生故障轉移不存在primary期間將沒有節點可讀。如果業務允許,則應選擇primaryPerference
- 使用Tag時也會遇到同樣的問題,如果只有一個節點擁有一個特定的Tag,則這個節點宕機時將無節點可讀。這在有時候是期望的結果,有時候不是:
- 如果報表使用的節點失效,即使不生成報表,通常也不希望將報表負載轉移到其他節點上,此時只有一個節點有報表Tag是合理的選擇
- 如果線上節點失效,通常希望有替代節點,則應該保持多個節點有同樣的Tag
- Tag有時需要與優先級、選舉權綜合考慮。例如做報表的節點通常不會希望它成為主節點,則優先級應為0
readConcern:
使用:db.test.find().readConcern("majority")
在readPerference選擇了指定節點后,readConcern決定這個節點上的數據哪些是可讀的,類似於關系型數據庫的隔離級別,包括:
- available:讀取所有可用的數據
- local(默認):讀取所有可用且屬於當前分片的數據
- majority:讀取再大多數節點上提交完成的數據
- linearizable:可線性化讀取文檔
- snapshot:讀取最近快照中的數據
readConcern:local和available
在復制集中local和available是沒有區別的,兩者的區別主要體現在分片集上,有以下場景:
- 一個chunk x 正在從shard1向shard2遷移
- 整個遷移過程中chunk x 中的部分數據會在shard1和shard2中同時存在,但源分片shard1仍然是chunk x的負責方
- 所有對chunk x的讀操作仍然進入shard1
- config中記錄的信息chunk x仍然屬於shard1
- 此時如果度shard2,則會體現出local和available的區別:
- local:只取應該由shard2負責的數據(不包括x)
- available:shard2上有什么就讀什么(包括x)
readConcern:majority
只讀取大多數數據節點上都提交了的數據,考慮如下場景:
- 集合中原有文檔 {x:0}
- 將x值更新為1
如果在各節點上應用 {readConcern:"majority"}來讀取數據:
readConcern:majority與臟讀
MongoDB中的回滾:
- 寫操作到達大多數節點之前都是不安全的,一旦主節點崩潰,而從節點還沒復制到該次操作,剛才的寫操作就丟失了
- 把一次寫操作視為一個事務,從事務的角度來看,可以認為事務被回滾了。
所以從分布式系統的角度來看,事務的提交被提升到了分布式集群的多個節點級別的“提交”,而不是單個節點的“提交”
在可能發生回滾的前提下考慮臟讀問題:
- 如果在一次寫操作到達大多數節點前讀取了這個寫操作,然后因為系統故障該操作回滾了,則發生臟讀問題,使用 {readConcern:"majority"}可以有效避免臟讀
readConcern:實現安全的讀寫分離
有以下場景
- 向主節點寫入一條數據
- 立即從從節點讀取這條數據
如何保證自己能夠讀取到剛剛寫入的數據?
下述方式有可能讀不到剛寫入的訂單:
db.order.insert({id:100,sku:"kite",q:1})
db.order.find({id:100}).readPerf("secondary")
使用writeConcern + readConcern majority來解決:
db.order.insert({id:100,sku:"kite",q:1},{writeConcern:{w:"majority"}})
db.order.find({id:100}).readPerf("secondary").readConcern("majority")
readConcern:linearizable
只讀取大多數節點確認過的數據。和majority最大差別是保證絕對的操作線性順序-在寫操作自然時間后面發生的讀,一定可以讀到之前的寫
- 只對讀取單個文檔時有效
- 可能導致非常慢的讀,因此總是建議配合使用 maxTimeMS
readConcern:snapshot
只在多文檔事務中生效。將一個事務的 readConcern設置為snapshot將保證在事務中的讀:
- 不出現臟讀
- 不出現不可重復讀
- 不出現幻讀
因為所有的讀都將使用同一個快照,直到事務提交為止該快照才被釋放
3.MongoDB的多文檔事務
對事務的使用原則應該是:能不用盡量不用
通過合理的設計文檔模型,可以避免大部分使用事務的必要性,因為事務=鎖,節點協調需要額外開銷,影響性能
MongoDB ACID多文檔事務支持
事務屬性 | 支持程度 |
---|---|
Atomocity 原子性 | 單表單文檔:1.x就支持 復制集多表多行:4.0復制集 分片集群多表多行:4.2 |
Consistency 一致性 | writeCOncern、readConcern(3.2) |
Isolation 隔離性 | readConcern(3.2) |
Durability 持久性 | Journal and Replication |
使用方法
MongoDB多文檔事務使用方式和關系型數據庫非常相似,以java為例
try(ClientSession clientSession = client.startSession()){
clientSession.startTransaction();
collection.insertOne(clientSession,docOne);
collection.insertOne(clientSession,docTwo);
clientSession.commitTransaction();
}
事務的隔離級別
- 事務完成前,事務外的操作對該事務所做的修改不可訪問
- 如果事務內使用{readConcern:"snapshot"},則可以達到可重復度 Repeatable Read
事務寫機制
MongoDB的事務錯誤處理機制不同於關系型數據庫
- 當一個事務開始后,如果事務要修改的文檔在事務外部被修改過,則事務修改這個文檔時會出發Abort錯誤,因為此時修改沖突了,這種情況下,只需要簡單的重做事務就可以了
- 如果一個事務已經開始修改一個文檔,在事務以外嘗試修改同一個文檔,則事務以外的修改會等待事務完成才能繼續進行
注意事項
- 可以實現和關系型數據庫類似的事務場景
- 必須使用與MongoDB4.2兼容的驅動
- 事務默認必須在60s(可調)內完成,否則將被取消
- 涉及事務的分片不能使用仲裁節點
- 事務會影響chunk遷移效率。正在遷移的chunk也可能造成事務提交失敗(重試即可)
- 多文檔事務中的讀操作必須使用主節點讀
- readConcern只應該在事務級別設置,不能設置在每次讀寫操作上