Redo Log——第一篇

mysql重點(diǎn)Log三部曲第一部:redo log,接下來還有undo log和binlog,敬請(qǐng)期待

什么是Redo Log

在InnoDB存儲(chǔ)引擎中,所有的操作都是以頁為單位的。而在我們的客戶端在進(jìn)行數(shù)據(jù)的操作時(shí),主要都會(huì)經(jīng)過buffer pool這個(gè)緩沖池來完成,也就是說,真正訪問頁面之前,都需要把磁盤上的頁緩存到buffer pool之后才可以訪問。我們都了解事務(wù)有ACID四個(gè)特性,其中的C——持久性,說的是,已經(jīng)提交的事務(wù),在事務(wù)提交以后即使系統(tǒng)發(fā)生了崩潰,這個(gè)事務(wù)對(duì)數(shù)據(jù)庫的更改也不可以丟失,但是試想,如果把數(shù)據(jù)直接讀到buffer pool中,事務(wù)在提交后發(fā)生了故障,數(shù)據(jù)并沒有及時(shí)同步到磁盤,內(nèi)存中的數(shù)據(jù)丟失,這個(gè)就已經(jīng)不滿足于持久性了。這個(gè)時(shí)候可能會(huì)想到有以下這個(gè)解決方案:

  • 在事務(wù)提交之前,把該事務(wù)涉及到修改的頁面全部刷到磁盤中去

但是這個(gè)做法有一些問題:

  • 將數(shù)據(jù)刷到磁盤中的基本單位是頁,如果只是修改了某一行數(shù)據(jù),也會(huì)將整個(gè)頁刷盤,這個(gè)實(shí)在是很浪費(fèi)
  • 隨機(jī)IO速度低。一個(gè)事務(wù)修改的數(shù)據(jù)可能并不在一個(gè)頁里面,這些頁面可能本身在物理上就不相鄰,這種情況下,就會(huì)產(chǎn)生了大量的隨機(jī)IO,需要經(jīng)過一個(gè)不停的尋址過程,隨機(jī)IO的效率比順序IO低很多

我們想到的方式也給否定了,那該如何處理呢?再回到我們想說的問題:對(duì)于已經(jīng)提交了的事務(wù)對(duì)數(shù)據(jù)庫中數(shù)據(jù)的修改永久生效,即使是系統(tǒng)宕機(jī),重啟后也可恢復(fù)
那么這塊在mysql中,對(duì)于一條修改的數(shù)據(jù),就記錄了這個(gè)數(shù)據(jù)哪些地方修改了來完成的。比如

update table set a = 1 where id = 1;

就會(huì)記錄一條日志:

把第10表空間的第90號(hào)頁面的偏移量為1024處的值更新為1

提交事務(wù)后,把這條操作日志刷到磁盤中,之后如果系統(tǒng)崩潰了,我們也可以找到這條日志完成對(duì)應(yīng)數(shù)據(jù)的恢復(fù)。因此,上面的操作日志也被稱為redo log。那么使用redo log的好處到底是什么呢?

  1. redo log占用的空間很小,而且可以通過參數(shù)進(jìn)行動(dòng)態(tài)設(shè)置。整體redo log占用的空間是一定的,并不會(huì)無線增大
  2. redo是順序?qū)懭?,比隨機(jī)IO效率會(huì)高很多(順序IO的文件通過預(yù)讀方式能夠大大的提升效率)

PS:保證事務(wù)持久性并不單單只有redo log,其實(shí)還有mysql的重要機(jī)制——double write,這塊在講完redo log后再說下

Redo log記錄結(jié)構(gòu)

下面是大部分類型的redo log的通用結(jié)構(gòu):

通用結(jié)構(gòu)
  • type:redo log的類型,目前redo log的類型很多,下面會(huì)簡單地提集中來了解
  • Space ID:表空間ID
  • page number:頁號(hào)
  • data:一條redo log的內(nèi)容

展示一下源碼的數(shù)據(jù)結(jié)構(gòu)的樣子

struct alignas(INNOBASE_CACHE_LINE_SIZE) log_t {
    atomic_sn_t sn;                       // 目前l(fā)og buffer申請(qǐng)的空間大小
    aligned_array_pointer<byte, OS_FILE_LOG_BLOCK_SIZE> buf;  // log buffer的內(nèi)存區(qū)
    Link_buf<lsn_t> recent_written;               // 解決并發(fā)插入Redo Log Buffer后刷入ib_logfile存在空洞的問題
    Link_buf<lsn_t> recent_closed;        // 解決并發(fā)插入flush_list后確認(rèn)checkpoint_lsn的問題
    atomic_lsn_t write_lsn;           // write_lsn之前的數(shù)據(jù)已經(jīng)寫入系統(tǒng)的Cache, 但不保證已經(jīng)Flush
    atomic_lsn_t flushed_to_disk_lsn;         // 已經(jīng)被flush到磁盤的數(shù)據(jù)
    size_t buf_size;                  // log buffer緩沖區(qū)的大小
    lsn_t available_for_checkpoint_lsn;      // 在此lsn之前的所有被添加到buffer pool的flush list的log數(shù)據(jù)已經(jīng)被flsuh, 下一次checkpoint可以make在這個(gè)lsn. 與last_checkpoint_lsn的區(qū)別是該lsn尚未被真正的checkpoint.
    lsn_t requested_checkpoint_lsn;     // 下次需要進(jìn)行checkpoint的lsn
    atomic_lsn_t last_checkpoint_lsn;       // 目前最新的checkpoint的lsn
    uint32_t write_ahead_buf_size;      // write ahead的Buffer大小
    lsn_t current_file_lsn;         // 
    uint64_t current_file_real_offset;      //
    uint64_t current_file_end_offset;       // 當(dāng)前ib_logfile文件末尾的offset
    uint64_t file_size;             // 當(dāng)前ib_logfile的文件大小
}

就不按照每一條說了,后面基本都會(huì)提到,沒提到的大家可以自行了解一下~

Redo log類型

基礎(chǔ)類型

redo log類型主要是通過上面記錄中的type體現(xiàn)的。比較基礎(chǔ)的有以下幾個(gè)(基礎(chǔ)的類似于java里面的基本類型):

  1. MLOG_1BYTE:type字段對(duì)應(yīng)的十進(jìn)制為1,表示在頁面的某個(gè)偏移量處寫入一個(gè)字節(jié)
  2. MLOG_2BYTES:type字段對(duì)應(yīng)的十進(jìn)制為2,表示在頁面的某個(gè)偏移量處寫入兩個(gè)字節(jié)
  3. MLOG_4BYTES:type字段對(duì)應(yīng)的十進(jìn)制為4,表示在頁面的某個(gè)偏移量處寫入四個(gè)字節(jié)
  4. MLOG_8BYTES:type字段對(duì)應(yīng)的十進(jìn)制為8,表示在頁面的某個(gè)偏移量處寫入八個(gè)字節(jié)
  5. MLOG_WRITE_STRING::type字段對(duì)應(yīng)的十進(jìn)制為30,表示在頁面的某個(gè)偏移量處寫入一串?dāng)?shù)據(jù)

現(xiàn)在舉一個(gè)例子。我們大部分情況下用的自增主鍵id都是int型或者是long型的,int為四個(gè)字節(jié),long為八個(gè)字節(jié),現(xiàn)在如果插入一條數(shù)據(jù)的話,這條數(shù)據(jù)實(shí)際是修改在buffer pool中的,然后通過redo log記錄下當(dāng)前的修改情況。那么這個(gè)時(shí)候,插入一條id(int)為9的數(shù)據(jù)的redo log應(yīng)該是這樣子的。

插入數(shù)據(jù)后

含義:在90表空間,編號(hào)為10頁面,偏移量為1000處,寫入四個(gè)字節(jié),具體數(shù)據(jù)為0000 0000 0000 1001

復(fù)雜類型

對(duì)于正常的一條insert語句,不管這個(gè)表中有多少課索引樹,都會(huì)將其更新,每更新一顆索引樹,不光會(huì)更新葉子節(jié)點(diǎn)的頁面,也很有可能會(huì)更新內(nèi)節(jié)點(diǎn)的頁面,甚至也有可能會(huì)新建頁面。
而insert語句過程中對(duì)所有頁面的修改都會(huì)記錄到redo log中去,但是對(duì)頁面更新的時(shí)候,不單單會(huì)只更新數(shù)據(jù),還會(huì)更新File Header、Page Header、Slot(這塊牽扯到頁的結(jié)構(gòu),有興趣的可以自己看看)等部分,因此在更新的時(shí)候再用上面的簡單類型的redo log就不那么能滿足需求了,也就是說,一條insert操作,一個(gè)頁面修改的地方會(huì)異常地多,那么下面有幾種方案:

  1. 每一處修改都記錄一條redo log。這種情況的優(yōu)勢就是類型簡單,記錄簡單,便于理解;但是弊端很明顯,redo log記錄太多,導(dǎo)致redo log占用了大量的空間,浪費(fèi)資源
  2. 每個(gè)頁第一處修改和最后一處修改當(dāng)一條redo log中的具體數(shù)據(jù)。但是這種方案也有比較明顯的缺點(diǎn),第一處修改和最后一處修改中間有大量的未修改的記錄,全部記錄在redo log里面也是占用了大量無用的空間

基于這種情況下,InnoDB提出了一些新的redo日志類型:

  1. MLOG_REC_INSERT:type對(duì)應(yīng)的十進(jìn)制為9,表示插入一條使用非緊湊行格式的記錄時(shí)的redo log類型
  2. MLOG_COMP_REC_INSERT:type對(duì)應(yīng)的十進(jìn)制為38,表示插入一條使用緊湊行格式的記錄時(shí)的redo log類型
    ......
    那么這些類型都是如何完成一條redo log的記錄呢?通過MLOG_REC_INSERT舉個(gè)例子
MLOG_REC_INSERT
  • type:MLOG_REC_INSERT
  • spaceId:表空間id
  • page number:頁號(hào)
  • record offset:當(dāng)前記錄的地址
  • record length:當(dāng)前記錄長度
  • info bits:表示記錄頭信息的前4個(gè)比特位的值以及record_type的值
  • record origin offset:前一條記錄的地址。(每向數(shù)據(jù)頁插入一條記錄,都需要修改該頁面中維護(hù)的記錄鏈表,每條記錄的記錄頭信息中都包含一個(gè)稱為next_record的屬性,所以在插入新記錄時(shí),需要修改前一條記錄的next_record屬性。)
  • mismatch index:為了節(jié)省redo log大小設(shè)立的字段,暫時(shí)可忽略
  • record data:記錄的真實(shí)數(shù)據(jù)

Mini-Transaction

redo log的寫入方式

一條insert一局可能會(huì)涉及到若干棵B+樹,這些修改直接都操作的是buffer pool,在buffer pool中修改完后,才會(huì)記錄相關(guān)的redo log,而這些redo log是會(huì)被切分成不可分割的組(原子性)。比如:

  1. 修改聚簇索引時(shí)是不可分割的
  2. 修改二級(jí)索引時(shí)是不可分割的

那么具體這一層面的不可分割是什么意思呢?通過下面兩個(gè)圖理解一下

初始化

如上圖所示,當(dāng)前索引樹一共有兩個(gè)葉子節(jié)點(diǎn),現(xiàn)在想插入一些記錄,結(jié)果為下圖

新增20

這個(gè)過程其實(shí)并不需要很復(fù)雜的處理,因?yàn)槿~子節(jié)點(diǎn)空間是足夠的,那么總有不足夠的時(shí)候,要怎么辦呢?每個(gè)葉子節(jié)點(diǎn)可以存放20條記錄,可以看出第一個(gè)節(jié)點(diǎn)已經(jīng)飽和了,那么我現(xiàn)在想再插入一個(gè)值為9.9的記錄,該怎么辦呢?

新增9.9過程

從上圖可以看出,找到9.9的位置后,發(fā)現(xiàn)第一個(gè)頁已經(jīng)飽和了,這個(gè)時(shí)候就需要頁分裂(頁分裂一般會(huì)將之前頁的數(shù)據(jù)量對(duì)半分配到兩個(gè)葉子中),由于多了一個(gè)葉子節(jié)點(diǎn),需要在對(duì)應(yīng)的內(nèi)節(jié)點(diǎn)中,多出一條記錄,以便于索引,插入9.9記錄后的索引樹如下圖所示

新增9.9結(jié)果

從上邊的過程可以看出,插入過程中,不僅僅是對(duì)其中一個(gè)葉子節(jié)點(diǎn)會(huì)有改變,還可能會(huì)進(jìn)行頁分裂,對(duì)目錄索引節(jié)點(diǎn)也會(huì)有改變,這些改變都會(huì)新增出很多redo log。這個(gè)時(shí)候就會(huì)有原子性問題。如果不保證這些redo log的原子性,好比說上邊插入9.9的過程,葉子節(jié)點(diǎn)分裂出來了,但是目錄索引節(jié)點(diǎn)并未多出目錄項(xiàng)記錄,這個(gè)就會(huì)導(dǎo)致通過redo log恢復(fù)崩潰前的系統(tǒng)狀態(tài)這一操作出現(xiàn)錯(cuò)誤。因此,redo log在某種程度上,必須能夠保證原子性。那么是如何保證的呢?

————將這些要保證原子性的redo log放到一個(gè)組中
那么是如何放到這個(gè)組里面的呢?
————在該組的最后一條redo log后邊加一條特殊類型的redo log,類型為MLOG_MULTI_REC_END(type的十進(jìn)制為31),當(dāng)系統(tǒng)崩潰重啟進(jìn)行恢復(fù)的時(shí)候,只有解析到類型為MLOG_MULTI_REC_END的redo log,才認(rèn)為解析到了一組完整的redo,才會(huì)進(jìn)行恢復(fù),否則會(huì)拋棄前邊解析到的redo log

那么這個(gè)時(shí)候可能會(huì)有疑問,redo log可能一組里面有很多redo log,可能只有一條,這種只有一條的本身就滿足原子性,還需要在后邊再加一個(gè)MLOG_MULTI_REC_END?這不是很浪費(fèi)空間么?實(shí)際上不是這樣的。對(duì)于redo log的type字段來說,一共占用了8個(gè)比特位,但是實(shí)際上解釋redo log類型的,只是占了7個(gè)比特位,剩余的一個(gè)比特位,用來說明當(dāng)前的redo log是一個(gè)單一的日志還是需要在一個(gè)組里面的redo log,如下圖所示

redo log 結(jié)構(gòu)

如果第一個(gè)比特位為1,說明這是單一的一條redo log,否則表示這是個(gè)需要在組內(nèi)保持原子性的redo log

Mini-Transaction

上邊說的原子性地訪問過程,稱為一個(gè)Mini-Transaction。這個(gè)其實(shí)很形象,正常的事務(wù)為Transaction,但是這個(gè)是事務(wù)中的部分操作,是更細(xì)粒度的,因此叫做Mini-Transaction,也可以理解,一個(gè)Mini-Transaction包含很多redo log,映射關(guān)系如下

事務(wù)、MTR和redo log關(guān)系圖

redo log的寫入過程

redo log block

通過mtr生成的redo log都放在了頁中(頁的大小為512字節(jié))。但是我們知道,在InnoDB存儲(chǔ)引擎中,索引的最基本單位是頁,索引中的頁跟redo log存放的頁是不同的,在這塊我們稱redo log存放的頁為block,具體的結(jié)構(gòu)圖如下

Block結(jié)構(gòu)圖
  • log block header存放管理信息
  • log block body存放redo log
  • log block trailer存放block的校驗(yàn)值,用于正確性校驗(yàn)

redo log buffer

我們也都了解過,InnoDB有一個(gè)Buffer Pool,是為了解決磁盤速度過慢問題,而衍生出的數(shù)據(jù)緩存池。那么對(duì)于redo log也是需要將數(shù)據(jù)寫入到磁盤的,只要是將數(shù)據(jù)從內(nèi)存寫入磁盤,就肯定會(huì)面對(duì)一個(gè)問題:內(nèi)存速度與磁盤速度的不平等性。而這個(gè)時(shí)候,大部分情況都會(huì)使用一個(gè)方案:在內(nèi)存和磁盤中間加一層buffer。那么針對(duì)redo log寫入磁盤的過程,也有一層redo log buffer。buffer中的基本數(shù)據(jù)單位就是上邊提到的redo log block,其實(shí)很容易理解,因?yàn)閞edo log的基本單位是block,那么緩沖池基本上是要跟redo log存放的基本單位是一樣的,也便于數(shù)據(jù)計(jì)算和統(tǒng)計(jì)。那么可以理解,redo log buffer的結(jié)構(gòu)應(yīng)該是下邊這樣的

redo log buffer

redo log buffer的大小是可控的,innodb_log_buffer_size通過這個(gè)參數(shù)可以設(shè)置,默認(rèn)大小是16M

redo log寫入redo log buffer

介紹完了存儲(chǔ)redo log的數(shù)據(jù)結(jié)構(gòu),下面我們就介紹一下redo log是如何寫入這層buffer的

redo log寫入redo log buffer的過程是順序的,也就是入redo log buffer的結(jié)構(gòu)圖所示,先寫第一塊block,寫滿了以后寫第二塊...以此類推。

現(xiàn)在有一條redo log數(shù)據(jù),要寫入buffer,我們會(huì)遇到第一個(gè)問題:這個(gè)數(shù)據(jù)要寫在哪個(gè)位置?在InnoDB中提供了一個(gè)buf_free的全局變量,來標(biāo)識(shí)后續(xù)的redo log要寫到哪里

redo log buffer 寫入數(shù)據(jù)

還記得之前說過的mtr,一個(gè)mtr產(chǎn)生的多條redo log一定要保證原子性——不可分割,這塊可以理解,redo log并不是產(chǎn)生一條就寫入一條,而是說為了保證原子性,將一個(gè)mtr下的redo log一次性寫入buffer。那么這里面就有一個(gè)問題了:每次mtr在完全產(chǎn)生redo log之前,每一條redo log放在哪里呢?其實(shí)這塊只是暫時(shí)存放于內(nèi)存非buffer的位置了,并沒有特殊處理。

那么這個(gè)時(shí)候可能有人會(huì)問,那如果是兩個(gè)事務(wù),每個(gè)事務(wù)里面有多個(gè)mtr,這種情況下是不是就會(huì)產(chǎn)生同一個(gè)事務(wù)的redo log非連續(xù)的情況呢?

下面用例子來說明一下,多個(gè)事務(wù),每個(gè)事務(wù)多個(gè)mtr是,redo log是如何寫入的

現(xiàn)在有事務(wù)A,事務(wù)A下有mtrA_1和mtrA_2,現(xiàn)在有事務(wù)B,事務(wù)B下有mtrB_1和mtrB_2,看下每個(gè)事務(wù)產(chǎn)生redo log的情況

事務(wù)、mtr和redo log的demo圖

事務(wù)之間并非串行,大多數(shù)都是并行運(yùn)行,也就是說,可能先產(chǎn)生了mtrA_1的redo log,然后產(chǎn)生了mtrB_1的redo log,再是mtrA_2的redo log,最后產(chǎn)生了mtrB_2的redo log,這種情況看下是如何在redo log buffer中分布的

redo log在buffer中的分布圖

可以看出,redo log buffer中的日志排列,其實(shí)就是根據(jù)mtr的生成順序來排列的,而且可以將整個(gè)buffer理解為一個(gè)空間,一個(gè)mtr產(chǎn)生的redo log可以分布在多個(gè)block中,一個(gè)block也可以存入多個(gè)redo log組

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容