通過上篇文章,我們知道MySQL是采用兩段提交策略來保證事務的原子性的,redo log刷盤的時機是在事務提交的commit階段采取刷盤的,在此之前,redo log都存在於redo log buffer這塊指定的內存區域中。
1:write和fsync區別
這里我們首先要明確兩個概念和兩個參數:
write:刷盤
fsync:持久化到磁盤
write(刷盤)指的是MySQL從buffer pool中將內容寫到系統的page cache中,並沒有持久化到系統磁盤上。這個速度其實是很快的。
fsync指的是從系統的cache中將數據持久化到系統磁盤上。這個速度可以認為比較慢,而且也是IOPS升高的真正原因。
2:MySQL日志刷盤控制參數
innodb_flush_logs_at_trx_commit(redo log)
取值0:每次提交事務都只把redo log留在redo log buffer中
取值1:每次提交事務都將redo log 持久化到磁盤上,也就是write+fsync
取值2:每次都把redo log寫到系統的page cache中,也就是只write,不fsync
sync_binlog(binlog)
取值0:每次提交都將binlog 從binlog cache中 write到磁盤上,而不fsync到磁盤
取值1:每次提交事務都將binlog fsync到磁盤上
取值N:每次提交事務都將binlog write到磁盤上,累計N個事務之后,執行fsync
3:MySQL沒動刷盤場景
在某些特定場景下,redo log會在commit這個動作到來之前進行刷盤操作,例如下面的兩種情況會讓沒有提交的事務的redo log寫入磁盤:
1、redo log buffer占用的空間即將達到buffer pool的一般的時候,后台線程會主動刷盤,這個時候,由於事務沒有提交,所以僅僅是將redo log buffer中的內容通過write的方法寫入到系統的cache中,沒有進行fsync的持久化動作。
2、並行提交事務的時候,會順帶將上一個事務的部分redo log從redo log buffer中fsync到磁盤上,例如下面的例子:
假設redo log buffer中的內容如下(假設每個事務的redo log有4部分):
redo log B1
redo log A1
redo log B2
此時,事務B發生了commit操作,而設置的innodb_flush_logs_at_trx_commit的值是1,那么會觸發事務B的redo log持久化到磁盤。此時事務A的一部分redo log,也就是redo log A1會被順帶着持久化fsync到磁盤中。
這里還需要說明一點,因為MySQL的innodb存儲引擎時需要支持崩潰恢復的,依賴prepare階段的redo log ,所以,如果innodb_flush_logs_at_trx_commit的值是1,MySQL會在redo log的prepare階段就進行一次持久化redo log的fsync操作。這個fsync的存在,再加上每秒一次的后台刷盤操作,innodb會認為redo log在commit的時候,就不需要fsync了,只write到文件系統的page cache就夠了。
所以,真正的兩階段提交,應該是下圖所示:

之所以redo log的write和fsync沒有連接在一起,其實是考慮到了組提交的功能,分開來進行這兩個步驟,在並發的場景下,可以讓這一組一次性提交的redo log更多一點,從而一次性fsync更多的組員。
4:MySQL兩段提交失敗分析
那么兩段提交過程中失敗會發生什么結果呢?MySQL又是怎樣處理的呢?
首先我們先放一下兩段提交的圖

接下來,我們就一起分析一下在兩階段提交的不同時刻,MySQL 異常重啟會出現什么現象。
如果在圖中時刻 A 的地方,也就是寫入 redo log 處於 prepare 階段之后、寫 binlog 之前,發生了崩潰(crash),由於此時 binlog 還沒寫,redo log 也還沒提交,所以崩潰恢復的時候,這個事務會回滾。這時候,binlog 還沒寫,所以也不會傳到備庫。
如果在圖中在時刻 B,也就是 binlog 寫完,redo log 還沒 commit 前發生 crash,那崩潰恢復的時候 MySQL 會怎么處理?
我們先來看一下崩潰恢復時的判斷規則。
如果 redo log 里面的事務是完整的,也就是已經有了 commit 標識,則直接提交;
如果 redo log 里面的事務只有完整的 prepare,則判斷對應的事務 binlog 是否存在並完整:
a. 如果是,則提交事務;
b. 否則,回滾事務。
這里,時刻 B 發生 crash 對應的就是 2(a) 的情況,崩潰恢復過程中事務會被提交。
那么問題又來了,MySQL是如何判斷binlog是不是完整的呢?
我們都知道binlog有三種格式statement、row、mix。其中mix是前兩種方式的組合,一個事務的binlog是有完整的格式的,
statement 格式的 binlog,最后會有 COMMIT;
row 格式的 binlog,最后會有一個 XID event。
另外,在 MySQL 5.6 版本以后,還引入了 binlog-checksum 參數,用來驗證 binlog 內容的正確性。對於 binlog 日志由於磁盤原因,可能會在日志中間出錯的情況,MySQL 可以通過校驗 checksum 的結果來發現。所以,MySQL 還是有辦法驗證事務 binlog 的完整性的。
而且redolog和binlog有一個共同的數據字段,叫 XID。崩潰恢復的時候,會按順序掃描 redo log:如果碰到既有 prepare、又有 commit 的 redo log,就直接提交; 如果碰到只有 parepare、而沒有 commit 的 redo log,就拿着 XID 去 binlog 找對應的事務。這樣在兩段提交的前提下就能完全保證事務的特性了。
