MongoDB 中的事務

ZhanLi發表於2023-11-22

MongoDB 事務

前言

在 MongoDB 中,對單個文件的操作都是原子的。因為可以在單個文件結構中使用內嵌文件和資料獲得資料之間的關係,所以不必跨多個文件和集合進行正規化化,這種
結構特性,避免了很多場景中的對多文件事務的需求。

對於需要多個文件進行原子讀寫的場景,MongoDB 中引入了多文件事務和分散式事務。

  • 在4.0版本中,MongoDB支援副本集上的多文件事務;

  • 在4.2版本中,MongoDB 引入了分散式事務,增加了對分片叢集上多文件事務的支援,併合並了對副本集上多文件事務的現有支援,事務可以跨多個操作、集合、資料庫、文件和分片使用,這種方式事務的實現主要是藉助於兩階段提交協議(2PC)實現的。

如何使用

MongoDB 中從 4.0 開始支援了事務,這裡來看看 MongoDB 中的事務是如何使用的呢?

首先登陸 MongoDB 命令列

mongo -u <name> --port 27017 --host 127.0.0.1 admin  -p <pass>

1、開啟 session

session = db.getMongo().startSession( { readPreference: { mode: "primary" } } );

2、將需要操作的 collection 進行變數繫結

testCollection = session.getDatabase("gleeman").test_explain;
test1Collection = session.getDatabase("gleeman").test_explain_1;

3、開始事務標註,指定MVCC的模式,寫模式

session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

4、拼接執行語句,將需要執行的語句進行事務封裝

try (ClientSession clientSession = client.startSession()) {
    clientSession.startTransaction();
    collection.insertOne(clientSession, docOne);
    collection.insertOne(clientSession, docTwo);
    clientSession.commitTransaction();
}

5、提交事務

session.commitTransaction();

6、關閉session

session.endSession();

事務的原理

MongoDB 中的 WiredTiger 儲存引擎是目前使用最廣泛的,這裡主要介紹下 WiredTiger 中事務的實現原理。

WiredTiger 儲存引擎支援 read-uncommitted 、read-committedsnapshot 3 種事務隔離級別,MongoDB 啟動時預設選擇 snapshot 隔離。

事務和複復制集以及儲存引擎之間的關係

1、事務和複製集

複製集配置下,MongoDB 整個事務在提交時,會記錄一條 oplog,包含了事務所有的操作,備節點拉取 oplog,並在本地重放事務操作。事務 oplog 包含了事務操作的 lsid,txnNumber,以及事務內所有的操作日誌( applyOps 欄位)。

WiredTiger 是如何實現事務和 ACID 呢。WiredTiger 事務主要使用了三個技術 snapshot(事務快照)、MVCC (多版本併發控制)和 redo log(重做日誌)。同時為了實現這三個技術,還定義了一個基於這三個技術的事務物件和全域性事務管理器。

wt_transaction{
	transaction_id:    // 本次事務的全域性唯一的ID,用於標示事務修改資料的版本號
	snapshot_object:   // 當前事務開始或者操作時刻其他正在執行且並未提交的事務集合,用於事務隔離
	operation_array:   // 本次事務中已執行的操作列表,用於事務回滾。
	redo_log_buf:      // 操作日誌緩衝區。用於事務提交後的持久化
	State:             // 事務當前狀態
}

WiredTiger 中的 MVCC 是基於 key/value 中 value 值的連結串列,每個連結串列單元中儲存有當先版本操作的事務 ID 和操作修改後的值。

wt_mvcc{
	transaction_id:    // 本次修改事務的ID
	value:             // 本次修改後的值
}

WiredTiger 中資料修改都是在這個連結串列中進行 append 操作,每次對值的修改都是 append 到連結串列頭,每次讀取值的時候讀是從連結串列頭根據對應的修改事務 transaction_id 和本次事務的 snapshot 來判斷是否可讀,如果不可讀,向連結串列尾方向移動,直到找到都事務可以讀到的資料版本。

什麼是 snapshot 呢?

事務開始或者結束操作之前都會對整個 WiredTiger 引擎內部正在執行的或者將要執行的事務進行一次快照,儲存當時整個引擎的事務狀態,確定那些事務是對自己可見的,哪些事務是自己不可見的。

WiredTiger 中的事務隔離級別

傳統的事務級別都分成下面四種:讀未提交(read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和序列化(serializable )。

  • 讀未提交:一個事務還沒提交時,它的變更就能被別的事務看到,讀取未提交的資料也叫做髒讀;

  • 讀提交:一個事務提交之後,它的變更才能被其他的事務看到;

  • 可重複讀:一個事務執行的過程中看到的資料,總是跟這個事務在啟動時看到的資料是一致的,在此隔離級別下,未提交的變更對其它事務也是不可見的,此隔離級別基本上避免了幻讀;

  • 序列化:這是事務的最高階別,顧名思義就是對於同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。

我們熟知的 MySQL 對於事務隔離級別的實現,可重複讀讀提交 主要是透過 MVCC 來實現,MVCC 的實現主要用到了 undo log 日誌版本鏈和 Read View。序列化 和 讀未提交,主要實現方式是透過加鎖來實現的。

其中 Read View 可以在理解為一個資料的快照,可重複讀隔離級別會在每次啟動的事務的時候生成一個 Read View 記錄下當前事務啟動瞬間,當前所有活躍的事務 ID。具體細節可參見 MySQL中的事務的隔離級別

WiredTiger 儲存引擎支援 read-uncommitted、read-committedsnapshot 3種事務隔離級別,MongoDB 啟動時預設選擇 snapshot 隔離。

  • Read-Uncommited:讀未提交,一個事務還沒提交時,它的變更就能被別的事務看到,讀取未提交的資料也叫做髒讀,WiredTiger 引擎在實現這個隔方式時,就是將事務物件中的 snap_object.snap_array 置為空即可,那麼在讀取 MVCC list 中的版本值時,總是讀取到 MVCC list 連結串列頭上的第一個版本資料,這樣就總是能讀取到最新的資料了;

  • read-committed:一個事務提交之後,它的變更才能被其他的事務看到;這種對於一個長事務可能存在多次讀取,讀取到的值不一樣,因為每次讀取都是讀取的最新提交的資料,WiredTiger 引擎在實現該事務隔離級別,就是在事務在每次執行之前,都對系統機型一次快照,然後在這個事務快照中讀取最新提交的資料;

  • snapshot:快照隔離方式,一個事務開始時,就進行一次快照,並且只會進行一次快照,這樣事務看到的值提交版本,這個值在整個事務過程中看到的都是一樣;

WiredTiger 中對於事務的實現也是基於 MVCC 實現的,MVCC 可以提供基於某個時間點的快照,有了這個快照,就能確定當前事務能看到的資料了,透過這個來實現對應的事務隔離級別,這點也個人感覺和 mysql 中的 Read View 類似,不展開分析了。

WiredTiger 沒有使用傳統的事務獨佔鎖和共享訪問鎖來保證事務隔離,而是透過對系統中寫事務的 snapshot 截圖來實現。這樣做的目的是在保證事務隔離的情況下又能提高系統事務併發的能力。

WiredTiger 事務過程

一般事務有三個階段:開啟事務,執行事務,提交事務。如果事務執行失敗,會進行事務的回滾操作,事務正常執行,最近進行事務的提交 (commit) 即可。

事務開啟

事務開啟的過程中,首先會為事務建立一個事務物件並把這個物件加入到全域性的事務管理器當中,然後根據配置確定事務的隔離級別和 redo_log 的刷盤方式,並將事務狀態設定成執行狀態,最後判斷事務的隔離級別,如果是 snapshot 級的事務隔離,在本次事務執行之前會建立一個系統併發事務的 snapshot 截圖,,儲存當時整個引擎的事務狀態,確定那些事務是對自己可見的,哪些事務是自己不可見的。

事務執行

事務在執行階段,如果是讀操作,不做任何處理,因為讀操作不需要回滾和提交。如果是寫操作,WiredTiger 會對每個操作做詳細的記錄。

這裡就會用使用到上面介紹的事務物件(wt_transaction)中的 operation_array 和 redo_log_buf。

operation_array:主要記錄本次事務中已經提交的操作列表,陣列單元中,會包含一個指向 MVCC list 對應修改版本值的指標,用於事務的回滾。

redo_log_buf: 操作日誌緩衝區。用於事務提交後的持久化。

來描述下具體的更新操作過程:

1、建立一個 mvcclist 的值物件 update;

2、根據事務物件的 transaction_id 和事務狀態判斷是為本次事務建立寫的事務id,如果沒有,為本次事務分配一個事務id,並將事務的狀態設定成 HAS_TXN_ID 狀態;

3、將本次事務的 ID 設定到 update 單元中作為 mvcc 版本號;

4、同時會建立一個 operation 物件,這個物件的指標會指向 update,這個物件會加入到 operation_array 中,用來進行操作事務的回滾;

5、update 會被加入到 mvcclist 的頭部;

6、最後會寫一條 redo_log 到本次事務的 redo_log_buffer 當中。

事務提交

事務提交

提交事務物件中的 redo_log_buf 中的資料到 redo_log_file(重做日誌中),並將 redo_log_file 持久化到磁碟上,清除提交事務物件的 snapshot,再將事務物件的transaction_id 設定成 WT_TNX_NODE,保證其他事務在建立 snapshot 時本次事務的狀態是已提交的狀態。

事務回滾

WiredTiger 引擎對事務的回滾過程比較簡單,首先遍歷 operation_array ,對每個陣列單元對應的 update 事務 id 設定一個 WT_TXN_ABORTED ,標識 mvcc 對應的事務單元被回滾,在其它事務進行 mvcc 讀操作的時候,跳過這個放棄的值即可,整個過程是一個無鎖的操作,高效,簡潔。

事務日誌(journal)

Journal 是一種 WAL(Write Ahead Log)事務日誌,目的是實現事務提交層面的資料持久化。

Journal 是 MongoDB 儲存引擎層面的概念,MongoDB 主要支援的 mmapv1、wiredtiger、mongorocks 等儲存引擎,都⽀持配置 JournalMongoDB 可以基於 Journal 來恢復因為崩潰未及時寫到磁碟的資訊。

Journal 持久化的物件不是修改的資料,而是修改的動作,以日誌形式先儲存到事務日誌快取中,再根據相應的配置按一定的週期,將快取中的日誌資料寫入日誌檔案中。

事務日誌落盤的規則如下。

  • 1、按時間週期落盤。

在預設情況下,以50毫秒為週期,將記憶體中的事務日誌同步到磁碟中的日誌檔案。

  • 2、提交寫操作時強制同步落盤。

當設定寫操作的寫關注為j:true時,強制將此寫操作的事務日誌同步到磁碟中的日誌檔案。

  • 3、事務日誌檔案的大小達到100MB。

總結

1、在4.0版本中,MongoDB支援副本集上的多文件事務;

2、在4.2版本中,MongoDB 引入了分散式事務,增加了對分片叢集上多文件事務的支援,併合並了對副本集上多文件事務的現有支援,事務可以跨多個操作、集合、資料庫、文件和分片使用;

3、MongoDB 中的 WiredTiger 儲存引擎是目前使用最廣泛的,WiredTiger 儲存引擎支援 read-uncommitted 、read-committed 和 snapshot 3 種事務隔離級別,MongoDB 啟動時預設選擇 snapshot 隔離;

  • Read-Uncommited:讀未提交,一個事務還沒提交時,它的變更就能被別的事務看到,讀取未提交的資料也叫做髒讀,WiredTiger 引擎在實現這個隔方式時,就是將事務物件中的 snap_object.snap_array 置為空即可,那麼在讀取 MVCC list 中的版本值時,總是讀取到 MVCC list 連結串列頭上的第一個版本資料,這樣就總是能讀取到最新的資料了;

  • read-committed:一個事務提交之後,它的變更才能被其他的事務看到;這種對於一個長事務可能存在多次讀取,讀取到的值不一樣,因為每次讀取都是讀取的最新提交的資料,WiredTiger 引擎在實現該事務隔離級別,就是在事務在每次執行之前,都對系統機型一次快照,然後在這個事務快照中讀取最新提交的資料;

  • snapshot:快照隔離方式,一個事務開始時,就進行一次快照,並且只會進行一次快照,這樣事務看到的值提交版本,這個值在整個事務過程中看到的都是一樣;

4、WiredTiger 中對於事務的實現也是基於 MVCC 實現的,MVCC 可以提供基於某個時間點的快照,有了這個快照,就能確定當前事務能看到的資料了,透過這個來實現對應的事務隔離級別,這點也個人感覺和 mysql 中的 Read View 類似,不展開分析了;

5、WiredTiger 沒有使用傳統的事務獨佔鎖和共享訪問鎖來保證事務隔離,而是透過對系統中寫事務的 snapshot 截圖來實現。這樣做的目的是在保證事務隔離的情況下又能提高系統事務併發的能力。

參考

【MongoDB事務】https://docs.mongoing.com/transactions
【WiredTiger的事務實現詳解 】https://blog.csdn.net/daaikuaichuan/article/details/97893552
【MongoDB中併發控制】https://blog.csdn.net/baijiwei/article/details/89436861

相關文章