掌握WiredTiger存儲引擎,幫你解決分布式事務(wù)難題!
??點擊“博文視點Broadview”,獲取更多書訊

MongoDB作為領(lǐng)先的NoSQL,為了支撐更多的需求場景,也在不斷完善其功能。從早期支持大吞吐量讀/寫操作的MMAPv1存儲引擎,到引入支持高并發(fā)操作的WiredTiger存儲引擎,以及對事務(wù)功能的持續(xù)演進(jìn),MongoDB不僅保留了最初的架構(gòu)優(yōu)勢,同時又汲取了其他數(shù)據(jù)庫的優(yōu)點。
MongoDB從 3.0版本引入WiredTiger存儲引擎之后開始支持事務(wù),MongoDB 3.6之前的版本只能支持單文檔的事務(wù),從MongoDB 4.0版本開始支持復(fù)制集部署模式下的事務(wù),從MongoDB 4.2版本開始支持分片集群中的事務(wù)。
本文就主要對MongoDB事務(wù)的基本原理、事務(wù)的snapshot隔離、實現(xiàn)事務(wù)間并發(fā)操作的MVCC并發(fā)控制機制,以及事務(wù)日志做一些介紹!
事務(wù)的基本原理
與關(guān)系型數(shù)據(jù)庫一樣,MongoDB事務(wù)同樣具有ACID特性,說明如下。
原子性(Automicity):一個事務(wù)要么完全執(zhí)行成功,要么不做任何改變。
一致性(Consistency):當(dāng)多個事務(wù)并行執(zhí)行時,元素的屬性在每個事務(wù)中保持一致。
隔離性(Isolation):當(dāng)多個事務(wù)同時執(zhí)行時,互不影響。WiredTiger本身支持多種不同類型的隔離級別,如讀-未提交(read-uncommitted)、讀-已提交(read-committed)和快照(snapshot)隔離。MongoDB默認(rèn)選擇的是快照隔離。
持久性(Durability):一旦提交事務(wù),數(shù)據(jù)的更改就不會丟失。
在不同隔離級別下,一個事務(wù)的生命周期內(nèi),可能出現(xiàn)臟讀、不可重復(fù)讀、幻讀等現(xiàn)象。
下面介紹這3種現(xiàn)象出現(xiàn)的場景與含義。
1. 臟讀現(xiàn)象
例如,某款手機在數(shù)據(jù)庫中的庫存還有1部,客戶A發(fā)起一個查詢手機庫存的事務(wù),同時,客戶B發(fā)起了一個購買手機的事務(wù)(但未提交事務(wù)),此時客戶A讀到手機庫存為0部,認(rèn)為售完了。但客戶B突然不想購買這款手機了,于是回滾了此事務(wù),手機庫存又變?yōu)?部,客戶A讀到的手機庫存為0部就是一個臟讀數(shù)據(jù),如下圖所示。


2. 不可重復(fù)讀現(xiàn)象
例如,某款手機在數(shù)據(jù)庫中的庫存還有1部,客戶A發(fā)起一個查詢手機庫存的事務(wù)(事務(wù)還未完成),讀到其值為1。同時,客戶B發(fā)起了一個購買手機的事務(wù)(提交了事務(wù)),此時客戶A再次查詢手機庫存,讀到其值為0??蛻鬉在同一個事務(wù)中讀到的同一條記錄的取值不一樣,這種現(xiàn)象就是不可重復(fù)讀,如下圖所示。


3. 幻讀現(xiàn)象
例如,某款手機在數(shù)據(jù)庫中的庫存還有1部,客戶A發(fā)起一個購買手機的事務(wù)(事務(wù)還未完成),讀到其值為1。同時,管理員B發(fā)起了一個增加1部手機的事務(wù)(提交了事務(wù)),此時客戶A再次查詢手機庫存,讀到其值為1(有新增數(shù)據(jù))。客戶A在同一個事務(wù)中本來應(yīng)該讀到的庫存值為0,認(rèn)為手機已經(jīng)售完,但發(fā)現(xiàn)庫存中還有1部手機,客戶A兩次讀到的數(shù)據(jù)集不一樣,這種現(xiàn)象就是幻讀,如下圖所示。

下面介紹與事務(wù)相關(guān)的數(shù)據(jù)結(jié)構(gòu),如下圖所示。

其中,
(1)id字段:這是事務(wù)的全局唯一標(biāo)識,通過分析它與具體的操作關(guān)聯(lián),就能夠知道一個事務(wù)包含哪些操作。
(2)snapshot_data字段:MongoDB使用的是快照隔離級別的事務(wù),這個字段用于保存事務(wù)的快照信息,具體來說它會有snap_min和snap_max兩個屬性,通過這兩個屬性能夠計算一個事務(wù)開始時的數(shù)據(jù)范圍,每個事務(wù)開始時都會構(gòu)造一個這樣的快照。
(3)commit_timestamp字段:表示事務(wù)提交的時間。
(4)durable_timestamp字段:表示事務(wù)修改的數(shù)據(jù)已持久化的時間,與具體操作中的durable_ts字段關(guān)聯(lián)。
(5)prepare_timestamp字段:表示事務(wù)開始準(zhǔn)備的時間。
(6)WT_TXN_OP字段:包含事務(wù)的修改操作,用于事務(wù)回滾和生成事務(wù)日志(Journal)。
(7)logrec字段:表示事務(wù)日志的緩存,用于在內(nèi)存中保存事務(wù)日志(對于MongoDB來說Journal日志就是事務(wù)日志)。
事務(wù)的snapshot隔離
WiredTiger存儲引擎支持read-uncommitted、read-committed和snapshot3種事務(wù)隔離級別,MongoDB啟動時默認(rèn)選擇snapshot隔離。
事務(wù)開始時,系統(tǒng)會創(chuàng)建一個快照,從已提交的事務(wù)中獲取行版本數(shù)據(jù),如果行版本數(shù)據(jù)標(biāo)識的事務(wù)尚未提交,則從更早的事務(wù)中獲取已提交的行版本數(shù)據(jù)作為其事務(wù)開始時的值。
通過事務(wù)可以看到其他還未提交的事務(wù)修改的行版本數(shù)據(jù),但不會看到事務(wù)id大于snap_max的事務(wù)修改的數(shù)據(jù)。
快照數(shù)據(jù)的獲取流程如下圖所示。

假設(shè)圖中的5個事務(wù)對同一條記錄進(jìn)行操作,E事務(wù)開始時,生成的快照數(shù)據(jù)包含B、D兩個未完成的事務(wù),同時獲取離它最近且完成了的C事務(wù)修改后的值作為事務(wù)開始時的取值,即2。
如果E事務(wù)為寫事務(wù),對庫存值進(jìn)行修改,則會進(jìn)行沖突檢測,以防止對過期數(shù)據(jù)的修改,保證數(shù)據(jù)的一致性(如D事務(wù)在E事務(wù)提交之前完成,行版本已發(fā)生變化,若E事務(wù)還要進(jìn)行修改,則提交時會產(chǎn)生沖突)。
通過一段代碼加深對快照隔離級別事務(wù)的認(rèn)識:
session1 = client.start_session() //開啟一個sessionsession1.start_transaction() //在session內(nèi)部,開啟一個事務(wù)inventory.insert_one({'_id': 4, 'model':'switch', 'count': 200}, session= session)doc1 = inventory.find_one({'_id': 4}, session=session1)pprint.pprint(doc1)doc2 = inventory.find_one({'_id': 4})pprint.pprint(doc2)session1.commit_transaction() //提交事務(wù)doc3 = inventory.find_one({'_id': 4})pprint.pprint(doc3)session1.end_session() //結(jié)束session
任何事務(wù)都是封裝在一個session中進(jìn)行的。
MVCC并發(fā)控制機制
要實現(xiàn)事務(wù)之間的并發(fā)操作,可以使用鎖機制或MVCC控制等。對于WiredTiger來說,使用MVCC控制來實現(xiàn)并發(fā)操作,相較于其他鎖機制的并發(fā),MVCC實現(xiàn)的是一種樂觀并發(fā)機制。
MVCC并發(fā)控制機制如下圖所示:


(1)A事務(wù)首先從表中讀取要修改的行數(shù)據(jù),讀取的庫存值為100,行記錄的版本號為1。
(2)B事務(wù)也從中讀取要修改的相同行數(shù)據(jù),讀取的庫存值為100,行記錄的版本號為1。
(3)A事務(wù)修改庫存值后提交,同時行記錄版本號加1,變?yōu)?,大于A事物一開始讀取行記錄版本號1,A事務(wù)可以提交。
(4)但B事務(wù)提交時發(fā)現(xiàn)此時行記錄版本號已經(jīng)變?yōu)?,產(chǎn)生沖突,B事務(wù)提交失敗。
(5)B事務(wù)嘗試重新提交,此時再次讀取的版本號為2,加1后版本號變?yōu)?,不會產(chǎn)生沖突,正常提交B事務(wù)。
通過代碼分析事務(wù)的并發(fā)與沖突。
session1 = client.start_session() //開啟一個session1session1.start_transaction() //在session1中開啟一個事務(wù)1inventory.delete_one({'_id':4}, session=session1)doc1 = inventory.find_one({'_id': 4},session=session1)pprint.pprint(doc1) //輸出none,說明在事務(wù)中已經(jīng)刪除session2 = client.start_session() //開啟一個session2session2.start_transaction() //在session2中開啟一個事務(wù)2inventory.delete_one({'_id':4}, session=session2) //執(zhí)行產(chǎn)生事務(wù)沖突session1.abort_transaction() //終止事務(wù)1session1.end_session() //結(jié)束session1session2.abort_transaction() //終止事務(wù)2session2.end_session() //結(jié)束session2doc2 = inventory.find_one({'_id': 4}) //隱式開啟第3個session和事務(wù)pprint.pprint(doc2) //在事務(wù)外可以找到,說明事務(wù)1被終止后回滾了
事務(wù)日志(Journal)
Journal是一種WAL(Write Ahead Log)事務(wù)日志,目的是實現(xiàn)事務(wù)提交層面的數(shù)據(jù)持久化。
Journal持久化的對象不是修改的數(shù)據(jù),而是修改的動作,以日志形式先保存到事務(wù)日志緩存中,再根據(jù)相應(yīng)的配置按一定的周期,將緩存中的日志數(shù)據(jù)寫入日志文件中。
事務(wù)日志落盤的規(guī)則如下。
(1)按時間周期落盤。
在默認(rèn)情況下,以50毫秒為周期,將內(nèi)存中的事務(wù)日志同步到磁盤中的日志文件。
(2)提交寫操作時強制同步落盤。
當(dāng)設(shè)置寫操作的寫關(guān)注為j:true時,強制將此寫操作的事務(wù)日志同步到磁盤中的日志文件。
(3)事務(wù)日志文件的大小達(dá)到100MB。
以上內(nèi)容節(jié)選自《MongoDB核心原理與實踐》一書,歡迎閱讀本書更多精彩內(nèi)容。

下單立減50,快快掃碼搶購吧!
如果喜歡本文 歡迎 在看丨留言丨分享至朋友圈 三連 熱文推薦
▼點擊閱讀原文,了解本書詳情~
