《解密未來數據庫設計:MongoDB新存儲引擎WiredTiger實現(事務篇)》要點:
本文介紹了解密未來數據庫設計:MongoDB新存儲引擎WiredTiger實現(事務篇),希望對您有用。如果有疑問,可以聯(lián)系我們。
相關主題:非關系型數據庫
導語:計算機硬件在飛速發(fā)展,數據規(guī)模在急速膨脹,但是數據庫仍然使用是十年以前的架構體系,WiredTiger 嘗試打破這一切,充分利用多核與大內存時代,開發(fā)一種真正滿足未來大數據管理所需的數據庫.本文由袁榮喜向「高可用架構」投稿,介紹對 WiredTiger 源代碼學習過程中對數據庫設計的感悟.
袁榮喜,學霸君工程師,2015年加入學霸君,負責學霸君的網絡實時傳輸和分布式系統(tǒng)的架構設計和實現,專注于基礎技術領域,在網絡傳輸、數據庫內核、分布式系統(tǒng)和并發(fā)編程方面有一定了解.
WiredTiger 從被 MongoDB 收購到成為 MongoDB 的默認存儲引擎的一年半,得到了迅猛的發(fā)展,也逐步被外部熟知.
現代計算機近 20 年來 CPU 的計算能力和內存容量飛速發(fā)展,但磁盤的訪問速度并沒有得到相應的提高,WT 就是在這樣的一個情況下研發(fā)出來,它設計了充分利用 CPU 并行計算的內存模型的無鎖并行框架,使得 WT 引擎在多核 CPU 上的表現優(yōu)于其他存儲引擎.
針對磁盤存儲特性,WT 實現了一套基于 BLOCK/Extent 的友好的磁盤訪問算法,使得 WT 在數據壓縮和磁盤 I/O 訪問上優(yōu)勢明顯.實現了基于 snapshot 技術的 ACID 事務,snapshot 技術大大簡化了 WT 的事務模型,摒棄了傳統(tǒng)的事務鎖隔離又同時能保證事務的 ACID.WT 根據現代內存容量特性實現了一種基于 Hazard Pointer 的 LRU cache 模型,充分利用了內存容量的同時又能擁有很高的事務讀寫并發(fā).
在本文中,我們主要針對 WT 引擎的事務來展開分析,來看看它的事務是如何實現的.說到數據庫事務,必然先要對事務這個概念和 ACID 簡單的介紹.
事務就是通過一系列操作來完成一件事情,在進行這些操作的過程中,要么這些操作完全執(zhí)行,要么這些操作全不執(zhí)行,不存在中間狀態(tài),事務分為事務執(zhí)行階段和事務提交階段.一般說到事務,就會想到它的特性— ACID,那么什么是 ACID 呢?我們先用一個現實中的例子來說明:AB 兩同學賬號都有 1,000 塊錢,A 通過銀行轉賬向 B 轉了 100,這個事務分為兩個操作,即從 A 同學賬號扣除 100,向 B 同學賬號增加 100.
組成事務的系列操作是一個整體,要么全執(zhí)行,要么不執(zhí)行.通過上面例子就是從 A 同學扣除錢和向 B 同學增加 100 是一起發(fā)生的,不可能出現扣除了 A 的錢,但沒增加 B 的錢的情況.
在事務開始之前和事務結束以后,數據庫的完整性和狀態(tài)沒有被破壞.這個怎么理解呢?就是 A、B 兩人在轉賬錢的總和是 2,000,轉賬后兩人的總和也必須是 2,000.不會因為這次轉賬事務破壞這個狀態(tài).
多個事務在并發(fā)執(zhí)行時,事務執(zhí)行的中間狀態(tài)是其他事務不可訪問的.A 轉出 100 但事務沒有確認提交,這時候銀行人員對其賬號查詢時,看到的應該還是 1,000,不是 900.
事務一旦提交生效,其結果將永久保存,不受任何故障影響.A 轉賬一但完成,那么 A 就是 900,B 就是 1,100,這個結果將永遠保存在銀行的數據庫中,直到他們下次交易事務的發(fā)生.
知道了基本的事務概念和 ACID 后,來看看 WT 引擎是怎么來實現事務和 ACID.要了解實現先要知道它的事務的構造和使用相關的技術,WT 在實現事務的時使用主要是使用了三個技術:
為了實現這三個技術,它還定義了一個基于這三個技術的事務對象和全局事務管理器.事務對象描述如下
wt_transaction{
transaction_id: ? ?本次事務的全局唯一的ID,用于標示事務修改數據的版本號
snapshot_object: ? 當前事務開始或者操作時刻其他正在執(zhí)行且并未提交的事務集合,用于事務隔離
operation_array: ? 本次事務中已執(zhí)行的操作列表,用于事務回滾.
redo_log_buf: ? ? ?操作日志緩沖區(qū).用于事務提交后的持久化
State: ? ? ? ? ? ? 事務當前狀態(tài)
}
WT 中的 MVCC?是基于 key/value 中 value 值的鏈表,這個鏈表單元中存儲有當先版本操作的事務 ID 和操作修改后的值.描述如下:
wt_mvcc{
transaction_id: ? ?本次修改事務的ID
value: ? ? ? ? ? ? 本次修改后的值
}
WT 中的數據修改都是在這個鏈表中進行 append 操作,每次對值做修改都是 append 到鏈表頭上,每次讀取值的時候讀是從鏈表頭根據值對應的修改事務 transaction_id 和本次讀事務的 snapshot 來判斷是否可讀,如果不可讀,向鏈表尾方向移動,直到找到讀事務能都的數據版本.樣例如下:
圖1,點擊圖片可以全屏縮放
上圖中,事務 T0 發(fā)生的時刻最早,T5 發(fā)生的時刻最晚.T1/T2/T4 是對記錄做了修改.那么在 MVCC list 當中就會增加 3 個版本的數據,分別是 11/12/14.如果事務都是基于 snapshot 級別的隔離,T0 只能看到 T0 之前提交的值 10,讀事務 T3 訪問記錄時它能看到的值是 11,T5 讀事務在訪問記錄時,由于 T4 未提交,它也只能看到 11 這個版本的值.這就是 WT 的 MVCC 基本原理.
上面多次提及事務的 snapshot,那到底什么是事務的 snapshot 呢?其實就是事務開始或者進行操作之前對整個 WT 引擎內部正在執(zhí)行或者將要執(zhí)行的事務進行一次快照,保存當時整個引擎所有事務的狀態(tài),確定哪些事務是對自己見的,哪些事務都自己是不可見.說白了就是一些列事務 ID 區(qū)間.WT 引擎整個事務并發(fā)區(qū)間示意圖如下:
圖2,點擊圖片可以全屏縮放
WT 引擎中的 snapshot_oject 是有一個最小執(zhí)行事務 snap_min、一個最大事務 snap max 和一個處于 [snap_min, snap_max] 區(qū)間之中所有正在執(zhí)行的寫事務序列組成.如果上圖在 T6 時刻對系統(tǒng)中的事務做一次 snapshot,那么產生的
snapshot_object = {
snap_min=T1,
snap_max=T5,
snap_array={T1, T4, T5},
};
T6 能訪問的事務修改有兩個區(qū)間:所有小于 T1 事務的修改 [0, T1) 和 [snap_min, snap_max] ?區(qū)間已經提交的事務 T2 的修改.換句話說,凡是出現在 snap_array 中或者事務 ID 大于 snap_max 的事務的修改對事務 T6 是不可見的.如果 T1 在建立 snapshot 之后提交了,T6 也是不能訪問到 T1 的修改.這個就是 snapshot 方式隔離的基本原理.
通過上面的 snapshot 的描述,我們可以知道要創(chuàng)建整個系統(tǒng)事務的快照截屏,就需要一個全局的事務管理來進行事務快照時的參考,在 WT 引擎中是如何定義這個全局事務管理器的呢?在 CPU 多核多線程下,它是如何來管理事務并發(fā)的呢?下面先來分析它的定義:
wt_txn_global{
current_id: ? ? ? 全局寫事務ID產生種子,一直遞增
oldest_id: ? ? ? ?系統(tǒng)中最早產生且還在執(zhí)行的寫事務ID
transaction_array: 系統(tǒng)事務對象數組,保存系統(tǒng)中所有的事務對象
scan_count: ?正在掃描transaction_array數組的線程事務數,用于建立snapshot過程的無鎖并發(fā)
}
transaction_array 保存的是圖 2 正在執(zhí)行事務的區(qū)間的事務對象序列.在建立 snapshot 時,會對整個 transaction_array 做掃描,確定 snap_min/snap_max/snap_array 這三個參數和更新 oldest_id,在掃描的過程中,凡是 transaction_id 不等于 WT_TNX_NONE 都認為是在執(zhí)行中且有修改操作的事務,直接加入到 snap_array 當中.整個過程是一個無鎖操作過程,這個過程如下:
圖3,點擊圖片可以全屏縮放
創(chuàng)建 snapshot 快照的過程在 WT 引擎內部是非常頻繁,尤其是在大量自動提交型的短事務執(zhí)行的情況下,由創(chuàng)建 snapshot 動作引起的 CPU 競爭是非常大的開銷,所以這里 WT 并沒有使用 spin lock,而是采用了上圖的一個無鎖并發(fā)設計,這種設計遵循了我們開始說的并發(fā)設計原則.
從 WT 引擎創(chuàng)建事務 snapshot 的過程中,現在可以確定,snapshot 的對象是有寫操作的事務,純讀事務是不會被 snapshot 的,因為 snapshot 的目的是隔離 MVCC list 中的記錄,通過 MVCC 中 value 的事務 ID 與讀事務的 snapshot 進行版本讀取,與讀事務本身的 ID 是沒有關系.
在 WT 引擎中,開啟事務時,引擎會將一個 WT_TNX_NONE(= 0) 的事務 ID 設置給開啟的事務,當它第一次對事務進行寫時,會在數據修改前通過全局事務管理器中的 current_id 來分配一個全局唯一的事務 ID.這個過程也是通過 CPU 的 CAS_ADD 原子操作完成的無鎖過程.
一般事務是兩個階段:事務執(zhí)行和事務提交.在事務執(zhí)行前,我們需要先創(chuàng)建事務對象并開啟它,然后才開始執(zhí)行,如果執(zhí)行遇到沖突和或者執(zhí)行失敗,我們需要回滾事務(rollback).如果執(zhí)行都正常完成,最后只需要提交(commit)它即可.
從上面的描述可以知道事務過程有:創(chuàng)建開啟、執(zhí)行、提交和回滾.從這幾個過程中來分析 WT 是怎么實現這幾個過程的.
WT 事務開啟過程中,首先會為事務創(chuàng)建一個事務對象并把這個對象加入到全局事務管理器當中,然后通過事務配置信息確定事務的隔離級別和 redo log 的刷盤方式并將事務狀態(tài)設為執(zhí)行狀態(tài),最后判斷如果隔離級別是 ISOLATION_SNAPSHOT(snapshot 級的隔離),在本次事務執(zhí)行前創(chuàng)建一個系統(tǒng)并發(fā)事務的 snapshot.至于為什么要在事務執(zhí)行前創(chuàng)建一個 snapshot,在后面 WT 事務隔離章節(jié)詳細介紹.
事務在執(zhí)行階段,如果是讀操作,不做任何記錄,因為讀操作不需要回滾和提交.如果是寫操作,WT 會對每個寫操作做詳細的記錄.在上面介紹的事務對象(wt_transaction)中有兩個成員,一個是操作 operation_array,一個是 redo_log_buf.這兩個成員是來記錄修改操作的詳細信息,在 operation_array 的數組單元中,包含了一個指向 MVCC list 對應修改版本值的指針.詳細的更新操作流程如下:
示意圖如下:
WT 引擎對事務的提交過程比較簡單,先將要提交的事務對象中的 redo_log_buf 中的數據寫入到 redo log file(重做日志文件)中,并將 redo log file 持久化到磁盤上.清除提交事務對象的 snapshot object,再將提交的事務對象中的 transaction_id 設置為 WT_TNX_NONE,保證其他事務在創(chuàng)建系統(tǒng)事務 snapshot 時本次事務的狀態(tài)是已提交的狀態(tài).
WT 引擎對事務的回滾過程也比較簡單,先遍歷整個operation_array,對每個數組單元對應 update 的事務 id 設置以為一個 WT_TXN_ABORTED(= uint64_max),標示 MVCC 對應的修改單元值被回滾,在其他讀事務進行 MVCC 讀操作的時候,跳過這個放棄的值即可.整個過程是一個無鎖操作,高效、簡潔.
傳統(tǒng)的數據庫事務隔離分為:
WT 引擎并沒有按照傳統(tǒng)的事務隔離實現這四個等級,而是基于 snapshot 的特點實現了自己的 Read-Uncommited、Read-Commited 和一種叫做 snapshot-Isolation(快照隔離)的事務隔離方式.
在 WT 中不管是選用的是那種事務隔離方式,它都是基于系統(tǒng)中執(zhí)行事務的快照來實現的.那來看看 WT 是怎么實現上面三種方式?
圖5,點擊圖片可以全屏縮放
Read-Uncommited(未提交讀)隔離方式的事務在讀取數據時總是讀取到系統(tǒng)中最新的修改,哪怕是這個修改事務還沒有提交一樣讀取,這其實就是一種臟讀.WT 引擎在實現這個隔方式時,就是將事務對象中的 snap_object.snap_array 置為空即可,在讀取 MVCC list 中的版本值時,總是讀取到 MVCC list 鏈表頭上的第一個版本數據.
舉例說明,在圖 5 中,如果 T0/T3/T5 的事務隔離級別設置成 Read-uncommited 的話,T1/T3/T5 在 T5 時刻之后讀取系統(tǒng)的值時,讀取到的都是 14.一般數據庫不會設置成這種隔離方式,它違反了事務的 ACID 特性.可能在一些注重性能且對臟讀不敏感的場景會采用,例如網頁 cache.
Read-Commited(提交讀)隔離方式的事務在讀取數據時總是讀取到系統(tǒng)中最新提交的數據修改,這個修改事務一定是提交狀態(tài).這種隔離級別可能在一個長事務多次讀取一個值的時候前后讀到的值可能不一樣,這就是經常提到的“幻象讀”.在 WT 引擎實現 read-commited 隔離方式就是事務在執(zhí)行每個操作前都對系統(tǒng)中的事務做一次快照,然后在這個快照上做讀寫.
還是來看圖 5,T5 事務在 T4 事務提交之前它進行讀取前做事務
snapshot={
snap_min=T2,
snap_max=T4,
snap_array={T2,T4},
};
在讀取 MVCC list 時,12 和 14 修改對應的事務 T2/T4 都出現在 snap_array 中,只能再向前讀取 11,11 是 T1 的修改,而且 T1 沒有出現在 snap_array,說明 T1 已經提交,那么就返回 11 這個值給 T5.
之后事務 T2 提交,T5 在它提交之后再次讀取這個值,會再做一次
snapshot={
snap_min=T4,
snap_max=T4,
snap_array={T4},
},
這時在讀取 MVCC list 中的版本時,就會讀取到最新的提交修改 12.
Snapshot-Isolation(快照隔離)隔離方式是讀事務開始時看到的最后提交的值版本修改,這個值在整個讀事務執(zhí)行過程只會看到這個版本,不管這個值在這個讀事務執(zhí)行過程被其他事務修改了幾次,這種隔離方式不會出現“幻象讀”.WT 在實現這個隔離方式很簡單,在事務開始時對系統(tǒng)中正在執(zhí)行的事務做一個 snapshot,這個 snapshot 一直沿用到事務提交或者回滾.還是來看圖 5, T5 事務在開始時,對系統(tǒng)中的執(zhí)行的寫事務做
snapshot={
snap_min=T2,
snap_max=T4,
snap_array={T2,T4}
},
在他讀取值時讀取到的是 11.即使是 T2 完成了提交,但 T5 的 snapshot 執(zhí)行過程不會更新,T5 讀取到的依然是 11.
這種隔離方式的寫比較特殊,就是如果有對事務看不見的數據修改,事務嘗試修改這個數據時會失敗回滾,這樣做的目的是防止忽略不可見的數據修改.
通過上面對三種事務隔離方式的分析,WT 并沒有使用傳統(tǒng)的事務獨占鎖和共享訪問鎖來保證事務隔離,而是通過對系統(tǒng)中寫事務的 snapshot 來實現.這樣做的目的是在保證事務隔離的情況下又能提高系統(tǒng)事務并發(fā)的能力.
通過上面的分析可以知道 WT 在事務的修改都是在內存中完成的,事務提交時也不會將修改的 MVCC list 當中的數據刷入磁盤,WT 是怎么保證事務提交的結果永久保存呢?
WT 引擎在保證事務的持久可靠問題上是通過 redo log(重做操作日志)的方式來實現的,在本文的事務執(zhí)行和事務提交階段都有提到寫操作日志.WT 的操作日志是一種基于 K/V 操作的邏輯日志,它的日志不是基于 btree page 的物理日志.說的通俗點就是將修改數據的動作記錄下來,例如:插入一個 key = 10, value = 20 的動作記錄在成:
{
Operation = insert,(動作)
Key = 10,
Value = 20
};
將動作記錄的數據以 append 追加的方式寫入到 wt_transaction 對象中 redo_log_buf 中,等到事務提交時將這個 redo_log_buf 中的數據已同步寫入的方式寫入到 WT 的重做日志的磁盤文件中.如果數據庫程序發(fā)生異常或者崩潰,可以通過上一個 checkpoint(檢查點)位置重演磁盤上這個磁盤文件來恢復已經提交的事務來保證事務的持久性.
根據上面的描述,有幾個問題需要搞清楚:
1、操作日志格式怎么設計?
2、在事務并發(fā)提交時,各個事務的日志是怎么寫入磁盤的?
3、日志是怎么重演的?它和 checkpoint 的關系是怎樣的?
在分析這三個問題前先來看 WT 是怎么管理重做日志文件的,在 WT 引擎中定義一個叫做 LSN 序號結構,操作日志對象是通過 LSN 來確定存儲的位置的,LSN 就是 Log Sequence Number(日志序列號),它在 WT 的定義是文件序號加文件偏移,
wt_lsn{
file: ? ? ?文件序號,指定是在哪個日志文件中
offset: ? ?文件內偏移位置,指定日志對象文件內的存儲文開始位置
}
WT 就是通過這個 LSN 來管理重做日志文件的.
WT 引擎的操作日志對象(以下簡稱為 logrec)對應的是提交的事務,事務的每個操作被記錄成一個 logop 對象,一個 logrec 包含多個 logop,logrec 是一個通過精密序列化事務操作動作和參數得到的一個二進制 buffer,這個 buffer的數據是通過事務和操作類型來確定其格式的.
WT 中的日志分為 4 類,分別是:
這里介紹和執(zhí)行事務密切先關的 LOGREC_COMMIT,這類日志里面由根據 K/V 的操作方式分為:
這幾種操作都會記錄操作時的 key,根據操作方式填寫不同的其他參數,例如:update 更新操作,就需要將 value 填上.除此之外,日志對象還會攜帶 btree 的索引文件 ID、提交事務的 ID 等,整個 logrec 和 logop 的關系結構圖如下:
圖6,點擊圖片可以全屏縮放
對于上圖中的 logrec header 中的為什么會出現兩個長度字段:logrec 磁盤上的空間長度和在內存中的長度,因為 logrec 在刷入磁盤之前會進行空間壓縮,磁盤上的長度和內存中的長度就不一樣.壓縮是根據系統(tǒng)配置可選的.
WT 引擎在采用 WAL(Write-Ahead Log)方式寫入日志,WAL 通俗點說就是說在事務所有修改提交前需要將其對應的操作日志寫入磁盤文件.在事務執(zhí)行的介紹小節(jié)中我們介紹是在什么時候寫日志的,這里我們來分析事務日志是怎么寫入到磁盤上的,整個寫入過程大致分為下面幾個階段:
1、事務在執(zhí)行第一個寫操作時,先會在事務對象(wt_transaction)中的 redo_log_buf 的緩沖區(qū)上創(chuàng)建一個 logrec 對象,并將 logrec 中的事務類型設置成 LOGREC_COMMIT.
2、然后在事務執(zhí)行的每個寫操作前生成一個 logop 對象,并加入到事務對應的 logrec 中.
3、在事務提交時,把 logrec 對應的內容整體寫入到一個全局 log 對象的 slot buffer 中并等待寫完成信號.
4、Slot buffer 會根據并發(fā)情況合并同時發(fā)生的提交事務的 logrec,然后將合并的日志內容同步刷入磁盤(sync file),最后告訴這個 slot buffer 對應所有的事務提交刷盤完成.
5、提交事務的日志完成,事務的執(zhí)行結果也完成了持久化.
整個過程的示意圖如下:
圖7,點擊圖片可以全屏縮放
WT 為了減少日志刷盤造成寫 IO,對日志刷盤操作做了大量的優(yōu)化,實現一種類似 MySQL 組提交的刷盤方式.
這種刷盤方式會將同時發(fā)生提交的事務日志合并到一個 slot buffer 中,先完成合并的事務線程會同步等待一個完成刷盤信號,最后完成日志數據合并的事務線程將 slot buffer 中的所有日志數據 sync 到磁盤上并通知在這個 slot buffer 中等待其他事務線程刷盤完成.
并發(fā)事務的 logrec 合并到 slot buffer 中的過程是一個完全無鎖的過程,這減少了必要的 CPU 競爭和操作系統(tǒng)上下文切換.為了這個無鎖設計 WT 在全局的 log 管理中定義了一個 acitve_ready_slot 和一個 slot_pool 數組結構,大致如下定義:
wt_log{
. . .
active_slot:準備就緒且可以作為合并logrec的slot buffer對象
slot_pool:系統(tǒng)所有slot buffer對象數組,包括:正在合并的、準備合并和閑置的slot buffer.
}
slot buffer 對象是一個動態(tài)二進制數組,可以根據需要進行擴大.定義如下:
wt_log_slot{
. . .
state: ? ? ? ? ?當前 slot 的狀態(tài),ready/done/written/free 這幾個狀態(tài)
buf: 緩存合并 logrec 的臨時緩沖區(qū)
group_size: 需要提交的數據長度
slot_start_offset: 合并的logrec存入log file中的偏移位置
. . .
}
通過一個例子來說明這個無鎖過程,假如在系統(tǒng)中 slot_pool 中的 slot 個數為16,設置的 slot buffer 大小為 4KB,當前 log 管理器中的 active_slot 的 slot_start_offset=0,有 4 個事務(T1、T2、T3、T4)同時發(fā)生提交,他們對應的日志對象分別是 logrec1、logrec2、logrec3 和 logrec4.
Logrec1 size = 1KB, ?logrec2 szie = 2KB, logrec3 size = 2KB, logrec4 size = 5KB.他們合并和寫入的過程如下:
1、T1事 務在提交時,先會從全局的 log 對象中的 active_slot 發(fā)起一次 JOIN 操作,join 過程就是向 active_slot 申請自己的合并位置和空間,logrec1_size + slot_start_offset < slot_size 并且 slot 處于 ready 狀態(tài),那 T1 事務的合并位置就是 active_slot[0, 1KB],slot_group_size = 1KB
2、這是 T2 同時發(fā)生提交也要合并 logrec,也重復第 1 部 JOIN 操作,它申請到的位置就是 active_slot [1KB, 3KB], slot_group_size = 3KB.
3、在T1事務 JOIN 完成后,它會判斷自己是第一個 JOIN 這個 active_slot 的事務,判斷條件就是返回的寫入位置 slot_offset=0.如果是第一個它立即會將 active_slot 的狀態(tài)從 ready 狀態(tài)置為 done 狀態(tài),并未后續(xù)的事務從 slot_pool 中獲取一個空閑的 active_slot_new 來頂替自己合并數據的工作.
4、與此同時 T2 事務 JOIN 完成之后,它也是進行這個過程的判斷,T2 發(fā)現自己不是第一個,它將會等待 T1 將 active_slot 置為 done.
5、T1 和 T2 都獲取到了自己在 active_slot 中的寫入位置,active_slot 的狀態(tài)置為 done 時,T1 和 T2 分別將自己的 logrec 寫入到對應 buffer 位置.假如在這里 T1 比 T2 先將數據寫入完成,T1 就會等待一個 slot_buffer 完全刷入磁盤的信號,而 T2 寫入完成后會將 slot_buffer 中的數據寫入 log 文件,并對 log 文件做 sync 刷入磁盤的操作,最高發(fā)送信號告訴 T1 同步刷盤完成,T1 和 T2 各自返回,事務提交過程的日志刷盤操作完成.
那這里有幾種其他的情況,假如在第 2 步運行的完成后,T3 也進行 JOIN 操作,這個時候?slot_size(4KB) < slot_group_size(3KB)+ logrec_size(2KB),T3 不 JOIN 當時的 active_slot,而是自旋等待 active_slot_new 頂替 active_slot 后再 JOIN 到 active_slot_new.
如果在第 2 步時,T4 也提交,因為 logrec4(5KB) > slot_size(4KB),T4 就不會進行 JOIN 操作,而是直接將自己的 logrec 數據寫入 log 文件,并做 sync 刷盤返回.在返回前因為發(fā)現有 logrec4 大小的日志數據無法合并,全局 log 對象會試圖將 slot buffer 的大小放大兩倍,這樣做的目的是盡量讓下面的事務提交日志能進行 slot 合并寫.
WT 引擎之所以引入 slot 日志合并寫的原因就是為了減少磁盤的 I/O 訪問,通過無鎖的操作,減少全局日志緩沖區(qū)的競爭.
從上面關于事務日志和 MVCC list 相關描述我們知道,事務的 redo log 主要是防止內存中已經提交的事務修改丟失,但如果所有的修改都存在內存中,隨著時間和寫入的數據越來越多,內存就會不夠用,這個時候就需要將內存中的修改數據寫入到磁盤上.
一般在 WT 中是將整個 BTREE 上的 page 做一次 checkpoint 并寫入磁盤.WT 中的 checkpoint 是 append 方式管理,也就是說 WT 會保存多個 checkpoint 版本.不管從哪個版本的 checkpoint 開始都可以通過重演 redo log 來恢復內存中已提交的事務修改.整個重演過程就是就是簡單的對 logrec 中各個操作的執(zhí)行.
這里值得提一下的是因為 WT 保存多個版本的 checkpoint,那么它會將 checkpoint 做為一種元數據寫入到元數據表中,元數據表也會有自己的 checkpoint 和 redo log,但是保存元數據表的 checkpoint 是保存在 WiredTiger.wt 文件中,系統(tǒng)重演普通表的提交事務之前,先會重演元數據事務提交修改.后文會單獨用一個篇幅來說明 btree、checkpoint 和元數據表的關系和實現.
WT 的 redo log 是通過配置開啟或者關閉的,MongoDB 并沒有使用 WT 的 redo log 來保證事務修改不丟,而是采用了 WT 的 checkpoint 和 MongoDB 復制集的功能結合來保證數據的完整性.
大致的細節(jié)是如果某個 MongoDB 實例宕機了,重啟后通過 MongoDB 的復制協(xié)議將自己最新 checkpoint 后面的修改從其他的 MongoDB 實例復制過來.
雖然 WT 實現了多操作事務模型,然而 MongoDB 并沒有提供事務,這或許和 MongoDB 本身的架構和產品定位有關系.但是 MongoDB 利用了 WT 的短事務的隔離性實現了文檔級行鎖,對 MongoDB 來說這是大大的進步.
可以說 WT 在事務的實現上另辟蹊徑,整個事務系統(tǒng)的實現沒有用繁雜的事務鎖,而是使用 snapshot 和 MVCC 這兩個技術輕松的而實現了事務的 ACID,這種實現也大大提高了事務執(zhí)行的并發(fā)性.
除此之外,WT 在各個事務模塊的實現多采用無鎖并發(fā),充分利用 CPU 的多核能力來減少資源競爭和 I/O 操作,可以說 WT 在實現上是有很大創(chuàng)新的.通過對 WiredTiger 的源碼分析和測試,也讓我獲益良多,不僅僅了解了數據庫存儲引擎的最新技術,也對 CPU 和內存相關的并發(fā)編程有了新的理解,很多的設計模式和并發(fā)程序架構可以直接借鑒到現實中的項目和產品中.
后續(xù)的工作是繼續(xù)對 Wiredtiger 做更深入的分析、研究和測試,并把這些工作的心得體會分享出來,讓更多的工程師和開發(fā)者了解這個優(yōu)秀的存儲引擎.
文/袁榮喜
高可用架構「ArchNotes」微信公眾號
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/4506.html