天龙源码框架分析_MySQL8-InnoDB总体架构和运行机制的系统分析(上)
1. 前文回顧:四個階段和兩種方法
首先讓我們回顧下,在上一篇文章介紹的MySQL8代碼分析的四個階段和兩種方法。
四個階段: 借鑒瀑布式軟件開發流程,我們將從熟悉MySQL的使用和運維,到吃透MySQL8代碼的整個過程分為4個階段,如上圖所示。 其中第二階段是采用演繹法推導MySQL8總體架構和運行機制的過程, 而三、四階段合并起來,則是一個采用歸納法對總體架構中每一個模塊求解吃透的過程。
演繹法:演繹法適用于大型代碼分析工作中,直接用歸納法無法吃透代碼的情況。 此時以源碼為基礎,借助社區文檔,采用大膽假設小心求證的方法,梳理出大型軟件的核心運行機制,并將軟件的大規模代碼降解為若干個正交的小模塊,定義模塊之間的交互,再對每個模塊采用歸納法做梳理。
歸納法:當目標代碼規模不大時,采用從細節到整體的梳理方法,沿著代碼->結構->思路和方案->能力這個路徑來吃透代碼,最終理解軟件作者面對的問題、解決思路和解決方案,并形成熟練掌握、自如修改目標代碼的能力。
綜上,演繹法和歸納法并不是二選一相互替代的關系,而是在代碼分析的不同階段,適用不同的方法。演繹法從上而下根據代碼、社區文檔等材料推導出軟件的總體架構和運行機制,將軟件的大規模代碼降解為小模塊,進而可采用歸納法對小模塊做梳理和吃透。
2. 本文討論的內容和范圍
在上一篇文章中,我們提出用兩篇文章的篇幅來介紹我們團隊的MySQL8代碼分析方法。 前文是第一篇,主要討論了MySQL8代碼分析的難點和問題,以及我們對應的解決思路(四個階段和兩種方法);本文是第二篇,將以MySQL8 InnoDB為例,介紹如何采用演繹法求解InnoDB的總體架構和運行機制(說明:由于本文實際的內容很長,所以將文章分為上下兩篇發出。這一篇文章是上篇。為了行文方便,以下統一用 本文 指代這兩篇文章)。
之所以將討論范圍限定在InnoDB代碼, 是因為MySQL演進到MySQL8.0版本,實際上已經劃分為3個子系統:
在一篇文章里面將三個子系統的架構講清楚,是有些困難的。 而正如前文所述,我們希望用兩篇文章的篇幅,對我們團隊的MySQL代碼分析方法和思路做系統性地介紹,而不僅僅是給出分析的結果。現階段MySQL代碼還處于高速向前演進當中,代碼分析的結果可能會過時,但好的方法卻有更長遠的生命力,更有助于推進國內MySQL社區對MySQL代碼的理解。因此,我們選擇了MySQL8 InnoDB這一經典的存儲引擎作為代碼分析的目標,以此為例展開介紹我們的分析方法。
同時,在這篇文章中我們把InnoDB代碼的分析,限定在如何采用演繹法求解InnoDB的總體架構和運行機制上。 其原因正如上一篇文章所述, 當我們采用演繹法自上而下推導出InnoDB代碼的總體架構和核心運行機制后, InnoDB龐大的代碼便可以降解為一個個正交的子模塊,此后便可以采用歸納法對每個子模塊代碼做詳細分析并吃透。而歸納法,則是程序員在日常工作中不斷反復使用的,自然的代碼分析方法。
在后續的文章中,我們會繼續分析和討論MySQL8新增的數據字典子系統。而對于SQL Layer子系統的分析,由于時間有限,短期內不會有文章推出。我們期待有興趣的同學能夠參考我們介紹的方法,展開對SQL Layer的系統性分析,同時寫成文章提供給我們。投稿的方式推薦用知乎。也就是說,作者可以先在自己知乎賬號下把文章寫好并發表,然后推薦給我們。如果我們覺得合適,將建議作者投稿到這個專欄并最終發表。我們誠摯希望,和業內同仁一起共建最具系統性的MySQL8源碼分析文檔。
3. 本文的目標
所以,本文的目標是介紹如何采用演繹法求解InnoDB的總體架構和運行機制。 這句話包含兩個重點,一個是何為InnoDB的總體架構和運行機制, 另一個是如何采用演繹法求解。 本文只有把這兩點講透了,才能稱得把我們團隊的MySQL代碼分析方法做了系統性的介紹,能夠幫助DBA、后臺開發甚至MySQL內核開發同學更好地掌握MySQL內核代碼。
3.1 總體架構和運行機制
總體架構和運行機制,似乎是兩個非常寬泛的概念,兩者究竟包含什么內容,首先需要做一番探討。
我們在上一篇文章中提到,軟件源碼分析流程即為瀑布式軟件開發流程的逆過程:
從這點來看,總體架構和運行機制的分析,就是概要設計的逆過程。我們假設MySQL8的InnoDB代碼,是按照嚴格的瀑布式開發流程(需求分析->概要設計->詳細設計->編碼和測試)一次性開發測試完成。那么概要設計階段,需要輸出什么樣的文檔,才能夠指導并驅動后續階段的工作?我們認為, 這份概要設計文檔至少應該包含這幾方面的內容:
1.InnoDB的核心概念
2.InnoDB的對外接口
3.InnoDB的線程架構
4.InnoDB中庫表的數據組織方式,包含邏輯結構(內存中的結構)和物理結構(磁盤中的結構)
5.InnoDB的磁盤IO管理機制
6.InnoDB的數據字典
7.InnoDB的事務機制
8.InnoDB的模塊結構和模塊間交互
進一步地,上述8點內容可分為4類:
a. 概念結構和對外接口:1、2
b. 系統運行機制設計:3、5、7
c. 核心數據結構設計: 4、6
d. 代碼總體架構設計:8
進一步歸結為兩大類:
總體架構: a、c、d
運行機制:b
所以,概要設計階段所關注的8點內容, 都可歸納為總體架構和運行機制兩大類。 而總體架構和運行機制,正是代碼分析階段我們所要探討的。反過來說,當我們在代碼分析階段要探討InnoDB的總體架構和運行機制時, 可以轉換為對概要設計上述8點的探討。由此,我們定義清楚了總體架構和運行機制的詳細內容。
當然,嚴格的瀑布式開發流程只有理論上的意義,InnoDB實際的迭代過程,要比這個流程復雜太多。 但當我們面對一份靜態不變的MySQL8 InnoDB代碼時, 該假設對于我們求解InnoDB的總體架構和運行機制是適用的,它化繁為簡,讓我們抓住InnoDB代碼中最關鍵的部分,進而打開局面。
3.2 演繹法求解
以上8點是我們代碼分析工作的目標,是分析工作的輸出結果。但如果只是把結果給出而不給出分析的方法,就顯得過于單薄且缺乏誠意。 因此,我們的重點將放在如何把分析的過程給講透。
我們把分析的過程,稱之為演繹法求解,這其實是一個不那么嚴謹的說法。 學術上,演繹法(更準確的說法叫演繹推理 Deductive reasoning)是一個邏輯學上的概念,比如亞里士多德的三段論(大前提、小前提、結論)既演繹推理的一種。我們的方法并不是學術意義上的演繹推理,但和演繹推理具有相同的內涵,都強調通過從一般的、普遍的信息和知識中推導出更深層、更有價值的知識。
所以,沿著從一般普遍的知識到更深層、更有價值的知識這個推導思路,我們的演繹法求解InnoDB概要設計包含1個前提和4個步驟:
前提:前提包括源碼、社區文檔、經驗和基礎知識四個方面。 源碼指的就是MySQL源碼,我們團隊使用的是MySQL8.0.15版本; 社區文檔指MySQL內核的說明和分析文檔,包括MySQL8官方參考手冊、MySQL研發團隊WorkLog、和專欄第一篇文章提到的網站和博客等; 經驗則是指我們作為用戶去深度使用MySQL,吃透MySQL中的核心概念,深入了解每個概念含義后得到的經驗和知識,基礎知識則包括數據結構和算法、操作系統、數據庫系統原理等基礎知識。
作為用戶去深度使用MySQL,我們認為這對于MySQL源碼的分析和學習是非常重要的,同時不推薦沒有一定 MySQL 運維經驗的同學直接去啃MySQL源碼。因為可能對MySQL的一些概念和運行機制事實而非,同時也提不出比較深入的問題,不能帶著問題去分析源碼。
源碼、社區文檔、經驗和基礎知識,此四點都是一般性的知識,是有一定經驗的DBA或后臺開發都具備的, 以此為前提開始推導InnoDB的總體架構和運行機制,要求不算高,但如果采用我們的方法,成功的可能性卻足夠大。
步驟1-建立分析框架:有志于掌握MySQL內核的同學,第一個操作往往都是打開調試環境,深入debug MySQL執行一條SQL的過程。這是我們面對龐大MySQL源碼的第一反應,但并不是最有效的。 我們的做法是在InnoDB源碼分析之前,先根據數據庫系統的實現原理搭建出分析框架,通過這個分析框架來更有系統性分析和梳理源碼以及社區文檔,同時在分析和梳理的過程中,對該分析框架不斷求精和細化。
將InnoDB粗略劃分為若干個正交的大塊, 每個大塊代表InnoDB中的一塊重要功能,有了這個分析框架后,把InnoDB中的常見概念填入該分析框架,進一步得到概念框架;有了分析框架和概念框架后,則可在這兩個框架的指導下分析代碼和社區文檔,不斷挖掘和細化InnoDB的內部結構。
步驟2-數據對象分析:分析框架建立后,接下來要做的,就是通過閱讀代碼、閱讀社區文檔以及debug等方式,分析刻畫出InnoDB內部的數據對象和數據結構,包括兩個部分:磁盤中的數據對象和數據組織方式,以及內存中的數據結構。 不管是分析代碼和編寫代碼,都應該數據結構先行,有了清晰全面的數據結構,代碼邏輯和算法的梳理則顯得容易。
步驟3-運行機制分析:有了數據結構后,接下來就是要分析InnoDB內部的運行機制,包括:線程架構、SQL處理流程、事務機制、后臺機制、磁盤IO管理機制、數據字典。詳盡地分析梳理清楚InnoDB內部各種運行機制,這是一個規模龐大的工程,一是因為InnoDB包含的功能邏輯足夠多;二是一些功能邏輯經歷多年的優化,變得足夠復雜,需要大量的時間。但在概要設計求解階段,我們能夠以較粗的粒度,分析清楚以上6個運行機制,就足以幫助我們搞清楚InnoDB內部大概的運作方式,從而幫助后續的代碼詳細分析階段以子模塊為單位吃透代碼。
步驟4-代碼結構梳理:完成上述1-3步驟后,接下來就到了最后也是最難的一個步驟:代碼結構的梳理。最難的原因在于,InnoDB的代碼組織,可能是主流數據庫軟件中最無序和混亂的(比如相對于redis、MongoDB)。目前社區文檔中,對InnoDB內部數據結構和運行機制的剖析文章眾多,但系統性地梳理InnoDB代碼總體結構的文章近乎沒有(即使有參考價值也不大),可見該問題的難度之高。在本文(下篇)中,我們將嘗試以一個.cc為一個模塊單位,盡最大努力去描繪出InnoDB代碼的總體結構。
行文至此,本文要討論的兩個核心目標:1.總體架構和運行機制;2.演繹法求解總體架構和運行機制的4個步驟,就已經大致介紹完畢。下面我們將對演繹法的4個步驟,做詳細分析和討論,并在每個步驟的討論中,給出InnoDB的總體架構和運行機制8點內容中的對應該步驟的內容。
4. 建立最小范圍的InnoDB代碼分析框架
前面提到,建立分析框架的目的是為了更系統性地分析InnoDB源碼。InnoDB是一個結構化數據存儲引擎,所以我們當然可以根據結構化數據存儲引擎的一般特點,來構建InnoDB的分析框架。
任何一個存儲引擎,都需要提供三個基本操作接口,來操作基礎數據對象: 1.讀:支持基礎數據對象的讀取; 2.寫:支持基礎數據對象的插入、更新; 3.掃描:支持掃描某區間的基礎數據對象。
而所謂的基礎數據對象, 既該存儲引擎最基本的數據組織單元。比如,InnoDB的基礎數據對象是行(記錄),MongoDB則是文檔。存儲引擎的類型,亦往往按照基礎數據對象的特征來歸類。InnoDB之所以稱之為結構化數據存儲引擎,因為InnoDB中的行記錄遵循嚴格的schema規范;而MongoDB稱之為半結構數據存儲引擎,是因為Mongo中的文檔并沒有嚴格的schema規范,但有具備一定的結構性特征和約束。
考察任何一款存儲引擎,首要的是考察以上三個操作如何實現。 以這三個操作為線索去梳理該存儲引擎中的各種概念, 理清楚概念之間的聯系,即可建立出一個最小范圍的分析框架。
根據以上思路, 讓我們做進一步分析。
InnoDB是建立在文件系統之上的。文件系統亦是一個存儲引擎,向上層應用提供一段連續的存儲空間,并實現讀、寫、掃描三個功能:
讀:文件系統提供read接口, 運行上層應用在指定數據位置的情況下,快速地讀取某段數據; 寫:文件系統提供write接口,允許上層應用準確地將某段數據寫入到文件某個位置; 掃描:蘊含在讀功能中。
要考察和建立InnoDB的分析框架,可以從InnoDB對外提供的讀、寫、掃描操作如何轉換為文件系統的讀、寫、掃描操作這點來切入。從這點切入,我們建立出了InnoDB內部最基本的層次關系:
1.行(row): 行是InnoDB的基礎數據對象,InnoDB存儲引擎對外提供的讀、寫、掃描三大操作接口,都是以行為粒度來進行;
2.表(Table): 表是同一種類型的行記錄的集合。 有了表和行兩個概念后,關系型數據庫才具備了對真實世界數據的建模能力(亦有對表做歸類的需求,由此產生庫這個概念,但庫不在本文討論的主干路徑中,因此不做贅述)。 表是InnoDB對外提供的基本邏輯操作單元,所有結構化數據存儲和管理的語義,都可以映射為對表的操作和管理。
3.頁(page):頁是存放行的盒子,一個頁包含同屬于一張表的多條行記錄。頁的長度固定,在InnoDB中默認為16KB。行記錄在磁盤中的寫入和讀取,是以頁為單位進行。因此,即使SQL layer只想從磁盤中讀取一行,InnoDB也會把行所在的整個頁加載到內存。由于同一頁的行記錄屬于同一張表,且其他行有較大概率在接下來被訪問到,由此以頁為單位加載數據,具備較高的性能。
4.B+樹: B+樹是表的物理實現。表中的行記錄,在InnoDB中以B+樹的形式組織起來。對表的讀、寫、掃描操作,最終將轉換為對B+樹的讀、寫、掃描操作。B+樹是一種平衡多路查找樹,每一個節點都是一個數據頁,節點按照存儲的數據和作用的不同,分為兩種:葉節點和內節點。葉節點存儲真實的行記錄,內節點存儲行記錄的某些key值,和指向葉節點或下一層內節點的指針。B+樹中一條行記錄的搜索過程,是從最上層內節點(根頁)出發,利用該記錄的key值,和搜索到的每一個內節點上的key值數組做比較,進而得出行記錄所在的葉節點的位置,或下一層內節點的位置,然后再做進一步搜索。
5.文件:文件既普通磁盤文件。InnoDB管理的所有用戶數據都持久化在文件當中。行記錄和整顆B+樹,最終以page為單位落到文件中(后綴為 .ibd )。InnoDB數據文件以頁為單位做格式化, 比如1個16GB的數據文件,內部將按照16KB的大小,被格式化為1M個數據頁。ibd文件中除了存儲行記錄的page,還包括存儲其他信息的page,后面將進一步討論到。
至此,我們可以基本推斷出InnoDB引擎執行讀、寫、掃描三個基本操作的邏輯:
讀:InnoDB提供rnd_pos等行記錄讀接口來讀取1條記錄。InnoDB內部根據記錄的key遍歷B+樹,找到頁然后找到記錄并返回;
寫:InnoDB提供write_row/update_row等行記錄寫接口來插入/修改1條記錄。InnoDB內部根據記錄的key(主鍵)遍歷B+樹,找到記錄所在的頁,再到頁中插入1條記錄或更新頁中對應的那條記錄;
掃描:innodb提供rnd_next等行記錄掃描接口,供上層模塊讀取某范圍內的多條記錄。InnoDB內部先根據查詢條件到B+樹和頁中定位到第一條記錄,然后內部維持一個游標,每次調用時獲取游標指向的下一條記錄,直到結束范圍查詢。
5. 實用意義上的InnoDB代碼分析框架
通過上一小節的分析,我們搭建出了一個最小范圍的分析框架。但這個分析框架僅僅具備理論上的意義, 離真正的實用還有很大一段距離。在本小節中,我們將做進一步分析,來彌補這段距離。
5.1 索引、段、簇
索引用來提升對表中記錄的查詢性能,和表一樣都是關系型數據庫最常見的概念。 在InnoDB中,表和索引都是采用B+樹來表示。 換句話說, 邏輯層面的表對象和索引對象,在物理層面都對應到B+樹對象。 邏輯層面表和索引對象的創建、刪除和管理(擴容、縮容),都對應到物理層面B+樹對象的創建、刪除和管理(擴容、縮容)。
我們來進一步考察圍繞B+樹的管理操作。一個B+樹由一個或多個頁構成。創建一個B+樹,就是為該B+樹分配一個根頁;釋放一個B+樹,則是把該B+樹的所有頁回收;擴容就是給B+樹分配新的頁,縮容則是回收B+樹的一些頁。可見,對B+樹的管理,本質上是對文件中頁的管理。
從性能角度看,如果在B+樹需要一個頁時,才從文件中為其分配一個頁,并不是效率最高的做法。普遍的做法是,在B+樹和文件之間再設置 段 這樣一個虛擬的數據對象,將文件中的數據頁以段為單位組織起來,當B+樹需要數據頁時,向段申請,B+樹要釋放數據頁時,則釋放到段當中。
一個B+樹對應兩個段,分別是內節點段和葉節點段。內節點段負責管理B+樹內節點的數據頁; 葉節點負責管理B+樹葉節點的數據頁。B+樹需要創建新的內節點時,向內節點段申請,此時內節點段會先看下內部是否有空閑頁,有則將空閑頁賦給B+樹作為內節點;如果沒有空閑頁,則由段向文件提出申請,由文件為段分配空閑頁,段獲取到空閑頁之后,再將空閑頁分配給B+樹。B+樹需要創建新的葉節點時,邏輯類似。
由此又引出簇這樣一個概念。 一般情況下,文件為段分配空閑頁時,是以簇為單位來分配的。一個簇包含若干個連續的數據頁(InnoDB默認是64個)。以簇為單位分配的好處,一是將空閑數據頁批量預分配給段后,段中有大量空閑頁,進而提高給上層對象(比如B+樹)分配空閑頁的效率;二是由于簇中的數據頁在空間上是連續的,這樣分配給B+樹的頁,則在空間上也是相鄰的,從而通過一次性預讀多個頁的方法,可以加快從文件加載數據頁的效率。
至此,我們從使用原理的角度出發,梳理了索引、段、簇3個概念,理清了它們出現的背景和邏輯。這些概念和行、表、頁、B樹、文件這5個概念之間的關系如下圖所示:
其中,行、頁、文件 為3種實體數據對象, B+樹、表、索引、段、簇為5種虛擬數據對象。實體數據對象承載真正的數據, 虛擬數據對象則是數據結構,實現了實體對象的管理機制。
5.2 表空間
接下來,我們考察一下表空間這個概念。如上圖所示, 不管是表對應的B+樹還是索引對應的B+樹, 其數據頁最終都存儲在文件中。那么,表和文件的數量有何的對應關系? 是1:1(一張表(及其索引)對應一個文件), 還是N:1(多張表對應一個文件),亦或1:N(一張表對應多個文件)?
對于現階段主流的文件系統(ext3、ext4、NTFS等)而言,單個文件的存儲量能夠到16TB及以上,因此把所有表(及其索引)數據放到1個文件中,似乎問題不大。但是回到Heikki Tuuri剛開始設計InnoDB的1995年,甚至Oracle、DB2剛推出的七八十年代,文件系統單個文件的容量遠沒有這么大(比如FAT32最大文件為4GB),如果表和文件是1:1 或者N:1的關系, 那么單張表的數據量則不能大于4GB,這對于工業級別的數據庫系統顯然是不能接受的。
為此,表和文件必須是1:N或者M:N的關系。 早期InnoDB的設計是M:N,既設計一個共享表空間,該表空間為多個文件的集合, 并將所有數據庫上的所有表存儲到該表空間中。 從MySQL5.6.6版本以來,開始推出獨立表空間功能,既每張用戶表可以對應1個獨立的表空間,該獨立表空間下面只有1個文件(由于文件系統的升級,設置多個文件的意義已經不大)。
將表空間的概念和原理引入到我們的分析框架,得到新的框架如下圖所示:
5.3 數據頁緩沖池
上面我們考察了InnoDB中主要的幾種數據對象,這些數據對象可以分為兩類,一類是實體數據對象,如行、頁、文件;一類是虛擬數據對象,如表/索引、B+樹,段、簇、表空間。實體數據對象承載了真正的數據,而虛擬數據對象則是將實體數據對象組織起來的一種數據結構,分別對應不同的使用場景。比如B+樹組織了頁和行,用于InnoDB的讀、寫、掃描三大操作的實現;段和簇用于頁分配回收和管理;表和索引則是作為行記錄的邏輯集合,為上層應用讀、寫、掃描行記錄,以及管理行集合提供了簡單和易于理解的概念和機制。表空間的作用其實是將多個實體文件捏合成一個連續的空間,并對外提供空間數據的分配機制。
InnoDB中還有另外一個重要的虛擬數據對象,那就是數據頁緩沖池(page buffer pool)。顧名思義, 數據緩沖池就是內存中緩存數據頁的buffer。緩存數據頁的目的有兩個,一個是緩存被讀到過的數據頁,以提高二次讀的性能;第二個是緩存被修改的數據頁,避免每次數據頁被修改后都刷入到磁盤,從而避免大量隨機寫(而真正的持久化機制,則是通過事務提交時redolog落盤加page定時持久化并推進check point的方式來實現,后面將進一步介紹)。
所以,在考慮數據頁緩沖池后,我們進一步升級分析框架如下圖所示:
至此,我們推導出的分析框架涵蓋了InnoDB中行記錄讀、寫、掃描操作,以及數據頁分配、回收和管理操作,將InnoDB數據存儲、訪問和流轉的主航道做了詳細刻畫。
5.4 事務的A和D
在分析推導完InnoDB數據流轉主航道后,我們把分析方向轉移到InnoDB的事務機制。事務是業務向數據庫發起的一連串的,數量有限的讀寫操作。當這些操作執行完成后,上層應用看到的結果和數據庫系統數據的最終狀態,要滿足 ACID 4個特性。 在一個結構化存儲系統中, 事務完整支持 = ACID的完整保證,ACID 4個特性全面又相互正交地覆蓋了事務操作對數據庫系統的全部要求。ACID可以劃分為兩類:
可能讀者看到這里會有一些疑惑:C(一致性)上哪去了? 數據庫的一致性難道跟并發控制或崩潰恢復沒有關系嗎?臟讀、不可重復讀等問題,難道不是跟一致性有關的問題嗎?
必須要指出的是,在經典的數據庫事務理論中,C(一致性)和并發控制沒有半毛錢關系。I(隔離性)的保證,覆蓋了多線程環境下事務并發控制問題的全部(包括臟讀、不可重復讀等問題的解決)。C(一致性)只和數據庫的正確性有關,舉兩個和C有關的例子:
1.在單機數據庫里,一個事務到A賬戶扣了50塊錢,到B賬戶加了50塊錢,那么C(一致性)的保證就是數據庫的代碼是正確的,事務執行完成后,A賬戶少了50塊錢,B賬戶多了50塊錢;
2.在分布式數據庫里,如果A賬戶和B賬戶位于兩個不同的數據庫節點,那么為了保證一致性,就需要引入兩階段提交等分布式事務控制機制,但該場景跟并發控制并無任何關系。
總的來說,數據庫事務對C(一致性)的要求,其實就是要求數據庫必須是一個邏輯上寫對的程序,同時能夠正確處理內部的多節點分區。在MySQL8.0中,跟C(一致性)有關且值得討論的,基本只有內部兩階段提交了,但該問題由于過于細節,不在本文的討論范圍中。
在這一節,我們先來討論事務的崩潰恢復機制,既A和D的保證。A代表原子性,要求一個事務完成后,該事務對數據庫系統數據的修改操作,要么全部執行,要么都不執行;D則代表一個事務提交后,該事務修改的數據必須全部持久化。在InnoDB中,采用redolog + 前滾和undolog + 回滾,來實現事務的A和D。
為了搞清楚這一點, 我們可以分析兩個概念(redolog、undolog)和兩個操作(事務執行時保證A和D的一般流程,崩潰恢復的一般流程)。
redolog: redolog 是 InnoDB 中 page 修改操作的 Write AHead Log(WAL) 。 WAL出現的原因,是為了獲取更好的寫性能。InnoDB在修改 page 之前將生成一份redolog ,該redolog忠實記錄了對page的修改操作。 當事務提交時,該 redolog 將被持久化到 redolog 文件,但修改后的 page 并不持久化。這樣做的好處,是將持久化 page 產生的隨機寫,轉換寫 redolog 的順序寫,由于傳統磁盤順序寫性能遠好于隨機寫,因此 WAL 賦予了存儲引擎更好的寫性能。
當然,由于事務提交 page 不持久化,就導致了磁盤中 page 的版本,會落后于內存中的版本,此時如果進程或機器崩潰,將會出現數據丟失。為解決這個問題,InnoDB 在崩潰后的恢復流程中,將重放 redolog 文件中的redolog,利用 redolog 將 page 更新為崩潰時刻內存中的那個版本。 這個操作稱之為前滾。
undolog: 如果只有redolog+前滾,還不足以保證事務的A和D。 redolog記錄的是page修改后的新值, 如果事務在崩潰前成功提交,利用redolog可以把page修改為最終版本;但如果在修改page后,事務在崩潰前未成功提交,此時為了原子性,則需要撤銷事務對page的修改。為了解決這一問題,則需引入undolog+回滾。
從原理上看,一條 undolog 記錄了事務修改的某條表記錄的舊值,可以認為當delete一條記錄時,undo log中會記錄一條對應的insert記錄,反之亦然;當update一條記錄時,它記錄一條對應相反的update記錄。在崩潰恢復階段,先利用 redolog + 前滾將所有 page 恢復到崩潰時的那個版本,然后再讀取每一條 undolog ,利用 undolog 來撤銷未提交事務在相應記錄上的修改。
從實現角度看,undolog 實際上也存放在page中,因此undolog page并不是在事務提交時立即刷盤,而是和表page一樣延遲刷盤。而undolog page的持久化保證亦是通過redolog:在事務生成一條undolog之前, 會先生成對應的redolog,該redolog在事務提交時,將和事務產生其他redolog一樣,被刷入磁盤。崩潰恢復階段,利用redolog前滾完成后,表page和undolog page都被恢復到了崩潰時的版本, 此時利用undolog page 中的每條undolog,去撤銷未提交事務對表page的修改。
事務執行時保證A和D的一般流程: 為了保證事務的A和D, 事務在執行過程中需要做以下事情:
1.1 為生成undolog這個操作,生成redolog;
1.2 生成一條undolog;
2.1 為記錄更新操作,生成redolog;
2.2 更新這條記錄;
崩潰恢復的一般流程: 正如前面提到,崩潰恢復通過redolog前滾和undolog后滾,來保證事務的原子性。具體的流程如下:
至此, InnoDB中事務AD保證的基本原理和主干邏輯,已經梳理完畢。從原理和邏輯層面理清后,我們接著來看下相關的代碼實現,如何整合到InnoDB的整體分析框架中。
經過梳理,我們得到考慮事務A&D保證的InnoDB分析框架如下:
5.5 事務的I
梳理完A和D,我們再來看下I,也就是InnoDB事務的并發控制機制。數據庫事務對I(隔離性)的要求,體現了對事務并發控制的要求。
通常意義上的并發控制,指的是多線程訪問共享資源時,避免讀寫沖突的方法。 由此推廣到事務場景,那就是多線程并發執行事務操作時,避免共享資源讀寫沖突的方法。事務并發控制的關鍵,在于如何對并發執行的多個事務做正確調度,保證每個事務能夠正確地寫數據/讀到正確的數據。 在數據庫理論上,采用沖突可串行化來衡量事務并發調度的效果。
數據庫對并發執行的多個事務的一個調度,如果該調度可通過不斷交換兩個相鄰的無沖突操作,最終轉為為一個串行調度,則稱該并發調度為沖突可串行化調度。
如果數據庫每一次對并發事務的調度,都做到了沖突可串行化調度,則事務的隔離性得到了嚴格的保證。但這種嚴格調度的代價太大,導致事務的并發程度大為降低,從而嚴重影響事務處理性能。為此,才有了隔離級別的概念。隔離級別是對嚴格隔離性的弱化或妥協,目的是降低可串行化要求,進而提升系統的并發度。數據庫的四個隔離級別如下:
串行化:在該級別下,隔離性被嚴格保證,并發事務的調度結果等價于一個串行化調度; 可重復讀:隔離性相對串行化有所弱化,能夠保證事務多次讀取某條數據時讀到相同的數據,但存在幻讀問題; 讀提交:隔離性進一步弱化,能夠保證事務讀到的數據都是已提交的數據,但不能保證可重復讀; 讀未提交:隔離性最弱。只能夠保證寫的一致性,但不能保證讀提交。
通過以上分析,我們從理論上對隔離性這個概念做了一次梳理。接下來我們將進一步討論隔離性在InnoDB中如何保證,也就是InnoDB的事務并發控制機制。
業界事務并發控制的實現思路,主流的有以下兩種:
1.鎖:既對共享資源做必要的保護;
2.時間戳:比較事務和記錄的時間戳,根據兩者大小來解決事務沖突;
值得一提的是, 多版本并發控制(MVCC)并不屬于鎖或者時間戳任何一種。MVCC作為一種技術,甚至無法和上述四個隔離級別聯系起來。 原因在于四個隔離級別是在MVCC技術之前出現的提出的,出發點是在只有鎖、時間戳等技術的前提下,逐級弱化可串行化的要求,提高事務并發執行效率。而MVCC出現在四個隔離級別提出之后,隨后和鎖等并發控制機制相結合,去進一步完善數據庫對事務的并發調度。因此,有了MVCC、ICC(基于索引的并發控制)等技術之后,主流數據庫對事務并發控制的實現,遠比經典理論總結的要復雜。關于這塊的細節,可參考李海翔:《數據庫事務處理的藝術》一書。
InnoDB的并發控制,則完全建立在鎖保護 + MVCC的基礎上,而MVCC,我們未嘗不可視之為一種廣義的鎖(鎖既多個線程之間的契約)。為此,我們只需要跟著鎖走,把InnoDB 相關的機制梳理清楚,則事務的并發控制機制自然水落石出。
先不考慮MVCC,InnoDB的鎖可分為兩大類,Lock和Latch。 Lock是和事務并發控制有關的鎖,而Latch則是和多線程并發爭用資源有關的鎖。兩者具體含義和區別如下:
因此,涉及事務隔離性和隔離級別的保證, 只涉及到Lock,對應的模塊就是InnoDB的鎖系統。 在我個人看來, InnoDB的鎖系統(含MVCC的實現),是基礎軟件中罕見的精品,Heikki Tuuri把給事務加鎖這個事情做成了一門藝術,值得每一位有志于基礎軟件研發的同學好好學習。
InnoDB啟動開發是在1995年左右。此時事務封鎖的機制已經是學術上一個被充分研究的領域,事務相關的概念和模型,比如ACID四個正交特性、多粒度封鎖、兩階段封鎖、讀沖突的幾種情況和4個隔離級別等,都在學術上得到了透徹的討論,從而有效地指導工業實現(當然這并不是說,事務封鎖機制是先有完整的學術理論,然后再落地到工業界。相反,以Jim Gray為代表的數據庫事務理論奠基者們,一直是在工業界開展工作,工業界大量的技術創新在他們卓越研究能力的催生下,才誕生了堪稱偉大的數據庫事務理論。正如Gray在《事務處理:概念和技術》中所言,“事務處理是一個實踐引導理論的研究領域,通常在商業系統實現了某些思想很久以后,該思想才會在學術界出現”)。
所以1995年左右當Heikki開始寫InnoDB時, 事務處理已經是成熟的技術,而Oracle、DB2亦是珠玉在前。按理說Heikki Tuuri只需要按照成熟方法去做一版工整的實現即可,但Heikki卻通過設計上的巧思和精湛的實現,在Oracle、DB2之后發出了自己的聲音,成就又一個經典。
在我看來,InnoDB鎖系統(含MVCC)的經典之處在于:
1.一鎖多用的行鎖設計。InnoDB中的行鎖對象 struct lock_rec_t 不僅僅是行鎖,也是gap鎖,next-key鎖,甚至是頁鎖。傳統意義上,行鎖的定義非常簡單,就是作用于行上的一把X鎖(比如Oracle的行鎖),其加鎖原理來自于兩階段封鎖機制。而gap鎖、next-key鎖的(用于解決RR隔離級別下幻讀的問題),其設計思想更多是來自于前面提到的基于索引的并發控制技術。從這個角度看,InnoDB相當于是用 lock_rec_t 這樣一種數據結構,刻畫了更多類型的鎖。另外,在事務做范圍查詢或順序掃描時,一把InnoDB的 lock_rec_t 對象又起到了頁鎖的效果,可用很少的空間記錄一個數據頁中的各行記錄的加鎖情況。
究其根本,還是在于InnoDB中的行鎖,并不是傳統意義上的行鎖,而是索引鎖。在InnoDB的設計中,B+樹索引是表數據存儲的底層物理數據結構,表的數據既一個聚集索引。所以在鎖系統的設計中,InnoDB采用表鎖+索引鎖,而不是傳統意義的表鎖+行鎖設計,也是順理成章的事情。
2.打破常規的并發度和隔離性平衡方法。我們知道,四個事務隔離級別出現的原因,是為了最求更高的事務并發度,從而對分級別弱化嚴格隔離性。四個事務隔離級別是事務處理技術的經典法則,甚至被納入到ANS的在SQL標準規范中。但是,個人認為,Heikki在設計InnoDB鎖系統時,可能根本就沒去考慮RC級別該怎么實現,RR級別該怎么實現,他的目標只有1個:利用MVCC技術,做一個并發度最高,隔離性最好的鎖系統,有了這個設計后,才開始考慮兼容4個隔離級別。所以我們才會看到,InnoDB默認隔離級別是RR,在OLTP場景多數情況下,InnoDB的RR隔離級別性能要比RC隔離級別高這樣的情況。通知RR隔離級別通過GAP鎖、Next鎖等機制,可一定程度避免幻讀問題。
綜合以上兩點來看,我們會發現InnoDB鎖系統背后,存在極簡這樣的一個設計理念,這個理念我個人認為也是整個InnoDB的設計理念。比如表數據既一個聚集索引,一鎖多用的行鎖設計,打破常規的并發度和隔離性平衡,基于寫入字節數的LSN設計等,都體現了這一點。
極簡帶來的好處有很多,有實現成本上的,也有美學上的。從實現成本來看,不得不說InnoDB的代碼是極為精煉的,相同的代碼量包含的信息,要比其他存儲引擎多得多。而更少代碼意味著維護成本更低(當然反過來說也對閱讀者和開發者的能力提出了更高的要求)。從美學上來看,極簡一般來說總是更好的。可以說,極簡是任何一個技術門類發展的必然趨勢。在前人經典設計的基礎上,總會有后來者打破常規,用更簡潔的方式進行設計,在實現功能、穩定性、性能和性價比的同時,達到美學上的新高度。寫到這里,想起知乎上有一個問題:建筑如何輕盈起來,頗能對該現象做進一步闡述。
回到InnoDB鎖系統的代碼實現,我們可以看到InnoDB鎖系統的數據結構設計,是非常簡單清晰的。總共來說只有兩個基礎數據對象(lock_t 和 ReadView,其中lock_t把行鎖和表鎖折疊在一起)和三個全局結構(lock_sys->rec_hash, dict_table_t::locks 和 trx_sys->mvcc)。其中:
lock_sys->rec_hash: 為存儲行鎖對象的hash表;
dict_table_t::locks: dict_table_t對象中的locks鏈表,存儲該表當前的所有表鎖信息。dict_table_t對象,是InnoDB中表數據字典對象,一張表只有1個dict_table_t對象,所有的dict_table_t對象存儲在dict_sys->table_hash 中;
trx_sys->mvcc: InnoDB中的MVCC總控結構,內含兩個ReadView鏈表:m_free 和m_views。其中m_views存儲了當前被所有事務對象(trx_t)正在使用或暫時擁有的ReadView對象;m_free為空閑ReadView對象。
在事務對象(trx_t)中,維護了一個lock結構,指向該事務對象擁有的所有表鎖信息和行鎖信息; 同時維護了一個read_view對象,指向該事務正在使用或者暫時擁有的ReadView。
經過梳理,我們進一步得到考慮事務ACID保證的InnoDB分析框架如下:
6. 結語
本文是《MySQL8-InnoDB總體架構和運行機制的系統分析》的上篇。 主要內容分為兩塊,首先在前一篇文章的基礎上,進一步闡述了我們團隊的MySQL代碼分析方法,并給出了分析工作的4個步驟(3.2節-演繹法求解); 第二,詳細介紹了4個步驟的第一個步驟(建立分析框架),我們團隊所做的工作,給出了我們的InnoDB代碼分析框架。
本文的下篇將詳細介紹我們團隊在第2到第4個分析步驟所做的事情以及產出。具體包括:
步驟2-數據對象分析;
步驟3-運行機制分析;
步驟4-代碼結構梳理。
敬請期待。
作者:UCloud云數據庫內核團隊
總結
以上是生活随笔為你收集整理的天龙源码框架分析_MySQL8-InnoDB总体架构和运行机制的系统分析(上)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高德机器人的名字是怎么呼叫的_“一键呼叫
- 下一篇: 雨棚板弹性法计算简图_造价工程师:钢结构