MySQL之事務和redo日誌

大隊長11發表於2022-05-29

事務

事務的四個ACID特性。

Atomicity 原子性

Consistency 一致性

Isolation 隔離性

Durability 永續性

原子性

原子性即這個事務的任務要麼全做了,要麼全部沒做,不能出現做一半這種情況。

一致性

一致性即資料庫中的資料必須滿足資料滿足資料庫的約束。

隔離性

即事務與事務之間相互不打擾,比如兩個事務在實際過程中並不是原子的,兩個事務中的語句是交替執行的,但是隔離性就是要保證兩個事務之間狀態轉換不會互相影響。

永續性

就是一旦事務結束,就要將其儲存到磁碟中防止丟失。

事務的狀態

活躍的active:即事務正在執行其中的SQL語句。

部分提交的partially commited:事務執行完成,但是其結果還在記憶體中儲存著,沒有重新整理到磁碟中。

提交的 commited : 結果成功重新整理到磁碟,就從上面部分提交進入該狀態。

失敗的 failed : 就是事務執行過程出現資料庫或作業系統自身的錯誤,就導致了事務提交失敗。

中止 aborted : 就是事務提交失敗,需要將已經修改的語句回滾到事務未執行以前。

image

事務開啟和關閉

begin; 算開啟一個事務
....
commit; 提交事務
或者
rollback; 回滾事務

begin算一種開啟方式,但是它不能指定事務的開啟的型別,只讀、讀寫等

還有一種開始事務方式

start transaction; # 不加引數,預設讀寫事務
start transaction read only; # 只讀事務
start transaction read write; # 讀寫事務
start transaction read only, with consistent shapshot; # 開啟只讀事務和一致性讀。
....
commit; 提交事務
或者
rollback; 回滾事務

關閉就是上面兩個commit 和 rollback 兩種,一個是提交,一個是回滾。

還有就是自動提交。

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set, 1 warning (0.04 sec)

我們的自動提交是預設開啟的,自動提交就是我們在沒有指定start transaction 或 begin時,MySQL會為每個語句啟動一個事務,每一條語句相當與都是開了一個事務然後語句結束會自動幫我們提交。

隱式提交

當我們使用begin 或 start transaction ,或則我們關閉了自動提交。事務此時就不會提交,直到我們使用commit或則rollback。但是當出現以下情況,MySQL會幫我們偷偷提交事務。

  • 定義或修改資料庫物件即DDL語句。
  • 隱式使用或修改mysql資料庫中的表
  • 當我們沒提交一個事務時又begin 或 start transaction 就會繼續幫我們自動提交前面已執行的。
  • 載入資料的語句。load data
  • 關於MySQL複製的語句
  • 等等....

儲存點savepoint

即我們可以使用savepoint回滾到某個儲存點中,但是提交儲存點以前的語句,回滾儲存點以後的語句。

begin;
...sql語句
savepoint s1;
...SQL語句
rollback to s1; # 此時就會提交s1前的SQL,而回滾s1以後的SQL

redo日誌

如果我們對頁面進行修改的話,我們會先將修改的頁面儲存在記憶體的buffer pool中,但是如果出現斷電的情況,我們做的修改就會全部丟失了不是嗎。

我們對事務的永續性進行保證,就是對一個提交的事務做的頁面修改重新整理到磁碟中,最簡單粗暴的辦法就是事務提交後直接將記錄刷到磁碟中。

  • 但是重新整理到磁碟是十分慢的,而且如果我們只對頁面進行一些很微小的修改,我們都需要以頁為單位和磁碟進行互動,是一個十分不值當的行為。
  • 需要不斷進行隨機IO,因為頁面在磁碟上可能零零散散,我們需要不斷進行隨機IO,效率也是十分低下的。

redo日誌的目的:就是我們對於提交事務的修改進行永久的儲存,即使系統崩潰,我們重啟後也能將修改恢復到原樣。

簡單的redo日誌

簡單的redo日誌分為很多中型別,MLOG_1BYTE型別,MLOG_2BYTE型別,MLOG_4BYTE型別,MLOG_8BYTE型別,MLOG_WRITE_STRING型別。

就是如果我們修改只有1,2,4,8,或則連續的一小段個位元組,就會使用這種簡單的日誌進行儲存。比如我們對某個系統變數的修改。

image

image

有了簡單的redo日誌,我們可以根據表空間ID和頁號以及偏移量,我們就可以在重啟時找到這個頁,將對應偏移量的資料替換上去就可以了。

複雜的redo日誌

我們平時插入一條資料,可能修改了一個頁面的多個地方,比如頁滿了進行了頁分裂,那修改的地方可就大了去了,以及插入資料也對頁頭一些頁的基本資訊又影響。反正就是一個頁的插入可能影響到很多頁。

我們如果對於一個頁面有多處修改,我們使用簡單的redo日誌,一個地方一個地方的寫日誌,那要生成好多的redo日誌,在空間上可能比我們一整個頁面進行重新整理效率都低。所以出現了更復雜的redo日誌。

複雜的頁面有以下型別。

PS:緊湊行格式就是Compact、Dynamic行格式,最原始的redundant行格式就是非緊湊的

  • MLOG_REC_INSERT : 建立一個插入的非緊湊行格式頁面的記錄的redo日誌。
  • MLOG_COMP_REC_INSERT:建立一個插入的緊湊行格式頁面的記錄的redo日誌。
  • MLOG_COMP_PAGE_CREATE:建立一個儲存緊湊行格式的頁面的redo日誌。
  • MLOG_COMP_REC_DELETE:建立一個刪除的一個緊湊行格式頁面的記錄的redo日誌。
  • MLOG_COMP_LIST_START_DELETE:表示從某條記錄給定記錄開始刪除頁面中一系列使用緊湊行格式頁面的記錄的redo日誌。
  • MLOG_COMP_LIST_END_DELETE:表示刪除停止的記錄的redo日誌和MLOG_COMP_LIST_START_DELETE是一套的。
  • ....還有很多

我們要理解這個複雜頁面,就要把簡單redo頁面的想法拋棄掉。這個複雜redo頁面並不是儲存某個偏移量修改的新值,我把它理解為它儲存的是這個操作,就是我們插入一條資料,這個redo就是把這個操作儲存起來了。但是它實際上並不是這樣的哈。

這些redo日誌可以從物理和邏輯層面看。

  • 物理層面上看,這些日誌指明瞭對哪個表空間的那個頁進行修改了。
  • 邏輯層面上看,在系統崩潰重啟時,並不能直接載入這些型別的redo日誌。而是需要進行呼叫函式進行對這些redo日誌處理,然後才能恢復要原樣。

上面寫得很清楚了需要呼叫函式,說明這些redo只是儲存一些基礎資料,然後呼叫函式後才能根據這些基礎資料對頁面進行恢復。而並不是像簡單redo頁面那樣直接儲存頁面的資料哦。

看了好幾遍懵逼,就是一直認為它儲存的就是修改頁面的資料,其實不然,它儲存的是進行該操作後用來複原的基本資料。

歸根結底,說了redo的不同頁面型別只不過就是我們需要redo頁面然後將資料庫恢復要出錯前的模樣。

Mini-Transaction

以組的形式寫入redo日誌

我們在寫入redo日誌的時候,我們會考慮到一個情況就是我們的操作是原子的,比如說我們插入一條記錄,我們不僅僅要更改頁的資料,還要更改頁頭的基本資訊,有時候還要更新父索引節點的資料。這一系列操作,都是密不可分的,如果一個沒有恢復,那生成的資料將會是錯誤的。所以MySQL將會以組的形式寫入redo日誌。

MySQL將redo日誌分為組的形式,對於需要保證原子性的一系列操作,就會在redo日誌後面加上一個特殊型別的redo日誌。代表一條完整的redo日誌。

image

但是也會出現需要保證原子性操作的redo日誌只有一條redo日誌。因為MySQL要保證儘量節省空間嘛。所以會在型別的最高位設定代表是否是一條單一的redo日誌。

image

Mini-Transaction

MySQL將對頁面中的一次原子操作過程稱之為Mini-Transaction,簡稱mtr。一個mtr就代表一組redo日誌。我們接下來的redo的介紹很多都會以mtr為一個單位。

image

redo日誌的寫入

MySQL以mtr的形式來儲存每一組日誌,但是我們redo日誌是怎麼個順序寫入磁碟的呢?當然呢,和磁碟打交道就是意味著慢,所以redo日誌首先還是會寫入記憶體的緩衝區中然後在慢慢地寫入磁碟哈。我們先將寫入記憶體的過程。

MySQL設計了一個redo log block的資料結構來儲存mtr,大小為512位元組。

  • header 頭部呢就儲存一些基本資訊
    • HDR_NO 唯一標號,省略前面的英文單詞
    • HDR_DATA_LEN 已使用的資料長,初始為12,寫滿就是512.
    • FIRST_REC_GROUP 該block中第一個mtr中第一條redo日誌的偏移量
    • CHECKPOINT_NO 就是checkpoint的序號
  • body 就是儲存mtr的地方
  • trailer 就是尾部放檢查和。驗證完整性的。

image

然後我們有了這個資料結構,就可以引出log buffer 簡言之就是redo日誌緩衝區,用來快取redo日誌的,在MySQL伺服器啟動時會像作業系統申請的一段連續的記憶體空間,和buffer pool差不多。

我們通過innodb_log_buffer_size可以檢視redo日誌緩衝區的大小,預設為16M。

mysql> show variables like 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
1 row in set, 1 warning (0.00 sec)

image

結構如上圖,我們以mtr為單位將redo日誌寫入log buffer。

但是我們應該在哪裡插入呢?所以log buffer維護了一個叫做buf_free的全域性變數,用來指向空閒的值。然後我們獲取buf_free就可以直接在那個位置插入。

image

我們還有一個問題就是在log block header 中有個屬性, log_block_first_rec_group 這個屬性有什麼用呢?

image

如上圖,我們插入了4個mtr分別屬於兩個事務,我們用來記錄這個log_block_first_rec_group的這個屬性呢記錄了這個block中第一個mtr的第一個redo頁面的偏移量。

就像上面的mtr_t1_2一樣,一下佔了三個block,在第二個頁面中log_block_first_rec_group的記錄是512,就說明了當前的block是延續之前的mtr。同一第三個頁面我們就可以知道新的mtr在哪裡。

所以呢這個log_block_first_rec_group屬性值的作用是讓我們知道當前block有沒有接續之前block的部分,如果有才可以知道,不然我們無法識別這是一個新的mtr還是接續的mtr。

redo日誌刷盤

redo日誌從redo log buffer中儲存進入磁碟中是講究時機的,同時呢由於儲存到磁碟是很慢的,所以需要緩衝區的存在,讓執行緒阻塞在那裡等跟磁碟IO的資源那也是不理智的對不對。

以下是redo刷盤的時機

  • log buffer空間不足時。
  • 事務提交時(要保證事務的永續性就得把redo刷到磁碟中)
  • 後臺執行緒不斷刷盤,大概每秒刷一次。
  • 正常關閉伺服器
  • 做checkpoint時
  • 其他等情況。。。

redo日誌檔案

我們可以從根目錄下的data資料夾中檢視到兩個檔案,預設是兩個。

image

我們可以修改系統變數,在啟動時修改log檔案數量

mysql> show variables like 'innodb_log_files_in_group';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2     |
+---------------------------+-------+
1 row in set, 1 warning (0.00 sec)

在啟動時指定log檔案的大小一次來修改,預設48M。

mysql> show variables like 'innodb_log_file_size';
+----------------------+----------+
| Variable_name        | Value    |
+----------------------+----------+
| innodb_log_file_size | 50331648 |
+----------------------+----------+
1 row in set, 1 warning (0.00 sec)

image

我們將redo日誌寫入磁碟中,本質上就是把block從記憶體中複製了一份到磁碟的ib_logfile檔案中。

ib_logfile是由512位元組的block組成的,ib_logfile的前2048位元組即4個block用來儲存一些基本的管理資訊。後面剩餘的就是用來儲存從記憶體中讀取來的block,每個block同樣也是512位元組。

image

首先介紹前4個block塊主要是儲存哪些管理資訊。

image

  • log file header 的組成

image

  • checkpoint1組成

image

  • 第三個沒用,第四個和checkpoint1一樣。

Log Sequeue Number(LSN)

我們一直在前面提到的LSN值,所以它代表著什麼呢?我們可以叫日誌序列號,LSN的初始值預設為8704。

我們前面提到的log buffer作為redo日誌的緩衝區,有兩個指標我們可以回想一下,buf_free和buf_next_to_write兩個全域性變數,一個代表當前緩衝區空閒的地方,一個代表下一個要log buffer寫入磁碟的mtr地址。我們可以知道那些mtr還沒寫入磁碟中。

在buffer pool中維護著一個lsn值,當系統初始化沒有mtr插入時,就是8716 即8704 + 12 的block header。隨著mtr的插入到block中,會不斷增大。

每個mtr都有一個對應的lsn值,lsn值越小代表redo日誌產生得越早。它其實就和buf_free 差不多,只不過它是代表著一個序列號。

image

flushed_to_disk_lsn

innodb也在buffer pool中維護了一個全域性變數叫做flushed_to_disk_lsn,和這個buf_next_to_write有著異曲同工之處。它是用來維護buffer pool中已經重新整理到磁碟的lsn。

當我們沒有將緩衝區中的mtr重新整理到磁碟中,lsn就不會發生改變,當我們將mtr刷到磁碟的redo日誌檔案中時,lsn就會增加相應的偏移量 (不是很懂,上面講我們是以block的形式向磁碟重新整理redo頁面的)。當然如果我們又跨過了頁首或者頁尾,我們就還需要新增4位元組的頁尾長度。

思路好亂,感覺書上沒講清楚或者是我沒有get到作者的點吧。

flushed_to_disk_lsn直接點說就是一個從8706開始的數字,跟著重新整理到磁碟的大小增大而增大。

flush連結串列中的LSN

我們之前簡單提到過的flush的結構,在控制塊中會存放兩個關於頁面修改的LSN。

  • oldest_modification : 如果該頁面被修改,這裡將儲存頁面的第一次修改時mtr開始時的LSN值。可以理解為mtr插入到buffer pool前的lsn值。
  • newest_modification : 如果對該頁面進行修改,將儲存mtr插入結束後的lsn值。對於每一次修改,這個值都會改變。

我們知道flush連結串列是根據第一次修改的時間從大到小排序的,最新插入的會被排在連結串列首部。其實就是按照oldest_modification 的值進行從大到小排序的,最早進行修改,向log buffer 寫入mtr的頁面的LSN。

我們在這裡需要知道的是我們oldest_modification 儲存的是頁面第一次修改的時候向buffer pool插入mtr前buffer pool中維護的LSN值是多少,newest_modification 就是最近一次修改時buffer pool在插入mtr後buffer pool的值是多少。

image

像上面我們在mtr1中修改了a頁面,在mtr2中修改了b,c頁面。他們的LSN值就是上面的所示。我們可以算一些8716就是8704 + 12 就是第一個插入的mtr之前的LSN初始的大小嘛。8916-8716 = 200就是mtr1的大小嘛。

但是我們需要注意的是,重複修改的頁面不會重新進行插入控制塊嘛,前面文章好像說過,就是我們怎麼找連結串列中有沒有對應頁面的控制塊呢?就是通過雜湊表找到key為是表空間+頁號組成的鍵,然後我們修改其newest_modification 的值就好了。

redo日誌檔案的LSN

我們提到了在redo日誌檔案中log file header 儲存了一個redo檔案開始的LSN,LSN就是在檔案基本資訊2048位元組的位置LSN值為8704開始計算。

image

checkpoint

redo日誌對於系統崩潰恢復來說是十分重要的存在,但是如果系統不崩潰的話,這樣的操作是沒有意義的,且耗費效能的。但是當系統崩潰重啟的時候innodb是怎麼知道哪些redo日誌是已經重新整理到磁碟了,還是沒有呢?

我們將上述的mtr_1重新整理到磁碟了,這是在日誌檔案中我們就可以將mtr1的記錄覆蓋掉,我們會將日誌檔案中頭部的4個block中儲存checkpoint進行+1的操作,並修改其儲存的LSN。可以回過頭檢視redo日誌檔案的組成。以上這個操作就叫做伺服器做了一次checkpoint。

具體步驟如下:

  1. 首先我們去flush連結串列中找到最後一個控制塊,找到它的oldest_modification ,它的值就代表當前已經重新整理的LSN的值。為什麼呢?仔細想想,它代表著這個mtr插入前的LSN值,它又是最後一個控制塊,代表著這是還沒重新整理到磁碟的最早的髒頁 (重新整理到磁碟就不會在flush連結串列裡了)。說明這個oldest_modification 代表著還沒重新整理mtr的LSN。
  2. 將這個oldest_modification 的值賦值給checkpoint_LSN。
  3. 將日誌檔案頭部中的checkpoint中維護的基本資訊進行更新,包括編號、偏移量、LSN。

以上的checkpoint的資訊只會儲存到第一個redo日誌檔案的管理資訊中去。

還有一點就是checkpoint有1和2,對於他們來說,就是LSN是偶數的時候就儲存到2,奇數就儲存到1。

innodb中的LSN值

mysql> show engine innodb status;
LOG
---
Log sequence number          118084165
Log buffer assigned up to    118084165
Log buffer completed up to   118084165
Log written up to            118084165
Log flushed up to            118084165
Added dirty pages up to      118084165
Pages flushed up to          118084165
Last checkpoint at           118084165
16 log i/o's done, 0.00 log i/o's/second

對於事務一致性的控制

我們在事務中提到過的永續性,如果我們要保證事務的永續性,就得在事務結束的時候將該事務產生的mtr重新整理到磁碟上,但是在事務結束的時候立刻重新整理到磁碟上是十分耗時的。

但是呢如果我們不及時重新整理,選擇將其先放到緩衝區裡面,但是出現系統崩潰,事務的操作就沒有辦法恢復了,無法保證其一致性。

在效能和一致性上我們可以進行選擇。對innodb_flush_log_at_trx_commit系統變數進行設定

mysql> show variables like 'innodb_flush_log_at_trx_commit';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1     |
+--------------------------------+-------+
1 row in set, 1 warning (0.00 sec)
  • 0代表事務提交不會立刻將mtr重新整理到磁碟,而是讓後臺執行緒自己去慢慢刷。
  • 1即預設值,代表事務提交時必須把mtr重新整理到磁碟中。
  • 2代表事務提交必須將mtr刷到作業系統的緩衝區。

innodb_flush_log_at_trx_commit值為2,我們進行重新整理磁碟,從資料庫的緩衝區中下來呼叫作業系統的執行對磁碟進行操作,還會先進入作業系統的緩衝區中讓作業系統去操作,如果作業系統沒崩必然也可以保證事務的一致性,但是如果作業系統也崩了,那就不能保證了。我們值為1是代表必須重新整理到磁碟中,即作業系統將資料真正刷到磁碟上了。

崩潰恢復

確定恢復的起點

對於已經重新整理到磁碟的mtr來說,沒有必要進行再次恢復,所以我們需要對於起點進行確認。

我們從checkpoint1和checkpoint2拿出LSN,因為倆個地方都存了checkpoint的LSN,所以比較哪個最大,就可以確定需要恢復redo的起點。

確定恢復的終點

對於每個block來說,都維護這一個len,我們只要讀到len小於512的,就可以知道這一頁是沒有滿的,然後根據其具體長度,就可以知道恢復的終點。

怎麼恢復

我們就是從起點,慢慢掃描每一個redo日誌,對其進行復原,直到終點。

加速方法:

  1. 使用雜湊表

    就是將每個頁面的redo日誌,放入雜湊表中,根據spaceID和page Number來確定雜湊表的雜湊值,然後根據插入的先後排序,先插入在前。然後我們就可以根據一個頁面一個頁面進行更新,這樣避免了隨機IO。

  2. 跳過已經重新整理的頁面

    我們在做了一次checkpoint後,又有頁面從LRU連結串列或者flush連結串列中的頁面更新到磁碟中。因為checkpoint不是一直在做的。

    我們怎麼知道呢?在每個頁面的File Header中有一個FIL_PAGE_LSN的屬性,該屬性記錄了最近一次重新整理頁面的newest_modification 值。如果當前LSN小於這個FIL_PAGE_LSN的值,代表已經重新整理到後面的記錄了,不需要更新了,直接跳過。

相關文章