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