事務的四個ACID特性。
Atomicity 原子性
Consistency 一致性
Isolation 隔離性
Durability 永續性
原子性即這個事務的任務要麼全做了,要麼全部沒做,不能出現做一半這種情況。
一致性即資料庫中的資料必須滿足資料滿足資料庫的約束。
即事務與事務之間相互不打擾,比如兩個事務在實際過程中並不是原子的,兩個事務中的語句是交替執行的,但是隔離性就是要保證兩個事務之間狀態轉換不會互相影響。
就是一旦事務結束,就要將其儲存到磁碟中防止丟失。
活躍的active:即事務正在執行其中的SQL語句。
部分提交的partially commited:事務執行完成,但是其結果還在記憶體中儲存著,沒有重新整理到磁碟中。
提交的 commited : 結果成功重新整理到磁碟,就從上面部分提交進入該狀態。
失敗的 failed : 就是事務執行過程出現資料庫或作業系統自身的錯誤,就導致了事務提交失敗。
中止 aborted : 就是事務提交失敗,需要將已經修改的語句回滾到事務未執行以前。
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會幫我們偷偷提交事務。
即我們可以使用savepoint回滾到某個儲存點中,但是提交儲存點以前的語句,回滾儲存點以後的語句。
begin;
...sql語句
savepoint s1;
...SQL語句
rollback to s1; # 此時就會提交s1前的SQL,而回滾s1以後的SQL
如果我們對頁面進行修改的話,我們會先將修改的頁面儲存在記憶體的buffer pool中,但是如果出現斷電的情況,我們做的修改就會全部丟失了不是嗎。
我們對事務的永續性進行保證,就是對一個提交的事務做的頁面修改重新整理到磁碟中,最簡單粗暴的辦法就是事務提交後直接將記錄刷到磁碟中。
redo紀錄檔的目的:就是我們對於提交事務的修改進行永久的儲存,即使系統崩潰,我們重啟後也能將修改恢復到原樣。
簡單的redo紀錄檔分為很多中型別,MLOG_1BYTE型別,MLOG_2BYTE型別,MLOG_4BYTE型別,MLOG_8BYTE型別,MLOG_WRITE_STRING型別。
就是如果我們修改只有1,2,4,8,或則連續的一小段個位元組,就會使用這種簡單的紀錄檔進行儲存。比如我們對某個系統變數的修改。
有了簡單的redo紀錄檔,我們可以根據表空間ID和頁號以及偏移量,我們就可以在重啟時找到這個頁,將對應偏移量的資料替換上去就可以了。
我們平時插入一條資料,可能修改了一個頁面的多個地方,比如頁滿了進行了頁分裂,那修改的地方可就大了去了,以及插入資料也對頁頭一些頁的基本資訊又影響。反正就是一個頁的插入可能影響到很多頁。
我們如果對於一個頁面有多處修改,我們使用簡單的redo紀錄檔,一個地方一個地方的寫紀錄檔,那要生成好多的redo紀錄檔,在空間上可能比我們一整個頁面進行重新整理效率都低。所以出現了更復雜的redo紀錄檔。
複雜的頁面有以下型別。
PS:緊湊行格式就是Compact、Dynamic行格式,最原始的redundant行格式就是非緊湊的
我們要理解這個複雜頁面,就要把簡單redo頁面的想法拋棄掉。這個複雜redo頁面並不是儲存某個偏移量修改的新值,我把它理解為它儲存的是這個操作,就是我們插入一條資料,這個redo就是把這個操作儲存起來了。但是它實際上並不是這樣的哈。
這些redo紀錄檔可以從物理和邏輯層面看。
上面寫得很清楚了需要呼叫函數,說明這些redo只是儲存一些基礎資料,然後呼叫函數後才能根據這些基礎資料對頁面進行恢復。而並不是像簡單redo頁面那樣直接儲存頁面的資料哦。
看了好幾遍懵逼,就是一直認為它儲存的就是修改頁面的資料,其實不然,它儲存的是進行該操作後用來複原的基本資料。
歸根結底,說了redo的不同頁面型別只不過就是我們需要redo頁面然後將資料庫恢復要出錯前的模樣。
我們在寫入redo紀錄檔的時候,我們會考慮到一個情況就是我們的操作是原子的,比如說我們插入一條記錄,我們不僅僅要更改頁的資料,還要更改頁頭的基本資訊,有時候還要更新父索引節點的資料。這一系列操作,都是密不可分的,如果一個沒有恢復,那生成的資料將會是錯誤的。所以MySQL將會以組的形式寫入redo紀錄檔。
MySQL將redo紀錄檔分為組的形式,對於需要保證原子性的一系列操作,就會在redo紀錄檔後面加上一個特殊型別的redo紀錄檔。代表一條完整的redo紀錄檔。
但是也會出現需要保證原子性操作的redo紀錄檔只有一條redo紀錄檔。因為MySQL要保證儘量節省空間嘛。所以會在型別的最高位設定代表是否是一條單一的redo紀錄檔。
MySQL將對頁面中的一次原子操作過程稱之為Mini-Transaction,簡稱mtr。一個mtr就代表一組redo紀錄檔。我們接下來的redo的介紹很多都會以mtr為一個單位。
MySQL以mtr的形式來儲存每一組紀錄檔,但是我們redo紀錄檔是怎麼個順序寫入磁碟的呢?當然呢,和磁碟打交道就是意味著慢,所以redo紀錄檔首先還是會寫入記憶體的緩衝區中然後在慢慢地寫入磁碟哈。我們先將寫入記憶體的過程。
MySQL設計了一個redo log block的資料結構來儲存mtr,大小為512位元組。
然後我們有了這個資料結構,就可以引出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)
結構如上圖,我們以mtr為單位將redo紀錄檔寫入log buffer。
但是我們應該在哪裡插入呢?所以log buffer維護了一個叫做buf_free的全域性變數,用來指向空閒的值。然後我們獲取buf_free就可以直接在那個位置插入。
我們還有一個問題就是在log block header 中有個屬性, log_block_first_rec_group 這個屬性有什麼用呢?
如上圖,我們插入了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 log buffer中儲存進入磁碟中是講究時機的,同時呢由於儲存到磁碟是很慢的,所以需要緩衝區的存在,讓執行緒阻塞在那裡等跟磁碟IO的資源那也是不理智的對不對。
以下是redo刷盤的時機
我們可以從根目錄下的data資料夾中檢視到兩個檔案,預設是兩個。
我們可以修改系統變數,在啟動時修改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)
我們將redo紀錄檔寫入磁碟中,本質上就是把block從記憶體中複製了一份到磁碟的ib_logfile檔案中。
ib_logfile是由512位元組的block組成的,ib_logfile的前2048位元組即4個block用來儲存一些基本的管理資訊。後面剩餘的就是用來儲存從記憶體中讀取來的block,每個block同樣也是512位元組。
首先介紹前4個block塊主要是儲存哪些管理資訊。
我們一直在前面提到的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 差不多,只不過它是代表著一個序列號。
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連結串列是根據第一次修改的時間從大到小排序的,最新插入的會被排在連結串列首部。其實就是按照oldest_modification 的值進行從大到小排序的,最早進行修改,向log buffer 寫入mtr的頁面的LSN。
我們在這裡需要知道的是我們oldest_modification 儲存的是頁面第一次修改的時候向buffer pool插入mtr前buffer pool中維護的LSN值是多少,newest_modification 就是最近一次修改時buffer pool在插入mtr後buffer pool的值是多少。
像上面我們在mtr1中修改了a頁面,在mtr2中修改了b,c頁面。他們的LSN值就是上面的所示。我們可以算一些8716就是8704 + 12 就是第一個插入的mtr之前的LSN初始的大小嘛。8916-8716 = 200就是mtr1的大小嘛。
但是我們需要注意的是,重複修改的頁面不會重新進行插入控制塊嘛,前面文章好像說過,就是我們怎麼找連結串列中有沒有對應頁面的控制塊呢?就是通過雜湊表找到key為是表空間+頁號組成的鍵,然後我們修改其newest_modification 的值就好了。
我們提到了在redo紀錄檔檔案中log file header 儲存了一個redo檔案開始的LSN,LSN就是在檔案基本資訊2048位元組的位置LSN值為8704開始計算。
redo紀錄檔對於系統崩潰恢復來說是十分重要的存在,但是如果系統不崩潰的話,這樣的操作是沒有意義的,且耗費效能的。但是當系統崩潰重啟的時候innodb是怎麼知道哪些redo紀錄檔是已經重新整理到磁碟了,還是沒有呢?
我們將上述的mtr_1重新整理到磁碟了,這是在紀錄檔檔案中我們就可以將mtr1的記錄覆蓋掉,我們會將紀錄檔檔案中頭部的4個block中儲存checkpoint進行+1的操作,並修改其儲存的LSN。可以回過頭檢視redo紀錄檔檔案的組成。以上這個操作就叫做伺服器做了一次checkpoint。
具體步驟如下:
以上的checkpoint的資訊只會儲存到第一個redo紀錄檔檔案的管理資訊中去。
還有一點就是checkpoint有1和2,對於他們來說,就是LSN是偶數的時候就儲存到2,奇數就儲存到1。
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)
innodb_flush_log_at_trx_commit值為2,我們進行重新整理磁碟,從資料庫的緩衝區中下來呼叫作業系統的執行對磁碟進行操作,還會先進入作業系統的緩衝區中讓作業系統去操作,如果作業系統沒崩必然也可以保證事務的一致性,但是如果作業系統也崩了,那就不能保證了。我們值為1是代表必須重新整理到磁碟中,即作業系統將資料真正刷到磁碟上了。
對於已經重新整理到磁碟的mtr來說,沒有必要進行再次恢復,所以我們需要對於起點進行確認。
我們從checkpoint1和checkpoint2拿出LSN,因為倆個地方都存了checkpoint的LSN,所以比較哪個最大,就可以確定需要恢復redo的起點。
對於每個block來說,都維護這一個len,我們只要讀到len小於512的,就可以知道這一頁是沒有滿的,然後根據其具體長度,就可以知道恢復的終點。
我們就是從起點,慢慢掃描每一個redo紀錄檔,對其進行復原,直到終點。
加速方法:
使用雜湊表
就是將每個頁面的redo紀錄檔,放入雜湊表中,根據spaceID和page Number來確定雜湊表的雜湊值,然後根據插入的先後排序,先插入在前。然後我們就可以根據一個頁面一個頁面進行更新,這樣避免了隨機IO。
跳過已經重新整理的頁面
我們在做了一次checkpoint後,又有頁面從LRU連結串列或者flush連結串列中的頁面更新到磁碟中。因為checkpoint不是一直在做的。
我們怎麼知道呢?在每個頁面的File Header中有一個FIL_PAGE_LSN的屬性,該屬性記錄了最近一次重新整理頁面的newest_modification 值。如果當前LSN小於這個FIL_PAGE_LSN的值,代表已經重新整理到後面的記錄了,不需要更新了,直接跳過。