InnoDB數(shù)據(jù)存儲及事務兩階段提交原理解析
1、背景和目標
1.1 背景
MySQL在互聯(lián)網(wǎng)行業(yè)應用廣泛,性能強、可靠性高,云廠商還提供了許多擴展工具,生態(tài)相對其他數(shù)據(jù)庫而言比較成熟。
歸因于成熟的基建,業(yè)務研發(fā)人員更需關(guān)心的是數(shù)據(jù)庫設(shè)計方案、操作數(shù)據(jù)時的性能和一致性問題。如果我們在使用事務時,不知道數(shù)據(jù)存儲方式和事務實現(xiàn)原理,往往會在一個事務的多次讀寫過程中產(chǎn)生bug,即數(shù)據(jù)的變更不符合預期。因此當了解了MySQL事務的底層實現(xiàn)原理,我們就能知道如何編寫代碼以達到預期,就能知道數(shù)據(jù)庫引擎設(shè)計的精妙之處。
1.2 目標
詳細介紹MySQL InnoDB的數(shù)據(jù)模型、數(shù)據(jù)持久化策略、事務提交以及故障恢復原理。
2、InnoDB存儲結(jié)構(gòu)
2.1 InnoDB邏輯存儲結(jié)構(gòu)
InnoDB邏輯存儲結(jié)構(gòu)層級:表空間->段->區(qū)->頁->行
如上圖所示,數(shù)據(jù)表有許多數(shù)據(jù)行,分別存儲在16KB的Page上,把一定數(shù)量的Page整合為了一個Extent(默認是64個Page即共1M),而多個Extent又構(gòu)成了一個Segment,不同類型的Segment又組成了對應類型的表空間。
2.2 InnoDB物理存儲結(jié)構(gòu)
InnoDB總體結(jié)構(gòu)分為內(nèi)存結(jié)構(gòu)(下圖左側(cè))和磁盤結(jié)構(gòu)(右側(cè))兩部分。
3、InnoDB磁盤結(jié)構(gòu)詳解
3.1 表空間
磁盤部分包括各種表空間,包括系統(tǒng)表空間(System Tablespace)、獨立表空間(File-Per-Table Tablespaces)、undo表空間(Undo Tablespaces)、通用表空間(General Tablespaces)、臨時表空間(Temporary TableSpaces)5種表空間。
表空間可以看做是InnoDB存儲引擎邏輯結(jié)構(gòu)的最高層 ,所有的數(shù)據(jù)都是存放在表空間中。InnoDB通過參數(shù)InnoDB_file_per_table(DMS是ON)可以選擇使用系統(tǒng)表空間還是獨立表空間存儲表,如果不是ON,則所有InnoDB表都保存在ibdata1這個表文件中,否則一個表占據(jù)一個表文件,擁有自己獨立的表文件(用戶記錄、索引和插入緩沖Bitmap),即每個Table單獨存儲為一個“.ibd”文件,但change buffer等依然存放在系統(tǒng)表空間。
3.2 段
多個段組成一個表空間。常見的段有數(shù)據(jù)段、索引段、回滾段等,段是一個邏輯的概念,是一些零散頁面和一些完整的區(qū)的集合。不同類型的數(shù)據(jù)保存在單獨的段內(nèi),可以更好的保持該類型數(shù)據(jù)的連續(xù)性,可以提升訪問磁盤的效率。創(chuàng)建一個索引會創(chuàng)建數(shù)據(jù)段和索引段,即一個索引占用兩個段。
- 數(shù)據(jù)段:B+樹的葉子節(jié)點(Leaf node segment)
- 索引段:B+樹的非葉子節(jié)點(Non-leaf node segment)
- 回滾段(rollback segment):InnoDB中undo log是采用分段(segment)的方式進行存儲的,每一個rollback segment內(nèi)部由1024個undo segment組成,每個undo Tablespace最多會包含128個rollback segment。每一時刻一個undo segment都是被一個事務獨占的,每個寫事務都會持有至少一個undo segment,當有大量寫事務并發(fā)運行時,就需要存在多個undo segment。MySQL 8.0由于支持了最多128個獨立的Undo Tablespace,一方面避免了ibdata1的膨脹,方便undo空間回收,另一方面也大大增加了最大的rollback segment的個數(shù),增加了可支持的最大并發(fā)寫事務數(shù)(128*128*1024)。
注意,雖然InnoDB區(qū)分了數(shù)據(jù)段和索引段,但由于數(shù)據(jù)是以主鍵為索引來組織數(shù)據(jù)的存儲的,所以索引文件和數(shù)據(jù)文件都在同一個文件中,都在“.ibd”文件里面。
3.3 區(qū)
表空間中的頁實在是太多了,為了更好的管理這些頁面,InnoDB提出了區(qū)的概念。一個表空間劃分為多個區(qū)(extent),一個區(qū)內(nèi)包含物理上連續(xù)的64個頁,因此一個區(qū)空間大小為64*16KB=1M。區(qū)就是為了保證頁的連續(xù)性,InnoDB一次會從磁盤申請4~5個區(qū)。
段可以簡單理解為是一個邏輯的概念,而Extent是一個物理概念,每次B+樹的擴容都是以Extent為單位來擴容的,默認一次擴容不超過4個Extent。
段區(qū)分了數(shù)據(jù)段和索引段,其實也就有了各自的區(qū),即葉子節(jié)點和非葉子節(jié)點都有自己獨立的區(qū)。想象一下,當B+樹按順序范圍查詢時,如果數(shù)據(jù)分布在磁盤的不同位置,就會產(chǎn)生隨機IO,而如果數(shù)據(jù)的物理位置相鄰,就可以通過順序IO讀取了。
3.4 頁
頁是InnoDB中管理數(shù)據(jù)的最小單元,是固定大小的一段連續(xù)磁盤空間,默認為16KB,用于存放數(shù)據(jù)、索引等各種類型的數(shù)據(jù)。
InnoDB中,常見的頁類型有數(shù)據(jù)索引頁、undo page、文件管理頁FSP_HDR/XDES、插入緩沖IBUF_BITMAP頁、INODE頁等。
在InnoDB中的設(shè)計中,頁與頁之間是通過一個雙向鏈表連接起來,而存儲在頁中的數(shù)據(jù)行則是通過單鏈表連接起來的,如下圖:
頁有通用的文件頭和尾(將頁的內(nèi)容進行封裝,通過文件頭和文件尾的checksum方式來確保頁的完整性),但是中部的內(nèi)容根據(jù)頁的類型不同而發(fā)生變化。我們主要關(guān)注數(shù)據(jù)頁和索引頁,這種類型的頁包括七個部分:
- File Header:文件頭,共38B,記錄了頁的地址、頁號、上一頁和下一頁指針、頁的類型信息、頁的校驗和checksum(校驗和在寫入磁盤前計算得到,當從磁盤中讀取時,重新計算校驗和并與數(shù)據(jù)頁中存儲的對比,如果發(fā)現(xiàn)不同,則會導致MySQL crash)、日志序列位置(LSN,Log Sequence Number,表示日志文件的長度,一個不斷遞增的unsigned long類型整數(shù))等。
- Page Header:數(shù)據(jù)頁頭,用來記錄數(shù)據(jù)頁的狀態(tài)信息,包括Free Space的地址、本頁中的記錄的數(shù)量、標記為刪除的記錄等,共56B。
- System records:Infimum + Supremum Records。InnoDB每頁中有兩個虛擬的行記錄,用來限定記錄的邊界。Infimum記錄是比該頁中任何主鍵值都要小的記錄,Supremum記錄是比該頁中任何主鍵值都要大的記錄。這兩個記錄在頁創(chuàng)建時被建立,并且在任何情況下不會被刪除,并且由于這兩條記錄不是我們自己定義的記錄,所以它們并不存放在頁的User Records部分。所以如果數(shù)據(jù)是順序存儲的,那么查詢數(shù)據(jù)是否在某一頁中就無需遍歷頁中的所有數(shù)據(jù),只需判斷這兩個記錄就行了。
- User Records:用戶記錄,以單鏈表的形式存儲,如下圖:
- Free Space:空閑空間,用于存放新記錄。在一開始生成頁的時候,并沒有User Records這個部分,每當插入一條記錄,就會從Free Space部分中申請一個記錄大小的空間到User Records部分,當Free Space用完時,這個頁也就使用完了。
- Page Directory:數(shù)據(jù)目錄(彌補單向鏈表查詢性能差的缺點),InnoDB會把頁中的記錄劃分為若干個組,每個組的最后一個記錄的地址偏移量作為一個槽,存放在Page Directory中,便于二分查找定位數(shù)據(jù)。對于分組中的記錄數(shù)是有規(guī)定的:Infimum記錄所在的分組只能有 1 條記錄,Supremum記錄所在的分組中的記錄條數(shù)只能在1~8條之間,中間的其它分組中記錄數(shù)只能在是4~8條之間。所以如果數(shù)據(jù)是順序存儲的,那么查詢數(shù)據(jù)在某一頁的位置就無需遍歷頁中的所有數(shù)據(jù),只通過二分法就可以快速定位到對應的槽,然后再遍歷該槽對應分組中的記錄就能知道了。
- File Trailer:文件尾,共8B,包括頁的校驗和checksum(依賴于引擎選用的校驗算法,不一定與文件頭的checksum相同)、日志序列位置(LSN),與File Header中的相同。默認情況下,InnoDB每次從磁盤讀取一個頁就會檢測該頁的完整性,即File Trailer中的內(nèi)容需和File Header保持一致。
3.5 行
數(shù)據(jù)行即一行一行的數(shù)據(jù)。MySQL中單行數(shù)據(jù)最大能存儲64KB=65535B,故表中字段長度加起來如果超過該值就會拒絕創(chuàng)建表。以utf8mb4字符集下varchar(M)為例,該字符集下一個字符最多需要4B表示,如果M大于16383,那么總字節(jié)數(shù)就會超過4*16383=65532B,所以M的最大值就是16383個字符。
雖然單行數(shù)據(jù)最大值遠大于單頁(16KB),但MySQL為了在單頁中至少存儲2行數(shù)據(jù)(每行8KB),引入了行溢出機制,即只要一行記錄的總和超過8KB,就會溢出,比如varchar(9000) 或者 varchar(3000) + varchar(3000) + varchar(3000),當實際長度大于8k的時候,會對最大字段使用uncompress BLOB page單獨存儲(即一個字段獨享一個或多個頁),而在Barracuda文件格式下字段本身只會用20B存儲溢出行的地址和占用的字節(jié)數(shù)。
InnoDB的文件格式包括舊格式Antelope和新格式Barracuda(DMS使用該格式),兩者主要的不同在于對存儲數(shù)據(jù)時所占用的空間差異,每種文件格式有自己支持的行格式,行格式就是指數(shù)據(jù)行的存儲方式,包括是否緊湊存儲(占用磁盤空間)、是否可變長度存儲、大索引前綴支持、壓縮支持。差異如下:
行格式 | 緊湊的存儲特性 | 增強的可變長度列存儲 | 大索引鍵前綴支持 | 壓縮支持 | 支持的表空間類型 | 所需文件格式 |
REDUNDANT(冗余) | 否 | 否 | 否 | 否 | system, file-per-table, general | Antelope or Barracuda |
COMPACT(緊湊) | 是 | 否 | 否 | 否 | system, file-per-table, general | Antelope or Barracuda |
DYNAMIC(動態(tài)) | 是 | 是 | 是 | 否 | system, file-per-table, general | Barracuda |
COMPRESSED(壓縮) | 是 | 是 | 是 | 是 | file-per-table, general | Barracuda |
通過下列指令可以查詢到數(shù)據(jù)庫的文件格式和行格式配置:
REDUNDANT和其他幾種類型的區(qū)別在就是在于首部的內(nèi)容區(qū)別。REDUNDANT的存儲格式為首部是一個字段長度偏移列表(每個字段占用的字節(jié)長度及其相應的位移),其他類型的存儲格式為首部是一個非NULL的變長字段長度列表,這種方式存儲數(shù)據(jù)會更加緊湊(頁中存放的行數(shù)越多,性能就越高),數(shù)據(jù)布局如下圖:
- 針對VARCHAR、TEXT、BLOB這類變長字段,列中實際存儲了多少數(shù)據(jù)是不固定的,因此除了要把數(shù)據(jù)本身存下來,還需要記下它的長度。
- 如果字段值為NULL,其并不占該部分任何空間,除了占有NULL標志位,故兩個字段為NULL就占用2bit。
- 頭信息中包括刪除標記、當前記錄是否是分組中的最后一條、當前記錄在頁中的相對位置、記錄類型(0:普通記錄,1:B+樹非葉子節(jié)點目錄項記錄,2:Infimum記錄,3:Supremum記錄)、下一條記錄的相對位置等。
- 每行數(shù)據(jù)除了用戶定義的列外,還有3個隱藏列,包括trx_id列和roll_pointer列(見下文),分別為6字節(jié)和7字節(jié)的大小,若表沒有定義主鍵,每行還會增加一個6字節(jié)的rowid列。
注意,索引也是按這種方式存儲的:
- 對于聚簇索引,非葉子節(jié)點包含主鍵和child page number,葉子節(jié)點包含主鍵和具體的行;
- 對于非聚簇索引,也就是二級索引,非葉子節(jié)點包含二級索引和child page number,葉子節(jié)點包含二級索引和主鍵值。
4、InnoDB內(nèi)存結(jié)構(gòu)詳解
4.1 buffer pool
buffer pool是InnoDB的緩存,用來存放各種數(shù)據(jù),包括索引頁(index page)、數(shù)據(jù)頁(data page)、undo頁、插入緩沖、自適應哈希索引(AHI)、innodb存儲的鎖信息、數(shù)據(jù)字典等。把磁盤上的數(shù)據(jù)加載到緩沖池中(通過預讀機制加載當前頁、相鄰頁),可避免每次訪問都進行磁盤IO,起到加速訪問的作用。應用程序在對數(shù)據(jù)庫執(zhí)行增刪改操作的時候,實際上主要都是針對內(nèi)存里的buffer pool中的數(shù)據(jù)進行的。
buffer pool包含三種數(shù)據(jù)類型:
- free page:從未用過的頁。
- clean page:干凈的頁,即數(shù)據(jù)頁的數(shù)據(jù)和磁盤一致。
- dirty page:臟頁,即數(shù)據(jù)頁的數(shù)據(jù)和磁盤不一致。
針對這3種頁,InnoDB使用3種鏈表維護:
- free list:空閑頁鏈表,管理free page。
- flush list:臟頁鏈表,管理dirty page并在某個時刻對該鏈表的臟頁進行刷盤,按臟頁的修改時間排序,更新操作較早的臟頁先被刷盤。
- lru list:正在使用的內(nèi)存頁鏈表,里面包含clean page和dirty page,也就是說lru list中的頁包含flush list中的所有臟頁。lru list遵循lru算法管理緩存頁。
InnoDB需要保證buffer pool的數(shù)據(jù)都是熱點數(shù)據(jù),將無效的預讀數(shù)據(jù)快速刪除、不將讀入后立即使用的數(shù)據(jù)替換熱點數(shù)據(jù),就引入了變種lru算法(新生代+老生代、老生代停留時間窗口)來解決“預讀失效”與“緩沖池污染”的問題。通過下列指令可以查詢到數(shù)據(jù)庫設(shè)置冷熱分界線和成為熱塊的所需時間:
Mysql5.7.5之后,buffer pool有分塊(chunk)的特性,即一個buffer pool實例是由多個塊組成,每個塊的塊內(nèi)空間是連續(xù)的,塊與塊之間則是離散的。分塊是為了方便用戶在mysql運行期間能夠調(diào)整buffer pool的大小。
注意,為了提高讀寫性能,避免過少的數(shù)據(jù)刷盤或隨機IO,buffer pool一般不會對單個Page實時刷盤,所以這就出現(xiàn)了緩存和磁盤的一致性問題,InnoDB通過引入redolog來保存數(shù)量操作記錄從而解決此問題,見下文。
4.2 change buffer
change buffer(寫緩存)是一種特殊的數(shù)據(jù)結(jié)構(gòu),可以避免數(shù)據(jù)更改時因為隱式查詢數(shù)據(jù)帶來的磁盤IO。change buffer默認占buffer pool的 25%,最大允許占50%??梢愿鶕?jù)寫業(yè)務的量調(diào)整,寫操作越頻繁,change buffer帶來的性能提升越明顯。
change buffer工作原理如下:
- 當更改的頁存在于buffer pool的lru list,則直接在緩沖池中修改這個頁,這個頁會變成臟頁,鏈入到flush list中,但并不馬上刷盤;此時不涉及change buffer操作。
- 當更改的頁不存在于buffer pool的lru list,就要先從磁盤讀取要修改的數(shù)據(jù)頁到buffer pool后再修改(數(shù)據(jù)不會在磁盤中直接更改)。但為了避免修改操作引發(fā)的磁盤讀IO,系統(tǒng)會將DML操作記錄到change buffer中,并不馬上刷盤。等下次對這些修改的頁進行查詢時,由于lru list不存在該頁,會從磁盤讀取(磁盤頁是更改前的數(shù)據(jù)),為了避免讀到臟數(shù)據(jù),該磁盤頁會和change buffer中的更改合并后才鏈入到lru list。如果未來一段時間都不會查詢到這個修改了的頁,也會有insert buffer thread定時將change buffer的數(shù)據(jù)合并到磁盤頁中。
- 如果做出的更改是對唯一鍵索引的值的修改,InnoDB要做唯一性校驗,必須查詢磁盤,再在lru list上的頁修改,不會在change buffer中操作。
綜上:change buffer適合寫多讀少的場景,并且滿足非唯一索引。
4.3 Adaptive Hash Index
Adaptive Hash Index(AHI,自適應哈希索引),是指InnoDB存儲引擎通過監(jiān)控表上索引頁的查找模式,自動根據(jù)查找模式對“熱點數(shù)據(jù)”來創(chuàng)建哈希索引。因為對B+樹索引的訪問需要依次訪問根節(jié)點>中間節(jié)點>葉子節(jié)點,而對哈希索引的訪問僅需要一次HASH計算即可定位到目標位置。一些資料統(tǒng)計,啟用AHI后,讀取和寫入速度可以提高2倍,輔助索引的連接操作性能可以提高5倍。
通過下列指令可以查詢到數(shù)據(jù)庫的相關(guān)設(shè)置:
AHI使用條件:
- 索引被訪問了17次(BTR_SEARCH_HASH_ANALYSIS)
- 索引中的某個頁已經(jīng)被訪問了至少100次(BTR_SEARCH_BUILD_LIMIT)
- 數(shù)據(jù)頁被相同模式(相同的查詢條件)訪問N次(N=頁中記錄*1/16)
AHI使用buffer pool中的數(shù)據(jù)頁進行構(gòu)造,僅保存在內(nèi)存中,且僅對熱點數(shù)據(jù)進行處理,因此構(gòu)造AHI速度極快。
4.4 log buffer
log buffer就是redolog buffer的簡稱,是存儲要寫入磁盤上的redolog的內(nèi)存區(qū)域。
log buffer由變量innodb_log_buffer_size定義大小,默認為16MB(DMS中設(shè)置了8GB)。log buffer的內(nèi)容會根據(jù)設(shè)置刷盤,足夠大的log buffer可以使得大事務完全依賴緩存運行,而不需要在事務提交前將redolog數(shù)據(jù)寫入磁盤。因此,如果有更新、插入或刪除許多行的事務,增加log buffer的大小可以節(jié)省磁盤I/O。
log buffer是順序?qū)懙模⒈P也是順序的,所以當某個臟頁對應的redolog從log buffer刷盤時,會保證將在其之前產(chǎn)生的redolog也刷盤,詳情見下文redolog的介紹。
5、三種log類型和作用
5.1 undolog
undolog是InnoDB的日志,又稱撤銷日志文件,屬于邏輯日志。undolog內(nèi)存數(shù)據(jù)存儲在buffer pool中,磁盤數(shù)據(jù)則存儲在undo tablespace。
undolog保存類型為FIL_PAGE_UNDO_LOG在undo page中,一個undo page可以保存多條undolog記錄。每條undolog記錄包含該undolog在undo page的頁內(nèi)地址、undolog對應的記錄所在的tableId(tableId全局唯一)、undolog類型、undolog編號、下一條undolog的地址、old_trx_id、old_roll_pointer、主鍵的每個列占用的存儲空間大小和真實值、被修改字段的修改前后信息等。
undolog提供回滾和多個行版本控制(MVCC)的兩個能力,保證了事務的原子性:
- 回滾:undolog分為3類,包括TRX_UNDO_INSERT_REC、TRX_UNDO_DEL_MARK_REC、TRX_UNDO_UPD_EXIST_REC,分別對應增、刪、改操作。上文講到每一行數(shù)據(jù)有兩個隱藏字段trx_id和roll_pointer,它們主要作用于數(shù)據(jù)庫的事務。所謂的事務就是指對一個或多個數(shù)據(jù)庫的一系列操作,這些操作需保證ACID的規(guī)則。當某個事務執(zhí)行過程中對某個表執(zhí)行了增、刪、改操作,InnoDB會給該事務分配一個遞增的獨一無二的trx_id(全局變量,每增加256時會刷盤),而且會生成對應的undolog,而roll_pointer就是一個指向記錄對應的undolog的一個指針,數(shù)據(jù)行會存儲最近提交的trx_id和roll_pointer。事務開啟后,在本事務或其他事務(根據(jù)事務隔離級別)對該數(shù)據(jù)行的一次次修改中,生成的undolog會記錄old_trx_id(修改該記錄的上一次的trx_id)和old_roll_pointer(對應undolog地址),這樣就生成了一個版本鏈,當需要回滾時就能沿著old_trx_id和old_roll_pointer找到一條記錄的所有歷史版本,從而實現(xiàn)事務的回滾能力。
- MVCC:InnoDB復用了undolog中已經(jīng)記錄的歷史版本數(shù)據(jù)來實現(xiàn)MVCC機制。當用戶讀取一行記錄時,若該記錄已經(jīng)被其他事務占用,當前事務可以通過undolog讀取之前的行版本信息,以此實現(xiàn)非鎖定讀取。此外,根據(jù)trx_id是遞增的特性,InnoDB還引入了ReadView機制,用于保存創(chuàng)建事務時的活躍trx_id。ReadView有三個屬性,分別是m_ids(活躍的trx_id列表)、min_trx_id(活躍的最小trx_id)、max_trx_id(下一個該分配的trx_id),基于這三個屬性,實現(xiàn)了READ COMMITTED和REPEATABLE READ兩種隔離級別。
因為一個事務可能包含多個增、刪、改操作,為了提高并發(fā)執(zhí)行多個事務寫入undolog的性能,InnoDB將各個事務的各種操作通過上文提到的undo segment分開存儲(undo segment的undo page通過鏈式存儲,即每個事務都有自己的insert undo鏈表、update undo鏈表),而每個段的第一個undo page通過TRX_UNDO_STATE屬性存儲了該段的一些事務信息,取值有下面幾個:
- TRX_UNDO_ACTIVE: 活躍狀態(tài),即一個活躍的事務正在往這個段里邊寫入undolog。
- TRX_UNDO_CACHED:被緩存的狀態(tài),即該狀態(tài)下的段等待著之后被其他事務重用。
- TRX_UNDO_TO_FREE: 可以釋放,對于insert undo鏈表來說,如果在它對應的事務提交之后,該鏈表不能被重用,那么就會處于這種狀態(tài)。
- TRX_UNDO_TO_PURGE: 可以清理,對于update undo鏈表來說,如果在它對應的事務提交之后,該鏈表不能被重用,那么就會處于這種狀態(tài)。
- TRX_UNDO_PREPARED: 準備狀態(tài),還未提交。
在事務未提交前TRX_UNDO_STATE是TRX_UNDO_PREPARED狀態(tài),事務提交后,根據(jù)不同的操作類型轉(zhuǎn)換成TRX_UNDO_CACHED、TRX_UNDO_TO_FREE或者TRX_UNDO_TO_PURGE狀態(tài),表示滿足一定條件后可以清理這些undolog,事務如果需要回滾的話,必須是TRX_UNDO_ACTIVE或者TRX_UNDO_PREPARED狀態(tài),故事務的提交是由該屬性判斷的,詳情見下文的事務執(zhí)行流程。
5.2 redolog
redolog是InnoDB存儲引擎層的日志,又稱重做日志文件,屬于物理日志。redolog內(nèi)存數(shù)據(jù)存儲在log buffer中,磁盤數(shù)據(jù)則存儲在以ib_logfile0、ib_logfile1…命名的日志文件中。
上文提到,InnoDB通過buffer pool(包括change buffer、undolog)提高讀寫性能,但如果進程或機器崩潰會導致緩存丟失,為了能實現(xiàn)故障恢復就引入了redolog。事務在執(zhí)行過程中對數(shù)據(jù)庫所做的所有修改(聚集索引、二級索引、undolog等修改)都會生成對應的redolog,并保證redolog早于緩存落盤(WAL機制),當故障發(fā)生后,InnoDB會在重啟時,通過重放redolog來恢復所做的修改。
到MySQL8.0為止,為了應對各種各樣不同的需求,InnoDB已經(jīng)有多達65種(上限127種)的redolog類型用來記錄各種信息,而恢復數(shù)據(jù)時需要判斷不同的類型,來做對應的解析。redolog長度是動態(tài)的,常見的數(shù)據(jù)結(jié)構(gòu)包括日志類型、Space ID、頁號、數(shù)據(jù)頁中的偏移量、修改的長度和具體的值。
根據(jù)redolog不同的作用對象,可以將這些類型劃分為三個大類:作用于Page、作用于Space以及提供額外信息的Logic類型。redolog記錄的是作用于頁的,如果作用于Space,那么頁號的值為0。
不管是在內(nèi)存還是磁盤中,redolog都以塊為單位進行存儲,默認每個塊占512B,等于磁盤扇區(qū)的大小,這稱為redolog block。每個redolog block由3部分組成:日志塊頭(12B)、日志塊尾(4B)和日志主體(492B),log buffer則是由若干個連續(xù)的redolog block組成的,總數(shù)不能超過1GB個(基于LSN的長度限制)。
InnoDB為了提高redolog的性能和保證數(shù)據(jù)一致性,還引入的mini-transaction機制(簡稱mtr),mtr就是redolog組的概念,比如對一些頁面的訪問、向聚簇索引或二級索引插入一條記錄等操作時產(chǎn)生的redolog是不可分割的(插入數(shù)據(jù)如果引起索引分裂,會產(chǎn)生許多redolog)。每組的最后一條redolog后邊會加上一條類型為MLOG_MULTI_REC_END的redolog,來標識該組的結(jié)束。
log buffer中寫入redolog的過程是順序的,但不是一條一條寫入,而是一個mtr完成后,將里面所有的redolog一起復制到log buffer中(還會把執(zhí)行過程中可能修改過的頁面加入到Buffer Pool的flush鏈表),也就是存儲到redolog block中,可能占用不到一個block,也可能占用多個block。一個事務可以包含多個mtr,那么多個事務的mtr就會有交集,事務間的mtr會相互穿插。
5.3 binlog
binlog是屬于MySQL Server層面的,又稱為歸檔日志,屬于邏輯日志,是以二進制的形式記錄的,是sql語句的原始邏輯,主要是用于進行集群中保證主從一致以及執(zhí)行異常操作后恢復數(shù)據(jù)。
binlog日志文件默認大小由磁盤決定,順序追加寫入。binlog內(nèi)存數(shù)據(jù)存儲在binlog cache中(大小由binlog_cache_size控制),磁盤數(shù)據(jù)則存儲在binlog file中。
binlog有三種格式,分別是Row、Statement、Mixed。
- Row格式記錄了操作語句對具體行的操作以及操作前的整行信息,缺點是占空間大(一條sql影響的行數(shù)),優(yōu)點是能保證數(shù)據(jù)安全,不會發(fā)生遺漏,是5.7版本默認格式。
- Statement格式記錄了修改的sql(只是一條sql語句),缺點是在集群中可能會導致操作不一致從而使得數(shù)據(jù)不一致,如執(zhí)行now()函數(shù)可能會導致不同機器值不同。
- Mixed格式會針對于操作的sql選擇使用Row還是Statement,相比于Row更省空間,但還是可能發(fā)生主從不一致的情況。
binlog和redolog雖然都保存了記錄的修改日志,但兩者有一些區(qū)別:
- binlog是邏輯日志,記錄的是對哪一個表的哪一行做了什么修改;redolog是物理日志,記錄的是對哪個數(shù)據(jù)頁中的哪個記錄做了什么修改。
- binlog是追加寫;redolog是循環(huán)寫,日志文件有固定大小,會覆蓋之前的數(shù)據(jù)。
- binlog是Server層的日志;redolog是InnoDB的日志。如果不使用InnoDB引擎,就沒有redolog。
6、InnoDB持久化策略
6.1 InnoDB兩種持久化策略
InnoDB內(nèi)存部分包括緩沖池(buffer pool) 和日志緩沖(log buffer),兩者刷盤方式不同,前者走direct_io模式(直接繞過Page Cache來訪問磁盤),后者走Page Cache模式(IO操作需要委托操作系統(tǒng)來完成)。
- 是否使用Page Cache的區(qū)別是什么?
OS的Page Cache對讀寫做了不少優(yōu)化,包括按順序預讀?。ò错撟x?。?、在成簇磁盤塊(n次方個扇區(qū))上執(zhí)行IO、允許訪問同一文件的多個進程共享高速緩存的緩沖區(qū)等,但數(shù)據(jù)必須在用戶進程與內(nèi)核互相拷貝。
direct_io的優(yōu)點是減少操作系統(tǒng)緩沖區(qū)和用戶地址空間的拷貝次數(shù),降低了CPU和內(nèi)存帶寬的開銷。而InnoDB本身也已處理好buffer pool與磁盤數(shù)據(jù)的對應關(guān)系,所以可以舍去Page Cache。
- 先刷buffer pool還是先刷log buffer?
先寫日志,再寫磁盤(WAL機制,Write-Ahead Logging),即redolog和binlog等日志數(shù)據(jù)刷盤到log文件完成后,才會將臟頁從buffer pool刷盤到表文件。
為什么運用WAL機制?因為順序?qū)懘疟P的性能堪比寫內(nèi)存,所以寫日志會比數(shù)據(jù)刷盤的性能高很多,只要保證日志寫入成功,再通過代碼保證日志和需刷盤數(shù)據(jù)的一致性,就能在保證數(shù)據(jù)不丟失的情況下大大提高性能。
順序?qū)戇\用很廣泛,比如kafka追加寫實現(xiàn)了事務消息,即提交或回滾事務時,會追加寫入一條控制類型的消息來標識是commit或rollback。
6.2 buffer pool持久化過程
buffer pool刷盤時機主要有以下四種:
- MySQL正常關(guān)閉之前,會把所有的臟頁刷盤;
- Master Thread會以每秒或者每10秒一次的頻率定期將適量的臟頁刷盤。上文講到buffer pool通過變種lru算法區(qū)分冷熱數(shù)據(jù),故后臺線程會優(yōu)先刷冷數(shù)據(jù),因為熱數(shù)據(jù)在短時間可能被多次修改,如果優(yōu)先刷盤熱數(shù)據(jù)頁,這個頁很快又會被修改,又需要再刷盤,不如等它變成冷數(shù)據(jù)再刷盤。
- lru空閑列表不足、log buffer或磁盤空間不足時,page cleaner線程會異步將臟頁刷盤。
- buffer pool空間不足時,用戶線程從磁盤讀取某個頁要鏈入lru list,lru list會釋放尾部的一個頁。假設(shè)這個釋放的頁是一個臟頁,那么用戶線程就不得不親自把這個臟頁刷盤,這樣就會降低響應用戶請求的速度。之所以需要后臺線程定時刷盤臟頁就是為了盡可能避免發(fā)生這種主動刷盤的情況。
InnoDB還引入了double write buffer物理存儲空間,來處理buffer pool刷盤時的異常情況。buffer pool的臟頁要刷盤時,數(shù)據(jù)頁的空間為16KB,OS文件系統(tǒng)的頁空間一般為4KB,磁盤的扇區(qū)每片一般為512B,最終都會一片片的刷扇區(qū)。計算機硬件和操作系統(tǒng),在極端情況下(比如斷電)往往并不能保證這一操作的原子性,如果16KB的數(shù)據(jù)在寫入4KB時發(fā)生了系統(tǒng)斷電/os crash,只有一部分寫是成功的,這種情況寫就是partial page write。
mysql在恢復的過程中是檢查頁的checksum(頁的校驗和,見上文),發(fā)生partial page write問題時, Page已經(jīng)損壞,找不到該頁的checksum,就無法通過redolog恢復。
因此根據(jù)上述問題,InnoDB將buffer pool中的臟頁刷盤時,會先通過memcpy函數(shù)將Page刷到double write buffer,再將數(shù)據(jù)拷貝到數(shù)據(jù)文件對應的位置。
double write buffer是物理磁盤上共享表空間中連續(xù)的128個頁(每頁16KB,大小共2MB, 每次寫入1MB)。
- 如果寫double write buffer失敗,那么這些數(shù)據(jù)不會刷盤,InnoDB會載入磁盤原始數(shù)據(jù)和redo日志比較,并重新刷到double write buffer,然后再刷盤。
- 如果寫double write buffer成功,但是刷盤失?。╬artial page write問題),那么InnoDB就不會通過事務日志來恢復了,而是直接用double write buffer中的數(shù)據(jù)刷盤。
6.3 redolog持久化過程
redolog包括兩部分:一是內(nèi)存中的日志緩沖(log buffer),該部分日志是易失性的;二是磁盤上的重做日志文件(redolog file,以ib_logfile0、ib_logfile1…命名),該部分日志是持久的。
redolog可以通過參數(shù)InnoDB_log_files_in_group配置成多個文件(最大100),另外一個參數(shù)InnoDB_log_file_size表示每個文件的大小,因此總的redolog大小為InnoDB_log_files_in_group * InnoDB_log_file_size。
上文講到內(nèi)存中l(wèi)og buffer是由多個redolog block組成的(日志塊頭占12B、日志塊尾占4B),那么redolog file也是如此,每個redolog file的前4個block用于表示文件頭,存儲了一些管理信息,往后則存儲log buffer中的block鏡像。文件頭主要存儲了標記redolog file開始的LSN值(Log Sequence Number的簡稱)、標記redolog已刷盤的全局變量flushed_to_disk_lsn值、標記臟頁已刷盤的全局變量checkpoint_lsn等。
- LSN:LSN記錄了已經(jīng)寫入的redolog的日志量,是一個全局變量,初始值為8704。每次寫入一個mtr時,LSN就會累加上mtr所占的空間字節(jié)數(shù)和相應的block頭尾空間字節(jié)數(shù)。比如mtr_1產(chǎn)生的redolog為200B,那么LSN就變成了8704+12+200=8916,之后mtr_2又產(chǎn)生了1000B的redolog,那么LSN就變成了8916+296+4+512+12+208=9948。
- flushed_to_disk_lsn:系統(tǒng)第一次啟動時,flushed_to_disk_lsn值和初始的LSN值是相同的,都是8704。隨著系統(tǒng)的運行,redolog被不斷寫入log buffer,但是并不會立即刷盤,LSN的值就和flushed_to_disk_lsn的值拉開了差距,如果兩者的值相同時,說明log buffer中的所有redolog都已經(jīng)刷盤了。
- checkpoint_lsn:checkpoint_lsn的初始值也是8704,當flush鏈表中的臟頁按順序被刷盤時,mtr生成的對應redolog就可以被覆蓋了,所以我們可以進行一個增加checkpoint_lsn的操作,我們把這個過程稱之為做一次checkpoint。臟頁是與redolog有關(guān)聯(lián)的,記錄了redolog的LSN信息,通過臟頁可以找到對應的redolog,通過redolog也可以恢復對應的臟頁。
下圖展示了一組4個文件的redolog日志,checkpoint_lsn之前的空間表示可以進行寫的文件。
我們再看下log buffer刷盤的具體過程:
- 客戶端向數(shù)據(jù)庫發(fā)送寫命令。
- 數(shù)據(jù)庫收到寫命令。
- 數(shù)據(jù)庫通過系統(tǒng)調(diào)用將數(shù)據(jù)寫入內(nèi)核緩沖區(qū)(Page Cache)。
- 操作系統(tǒng)將緩沖區(qū)數(shù)據(jù)傳輸至磁盤控制器,暫存在磁盤緩沖區(qū)。
- 磁盤控制器將數(shù)據(jù)精準的寫入物理磁盤。
如果數(shù)據(jù)庫停機,那么第三步之后操作系統(tǒng)可以保證數(shù)據(jù)寫入磁盤;如果是操作系統(tǒng)停機,此時磁盤也無法正常工作,那就必須完成這五步才能保證數(shù)據(jù)落盤。
如上所述,在將寫操作寫入redolog的過程中也不是直接就進行磁盤IO來完成的,而是分為三個步驟:
- 寫入log buffer中,這部分是屬于MySQL的內(nèi)存中,是全局公用的。
- 在事務編寫完成后,就可以執(zhí)行write操作,寫到文件系統(tǒng)的Page Cache中。
- 執(zhí)行fsync(持久化)操作,將Page Cache中的數(shù)據(jù)正式寫入磁盤上的redolog文件中,也就是圖中的hard disk。
InnoDB_flush_log_at_trx_commit參數(shù)控制了log buffer的刷盤時機(值可為0、1、2,默認1):
- 設(shè)置為0:每隔1秒從log buffer寫入Page cache,并馬上刷盤,mysql服務故障或者主機宕機則丟失1秒(由log buffer的innodb_flush_log_at_timeout參數(shù)控制)數(shù)據(jù)。
- 設(shè)置為1:事務提交時,立刻從log buffer寫入Page cache, 并馬上刷盤,mysql服務故障或者主機宕機不會丟失數(shù)據(jù),但會頻繁發(fā)生磁盤IO。
- 設(shè)置為2:事務提交時,立刻從log buffer寫入Page cache,每隔1秒刷盤,mysql服務故障不會丟失數(shù)據(jù),因為數(shù)據(jù)已經(jīng)進入操作系統(tǒng)緩存,與mysql進程無關(guān)了,主機宕機則丟失1秒數(shù)據(jù)。
除此之外,當log buffer空間不足、做checkpoint、Mysql正常關(guān)閉、binlog切換等情況也會觸發(fā)redolog刷盤。刷盤操作是異步IO,由專門的線程完成這件事,不會阻塞用戶請求的處理。redolog如果沒有及時刷盤或者只刷盤一部分,是會導致事務丟失的。
6.4 undolog持久化過程
InnoDB的undolog嚴格的講不是Log,而是數(shù)據(jù),因此他的管理和落盤都跟數(shù)據(jù)一樣:
- undolog的磁盤結(jié)構(gòu)并不是順序的,而是像數(shù)據(jù)一樣按Page管理。
- undolog寫入時,也像數(shù)據(jù)一樣產(chǎn)生對應的redolog。
- undolog的Page也像數(shù)據(jù)一樣緩存在Buffer Pool中,跟數(shù)據(jù)Page一起做lru換入換出,以及刷臟。undo page的刷臟也像數(shù)據(jù)一樣要等到對應的redolog落盤之后。
之所以這樣實現(xiàn),首要的原因是undolog需要承擔MVCC對歷史版本的管理作用,設(shè)計目標是高事務并發(fā),方便的管理和維護,因此當做數(shù)據(jù)更合適。
6.5 binlog持久化過程
binlog也有獨立的刷盤策略,通過sync_binlog參數(shù)控制(值分別為0、1、N,默認為1):
- 設(shè)置為0 :每次提交事務都只將binlog cache進行write,不fsync。
- 設(shè)置為1 :每次提交事務都會將binlog cache進行write,并執(zhí)行fsync。
- 設(shè)置為N :表示每次提交事務都會將binlog cache進行write,但累積N個事務后才fsync。
由于binlog是屬于MySQL Server層面的日志,只需追加寫入即可。
7、MySQL事務提交和崩潰恢復
7.1 MySQL中的XA協(xié)議
有一個名叫X/Open的組織提出了一個名為XA的規(guī)范。這個XA規(guī)范提出了2個角色:
- 一個全局事務由多個小的事務組成,所以我們得在某個地方找一個總攬全局的角色用于和各個小事務進行溝通,指導它們是提交還是回滾。這個角色被稱作事務協(xié)調(diào)器(Transaction Coordinator)。
- 管理一個小事務的角色被稱作事務管理器(Transaction Manager)。
要提交一個全局事務,那么屬于該全局事務的若干個小事務就應該全部提交,只要有任何一個小事務無法提交,那么整個全局事務就應該全部回滾。XA規(guī)范中指出,要提交一個全局事務,必須分為2步:
- Prepare階段:當協(xié)調(diào)器準備提交一個全局事務時,會依次通知各個管理器把在事務執(zhí)行過程中所產(chǎn)生的數(shù)據(jù)都刷盤。
- Commit階段:如果在Prepare階段各個管理器都完成了數(shù)據(jù)的刷盤,那么協(xié)調(diào)器就要真正通知各個管理器去提交事務了,否則就需要讓這些管理器回滾事務了。
XA規(guī)范把上述全局事務提交時所經(jīng)歷的兩個階段稱作兩階段提交。在單個MySQL實例中,將server層作為事務協(xié)調(diào)器,存儲引擎作為事務管理器,故本文將binlog作為事務協(xié)調(diào)器。
7.2 sql執(zhí)行流程
sql提交到MySQL時需要進行詞法語法分析、優(yōu)化(如果沒有命中索引,就會掃全表),才會執(zhí)行:
7.3 事務執(zhí)行流程
假設(shè)我們要更新一條數(shù)據(jù),語句如下:
- Server層的執(zhí)行器先調(diào)用引擎取出ID=2這一行。ID是主鍵,引擎直接用樹搜索找到這一行。如果 ID=2這一行所在的數(shù)據(jù)頁本來就在內(nèi)存中,就直接返回給執(zhí)行器;否則需要先從磁盤讀入內(nèi)存,然后再返回。
- 執(zhí)行器拿到數(shù)據(jù)把這個值+1(分配trx_id,開始記錄事務),得到新的數(shù)據(jù),再調(diào)用存儲引擎接口寫入這行新數(shù)據(jù)。此處會先記錄undolog,并將undolog對應的變化信息redolog保存到log buffer中,然后再去修改buffer pool,并且把buffer pool對應的變化信息redolog記錄到log buffer中,詳情見上文。
- InnoDB做完上述操作后,就準備提交事務了。此時處在Prepare階段,執(zhí)行器調(diào)用binlog_prepare接口,就會將上文提到的undo segment的狀態(tài)置為TRX_UNDO_PREPARED,并將本次提交事務的XID也寫入其中,同時生成對應的redolog。此時根據(jù)redolog的刷盤策略,本次事務對應的log buffer可能會被刷盤,而只要log buffer刷盤成功,那么即使之后系統(tǒng)崩潰,在重啟恢復的時候也可以將處于Prepare狀態(tài)的事務完全恢復(恢復buffer pool和undolog),然后回滾或者再次提交事務。
- 而到Commit階段,執(zhí)行器繼續(xù)調(diào)用binlog_commit接口提交事務,此時會先將事務執(zhí)行過程中產(chǎn)生的binlog(包括XID)按照binlog的刷盤策略刷入磁盤,再根據(jù)不同的操作類型把undo segment的狀態(tài)轉(zhuǎn)換成TRX_UNDO_CACHED、TRX_UNDO_TO_FREE或者TRX_UNDO_TO_PURGE(這幾個狀態(tài)是InnoDB的事務結(jié)束的標志),表示滿足一定條件后可以清理這些undolog,并將對應的redolog刷盤。至此這個事務就算是提交完了,注意事務提交需要三次刷盤(寫redolog,寫binlog,寫commit,InnoDB新版本通過組提交進行了優(yōu)化)。而臟頁并不一定隨著事務提交而刷盤,需依賴于buffer pool持久化策略。
- 對于處于Prepare狀態(tài)的事務,存儲引擎既可以提交,也可以回滾,這取決于目前該事務對應的binlog是否已經(jīng)寫入硬盤。這時就會讀取最后一個binlog日志文件,從日志文件中找一下有沒有該Prepare事務對應的XID記錄,如果有的話,就將該事務提交,否則就回滾好了。
7.4 如果沒有兩階段提交
redolog未寫入,binlog未寫入:此時MySQL異常重啟無法恢復數(shù)據(jù),認為sql就沒執(zhí)行。
redolog寫入,binlog未寫入:此時MySQL異常重啟能根據(jù)redolog恢復事務提交時的數(shù)據(jù),但binlog沒有記錄,后續(xù)使用binlog恢復臨時庫會出現(xiàn)數(shù)據(jù)丟失,導致狀態(tài)不一致。
binlog寫入,redolog未寫入:此時MySQL異常重啟臨時庫能根據(jù)binlog重放事務提交時的數(shù)據(jù),但redolog沒有記錄,如果主庫有一些臟頁已經(jīng)刷盤,本應先回滾再通過binlog重放,但現(xiàn)在無法回滾,會導致狀態(tài)不一致。
7.5 結(jié)論
所謂兩階段提交,就是指同時將redolog和binlog都寫成功,這樣既能保證通過binlog恢復臨時庫時和主庫無差異,又能保證通過redolog恢復主庫時和臨時庫無差異。