一文讀懂InnoDB數據存儲及事務兩階段提交原理解析

存儲矩陣 發佈 2022-12-23T14:49:38.983117+00:00

1背景和目標1.1 背景MySQL在網際網路行業應用廣泛,性能強、可靠性高,雲廠商還提供了許多擴展工具,生態相對其他資料庫而言比較成熟。歸因於成熟的基建,業務研發人員更需關心的是資料庫設計方案、操作數據時的性能和一致性問題。

1背景和目標

1.1 背景

Mysql在網際網路行業應用廣泛,性能強、可靠性高,雲廠商還提供了許多擴展工具,生態相對其他資料庫而言比較成熟。

歸因於成熟的基建,業務研發人員更需關心的是資料庫設計方案、操作數據時的性能和一致性問題。如果我們在使用事務時,不知道數據存儲方式和事務實現原理,往往會在一個事務的多次讀寫過程中產生bug,即數據的變更不符合預期。因此當了解了MySQL事務的底層實現原理,我們就能知道如何編寫代碼以達到預期,就能知道資料庫引擎設計的精妙之處。

1.2 目標

詳細介紹MySQL innodb的數據模型、數據持久化策略、事務提交以及故障恢復原理。

2 InnoDB存儲結構

2.1 InnoDB邏輯存儲結構

InnoDB邏輯存儲結構層級:表空間->段->區->頁->行

如上圖所示,數據表有許多數據行,分別存儲在16KB的Page上,把一定數量的Page整合為了一個Extent(默認是64個Page即共1M),而多個Extent又構成了一個Segment,不同類型的Segment又組成了對應類型的表空間。

2.2 InnoDB物理存儲結構

InnoDB總體結構分為內存結構(下圖左側)和磁碟結構(右側)兩部分。

3 InnoDB磁碟結構詳解

3.1 表空間

磁碟部分包括各種表空間,包括系統表空間(System Tablespace)、獨立表空間(File-Per-Table Tablespaces)、undo表空間(Undo Tablespaces)、通用表空間(General Tablespaces)、臨時表空間(Temporary TableSpaces)5種表空間。

表空間可以看做是InnoDB存儲引擎邏輯結構的最高層 ,所有的數據都是存放在表空間中。InnoDB通過參數InnoDB_file_per_table(DMS是ON)可以選擇使用系統表空間還是獨立表空間存儲表,如果不是ON,則所有InnoDB表都保存在ibdata1這個表文件中,否則一個表占據一個表文件,擁有自己獨立的表文件(用戶記錄、索引和插入緩衝Bitmap),即每個Table單獨存儲為一個「.ibd」文件,但change buffer等依然存放在系統表空間。

3.2 段

多個段組成一個表空間。常見的段有數據段、索引段、回滾段等,段是一個邏輯的概念,是一些零散頁面和一些完整的區的集合。不同類型的數據保存在單獨的段內,可以更好的保持該類型數據的連續性,可以提升訪問磁碟的效率。創建一個索引會創建數據段和索引段,即一個索引占用兩個段。

  • 數據段:B+樹的葉子節點(Leaf node segment)
  • 索引段:B+樹的非葉子節點(Non-leaf node segment)
  • 回滾段(rollback segment):InnoDB中undo log是採用分段(segment)的方式進行存儲的,每一個rollback segment內部由1024個undo segment組成,每個undo Tablespace最多會包含128個rollback segment。每一時刻一個undo segment都是被一個事務獨占的,每個寫事務都會持有至少一個undo segment,當有大量寫事務並發運行時,就需要存在多個undo segment。MySQL 8.0由於支持了最多128個獨立的Undo Tablespace,一方面避免了ibdata1的膨脹,方便undo空間回收,另一方面也大大增加了最大的rollback segment的個數,增加了可支持的最大並發寫事務數(128*128*1024)。

注意,雖然InnoDB區分了數據段和索引段,但由於數據是以主鍵為索引來組織數據的存儲的,所以索引文件和數據文件都在同一個文件中,都在「.ibd」文件裡面。

3.3 區

表空間中的頁實在是太多了,為了更好的管理這些頁面,InnoDB提出了區的概念。一個表空間劃分為多個區(extent),一個區內包含物理上連續的64個頁,因此一個區空間大小為64*16KB=1M。區就是為了保證頁的連續性,InnoDB一次會從磁碟申請4~5個區。

段可以簡單理解為是一個邏輯的概念,而Extent是一個物理概念,每次B+樹的擴容都是以Extent為單位來擴容的,默認一次擴容不超過4個Extent。

段區分了數據段和索引段,其實也就有了各自的區,即葉子節點和非葉子節點都有自己獨立的區。想像一下,當B+樹按順序範圍查詢時,如果數據分布在磁碟的不同位置,就會產生隨機IO,而如果數據的物理位置相鄰,就可以通過順序IO讀取了。

3.4 頁

頁是InnoDB中管理數據的最小單元,是固定大小的一段連續磁碟空間,默認為16KB,用於存放數據、索引等各種類型的數據。

InnoDB中,常見的頁類型有數據索引頁、undo page、文件管理頁FSP_HDR/XDES、插入緩衝IBUF_BITMAP頁、INODE頁等。

在InnoDB中的設計中,頁與頁之間是通過一個雙向鍊表連接起來,而存儲在頁中的數據行則是通過單鍊表連接起來的,如下圖:

頁有通用的文件頭和尾(將頁的內容進行封裝,通過文件頭和文件尾的checksum方式來確保頁的完整性),但是中部的內容根據頁的類型不同而發生變化。我們主要關注數據頁和索引頁,這種類型的頁包括七個部分:

  • File Header:文件頭,共38B,記錄了頁的地址、頁號、上一頁和下一頁指針、頁的類型信息、頁的校驗和checksum(校驗和在寫入磁碟前計算得到,當從磁碟中讀取時,重新計算校驗和並與數據頁中存儲的對比,如果發現不同,則會導致MySQL crash)、日誌序列位置(LSN,Log Sequence number,表示日誌文件的長度,一個不斷遞增的unsigned long類型整數)等。
  • Page Header:數據頁頭,用來記錄數據頁的狀態信息,包括Free Space的地址、本頁中的記錄的數量、標記為刪除的記錄等,共56B。
  • System records:Infimum + Supremum Records。InnoDB每頁中有兩個虛擬的行記錄,用來限定記錄的邊界。Infimum記錄是比該頁中任何主鍵值都要小的記錄,Supremum記錄是比該頁中任何主鍵值都要大的記錄。這兩個記錄在頁創建時被建立,並且在任何情況下不會被刪除,並且由於這兩條記錄不是我們自己定義的記錄,所以它們並不存放在頁的User Records部分。所以如果數據是順序存儲的,那麼查詢數據是否在某一頁中就無需遍歷頁中的所有數據,只需判斷這兩個記錄就行了。
  • User Records:用戶記錄,以單鍊表的形式存儲,如下圖:
  • Free Space:空閒空間,用於存放新記錄。在一開始生成頁的時候,並沒有User Records這個部分,每當插入一條記錄,就會從Free Space部分中申請一個記錄大小的空間到User Records部分,當Free Space用完時,這個頁也就使用完了。
  • Page Directory:數據目錄(彌補單向鍊表查詢性能差的缺點),InnoDB會把頁中的記錄劃分為若干個組,每個組的最後一個記錄的地址偏移量作為一個槽,存放在Page Directory中,便於二分查找定位數據。對於分組中的記錄數是有規定的:Infimum記錄所在的分組只能有 1 條記錄,Supremum記錄所在的分組中的記錄條數只能在1~8條之間,中間的其它分組中記錄數只能在是4~8條之間。所以如果數據是順序存儲的,那麼查詢數據在某一頁的位置就無需遍歷頁中的所有數據,只通過二分法就可以快速定位到對應的槽,然後再遍歷該槽對應分組中的記錄就能知道了。
  • File Trailer:文件尾,共8B,包括頁的校驗和checksum(依賴於引擎選用的校驗算法,不一定與文件頭的checksum相同)、日誌序列位置(LSN),與File Header中的相同。默認情況下,InnoDB每次從磁碟讀取一個頁就會檢測該頁的完整性,即File Trailer中的內容需和File Header保持一致。

3.5 行

數據行即一行一行的數據。MySQL中單行數據最大能存儲64KB=65535B,故表中欄位長度加起來如果超過該值就會拒絕創建表。以utf8mb4字符集下VARCHAR(M)為例,該字符集下一個字符最多需要4B表示,如果M大於16383,那麼總字節數就會超過4*16383=65532B,所以M的最大值就是16383個字符。

雖然單行數據最大值遠大於單頁(16KB),但MySQL為了在單頁中至少存儲2行數據(每行8KB),引入了行溢出機制,即只要一行記錄的總和超過8KB,就會溢出,比如varchar(9000) 或者 varchar(3000) + varchar(3000) + varchar(3000),當實際長度大於8k的時候,會對最大欄位使用uncompress BLOB page單獨存儲(即一個欄位獨享一個或多個頁),而在Barracuda文件格式下欄位本身只會用20B存儲溢出行的地址和占用的字節數。

InnoDB的文件格式包括舊格式Antelope和新格式Barracuda(DMS使用該格式),兩者主要的不同在於對存儲數據時所占用的空間差異,每種文件格式有自己支持的行格式,行格式就是指數據行的存儲方式,包括是否緊湊存儲(占用磁碟空間)、是否可變長度存儲、大索引前綴支持、壓縮支持。差異如下:

行格式

緊湊的存儲特性

增強的可變長度列存儲

大索引鍵前綴支持

壓縮支持

支持的表空間類型

所需文件格式

REDUNDANT(冗餘)

system, file-per-table, general

Antelope or Barracuda

COMPACT(緊湊)

system, file-per-table, general

Antelope or Barracuda

DYNAMIC(動態)

system, file-per-table, general

Barracuda

COMPRESSED(壓縮)

file-per-table, general

Barracuda

通過下列指令可以查詢到資料庫的文件格式和行格式配置:

show variables like "InnoDB_file_format";
show variables like "InnoDB_default_row_format";

REDUNDANT和其他幾種類型的區別在就是在於首部的內容區別。REDUNDANT的存儲格式為首部是一個欄位長度偏移列表(每個欄位占用的字節長度及其相應的位移),其他類型的存儲格式為首部是一個非NULL的變長欄位長度列表,這種方式存儲數據會更加緊湊(頁中存放的行數越多,性能就越高),數據布局如下圖:

  • 針對VARCHAR、TEXT、BLOB這類變長欄位,列中實際存儲了多少數據是不固定的,因此除了要把數據本身存下來,還需要記下它的長度。
  • 如果欄位值為NULL,其並不占該部分任何空間,除了占有NULL標誌位,故兩個欄位為NULL就占用2bit。
  • 頭信息中包括刪除標記、當前記錄是否是分組中的最後一條、當前記錄在頁中的相對位置、記錄類型(0:普通記錄,1:B+樹非葉子節點目錄項記錄,2:Infimum記錄,3:Supremum記錄)、下一條記錄的相對位置等。
  • 每行數據除了用戶定義的列外,還有3個隱藏列,包括trx_id列和roll_pointer列(見下文),分別為6位元組和7位元組的大小,若表沒有定義主鍵,每行還會增加一個6位元組的rowid列。

注意,索引也是按這種方式存儲的:

  • 對於聚簇索引,非葉子節點包含主鍵和child page number,葉子節點包含主鍵和具體的行;
  • 對於非聚簇索引,也就是二級索引,非葉子節點包含二級索引和child page number,葉子節點包含二級索引和主鍵值。

4 InnoDB內存結構詳解

4.1 buffer pool

buffer pool是InnoDB的緩存,用來存放各種數據,包括索引頁(index page)、數據頁(data page)、undo頁、插入緩衝、自適應哈希索引(AHI)、innodb存儲的鎖信息、數據字典等。把磁碟上的數據加載到緩衝池中(通過預讀機制加載當前頁、相鄰頁),可避免每次訪問都進行磁碟IO,起到加速訪問的作用。應用程式在對資料庫執行增刪改操作的時候,實際上主要都是針對內存里的buffer pool中的數據進行的。

buffer pool包含三種數據類型:

  • free page:從未用過的頁。
  • clean page:乾淨的頁,即數據頁的數據和磁碟一致。
  • dirty page:髒頁,即數據頁的數據和磁碟不一致。

針對這3種頁,InnoDB使用3種鍊表維護:

  • free list:空閒頁鍊表,管理free page。
  • flush list:髒頁鍊表,管理dirty page並在某個時刻對該鍊表的髒頁進行刷盤,按髒頁的修改時間排序,更新操作較早的髒頁先被刷盤。
  • lru list:正在使用的內存頁鍊表,裡面包含clean page和dirty page,也就是說lru list中的頁包含flush list中的所有髒頁。lru list遵循lru算法管理緩存頁。

InnoDB需要保證buffer pool的數據都是熱點數據,將無效的預讀數據快速刪除、不將讀入後立即使用的數據替換熱點數據,就引入了變種lru算法(新生代+老生代、老生代停留時間窗口)來解決「預讀失效」與「緩衝池污染」的問題。通過下列指令可以查詢到資料庫設置冷熱分界線和成為熱塊的所需時間:

show variables like 'InnoDB_old_blocks_pct'; -- 單位%,默認37,代表冷數據占比
show variables like 'InnoDB_old_blocks_time'; -- 單位ms,默認1000

Mysql5.7.5之後,buffer pool有分塊(chunk)的特性,即一個buffer pool實例是由多個塊組成,每個塊的塊內空間是連續的,塊與塊之間則是離散的。分塊是為了方便用戶在mysql運行期間能夠調整buffer pool的大小。

注意,為了提高讀寫性能,避免過少的數據刷盤或隨機IO,buffer pool一般不會對單個Page實時刷盤,所以這就出現了緩存和磁碟的一致性問題,InnoDB通過引入redolog來保存數量操作記錄從而解決此問題,見下文。

4.2 change buffer

change buffer(寫緩存)是一種特殊的數據結構,可以避免數據更改時因為隱式查詢數據帶來的磁碟IO。change buffer默認占buffer pool的 25%,最大允許占50%。可以根據寫業務的量調整,寫操作越頻繁,change buffer帶來的性能提升越明顯。

change buffer工作原理如下:

  1. 當更改的頁存在於buffer pool的lru list,則直接在緩衝池中修改這個頁,這個頁會變成髒頁,鏈入到flush list中,但並不馬上刷盤;此時不涉及change buffer操作。
  2. 當更改的頁不存在於buffer pool的lru list,就要先從磁碟讀取要修改的數據頁到buffer pool後再修改(數據不會在磁碟中直接更改)。但為了避免修改操作引發的磁碟讀IO,系統會將DML操作記錄到change buffer中,並不馬上刷盤。等下次對這些修改的頁進行查詢時,由於lru list不存在該頁,會從磁碟讀取(磁碟頁是更改前的數據),為了避免讀到髒數據,該磁碟頁會和change buffer中的更改合併後才鏈入到lru list。如果未來一段時間都不會查詢到這個修改了的頁,也會有insert buffer thread定時將change buffer的數據合併到磁碟頁中。
  3. 如果做出的更改是對唯一鍵索引的值的修改,InnoDB要做唯一性校驗,必須查詢磁碟,再在lru list上的頁修改,不會在change buffer中操作。

綜上:change buffer適合寫多讀少的場景,並且滿足非唯一索引。

4.3 Adaptive Hash Index

Adaptive Hash Index(AHI,自適應哈希索引),是指InnoDB存儲引擎通過監控表上索引頁的查找模式,自動根據查找模式對「熱點數據」來創建哈希索引。因為對B+樹索引的訪問需要依次訪問根節點>中間節點>葉子節點,而對哈希索引的訪問僅需要一次HASH計算即可定位到目標位置。一些資料統計,啟用AHI後,讀取和寫入速度可以提高2倍,輔助索引的連接操作性能可以提高5倍。

通過下列指令可以查詢到資料庫的相關設置:

show variables like '%hash_index';(DMS設置的是OFF)

AHI使用條件:

  1. 索引被訪問了17次(BTR_SEARCH_HASH_ANALYSIS)
  2. 索引中的某個頁已經被訪問了至少100次(BTR_SEARCH_BUILD_LIMIT)
  3. 數據頁被相同模式(相同的查詢條件)訪問N次(N=頁中記錄*1/16)

AHI使用buffer pool中的數據頁進行構造,僅保存在內存中,且僅對熱點數據進行處理,因此構造AHI速度極快。

4.4 log buffer

log buffer就是redolog buffer的簡稱,是存儲要寫入磁碟上的redolog的內存區域。

log buffer由變量innodb_log_buffer_size定義大小,默認為16MB(DMS中設置了8GB)。log buffer的內容會根據設置刷盤,足夠大的log buffer可以使得大事務完全依賴緩存運行,而不需要在事務提交前將redolog數據寫入磁碟。因此,如果有更新、插入或刪除許多行的事務,增加log buffer的大小可以節省磁碟I/O。

log buffer是順序寫的,刷盤也是順序的,所以當某個髒頁對應的redolog從log buffer刷盤時,會保證將在其之前產生的redolog也刷盤,詳情見下文redolog的介紹。

5 三種log類型和作用

5.1 undolog

undolog是InnoDB的日誌,又稱撤銷日誌文件,屬於邏輯日誌。undolog內存數據存儲在buffer pool中,磁碟數據則存儲在undo tablespace。

undolog保存類型為FIL_PAGE_UNDO_LOG在undo page中,一個undo page可以保存多條undolog記錄。每條undolog記錄包含該undolog在undo page的頁內地址、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,分別對應增、刪、改操作。上文講到每一行數據有兩個隱藏欄位trx_id和roll_pointer,它們主要作用於資料庫的事務。所謂的事務就是指對一個或多個資料庫的一系列操作,這些操作需保證ACID的規則。當某個事務執行過程中對某個表執行了增、刪、改操作,InnoDB會給該事務分配一個遞增的獨一無二的trx_id(全局變量,每增加256時會刷盤),而且會生成對應的undolog,而roll_pointer就是一個指向記錄對應的undolog的一個指針,數據行會存儲最近提交的trx_id和roll_pointer。事務開啟後,在本事務或其他事務(根據事務隔離級別)對該數據行的一次次修改中,生成的undolog會記錄old_trx_id(修改該記錄的上一次的trx_id)和old_roll_pointer(對應undolog地址),這樣就生成了一個版本鏈,當需要回滾時就能沿著old_trx_id和old_roll_pointer找到一條記錄的所有歷史版本,從而實現事務的回滾能力。
  • MVCC:InnoDB復用了undolog中已經記錄的歷史版本數據來實現MVCC機制。當用戶讀取一行記錄時,若該記錄已經被其他事務占用,當前事務可以通過undolog讀取之前的行版本信息,以此實現非鎖定讀取。此外,根據trx_id是遞增的特性,InnoDB還引入了ReadView機制,用於保存創建事務時的活躍trx_id。ReadView有三個屬性,分別是m_ids(活躍的trx_id列表)、min_trx_id(活躍的最小trx_id)、max_trx_id(下一個該分配的trx_id),基於這三個屬性,實現了READ COMMITTED和REPEATABLE READ兩種隔離級別。

因為一個事務可能包含多個增、刪、改操作,為了提高並發執行多個事務寫入undolog的性能,InnoDB將各個事務的各種操作通過上文提到的undo segment分開存儲(undo segment的undo page通過鏈式存儲,即每個事務都有自己的insert undo鍊表、update undo鍊表),而每個段的第一個undo page通過TRX_UNDO_STATE屬性存儲了該段的一些事務信息,取值有下面幾個:

  • TRX_UNDO_ACTIVE: 活躍狀態,即一個活躍的事務正在往這個段裡邊寫入undolog。
  • TRX_UNDO_CACHED:被緩存的狀態,即該狀態下的段等待著之後被其他事務重用。
  • TRX_UNDO_TO_FREE: 可以釋放,對於insert undo鍊表來說,如果在它對應的事務提交之後,該鍊表不能被重用,那麼就會處於這種狀態。
  • TRX_UNDO_TO_PURGE: 可以清理,對於update undo鍊表來說,如果在它對應的事務提交之後,該鍊表不能被重用,那麼就會處於這種狀態。
  • TRX_UNDO_PREPARED: 準備狀態,還未提交。

在事務未提交前TRX_UNDO_STATE是TRX_UNDO_PREPARED狀態,事務提交後,根據不同的操作類型轉換成TRX_UNDO_CACHED、TRX_UNDO_TO_FREE或者TRX_UNDO_TO_PURGE狀態,表示滿足一定條件後可以清理這些undolog,事務如果需要回滾的話,必須是TRX_UNDO_ACTIVE或者TRX_UNDO_PREPARED狀態,故事務的提交是由該屬性判斷的,詳情見下文的事務執行流程。

5.2 redolog

redolog是InnoDB存儲引擎層的日誌,又稱重做日誌文件,屬於物理日誌。redolog內存數據存儲在log buffer中,磁碟數據則存儲在以ib_logfile0、ib_logfile1…命名的日誌文件中。

上文提到,InnoDB通過buffer pool(包括change buffer、undolog)提高讀寫性能,但如果進程或機器崩潰會導致緩存丟失,為了能實現故障恢復就引入了redolog。事務在執行過程中對資料庫所做的所有修改(聚集索引、二級索引、undolog等修改)都會生成對應的redolog,並保證redolog早於緩存落盤(WAL機制),當故障發生後,InnoDB會在重啟時,通過重放redolog來恢復所做的修改。

到MySQL8.0為止,為了應對各種各樣不同的需求,InnoDB已經有多達65種(上限127種)的redolog類型用來記錄各種信息,而恢復數據時需要判斷不同的類型,來做對應的解析。redolog長度是動態的,常見的數據結構包括日誌類型、Space ID、頁號、數據頁中的偏移量、修改的長度和具體的值。

根據redolog不同的作用對象,可以將這些類型劃分為三個大類:作用於Page、作用於Space以及提供額外信息的Logic類型。redolog記錄的是作用於頁的,如果作用於Space,那麼頁號的值為0。

不管是在內存還是磁碟中,redolog都以塊為單位進行存儲,默認每個塊占512B,等於磁碟扇區的大小,這稱為redolog block。每個redolog block由3部分組成:日誌塊頭(12B)、日誌塊尾(4B)和日誌主體(492B),log buffer則是由若干個連續的redolog block組成的,總數不能超過1GB個(基於LSN的長度限制)。

InnoDB為了提高redolog的性能和保證數據一致性,還引入的mini-transaction機制(簡稱mtr),mtr就是redolog組的概念,比如對一些頁面的訪問、向聚簇索引或二級索引插入一條記錄等操作時產生的redolog是不可分割的(插入數據如果引起索引分裂,會產生許多redolog)。每組的最後一條redolog後邊會加上一條類型為MLOG_MULTI_REC_END的redolog,來標識該組的結束。

log buffer中寫入redolog的過程是順序的,但不是一條一條寫入,而是一個mtr完成後,將裡面所有的redolog一起複製到log buffer中(還會把執行過程中可能修改過的頁面加入到Buffer Pool的flush鍊表),也就是存儲到redolog block中,可能占用不到一個block,也可能占用多個block。一個事務可以包含多個mtr,那麼多個事務的mtr就會有交集,事務間的mtr會相互穿插。

5.3 binlog

binlog是屬於MySQL Server層面的,又稱為歸檔日誌,屬於邏輯日誌,是以二進位的形式記錄的,是sql語句的原始邏輯,主要是用於進行集群中保證主從一致以及執行異常操作後恢復數據。

binlog日誌文件默認大小由磁碟決定,順序追加寫入。binlog內存數據存儲在binlog cache中(大小由binlog_cache_size控制),磁碟數據則存儲在binlog file中。

binlog有三種格式,分別是Row、Statement、Mixed。

  • Row格式記錄了操作語句對具體行的操作以及操作前的整行信息,缺點是占空間大(一條sql影響的行數),優點是能保證數據安全,不會發生遺漏,是5.7版本默認格式。
  • Statement格式記錄了修改的sql(只是一條sql語句),缺點是在集群中可能會導致操作不一致從而使得數據不一致,如執行now()函數可能會導致不同機器值不同。
  • Mixed格式會針對於操作的sql選擇使用Row還是Statement,相比於Row更省空間,但還是可能發生主從不一致的情況。

binlog和redolog雖然都保存了記錄的修改日誌,但兩者有一些區別:

  • binlog是邏輯日誌,記錄的是對哪一個表的哪一行做了什麼修改;redolog是物理日誌,記錄的是對哪個數據頁中的哪個記錄做了什麼修改。
  • binlog是追加寫;redolog是循環寫,日誌文件有固定大小,會覆蓋之前的數據。
  • binlog是Server層的日誌;redolog是InnoDB的日誌。如果不使用InnoDB引擎,就沒有redolog。

6 InnoDB持久化策略

6.1 InnoDB兩種持久化策略

InnoDB內存部分包括緩衝池(buffer pool) 和日誌緩衝(log buffer),兩者刷盤方式不同,前者走direct_io模式(直接繞過Page Cache來訪問磁碟),後者走Page Cache模式(IO操作需要委託作業系統來完成)。

  1. 是否使用Page Cache的區別是什麼?

OS的Page Cache對讀寫做了不少優化,包括按順序預讀取(按頁讀取)、在成簇磁碟塊(n次方個扇區)上執行IO、允許訪問同一文件的多個進程共享高速緩存的緩衝區等,但數據必須在用戶進程與內核互相拷貝。

direct_io的優點是減少作業系統緩衝區和用戶地址空間的拷貝次數,降低了CPU和內存帶寬的開銷。而InnoDB本身也已處理好buffer pool與磁碟數據的對應關係,所以可以捨去Page Cache。

  1. 先刷buffer pool還是先刷log buffer?

先寫日誌,再寫磁碟(WAL機制,Write-Ahead Logging),即redolog和binlog等日誌數據刷盤到log文件完成後,才會將髒頁從buffer pool刷盤到表文件。

為什麼運用WAL機制?因為順序寫磁碟的性能堪比寫內存,所以寫日誌會比數據刷盤的性能高很多,只要保證日誌寫入成功,再通過代碼保證日誌和需刷盤數據的一致性,就能在保證數據不丟失的情況下大大提高性能。

順序寫運用很廣泛,比如kafka追加寫實現了事務消息,即提交或回滾事務時,會追加寫入一條控制類型的消息來標識是commit或rollback。

6.2 buffer pool持久化過程

buffer pool刷盤時機主要有以下四種:

  • MySQL正常關閉之前,會把所有的髒頁刷盤;
  • Master Thread會以每秒或者每10秒一次的頻率定期將適量的髒頁刷盤。上文講到buffer pool通過變種lru算法區分冷熱數據,故後台線程會優先刷冷數據,因為熱數據在短時間可能被多次修改,如果優先刷盤熱數據頁,這個頁很快又會被修改,又需要再刷盤,不如等它變成冷數據再刷盤。
  • lru空閒列表不足、log buffer或磁碟空間不足時,page cleaner線程會異步將髒頁刷盤。
  • buffer pool空間不足時,用戶線程從磁碟讀取某個頁要鏈入lru list,lru list會釋放尾部的一個頁。假設這個釋放的頁是一個髒頁,那麼用戶線程就不得不親自把這個髒頁刷盤,這樣就會降低響應用戶請求的速度。之所以需要後台線程定時刷盤髒頁就是為了儘可能避免發生這種主動刷盤的情況。

InnoDB還引入了double write buffer物理存儲空間,來處理buffer pool刷盤時的異常情況。buffer pool的髒頁要刷盤時,數據頁的空間為16KB,OS文件系統的頁空間一般為4KB,磁碟的扇區每片一般為512B,最終都會一片片的刷扇區。計算機硬體和作業系統,在極端情況下(比如斷電)往往並不能保證這一操作的原子性,如果16KB的數據在寫入4KB時發生了系統斷電/os crash,只有一部分寫是成功的,這種情況寫就是partial page write

mysql在恢復的過程中是檢查頁的checksum(頁的校驗和,見上文),發生partial page write問題時, Page已經損壞,找不到該頁的checksum,就無法通過redolog恢復。

因此根據上述問題,InnoDB將buffer pool中的髒頁刷盤時,會先通過memcpy函數將Page刷到double write buffer,再將數據拷貝到數據文件對應的位置。

double write buffer是物理磁碟上共享表空間中連續的128個頁(每頁16KB,大小共2MB, 每次寫入1MB)。

  • 如果寫double write buffer失敗,那麼這些數據不會刷盤,InnoDB會載入磁碟原始數據和redo日誌比較,並重新刷到double write buffer,然後再刷盤。
  • 如果寫double write buffer成功,但是刷盤失敗(partial page write問題),那麼InnoDB就不會通過事務日誌來恢復了,而是直接用double write buffer中的數據刷盤。

6.3 redolog持久化過程

redolog包括兩部分:一是內存中的日誌緩衝(log buffer),該部分日誌是易失性的;二是磁碟上的重做日誌文件(redolog file,以ib_logfile0、ib_logfile1…命名),該部分日誌是持久的。

redolog可以通過參數InnoDB_log_files_in_group配置成多個文件(最大100),另外一個參數InnoDB_log_file_size表示每個文件的大小,因此總的redolog大小為InnoDB_log_files_in_group * InnoDB_log_file_size。

上文講到內存中log 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記錄了已經寫入的redolog的日誌量,是一個全局變量,初始值為8704。每次寫入一個mtr時,LSN就會累加上mtr所占的空間字節數和相應的block頭尾空間字節數。比如mtr_1產生的redolog為200B,那麼LSN就變成了8704+12+200=8916,之後mtr_2又產生了1000B的redolog,那麼LSN就變成了8916+296+4+512+12+208=9948。
  • flushed_to_disk_lsn:系統第一次啟動時,flushed_to_disk_lsn值和初始的LSN值是相同的,都是8704。隨著系統的運行,redolog被不斷寫入log buffer,但是並不會立即刷盤,LSN的值就和flushed_to_disk_lsn的值拉開了差距,如果兩者的值相同時,說明log buffer中的所有redolog都已經刷盤了。
  • checkpoint_lsn:checkpoint_lsn的初始值也是8704,當flush鍊表中的髒頁按順序被刷盤時,mtr生成的對應redolog就可以被覆蓋了,所以我們可以進行一個增加checkpoint_lsn的操作,我們把這個過程稱之為做一次checkpoint。髒頁是與redolog有關聯的,記錄了redolog的LSN信息,通過髒頁可以找到對應的redolog,通過redolog也可以恢復對應的髒頁。

下圖展示了一組4個文件的redolog日誌,checkpoint_lsn之前的空間表示可以進行寫的文件。

我們再看下log buffer刷盤的具體過程:

  1. 客戶端向資料庫發送寫命令。
  2. 資料庫收到寫命令。
  3. 資料庫通過系統調用將數據寫入內核緩衝區(Page Cache)。
  4. 作業系統將緩衝區數據傳輸至磁碟控制器,暫存在磁碟緩衝區。
  5. 磁碟控制器將數據精準的寫入物理磁碟。

如果資料庫停機,那麼第三步之後作業系統可以保證數據寫入磁碟;如果是作業系統停機,此時磁碟也無法正常工作,那就必須完成這五步才能保證數據落盤。

如上所述,在將寫操作寫入redolog的過程中也不是直接就進行磁碟IO來完成的,而是分為三個步驟:

  1. 寫入log buffer中,這部分是屬於MySQL的內存中,是全局公用的。
  2. 在事務編寫完成後,就可以執行write操作,寫到文件系統的Page Cache中。
  3. 執行fsync(持久化)操作,將Page Cache中的數據正式寫入磁碟上的redolog文件中,也就是圖中的hard disk。

InnoDB_flush_log_at_trx_commit參數控制了log buffer的刷盤時機(值可為0、1、2,默認1):

  • 設置為0:每隔1秒從log buffer寫入Page cache,並馬上刷盤,mysql服務故障或者主機宕機則丟失1秒(由log buffer的innodb_flush_log_at_timeout參數控制)數據。
  • 設置為1:事務提交時,立刻從log buffer寫入Page cache, 並馬上刷盤,mysql服務故障或者主機宕機不會丟失數據,但會頻繁發生磁碟IO。
  • 設置為2:事務提交時,立刻從log buffer寫入Page cache,每隔1秒刷盤,mysql服務故障不會丟失數據,因為數據已經進入作業系統緩存,與mysql進程無關了,主機宕機則丟失1秒數據。

除此之外,當log buffer空間不足、做checkpoint、Mysql正常關閉、binlog切換等情況也會觸發redolog刷盤。刷盤操作是異步IO,由專門的線程完成這件事,不會阻塞用戶請求的處理。redolog如果沒有及時刷盤或者只刷盤一部分,是會導致事務丟失的。

6.4 undolog持久化過程

InnoDB的undolog嚴格的講不是Log,而是數據,因此他的管理和落盤都跟數據一樣:

  • undolog的磁碟結構並不是順序的,而是像數據一樣按Page管理。
  • undolog寫入時,也像數據一樣產生對應的redolog。
  • undolog的Page也像數據一樣緩存在Buffer Pool中,跟數據Page一起做lru換入換出,以及刷髒。undo page的刷髒也像數據一樣要等到對應的redolog落盤之後。

之所以這樣實現,首要的原因是undolog需要承擔MVCC對歷史版本的管理作用,設計目標是高事務並發,方便的管理和維護,因此當做數據更合適。

6.5 binlog持久化過程

binlog也有獨立的刷盤策略,通過sync_binlog參數控制(值分別為0、1、N,默認為1):

  • 設置為0 :每次提交事務都只將binlog cache進行write,不fsync。
  • 設置為1 :每次提交事務都會將binlog cache進行write,並執行fsync。
  • 設置為N :表示每次提交事務都會將binlog cache進行write,但累積N個事務後才fsync。

由於binlog是屬於MySQL Server層面的日誌,只需追加寫入即可。

7 MySQL事務提交和崩潰恢復

7.1 MySQL中的XA協議

有一個名叫X/Open的組織提出了一個名為XA的規範。這個XA規範提出了2個角色:

  • 一個全局事務由多個小的事務組成,所以我們得在某個地方找一個總攬全局的角色用於和各個小事務進行溝通,指導它們是提交還是回滾。這個角色被稱作事務協調器(Transaction Coordinator)。
  • 管理一個小事務的角色被稱作事務管理器(Transaction Manager)。

要提交一個全局事務,那麼屬於該全局事務的若干個小事務就應該全部提交,只要有任何一個小事務無法提交,那麼整個全局事務就應該全部回滾。XA規範中指出,要提交一個全局事務,必須分為2步:

  • Prepare階段:當協調器準備提交一個全局事務時,會依次通知各個管理器把在事務執行過程中所產生的數據都刷盤。
  • Commit階段:如果在Prepare階段各個管理器都完成了數據的刷盤,那麼協調器就要真正通知各個管理器去提交事務了,否則就需要讓這些管理器回滾事務了。

XA規範把上述全局事務提交時所經歷的兩個階段稱作兩階段提交。在單個MySQL實例中,將server層作為事務協調器,存儲引擎作為事務管理器,故本文將binlog作為事務協調器。

7.2 sql執行流程

sql提交到MySQL時需要進行詞法語法分析、優化(如果沒有命中索引,就會掃全表),才會執行:

7.3 事務執行流程

假設我們要更新一條數據,語句如下:

update T set c=c+1 where ID=2;
  1. Server層的執行器先調用引擎取出ID=2這一行。ID是主鍵,引擎直接用樹搜索找到這一行。如果 ID=2這一行所在的數據頁本來就在內存中,就直接返回給執行器;否則需要先從磁碟讀入內存,然後再返回。
  2. 執行器拿到數據把這個值+1(分配trx_id,開始記錄事務),得到新的數據,再調用存儲引擎接口寫入這行新數據。此處會先記錄undolog,並將undolog對應的變化信息redolog保存到log buffer中,然後再去修改buffer pool,並且把buffer pool對應的變化信息redolog記錄到log buffer中,詳情見上文。
  3. InnoDB做完上述操作後,就準備提交事務了。此時處在Prepare階段,執行器調用binlog_prepare接口,就會將上文提到的undo segment的狀態置為TRX_UNDO_PREPARED,並將本次提交事務的XID也寫入其中,同時生成對應的redolog。此時根據redolog的刷盤策略,本次事務對應的log buffer可能會被刷盤,而只要log buffer刷盤成功,那麼即使之後系統崩潰,在重啟恢復的時候也可以將處於Prepare狀態的事務完全恢復(恢復buffer pool和undolog),然後回滾或者再次提交事務。
  4. 而到Commit階段,執行器繼續調用binlog_commit接口提交事務,此時會先將事務執行過程中產生的binlog(包括XID)按照binlog的刷盤策略刷入磁碟,再根據不同的操作類型把undo segment的狀態轉換成TRX_UNDO_CACHED、TRX_UNDO_TO_FREE或者TRX_UNDO_TO_PURGE(這幾個狀態是InnoDB的事務結束的標誌),表示滿足一定條件後可以清理這些undolog,並將對應的redolog刷盤。至此這個事務就算是提交完了,注意事務提交需要三次刷盤(寫redolog,寫binlog,寫commit,InnoDB新版本通過組提交進行了優化)。而髒頁並不一定隨著事務提交而刷盤,需依賴於buffer pool持久化策略。
  5. 對於處於Prepare狀態的事務,存儲引擎既可以提交,也可以回滾,這取決於目前該事務對應的binlog是否已經寫入硬碟。這時就會讀取最後一個binlog日誌文件,從日誌文件中找一下有沒有該Prepare事務對應的XID記錄,如果有的話,就將該事務提交,否則就回滾好了。

7.4 如果沒有兩階段提交

redolog未寫入,binlog未寫入:此時MySQL異常重啟無法恢復數據,認為sql就沒執行。

redolog寫入,binlog未寫入:此時MySQL異常重啟能根據redolog恢復事務提交時的數據,但binlog沒有記錄,後續使用binlog恢復臨時庫會出現數據丟失,導致狀態不一致。

binlog寫入,redolog未寫入:此時MySQL異常重啟臨時庫能根據binlog重放事務提交時的數據,但redolog沒有記錄,如果主庫有一些髒頁已經刷盤,本應先回滾再通過binlog重放,但現在無法回滾,會導致狀態不一致。

7.5 結論

所謂兩階段提交,就是指同時將redolog和binlog都寫成功,這樣既能保證通過binlog恢復臨時庫時和主庫無差異,又能保證通過redolog恢復主庫時和臨時庫無差異。

關鍵字: