您好,登錄后才能下訂單哦!
這篇文章給大家介紹MongoDB 4.0 中怎么實現事務,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
MongoDB 4.0 引入的事務功能,支持多文檔ACID特性,例如使用 mongo shell 進行事務操作
> s = db.getMongo().startSession() session { "id" : UUID("3bf55e90-5e88-44aa-a59e-a30f777f1d89") } > s.startTransaction() > db.coll01.insert({x: 1, y: 1}) WriteResult({ "nInserted" : 1 }) > db.coll02.insert({x: 1, y: 1}) WriteResult({ "nInserted" : 1 }) > s.commitTransaction() (或者 s.abortTransaction()回滾事務)
支持 MongoDB 4.0 的其他語言 Driver 也封裝了事務相關接口,用戶需要創建一個 Session,然后在 Session 上開啟事務,提交事務。例如
python 版本
with client.start_session() as s: s.start_transaction() collection_one.insert_one(doc_one, session=s) collection_two.insert_one(doc_two, session=s) s.commit_transaction()
try (ClientSession clientSession = client.startSession()) { clientSession.startTransaction(); collection.insertOne(clientSession, docOne); collection.insertOne(clientSession, docTwo); clientSession.commitTransaction(); }
Session 是 MongoDB 3.6 版本引入的概念,引入這個特性主要就是為實現多文檔事務做準備。Session 本質上就是一個「上下文」。
在以前的版本,MongoDB 只管理單個操作的上下文,mongod 服務進程接收到一個請求,為該請求創建一個上下文 (源碼里對應 OperationContext),然后在服務整個請求的過程中一直使用這個上下文,內容包括,請求耗時統計、請求占用的鎖資源、請求使用的存儲快照等信息。有了 Session 之后,就可以讓多個請求共享一個上下文,讓多個請求產生關聯,從而有能力支持多文檔事務。
每個 Session 包含一個唯一的標識 lsid,在 4.0 版本里,用戶的每個請求可以指定額外的擴展字段,主要包括:
lsid: 請求所在 Session 的 ID, 也稱 logic session id
txnNmuber: 請求對應的事務號,事務號在一個 Session 內必須單調遞增
stmtIds: 對應請求里每個操作(以insert為例,一個insert命令可以插入多個文檔)操作ID
實際上,用戶在使用事務時,是不需要理解這些細節,MongoDB Driver 會自動處理,Driver 在創建Session 時分配 lsid,接下來這個 Session 里的所以操作,Driver 會自動為這些操作加上 lsid,如果是事務操作,會自動帶上 txnNumber。
值得一提的是,Session lsid 可以通過調用 startSession 命令讓 server 端分配,也可以客戶端自己分配,這樣可以節省一次網絡開銷;而事務的標識,MongoDB 并沒有提供一個單獨的 startTransaction的命令,txnNumber 都是直接由 Driver 來分配的,Driver 只需保證一個 Session 內,txnNumber 是遞增的,server 端收到新的事務請求時,會主動的開始一個新事務。
MongoDB 在 startSession 時,可以指定一系列的選項,用于控制 Session 的訪問行為,主要包括:
causalConsistency: 是否提供 causal consistency 的語義,如果設置為true,不論從哪個節點讀取,MongoDB 會保證 "read your own write" 的語義。參考 causal consistency
readConcern:參考 MongoDB readConcern 原理解析
writeConcern:參考 MongoDB writeConcern 原理解析
readPreference: 設置讀取時選取節點的規則,參考 read preference
retryWrites:如果設置為true,在復制集場景下,MongoDB 會自動重試發生重新選舉的場景; 參考 retryable write
針對多文檔的事務操作,MongoDB 提供 "All or nothing" 的原子語義保證。
太難解釋了,還有拋棄 Consistency 特性的數據庫?
MongoDB 提供 snapshot 隔離級別,在事務開始創建一個 WiredTiger snapshot,然后在整個事務過程中使用這個快照提供事務讀。
事務使用 WriteConcern {j: ture} 時,MongoDB 一定會保證事務日志提交才返回,即使發生 crash,MongoDB 也能根據事務日志來恢復;而如果沒有指定 {j: true} 級別,即使事務提交成功了,在 crash recovery 之后,事務的也可能被回滾掉。
復制集配置下,MongoDB 整個事務在提交時,會記錄一條 oplog(oplog 是一個普通的文檔,所以目前版本里事務的修改加起來不能超過文檔大小 16MB的限制),包含事務里所有的操作,備節點拉取oplog,并在本地重放事務操作。
事務 oplog 示例,包含事務操作的 lsid,txnNumber,以及事務內所有的操作日志(applyOps字段)
"ts" : Timestamp(1530696933, 1), "t" : NumberLong(1), "h" : NumberLong("4217817601701821530"), "v" : 2, "op" : "c", "ns" : "admin.$cmd", "wall" : ISODate("2018-07-04T09:35:33.549Z"), "lsid" : { "id" : UUID("e675c046-d70b-44c2-ad8d-3f34f2019a7e"), "uid" : BinData(0,"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") }, "txnNumber" : NumberLong(0), "stmtId" : 0, "prevOpTime" : { "ts" : Timestamp(0, 0), "t" : NumberLong(-1) }, "o" : { "applyOps" : [ { "op" : "i", "ns" : "test.coll2", "ui" : UUID("a49ccd80-6cfc-4896-9740-c5bff41e7cce"), "o" : { "_id" : ObjectId("5b3c94d4624d615ede6097ae"), "x" : 20000 } }, { "op" : "i", "ns" : "test.coll3", "ui" : UUID("31d7ae62-fe78-44f5-ba06-595ae3b871fc"), "o" : { "_id" : ObjectId("5b3c94d9624d615ede6097af"), "x" : 20000 } } ] } }
整個重放過程如下:
獲取當前 Batch (后臺不斷拉取 oplog 放入 Batch)
設置 OplogTruncateAfterPoint 時間戳為 Batch里第一條 oplog 時間戳 (存儲在 local.replset.oplogTruncateAfterPoint 集合)
寫入 Batch 里所有的 oplog 到 local.oplog.rs 集合,根據 oplog 條數,如果數量較多,會并發寫入加速
清理 OplogTruncateAfterPoint, 標識 oplog 完全成功寫入;如果在本步驟完成前 crash,重啟恢復時,發現 oplogTruncateAfterPoint 被設置,會將 oplog 截短到該時間戳,以恢復到一致的狀態點。
將 oplog 劃分到到多個線程并發重放,為了提升并發效率,事務產生的 oplog 包含的所有修改操作,跟一條普通單條操作的 oplog 一樣,會據文檔ID劃分到多個線程。
更新 ApplyThrough 時間戳為 Batch 里最后一條 oplog 時間戳,標識下一次重啟后,從該位置重新同步,如果本步驟之前失敗,重啟恢復時,會從 ApplyThrough 上一次的值(上一個 Batch 最后一條 oplog)拉取 oplog。
更新 oplog 可見時間戳,如果有其他節點從該備節點同步,此時就能讀到這部分新寫入的 oplog
更新本地 Snapshot(時間戳),新的寫入將對用戶可見。
WiredTiger 很早就支持事務,在 3.x 版本里,MongoDB 就通過 WiredTiger 事務,來保證一條修改操作,對數據、索引、oplog 三者修改的原子性。但實際上 MongoDB 經過多個版本的迭代,才提供了事務接口,核心難點就是時序問題。
MongoDB 通過 oplog 時間戳來標識全局順序,而 WiredTiger 通過內部的事務ID來標識全局順序,在實現上,2者沒有任何關聯。這就導致在并發情況下, MongoDB 看到的事務提交順序與 WiredTiger 看到的事務提交順序不一致。
為解決這個問題,WiredTier 3.0 引入事務時間戳(transaction timestamp)機制,應用程序可以通過WT_SESSION::timestamp_transaction 接口顯式的給 WiredTiger 事務分配 commit timestmap,然后就可以實現指定時間戳讀(read "as of" a timestamp)。有了 read "as of" a timestamp特性后,在重放 oplog 時,備節點上的讀就不會再跟重放 oplog 有沖突了,不會因重放 oplog 而阻塞讀請求,這是4.0版本一個巨大的提升。
/* * __wt_txn_visible -- * Can the current transaction see the given ID / timestamp? */ static inline bool __wt_txn_visible( WT_SESSION_IMPL *session, uint64_t id, const wt_timestamp_t *timestamp) { if (!__txn_visible_id(session, id)) return (false); /* Transactions read their writes, regardless of timestamps. */ if (F_ISSET(&session->txn, WT_TXN_HAS_ID) && id == session->txn.id) return (true); #ifdef HAVE_TIMESTAMPS { WT_TXN *txn = &session->txn; /* Timestamp check. */ if (!F_ISSET(txn, WT_TXN_HAS_TS_READ) || timestamp == NULL) return (true); return (__wt_timestamp_cmp(timestamp, &txn->read_timestamp) <= 0); } #else WT_UNUSED(timestamp); return (true); #endif }
從上面的代碼可以看到,再引入事務時間戳之后,在可見性判斷時,還會額外檢查時間戳,上層讀取時指定了時間戳讀,則只能看到該時間戳以前的數據。而 MongoDB 在提交事務時,會將 oplog 時間戳跟事務關聯,從而達到 MongoDB Server 層時序與 WiredTiger 層時序一致的目的。
WiredTiger(WT) 事務會打開一個快照,而快照的存在的 WiredTiger cache evict 是有影響的。一個 WT page 上,有N個版本的修改,如果這些修改沒有全局可見(參考 __wt_txn_visible_all),這個 page 是不能 evict 的(參考 __wt_page_can_evict)。
在 3.x 版本里,一個寫請求對數據、索引、oplog的修改會放到一個 WT 事務里,事務的提交由 MongoDB 自己控制,MongoDB 會盡可能快的提交事務,完成寫清求;但 4.0 引入事務之后,事務的提交由應用程序控制,可能出現一個事務修改很多,并且很長時間不提交,這會給 WT cache evict 造成很大的影響,如果大量內存無法 evict,最終就會進入 cache stuck 狀態。
為了盡量減小 WT cache 壓力,MongoDB 4.0 事務功能有一些限制,但事務資源占用超過一定閾值時,會自動 abort 來釋放資源。規則包括
事務的生命周期不能超過 transactionLifetimeLimitSeconds (默認60s),該配置可在線修改
事務修改的文檔數不能超過 1000 ,不可修改
事務修改產生的 oplog 不能超過 16mb,這個主要是 MongoDB 文檔大小的限制, oplog 也是一個普通的文檔,也必須遵守這個約束。
Read as of a timestamp 依賴 WiredTiger 在內存里維護多版本,每個版本跟一個時間戳關聯,只要 MongoDB 層可能需要讀的版本,引擎層就必須維護這個版本的資源,如果保留的版本太多,也會對 WT cache 產生很大的壓力。
WiredTiger 提供設置 oldest timestamp 的功能,允許由 MongoDB 來設置該時間戳,含義是Read as of a timestamp 不會提供更小的時間戳來進行一致性讀,也就是說,WiredTiger 無需維護 oldest timestamp 之前的所有歷史版本。MongoDB 層需要頻繁(及時)更新 oldest timestamp,避免讓 WT cache 壓力太大。
在 3.x 版本里,MongoDB 復制集的回滾動作是在 Server 層面完成,但節點需要回滾時,會根據要回滾的 oplog 不斷應用相反的操作,或從回滾源上讀取最新的版本,整個回滾操作效率很低。
4.0 版本實現了存儲引擎層的回滾機制,當復制集節點需要回滾時,直接調用 WiredTiger 接口,將數據回滾到某個穩定版本(實際上就是一個 Checkpoint),這個穩定版本則依賴于 stable timestamp。WiredTiger 會確保 stable timestamp 之后的數據不會寫到 Checkpoint里,MongoDB 根據復制集的同步狀態,當數據已經同步到大多數節點時(Majority commited),會更新 stable timestamp,因為這些數據已經提交到大多數節點了,一定不會發生 ROLLBACK,這個時間戳之前的數據就都可以寫到 Checkpoint 里了。
MongoDB 需要確保頻繁(及時)的更新 stable timestamp,否則影響 WT Checkpoint 行為,導致很多內存無法釋放。
MongoDB 4.0 支持副本集多文檔事務,并計劃在 4.2 版本支持分片集群事務功能。下圖是從 MongoDB 3.0 引入 WiredTiger 到 4.0 支持多文檔事務的功能迭代圖,可以發現一盤大棋即將上線,敬請期待。
關于MongoDB 4.0 中怎么實現事務就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。