新聞中心
1、背景和目標(biāo)
1.1 背景
MySQL在互聯(lián)網(wǎng)行業(yè)應(yīng)用廣泛,性能強(qiáng)、可靠性高,云廠商還提供了許多擴(kuò)展工具,生態(tài)相對(duì)其他數(shù)據(jù)庫(kù)而言比較成熟。

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


咨詢
建站咨詢
