萬字解析 mysql innodb 事務實現原理
0 前言
近期在持續學習 mysql innodb 底層技術細節相關內容,繼上期鎖原理篇之後,本期我們以 innodb 中的事務實現原理作爲主題,展開交流探討.
有關本篇內容的目錄大綱展示如下:
1 基本概念
事務是數據庫通常都需要具備的一項核心能力,也是其區別於一般文件系統的本質區別之一.
我試着用個人理解對事務的概念進行詮釋——所謂事務,指的是一段動態執行過程,它能夠將數據庫中的數據從一種正確狀態轉移到另一種滿足約束條件的正確狀態, 並且這個執行過程中需要滿足如下所述的四項性質,也就是經典的事務 ACID 性質:
1.1 原子性 A
原子性 Atomicity,指的是一個事務中的所有操作要能夠被組裝成一個不可分割的整體,看起來就像在一氣呵成地執行一個動作一樣,其執行結果要麼全部成功,要麼全部失敗,不會出現割裂的中間狀態.
在 innodb 中,針對事務原子性的支持是通過 undo log 機制加以實現的,這部分內容我們放在本文第 3 章詳細展開探討.
1.2 一致性 C
一致性 Consistency,指的是在事務執行前後,數據庫中的數據狀態都處於滿足業務預期的、正確一致的狀態之下.
一致性可以理解爲是數據庫追求的最終目標,藉由原子性 A、隔離性 I、持久性 D 的加持,共同推進該目標的達成.
1.3 隔離性 I
隔離性 Isolation,指的是在多個事務並行的場景下,各事務彼此間不會產生互相干擾,不會因爲對方的執行而發生數據視角混亂或者執行邏輯出錯.
具體而言,事務隔離性不能一概而論,其依附於不同事務隔離級別的語義,有着各自不同的標準及實現.
在 innodb 中,事務隔離性是通過 MVCC 機制結合鎖機制加以保證的,這部分內容將在本文第 4 章內容中展開介紹.
1.4 持久性 D
持久性 Durability 的概念很好理解,指的是因事務提交引起的變更能夠永久穩定生效,不會處於易失性的狀態.
在 innodb 中,事務隔離性通過 redo log 機制加以保證. redo log 的設計很好地支持 innodb 完成了對數據持久性以及寫操作性能之間的平衡,這部分內容在本文第 2 章中詳細展開介紹.
接下來各章中,我們針對 innodb 中爲實現事務各項性質而採取的對策,逐一展開詳細介紹.
2 持久性保證
我們知道,內存屬於易失性存儲,而磁盤(外存)則屬於穩定性較高的存儲介質,即便發生程序宕機、機器斷電等問題,也不會導致數據的丟失.
因此,所謂持久性,其實現的關鍵就在於,要將數據變更內容持久化保存到磁盤上.
在 innodb 中,事務持久性是通過重做日誌 redo log 加以保證的,在接觸學習一個新概念之時,我們應該帶着辯證思維而非一味被動接收信息的輸入. 這裏我們帶着如下兩個問題進行後續內容的展開學習:
-
redo log 方案的實現原理是什麼?
-
innodb 爲什麼要選擇這個方案?非它不可嗎?
2.1 redolog 概念
在針對 redo log 開始大講特講之前,我們先以一個場景案例作爲引子:
-
人物 I:小明是一名會計,就職於一家華夏村五百萬強企業.
-
背景設定補充 I: 該公司對於財務審計流程非常嚴格,針對各項營收與開銷,都會分門歸類、逐一進行精細化記錄. 長年累月下來,公司中形成了一份信息量龐大的 “全局賬簿” ,包含公司創建至今的全量財務數據
-
背景設定補充 II: 公司在白天營業期間,賬目變動會非常頻繁,且身爲社畜牛馬,小明除了維護 “賬簿” 之外,還有會各種瑣碎雜事找上門來. 所以頻繁翻閱並實時更新這本體量龐大的 “賬簿” 是一件異常耗費心力的事情
-
人物 II: 公司老闆人送外號 “查賬狂魔”,可能不定期進行查賬,要求小明基於財務賬簿,製作歷史至今的各項收支的明細報表,需要保證內容的實時性與精確性
-
問題思考: 身爲打工牛馬,小明自然無法反駁老闆的要求,於是聰明的小明採取瞭如下的實現方案,實現了打工人的 “自我救贖”
-
實現方案:
1)diff 更新: 在白天忙碌的營業期間,小明通過一塊 “小黑板” ,臨時記錄當天內涉及到的賬目變更內容. 從單天粒度來看,內容不會太多,因此 “小黑板” 可以很小,維護以及更新起來遠比 “全局賬簿” 來得輕便;
2)查詢響應: 某個時刻,如果老闆發起 “查賬” 請求,小明也可以通過 “全局賬簿” 疊加上 “小黑板” 的內容,保證給到老闆準確的答覆;
3)diff 歸併: 到了下班時間,牛馬小明只能選擇偷偷加個班,一次性將當天 “小黑板” 中的全部內容歸併到 “全局賬簿” 中,這個過程會比較費力,但好在只是一次性的行爲,並且此時是非營業時間,小明手上也沒有其它雜活兒,因此更能高度聚焦地把這件事情做好
發現了嗎,其實上述例子揭示的思路,在很大程度就和 innodb 中有關 redo log 的設計是具備着共性的.
我們知道,對於 mysql innodb 這樣以磁盤作爲存儲介質的數據庫來說,全量數據內容是存儲在磁盤上的. 但是在事務執行過程中,針對涉及到的數據記錄,會基於局部性原理,以數據所在的頁 page(默認 16KB)爲單位將對應內容從磁盤加載到內存中. 如果此時事務執行的是一筆更新操作並且事務被成功提交,那麼內存中的數據會被更新,且直到該數據被溢寫更新回到磁盤之前,該 page 在內存和磁盤中的結果是不一致的,我們稱這樣的頁 page 爲髒頁 dirty page.
【Tip:這裏各位需要甄別清楚髒數據 dirty data 和髒頁 dirty page 的區別.
1)髒數據(不合法): 指的在事務中修改了數據但是還未提交的一種非正式數據狀態. 這種髒數據本質上不應該被外界讀取到,因爲該事務後續是否能成功提交還屬於未知之數. 在讀未提交的事務隔離級別下可能發生的髒讀問題,指的就是因爲事務隔離程度不夠,導致外界讀取到了這種非正式態的髒數據,其背後引發的後果可能是很致命的
2)髒頁(合法): 髒頁本身對應的是正式、合法的數據,它是由於數據庫中內存與磁盤之間的狀態差異的,但這種差異只是臨時的,數據庫最終會通過合理的機制,保證髒頁的內容最終被溢寫到磁盤上,保證兩份存儲介質的內容一致性】
於是接下來的核心點就在於,在事務提交時,innodb 應該採取怎樣的持久化機制,來保證這部分變更的數據能夠被持久穩定地存儲下來.
一種實現方式是,直接將內存中的 page 溢寫回到磁盤文件中其原本從屬的位置中,這種方案足夠簡單粗暴,但是需要明白這背後面臨的就是一次磁盤隨機寫操作,性能方面會顯得比較差強人意.
這就好比上述例子中,在白天營業期間,每當有賬目變更動作觸發時,小明都選擇直接去翻閱並更新那份體量龐大的 “全局賬簿”,這種週而復始、重複枯燥、繁重到窒息的工作量很可能直接讓我們這位小明同學 “享年三十”.
與之相對的 ,innodb 選擇採用另一種方式,類似於上述例子中的 “小黑板” 方案,這裏的小黑板就類似於所謂的 redo log,而 “全局賬簿” 則類似於 innodb 中存儲了全量數據以及索引信息的 ibd 文件.
在 innodb 中,當遇到事務提交時,內存中對應的 dirty page 不直接從溢寫回到 ibd 文件對應位置,而是採取一種類似預寫日誌(WAL,write ahead log)的思路,以磁盤順序寫的方式生成該 page 對應的 redo log file. 這樣既通過磁盤存儲形式,保證了對事務持久性的支持,又能通過大幅度規避磁盤隨機寫操作的發生,在很大程度上提高了事務的執行性能.
需要明白的是,redo log 本身是爲了防止數據庫宕機而起到的一項保險措施, 在宕機後的數據恢復流程中,通過存量 ibd 文件以及 redo log file 就能還原出最真實精確的數據狀態.
而在數據庫正常運行場景中,其實是不需要使用到 redo log 的,因爲此時哪怕磁盤 ibd 文件內容與內存中的 dirty page 存在差異也不會影響數據的一致性:
-
當對應讀寫操作涉及到這部分 dirty page 時,會直接複用內存中實時性較高的數據,因此不會有問題
-
如果 dirty page 因內存淘汰策略即將被踢出內存時,也會確保持久化到 ibd 文件中,保證內容的一致性
2.2 redolog 產生機制
redo log 本質上由兩部分組成:
-
一部分是內存中的重做日誌緩存區——redo log buffer,屬於易失性存儲
-
另一部分是磁盤中的重做日誌文件——redo log file,屬於持久性存儲.
innodb 基於局部性原理,將邏輯上的最小存儲單元設定爲頁 page,而 redo log 同樣以 page 爲粒度,存儲的內容是一個 page 在更新後物理層面上的數據狀態.
1)redo log 生成: 每當有事務執行並執行寫操作時,會以數據所從屬的 page 爲單位,生成對應的 redo log,並將其投遞到 redo log buffer 中;
2)redo log 持久化: 在事務提交時,會把內存中的 redo log 持久化到磁盤上的 redo log file 中
針對上述第 2)步,這裏進一步加以說明,本質上這個持久化動作又可以被拆解爲兩個小步驟:
2-1)投遞文件系統緩衝區: 首先將 redo log 內容提交到文件系統緩衝區中,此時仍可能存在數據丟失的風險,當操作系統崩潰時,這部分內容會丟失
2-2)fsync 強制落盤: 接下來需要執行 fsync 操作,確保內容被落盤寫入到 redo log file 中,至此才真正達成了持久化的語義. 這種機制就稱之爲 Force Log At Commit 機制,而此處的 fsync 操作也就是性能瓶頸所在.
【Tip:針對這項 fsync 操作,此前我在之前發表的文章 etcd 存儲引擎之存儲設計篇的 3.2 小節中也有過介紹,知識之間是觸類旁通的,大家可以聯繫看待. 】
上述是常規的事務提交流程,只有通過 fsync 操作保證 redo log 強制落盤,才能在嚴格意義上實現事務的持久性語義. 然而正如上文所述的,由於 fsync 操作是整個流程的性能瓶頸所在,所以在實際應用場景中,使用方也可以在穩定性與高性能之間進行權衡取捨,選擇延遲或者降低 fsync 的執行頻次,捨棄一部分持久性的要求,進而提高整體性能表現.
具體來說可以通過修改參數 innodb_flush_log_at_trx_commit 來調整 redo log 落盤的策略——1)設置爲 0:選擇不主動落盤,而把 redo log 持久化時機交由 db master thread;2)只將 redo log 投遞到文件系統緩衝區而不執行 fsync,把真正落盤的執行時機託付給操作系統
2.3 redolog 對比 binlog
另外,有讀者經常會對重做日誌 redo log 和二進制日誌 binlog 之間的關係產生疑問. 這裏我們從幾個角度出發,統一作個對照說明:
-
產生源頭:binlog 是 mysql 數據庫層面產生的,不依附於任何存儲引擎,屬於全局共用的二進制日誌;redo log 是 innodb 存儲引擎專屬定製的,供引擎內部使用
-
存儲內容:binlog 的記錄內容是邏輯層面的增量執行 SQL 語句,主要用途可能用於數據庫之間的主從複製,通過重放增量 SQL 的方式復刻出完整的數據內容;redo log 以 page 爲粒度存儲物理意義上的頁數據,其目的是爲了兼顧事務的持久性以及寫操作的高性能
綜合上述兩點,有些讀者可能會產生一個新的疑惑——其實通過 binlog 是不是也能閉環實現數據的持久化,innodb 引入 redo log 是否存在重複設計?
以下是我個人針對這個問題的一些理解:
redo log 的採用是有必要的,這個問題的核心就在於數據恢復流程的效率問題. 在 innodb 中,每次啓動數據庫時,都會統一基於 redo log 執行數據恢復流程,而不會刻意區分此前數據庫是異常宕機還是正常終止. 同樣是通過持久化日誌還原數據,基於 binlog 這種邏輯增量記錄的方式,其效率是遠遠不如基於 redo log 這種物理日誌的.
此外還有另外一點,在 innodb 中,除了普通數據外,針對第 3 章中即將介紹的回滾日誌 undo log 也存在持久化的訴求,這些都需要通過 redo log 的能力加以保證.
最後再針對 binlog 和 redo log 的寫入時機進行對比分析.
-
binlog:其產生是在事務提交時,以事務爲粒度,一次打包產生,按照事務提交先後順序進行排列,與每個提交事務一一對應;
-
redo log:其產生時在事務修改數據時(可能沒有提交),以 page 爲粒度持續生成並投入到 redo log buffer,最終在事務提交時被強制持久化落盤. 在 innodb 中是能支持多事務並行的,因此多個事務可能會穿插生成 redo log,直到某個事務提交時,則強制將此前對應的一系列 redo log 進行強制落盤.
這裏還有另一個隱晦的問題值得探討一下:如果有多個事務併發對同一 page 下的不同行進行修改,是否可能會誤將對方的髒數據連帶落盤到 redo log file 中呢?
此處這個問題需要通過 undo log 來進行規避,該問題會本文 3.5 小節中重點探討,這裏不再重複贅述.
2.4 redolog 存儲格式
在邏輯意義上,每份 redo log 對應一個 page 的粒度;而在物理意義上,redo log 以 log block 爲單元進行存儲,整個 redo log buffer 可以視爲一個隊列,而每個 log block 則爲隊列中的一個元素.
每個 log block 大小固定爲 512B,剛好契合了磁盤扇區的大小,其由三部分組成:
- log block header:block 頭部元信息部分,共計 12B 大小,由下述字段組成:
1)block hdr no(4B):該 block 在 redo log buffer 中的 index
2)lock block hdr data len(2B):該 block 中已使用的空間大小,單位 Byte. 佔滿時爲 0x200(512B)
3)block first rec group(2B):該 block 中首條新 redo log 對應的偏移量.【比如 log block body 中,前 20B 爲上一個 block 末尾 redo log 的內容拼接延續,則該項值爲 12(header)+ 20(last log) = 32 】
4)log block checkpoint no(4B):該 block 被寫入時的檢查點信息
-
log block body:核心日誌內容,即 redo log 正文部分,大小爲 492B
-
lock block tailer:block 尾部,共計 8B,包含一份 lock block hdr no(4B)和填充空間 (4B)
邏輯意義上的一條 redo log 對應一個 page,其存儲位置位於 log block body 當中. 當 block body 仍有空間富餘時,多條 redo log 可以共用一個 block;當 block 有剩餘空間但不足以承載下一條抵達的 redo log 時,則會對 redo log 進行截斷,並將剩餘部分放置到下一個相鄰 block 中.
不同數據庫操作類型會對應不同的 redo log 格式,但總體來看,其大概包含了如下所屬的核心信息:
-
redo_log_type: 修改生成該份 redo log 的數據庫操作類型
-
space: 表空間對應的 id
-
page_no: 該 redo log 對應的是哪個 page
-
redo_log_body: page 中的具體數據內容
2.5 redolog 使用機制
基於 redo log 進行數據恢復的過程中,離不開對 LSN 的使用.
LSN 全稱 Log Sequence Number,其含義是,在某個時刻下,所有事務寫入重做日誌的總量.
我們可以把 LSN 理解爲一個邏輯時間軸. 比如時刻 A,總共已寫入的 redo log 大小爲 1000B,則全局 LSN 計數值 1000;接下來在時刻 B,一個事務又寫入了 100B 的 redo log,則此時全局的 LSN 計數值被更新爲 1100,且剛纔生成這筆 100 B 大小的 redo log 也會記錄其在生成時刻對應的 LSN 值,反映了其生成的時序.
除了 redo log 之外,ibd 文件中每個 page 中也會有一個 FIL_PAGE_LSN 值,記錄了該 page 最後更新時的全局 LSN 計數值,反映了其數據的實時程度. 這樣後續在通過 redo log 恢復該 page 數據時,所有 redo.LSN <= FIL_PAGE_LSN 的 redo log 都應該被忽略.
innodb 引擎在啓動時,不管上次數據庫是正常關閉還是異常宕機,都會基於 redo log 對 ibd 文件開啓數據恢復的流程. 由於 redo log 是基於 page 粒度的物理存儲日式,因此恢復性能是比較優秀的.
在恢復流程中,會將磁盤上的 redo log file 一一加載到內存的 redo log buffer 中(基於 LSN 先後順序排列). 此時會有一個全局的 checkpoint LSN,表示此前的 redo log 都已經完整歸併到 ibd 中,因此這前半部分的 redo log 是已經可以清除了的.
接下來只需要遍歷處理 checkpoint LSN 後半部分的 redo log 即可. 每次在將一筆 redo log 更新到 ibd 文件對應 page 之前,需要額外檢查一下 redo log 中的 LSN 和 ibd page 中 FIL_PAGE_LSN 的大小,只有 redo LSN > FIL_PAGE_LSN 時,才進行更新操作.
【ibd page lsn 可能大於 redo lsn,是因爲對應的 dirty page 可能因爲被提前淘汰出內存,已經進行過磁盤溢寫操作,因此其數據的實時程度可能更高】
3 原子性保證
3.1 undolog 概念
在 innodb 中,通過 undo log 的設計來實現對事務原子性的保證.
回滾日誌 undo log,顧名思義,其最直觀的用途是用於支持事務的回滾操作. 任何針對數據行記錄的修改操作,都會一種類似寫時複製(copy-on-write)策略的方式,在生成新版本數據的同時,也會通過 undo log 保留修改前的數據副本,這樣每條行記錄根據修改的先後順序,會形成一條 “版本鏈” 的拓撲結構,其中所謂的 “版本” 是通過觸發修改行爲的事務 id 進行標識,而之所以成 “鏈” ,是同一數據行的 undo log 之間,會通過修改先後順序,依次通過回滾指針 roll_ptr 指向上一個 “版本” 的 undo log.
基於上述設計,能夠很好地支持到事務原子性的語義:
1)屏蔽中間態數據: 一個事務產生的修改,會通過其事務 id 進行 “版本” 標識,這樣在事務未提交前,其作出的修改都不會被外界所認可,外界的讀操作可以藉助行記錄對應的 undo log,回溯並獲取到上一個已提交的正式數據版本
2)全部提交: 當事務提交時,其事務 id 會獲得 “正名” ,這樣一瞬間,其產生的所有行記錄對應的數據版本都會被外界所認可,體現了原子性中 “全部動作一起成功” 的語義
3)全部回滾:當事務回滾時,其事務 id 會失去 “正名” ,其產生的所有行記錄對應數據版本都被外界否定,與此同時,可以很方便地藉助 undo log 將涉及修改的行記錄內容回溯成上一個版本的狀態,體現了原子性中 “全部動作一起失敗” 的語義
此外,因爲 undo log 所謂 “版本鏈” 的存在,也爲外界在讀取行記錄時提供了一個能夠自由選取指定版本的能力,這樣就很好地契合了 MVCC 一致性非鎖定讀的實現,這部分細節可以參見本文第 4 章內容.
言歸正傳,undo log 以數據行記錄爲粒度,其存放在數據庫的特殊共享表空間 undo segment 內,可以將其理解爲一類特殊的數據,其本身也有自己從屬的表結構,也有數據持久化的訴求,因此在 innodb 中會通過 redo log 來保證 undo log 的持久性.
除了比較特別的 insert 操作外,update 和 delete 都可以視爲廣義上的 “更新” 操作,在對一行數據作出更新時,需要通過 undo log 的形式對前一個數據版本進行留痕,並且需要以及版本先後順序進行指針的串聯.
部分讀者認爲在利用 undo log 回滾數據時,是在物理層面上將 innodb 中的數據恢復到執行事務前的樣子,這個理解是比較片面的. 因爲 innodb 中的數據存儲單元是 page 的粒度,而以數據行爲粒度的 undo log 只能在邏輯層面上針對該行數據執行逆向操作,以使其邏輯性地恢復到上一個版本的樣子. 需要注意的是,此時由於併發事務的存在,page 中的其它行可能也在進行更新操作,因此整個 page 在物理層面上很可能已經演變成截然不同的樣子.
3.2 undolog 對比 redolog
有讀者認爲,undo log 屬於 redo log 的逆向操作,兩者呈對立關係. 這個理解是不到位的,這兩種日誌本身不屬於一個維度的東西. 下面通過幾個方面對兩者展開對比:
-
內容粒度:redo log 以頁 page 爲粒度,記錄一個 page 物理層面的數據內容;undo log 以行 record 爲粒度,記錄數據行記錄前一個版本的數據內容
-
使用目的:redo log 是一種類似於預寫日誌的內容,用於實現對數據持久性及寫操作性能的保證;undo log 採用一種類似寫時複製的策略,記錄一個行記錄上一版本的舊數據,並通過指針串聯成鏈,用以支持事務回滾操作以及 MVCC 的版本選取策略
-
存儲介質:redo log 通過位於磁盤上的 redo log file 進行持久化存儲;undo log 屬於一類特殊的數據,存放於 innodb 共享表空間 undo segment 中,本身也需要依賴於 redo log 實現數據持久化
3.3 undolog 存儲格式
前面提到,undo log 本身可以視爲一類特殊的數據,因此存儲時也會有自己從屬的 undo page,這部分內容位於數據庫內部的特殊共享表空間 undo segment 當中.
寫操作可以在廣義上分爲插入(insert)和更新(update/delete)兩大類. 根據不同的寫操作類別,會生成不同類型的 undo log.
針對於 insert 類型的 undo log,除了執行該操作的事務本身之外,對於其他事務都是不可見的,因此無需考慮與 MVCC 有關的內容. 這種 undo log 格式比較簡單,可以在事務提交後直接刪除,其格式如下:
-
next/start: 頭部的 next 字段記錄下一條 undo log 起始偏移量;尾部的 start 字段記錄本條 undo log 的起始偏移量
-
type_compl: 對應 undo log 的類型,insert 操作枚舉值爲 11
-
undo no: 本條 undo 記錄的編號
-
table id: 對應表的編號
-
n_unique_index: 該區域記錄了表中各個唯一鍵的內容長度以及對應的具體內容
針對執行 update 操作而形成的 undo log,由於需要對 MVCC 機制進行支持,因此需要形成版本鏈的拓撲結構,其在 insert 數據格式的基礎上,在以下幾項內容上會有所不同:
-
type_compl: undo 類型,枚舉值固定爲 12
-
trx_id: 執行操作的事務 id(版本)
-
roll_ptr: 指向上一版本 undo log 記錄的指針(指針)
-
update_vector: 本次操作更新的列及其舊值
-
n_bytes_below: 後續內容記錄各列的完整數據
至於 delete 操作對應生成的 undo log,其與 update 類型的格式基本一致,區別在於 type_compl 枚舉值變爲 14,以及少了 update_vector 模塊部分的內容:
3.4 undolog 產生機制
在事務執行過程中,每當在對一個數據行記錄進行寫操作時,不論是 insert update 還是 delete 操作,都會生成對應的 undo log. 其中 insert 操作相對比較簡單,下面以 undate 操作爲例,對流程步驟加以說明:
1)生成 undo log: 基於更新前的行數據版本,生成對應的 undo log,插入到該行對應 undo log list 的起點位置
2)修改行數據: 接下來再對 page 中對應的行記錄進行修改
如此一來,一條 undo log 就成功生成了. 值得一提的是,此刻生成的 undo log,也會在內存中處於一種 dirty page 的狀態,需要藉由這條 undo 記錄本身對應的 redo log 來實現數據持久性的保證.
這裏可能有讀者產生關於 undo log 爲什麼需要進行持久化的疑問. 持久化的目的是爲了防止因數據庫的突然宕機而導致數據內容的丟失,但是 undo log 的內容本身都與運行時的事務強相關,如果數據庫一旦發生宕機,那麼所有事務自然也就不復存在了,那麼爲什麼還需要依賴到 undo log 的內容呢?
我們從 undo log 兩個核心用途出發,進一步對上面的問題進行拆解:
-
支持事務回滾: 如果數據庫宕機了,事務自然執行失敗了,還需要依賴於 undo log 進行回滾嗎?
-
支持 MVCC:如果數據庫宕機了,那麼所有活躍事務自然也就不存在了,那麼也就不存在 MVCC 的使用場景了,還需要用到老版本 undo log 中的內容嗎?
這裏我們留個疑問,在隨後的 3.5 小節中,我們揭示這個謎題的答案.
3.5 undolog 使用機制
針對 undo log 其實包含了三大核心用途,下面一一進行講解:
- 支持事務回滾:
事務執行過程中,在修改行記錄前,會通過 undo log 保留前一個版本的數據副本,這樣一旦需要對事務進行回滾,就可以很方便地通過 undo log 進行數據狀態回溯
- 支持 MVCC:
在可重複讀的事務隔離級別下,爲了支持一致性非鎖定讀(MVCC)操作,老版本的 undo log 不能在新事務提交後立即刪除,因爲它可能還會被更早的活躍事務依賴到. 因此其需要在 undo log list 中保留一段時間,直到確保沒有任何事務依賴到它時,才能通過 purge 線程進行回收刪除
- 支持數據恢復流程:
此處來正面回答一下,undo log 也需要通過 redo log 進行持久化的原因.
這裏試想一個場景,在一個 page 中,有行 A 和 行 B 兩行記錄:
1)時刻 1:事務 I 對行 A 進行修改,也生成對應行 A 上一版本數據的 undo log
2)時刻 2:事務 II 對行 B 進行修改(也會生成對應行的 undo log,但在本 case 中不是重點)
3)時刻 3:事務 II 提交了,這樣基於 force log at commit 機制,本次提交行爲會把該 page 對應 redo log 持久化到磁盤的 redo log file 中(注意,此時事務 I 還沒提交,因此該 page 中行 A 還處於髒數據狀態,但同樣被連帶着持久化到 redo log file 中了 )
4)時刻 4:數據庫宕機了
串聯上述時間線及對應事件後,接下來在數據庫重啓時,會基於該 page 對應 redo log 進行數據恢復操作,可想而知就可能把行 A 恢復成事務 I 未提交的髒數據狀態,在這個環節中,如果行 A 對應的 undo log 因爲沒來得及持久化而丟失了,那麼之前正式版本的數據就無跡可尋了.
基於上述 badcase,可以明確一點, undo log 也是必須通過 redo log 來保證持久性的,否則在數據恢復流程出現正式數據丟失的問題.
3.6 undolog 回收機制
在 innodb 中會有一個異步的 purge 線程,專門負責對 undo log 對應的 page 進行內容清理,清理後的 page 可以在後續流程中重新進行分配複用.
在清理 undo log 時,需要遵循指定校驗條件,如下圖. 額外值得注意的就是,在可重複讀的事務隔離級別下,需要保證不存在更小編號的活躍事務存在時,才能回收一筆 undo log. 這部分內容可以參見我之前發表的文章——萬字解析 mysql innodb 鎖機制實現原理的 2.2.1 小節內容,關於 MVCC 中 read view 中 up_limit_id 字段的定義.
4 隔離性保證
4.1 事務隔離級別
事務的隔離性需要建立在具體的隔離級別標準之下,事務隔離級別根據嚴格程度從低到高可以分爲如下四種:
上表在展示事務隔離級別的同時,也展示了 innodb 在實現對應隔離級別的同時,可能存在哪些因隔離程度不夠而導致的數據視角不一致問題.
拋除比較少用到的讀未提交(視角一致性太差)和串行化(性能太差),針對讀已提交和可重複讀,innodb 主要採用的是 MVCC 機制結合鎖機制加以保證.
有關事務隔離性的內容,在我之前的文章——萬字解析 mysql innodb 鎖機制 中花了很大的篇幅進行詳細介紹,這裏不再贅述.
4.2 innodb 實現
innodb 中默認採用的事務隔離級別爲可重複讀 repeatable read. 與標準 SQL 定義標準有所不同的是,innodb 中,可重複讀的事務隔離級別能夠規避幻讀問題 Phantom Problem. 這裏分別從一致性非鎖定讀(MVCC)和一致性鎖定讀(LOCK)兩個視角出發,一一闡述 innodb 如何規避幻讀問題的發生:
-
一致性非鎖定讀: 在普通 select 操作執行時,忽略在事務期間新插入的行數據,innodb 通過 MVCC 結合一致性視圖 Consistent Read View 來實現讀取視角的一致性(但事實上,對應範圍內的數據條目可能已經發生變化,只是在查詢時爲了維持視角的一致性,通過這種偏被動的方式選擇了視而不見)
-
一致性鎖定讀: 在帶有加鎖行爲的 select 操作下, innodb 則是通過間隙鎖 gap lock,禁止併發事務在相鄰間隙內插入新的數據條目,從而通過這種主動防禦的機制,在真正意義上避免了幻讀問題的發生
5 分佈式事務
5.1 外部 XA 事務
所謂分佈式事務,指的是在一個事務涉及操作到多個獨立的數據資源,而這些數據資源之間可能是跨節點和組件的,因此屬於分佈式架構的範疇. 此處探討的數據資源是比較泛化的,其底層實現可以是類似 mysql 這樣的關係型數據庫,也可以是其他類型的存儲組件,
分佈式事務中一類經典的實現方案爲 XA (eXtended Architecture). 在 XA 事務架構中,允許不同種類的數據庫共同參與分佈式事務的協作,只要其能夠支持 XA 事務協議即可.
innodb 引擎提供了對 XA 事務(eXtended Architecture)能力的支持,進而能夠扮演 XA 架構中一個數據資源代理方的角色,與其它數據資源共同參與到全局分佈式事務的協作中. (參與 XA 事務時,innodb 需要將事務隔離級別設置爲串行化 SERIALIZABLE)
從 innodb 的視角出發,這種參與分佈式事務協作的 XA 事務稱爲外部 XA 事務,這套 XA 事務架構由一個全局的事務流程調度器 Transaction Manager 和多個資源管理器 Resource Manager 共同組成,而 innodb 就作爲其中的一個 Resource Manager.
其整體架構以及各模塊的職責如下:
-
流程調度器 TM: 串聯整個分佈式事務的執行流程,需要向各個資源管理器 RM 發號施令
-
資源管理器 RM: 提供訪問數據資源的方法,通常對應爲一個獨立數據庫
XA 事務的調度流程通常採用兩階段提交(2PC,two-phase-commit)的方式:
1)第一階段,由 TM 向所有 RM 發送 PREPARE 請求,告知未來的操作意圖並命令其開始準備;
2)第二階段, TM 根據第一階段收集到的來自 RM 的 響應,決定執行 COMMIT 還是 ROLLBACK
與本地事務的核心區別就在於,分佈式事務需要額外通過一個 PREPARE 階段,提高事務執行的容錯率. 之所以需要這樣設計,其根本原因還是在於分佈式事務的執行跨越了多個獨立的 RM,這種分佈式場景問題往往具有比較大的不確定性.
更多有關分佈式事務的內容,可以參見我之前發表的文章——萬字長文漫談分佈式事務實現原理.
5.2 內部 XA 事務
5.1 小節討論的場景屬於外部 XA 事務,其將整個 mysql 數據庫視爲一個整體. 而在 mysql 數據庫中,還有另一類內部 XA 事務,指的是從微觀視角出發,將 mysql 內部的存儲引擎、插件等模塊都視爲獨立的模塊,分別扮演不同的 RM 角色.
下面舉一個常見的例子加以說明——binlog 與 redo log 之間一致性保證.
在需要進行主從複製的場景,會針對 mysql master 節點啓用 binlog 功能. 在一筆事務提交時,需要 1)先寫 binlog,2)再寫 innodb 中的 redo log. 嚴格來說,這也是兩個獨立的步驟,倘若第一步 binlog 寫成功,但是執行第二步寫 redo log 前數據庫發生宕機,此時就會發生嚴重的數據不一致問題:
-
因爲 binlog 已經產出了,因此會導致 slave 節點同步到這部分增量內容
-
由於 master 節點沒來得及完成 redo log 寫入,因此這筆事務對應變更內容持久化失敗,也隨着數據庫的宕機發生數據丟失
最終,master 和 slave 之間出現出現數據不一致問題.
針對上述場景問題,mysql 會在 binlog 與 innodb 之間通過內部 XA 事務的方案加以解決,具體執行步驟如下:
1)一筆事務提交時,先對 innodb 執行一個 prepare 操作,寫入事務 xid
2)接下來進行 binlog 寫入(事實上,這一步執行完成後,就視爲事務提交成功了)
3)最後再進行 innodb redo log 的持久化
值得一提的是,如果上述第 2)步成功但是第 3)步執行前 mysql 宕機,那麼在數據庫重啓後,會再對 innodb 中已 prepare 的 xid 作一次校驗,判斷其是否已寫入 binlog 成功,是的話,則會補充執行一次事務提交操作.
6 總結
本文和大家介紹了有關 mysql innodb 事務機制的實現原理,下面對本文涉及到的知識點進行總結梳理:
-
事務的核心性質:原子性 atomicity | 一致性 consistentcy | 隔離性 isolation | 持久性 durability
-
innodb 針對持久性的實現:採用 redo log 的設計實現持久性與寫性能的權衡與保證
-
innodb 針對原子性的實現:採用 undo log 的設計支持事務回滾、MVCC 機制與數據恢復流程的數據一致性
-
innodb 針對隔離性的實現:採用 MVCC 與鎖機制保證不同事務隔離級別的語義
-
分佈式事務能力:對外,innodb 實現 XA 事務協議支持分佈式事務能力;對內,在數據庫上層與 innodb 之間通過內部 XA 事務保證 binlog 與 redo log 的一致性
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/dEgTmNioqjx5IyDTX4KE4g