官網傳送門:
https://docs.mongodb.com/manual/core/replica-set-write-concern/
https://docs.mongodb.com/manual/core/transactions/#read-concern-write-concern-read-preference
MongoDB ACID 多文檔事務支持
事務屬性 | 支持程度 |
Atomocity 原子性 | 單表單文檔 : 1.x 就支持 復制集多表多行:4.0 復制集 分片集群多表多行4.2 |
Consistency 一致性 | writeConcern, readConcern (3.2) |
Isolation 隔離性 | readConcern (3.2) |
Durability 持久性 | Journal and Replication |
Atomocity 原子性
一個事務作為一個提交單位,要么一起成功要么一起失敗,分布式關注的重點就是多節點之間數據的原子性控制。
Consistency 一致性
讀和寫的一致性,會不會讀到臟數據。
Isolation 隔離性
每個事務相互之間是獨立的,每個事務內與主表之間也是互不影響、相互獨立的。
Durability 持久性
數據落盤持久化,通過記錄Journal日志文件和備份副本。
先從mongoDB的一致性相關的writeConcern, readConcern說。
什么是 writeConcern ?
writeConcern 決定一個寫操作落到多少個節點上才算成功。writeConcern 的取值包括:
• 0:發起寫操作,不關心是否成功;
• 1~集群最大數據節點數:寫操作需要被復制到指定節點數才算成功;默認是1。
• majority:寫操作需要被復制到大多數節點上才算成功。
• all:寫入所有節點才算成功。
發起寫操作的程序將阻塞到寫操作到達指定的節點數為止
默認情況 w:“1”
大多數節點確認(即一半以上節點) w: “majority”
全部節點確認 w: “all”
j:true
writeConcern 可以決定寫操作到達多少個節點才算成功,journal 則定義如何才算成
功。取值包括:
• true: 寫操作落到 journal 文件中才算成功;
• false: 寫操作到達內存即算作成功。
數據庫數據寫入的順序 數據在內存中先寫日志文件(journal 中落地持久化日志文件),再寫數據文件
mongoDB shell中使用
db.test.insert( {count: 1}, {writeConcern: {w: "majority"}}) db.test.insert( {count: 1}, {writeConcern: {w: 3 }}) db.test.insert( {count: 1}, {writeConcern: {w: 4 }})
readPreference與readConcern
readPreference 設置 分布式數據庫從哪里讀
readConcern 什么樣的數據可以讀
readPreference
readPreference 決定使用哪一個節點來滿足正在發起的讀請求。可選值包括:
• primary: 只選擇主節點;
• primaryPreferred:優先選擇主節點,如果不可用則選擇從節點;
• secondary:只選擇從節點;
• secondaryPreferred:優先選擇從節點,如果從節點不可用則選擇主節點;
• nearest:選擇最近的節點;
場景舉例:大量高並發讀取的數據場景可以選擇從節點,寫入數據的時候,用主節點。
readPreference的 配置方式
通過 MongoDB 的連接串參數: • mongodb://host1:27107,host2:27107,host3:27017/?replicaSet=rs&readPreference=secondary 通過 MongoDB 驅動程序 API: • MongoCollection.withReadPreference(ReadPreference readPref) Mongo Shell: • db.collection.find({}).readPref( “secondary” )
readConcern
在 readPreference 選擇了指定的節點后,readConcern 決定這個節點上的數據哪些是可讀的,類似於關系數據庫的隔離級別。可選值包括:
• available:讀取所有可用的數據;
• local:讀取所有可用且屬於當前分片的數據;
• majority:讀取在大多數節點上提交完成的數據;
• linearizable:可線性化讀取文檔;
• snapshot:讀取最近快照中的數據;
在復制集中 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);
注意事項: • 雖然看上去總是應該選擇 local,但畢竟對結果集進行過濾會造成額外消耗。在一些無關緊要的場景(例如統計)下,也可以考慮 available; • MongoDB <=3.6 不支持對從節點使用 {readConcern: "local"}; • 從主節點讀取數據時默認 readConcern 是 local,從從節點讀取數據時默認readConcern 是 available(向前兼容原因)。
readConcern : ”majority” vs “local”
majority 如果有如下場景:
應業務需要做讀寫分離,主節點A主要做寫入操作,從節點做讀取操作;向主節點寫入一條數據;立即從從節點讀取這條數據。
如果用默認配置local,有的復制集從節點B到主A可能是有延時的,而導致在主節點A上修改數據后,立刻在從節點讀取,發現有臟數據,主從數據不同步。所以需要majority配置,和其他節點互交確認大多數據節點上都是一樣的數據才返回。
#注意配置文件內server參數開啟
enableMajorityReadConcern:ture
#這種方式讀取不到剛寫入的數據
db.orders.insert({ oid: 101, sku: ”kite", q: 1}) //主節點寫入
db.orders.find({oid:101}).readPref("secondary") //從節點讀取
#使用 writeConcern + readConcern majority來解決
db.orders.insert({ oid: 101, sku: "kiteboar", q: 1}, {writeConcern:{w: "majority”}}) //主節點寫入
db.orders.find({oid:101}).readPref(“secondary”).readConcern("majority") //從節點讀取
readConcern: majority 與臟讀
MongoDB 中的回滾:
• 寫操作到達大多數節點之前都是不安全的,一旦主節點崩潰,而從節還沒復制到該次操作,剛才的寫操作就丟失了;
• 把一次寫操作視為一個事務,從事務的角度,可以認為事務被回滾了。所以從分布式系統的角度來看,事務的提交被提升到了分布式集群的多個節點級別的“提交”,而不再是單個節點上的“提交”。在可能發生回滾的前提下考慮臟讀問題:
• 如果在一次寫操作到達大多數節點前讀取了這個寫操作,然后因為系統故障該操作回滾了,則發生了臟讀問題;
使用 {readConcern: “majority”} 可以有效避免臟讀
readConcern: linearizable
只讀取大多數節點確認過的數據。和 majority 最大差別是保證絕對的操作線性順序:
在寫操作自然時間后面的發生的讀,一定可以讀到之前的寫(會在讀取的節點,向其他所有節點發起確認請求)
- 只對讀取單個文檔時有效;
- 可能導致非常慢的讀,因此總是建議配合使用 maxTimeMS;
下圖情況是在主節點網絡異常時,從節點發起選舉期間發生的臟讀
readConcern: snapshot(最高級別)
{readConcern: “snapshot”} 只在多文檔事務中生效。將一個事務的 readConcern
設置為 snapshot,將保證在事務中的讀:
• 不出現臟讀;
• 不出現不可重復讀;
• 不出現幻讀。
因為所有的讀都將使用同一個快照,直到事務提交為止該快照才被釋放。
官網mongoDB Java Client 事務使用示例:
final MongoClient client = MongoClients.create(uri); /* Prereq: Create collections. CRUD operations in transactions must be on existing collections. */ client.getDatabase("mydb1").getCollection("foo") .withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("abc", 0)); client.getDatabase("mydb2").getCollection("bar") .withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("xyz", 0)); /* Step 1: Start a client session. */ final ClientSession clientSession = client.startSession(); /* Step 2: Optional. Define options to use for the transaction. */ TransactionOptions txnOptions = TransactionOptions.builder() .readPreference(ReadPreference.primary()) .readConcern(ReadConcern.LOCAL) .writeConcern(WriteConcern.MAJORITY) .build(); /* Step 3: Define the sequence of operations to perform inside the transactions. */ TransactionBody txnBody = new TransactionBody<String>() { public String execute() { MongoCollection<Document> coll1 = client.getDatabase("mydb1").getCollection("foo"); MongoCollection<Document> coll2 = client.getDatabase("mydb2").getCollection("bar"); /* Important:: You must pass the session to the operations. */ coll1.insertOne(clientSession, new Document("abc", 1)); coll2.insertOne(clientSession, new Document("xyz", 999)); return "Inserted into collections in different databases"; } }; try { /* Step 4: Use .withTransaction() to start a transaction, execute the callback, and commit (or abort on error). */ clientSession.withTransaction(txnBody, txnOptions); } catch (RuntimeException e) { // some error handling } finally { clientSession.close(); }
java 示例2:
//MongoDB 多文檔事務的使用方式與關系數據庫非常相似: try (ClientSession clientSession = client.startSession()) { clientSession.startTransaction(); collection.insertOne(clientSession, docOne); collection.insertOne(clientSession, docTwo); clientSession.commitTransaction(); }
db.fsyncLock() 與db.fsyncUnlock() 可以鎖定/解鎖節點的寫入,可以用來做事務代碼的測試。