MySQL資料庫本地事務原理

JosenZHANG發表於2022-01-23

在經典的資料庫理論裡,本地事務具備四大特徵:

  • 原子性
  • 事務中的所有操作都是以原子的方式執行的,要麼全部成功,要麼全部失敗;
  • 一致性
  • 事務執行前後,所有的資料都應該處於一致性狀態---即要滿足資料庫表的一致性約束,也要達到業務一致性(完成了業務目標);
  • 隔離性
  • 併發執行的事務不應該相互干擾;隔離性的強度由隔離級別決定;
  • 永續性
  • 事務一旦被提交,它新增/修改的資料不會隨著系統崩潰而丟失;

在MySQL(InnoDB引擎)中,原子性和永續性是通過Redo Log來實現的,一致性是通過Undo Log實現的,而隔離性則是通過鎖和MVCC來實現的。

 ARIES演算法

如果需要深入瞭解資料庫本地事務原理,不得不提到ARIES演算法,該演算法全稱為Algorithms for Recovery and Isolation Exploiting Semantics(基於語義的恢復與隔離演算法),眾多主流的關係型資料庫都受到該演算法的影響。

ARIES演算法主要針對使用No Force + Steal的資料寫入策略而採用的一種資料恢復方式。

該演算法主要基於三個主要的原則:

  • Write-ahead logging
  • 出於效能上的考慮,資料的修改都是在記憶體中進行,並將這些“修改操作”記錄到日誌(Redo Log和Undo Log)中,然後非同步將記憶體中的資料寫入到磁碟;
  • 通過Redo Log恢復資料
  • Redo Log用於記錄事務對資料的修改操作,在資料庫崩潰恢復時,ARIES通過Redo Log重放那些還未寫入到資料庫磁碟中的資料操作,將資料恢復至崩潰前的狀態;
  • 通過Undo Log回滾資料
  • 對崩潰前未提交的事務,通過Undo Log進行回滾;

Write-ahead logging

每個事務執行時,都是在記憶體中進行資料的修改,並將這些“修改操作”記錄到日誌,然後將記憶體中的資料非同步寫入到磁碟裡;

但日誌也並非立刻寫入至磁碟,而是先寫入到Log Buffer,再按照相應的引數配置進行磁碟的寫入操作;在寫入至磁碟時,資料會先寫入至作業系統核心緩衝區(OS Buffer),然後根據引數配置決定對核心緩衝區中的資料同步或非同步刷盤。

如在InnoDB中,Redo Log的磁碟寫入策略是由innodb_flush_log_at_trx_commit引數值來決定的:

    0:  當引數值設定為0時,每隔1秒將Redo Log Buffer中的資料寫入至OS Buffer,並同時呼叫fsync()函式完成刷盤操作;

    1:  每次事務提交時,立即將Redo Log Buffer中的資料寫入至OS Buffer,並同時呼叫fsync()函式完成刷盤操作;

    2:  每次事務提交時,立即將Redo Log Buffer中的資料寫入至OS Buffer,每隔1秒呼叫fsync()函式完成刷盤操作;

由此可見,當innodb_flush_log_at_trx_commit設定為0或2時,都會導致日誌資料丟失;

以上討論了“資料操作”日誌的寫入方式,而對於事務中真正修改的資料,Write-ahead logging根據事務提交的時間節點,將變動的資料寫入至磁碟的時間節點分為Force和Steal兩種:

    Force: 在事務提交時,是否強制將變動的資料完全寫入至磁碟?

    Steal: 在事務提交前,是否允許將變動的資料提前寫入至磁碟?

因此根據Force和Steal的值,資料的寫入策略可以分為以下四種:

  Steal No Steal
Force

事務提交時,強制將變動資料完全寫入至磁碟

事務提交前,允許將變動的資料提前寫入至磁碟

事務提交時,強制將變動資料完全寫入至磁碟

事務提交前,不允許變動的資料提前寫入至磁碟

No Force

事務提交時,不需要強制變動資料完全寫入至磁碟

事務提交前,允許將變動的資料提前寫入至磁碟

事務提交時,不需要強制變動資料完全寫入至磁碟

事務提交前,不允許變動的資料提前寫入至磁碟

直觀感覺就可以知道,採用No Force + Steal的方式,不需要在事務提交時,強制將所有的變動資料寫入至磁碟,同時允許變動的資料在事務提交前即可提早寫入至磁碟;這樣的寫入策略靈活性強且效能最好;MySQL InnoDB採用的就是此種寫入方式。

Redo Log

Physiological Logging

在崩潰並重啟後,資料庫重放Redo Log進行資料恢復時,由於並不知道崩潰前哪些變動的資料已經寫入到物理磁碟,因此需要保證Redo Log的重放是冪等的,即多次重放得到的結果不會改變;

InnoDB中所有的資料都是以資料頁(Page)的形式存在於磁碟中的,因此Redo Log中的每一條日誌,會記錄被修改的資料頁Page ID、被修改的記錄在該Page中的位移、記錄中哪些欄位被修改了、修改後的欄位值:

(Page ID,Record Offset,(Filed 1, Value 1) … (Filed i, Value i) … )

一個事務可能修改多條記錄(這些記錄可能位於同一個資料頁,也可能位於不同的資料頁),就會產生多條日誌;同時資料庫的多個事務都是並行執行的,出於效能的考慮,它們在Redo Log中並非以序列的方式寫入,而是多個事務產生的多條日誌互相穿插在Redo Log中,這就導致了Mini Transaction(Mtr)的產生。Mtr是資料庫事務在Redo Log中的最小儲存單元,一個資料庫事務被劃分為一個或多個Mtr,一個Mtr僅包含對一個資料頁的修改(由於一個資料頁可能包含多條記錄,因此一個Mtr中包含的日誌記錄也不止一條)。

雖然同一個事務的多個Mtr在Redo Log中可能是不連續的,但同一個Mtr中包含的多條日誌在Redo Log中一定是連續的。

我們把Redo Log這樣的的儲存方式稱之為Physiological Logging。

LSN機制

Redo Log不是一個無限膨脹的日誌檔案,它具有固定的長度,日誌先按照物理順序一直往後新增,當達到空間限制後跳轉到開始位置重新進行寫操作/覆蓋。

Redo Log中的記錄也並非永遠具有存在的價值,當事務所操作的資料已經被寫入到物理磁碟中,這個事務對應的日誌就沒有存在的意義,事實上是可以被刪除了。

MySQL資料庫使用CheckPoint的值來指定日誌檔案可以擦除的位置,也就是說該位置之前的日誌都是可以刪除的;CheckPoint的值用LSN來表示。

LSN(Log Sequence Number)並非物理位置,它是一個8位元組(64位)的整數且單調遞增,代表著自資料庫啟動以來,至當前時間點寫入至Redo Log中資料的總量(位元組數)。

Redo Log中的每一條日誌也使用LSN作為它的標識。

Double Write Buffer

上文談到資料庫通過非同步的方式將修改的資料寫入至物理磁碟,但如果無法保證資料寫入到物理磁碟的原子性,當恰好寫入了部分資料後發生崩潰,這會導致物理磁碟中存在一個被損壞的資料頁;而Redo Log只記錄哪些資料頁被修改,但不會記錄哪些資料頁被損壞,因此無法通過Redo Log來修復這些被損壞的資料頁;

MySQL InnoDB使用Double Write Buffer來解決寫資料至物理磁碟時崩潰後資料頁的恢復問題;

Double Write Buffer是一個儲存區,當InnoDB嘗試寫資料頁至物理磁碟之前,會先將資料頁寫入至該區域;如果寫資料時崩潰,恢復時InnoDB可以從Double Write Buffer中找到這個被損壞的資料頁的完整副本。

正如名稱Double Write所示,這會導致兩次磁碟寫操作:一次是寫入到Double Write Buffer,另外一次是寫入到真正的資料頁所在的磁碟位置。

另外一個問題:如果日誌從Redo Log Buffer寫入至磁碟時,資料庫崩潰了該如何處理?

Redo Log寫入至磁碟是通過日誌塊(Log Block)的方式進行寫入的,日誌塊不同於記憶體中的資料頁(1 Page = 16 KB),一個日誌塊的大小為512 Byte。每個日誌塊中包含該塊的摘要值(CheckSum),通過該摘要值可判斷寫入至磁碟的這個日誌塊是否完整。

當innodb_flush_log_at_trx_commit設定為2時,事務的提交是以日誌寫入磁碟作為結束標誌的,如果寫入時崩潰,則代表事務提交失敗,該日誌塊實際上可以直接丟棄。

Undo Log

在上文Redo Log的Physiological Logging中談到,Redo Log只會記錄某個資料欄位修改後的值,在資料庫崩潰後恢復階段會利用Redo Log對事務中的資料操作進行重放,其中包括已提交的事務和未提交的事務;對於那些未被提交的事務,需要使用Undo Log對其進行回滾操作;

與Redo Log使用的Physiological Logging格式不同,Undo Log儲存的是邏輯日誌;如果事務執行的是一個INSERT操作,Undo Log會儲存一條DELETE操作;如果事務中執行的是一個DELETE操作,Undo Log會儲存一條INSERT操作;如果事務執行的是一條UPDATE操作,Undo Log中會儲存一條反向的UPDATE操作......

Crash Recovery(崩潰恢復)

CheckPoint機制 

InnoDB使用了一個叫做Fuzzy Checkpointing的CheckPoint機制來實現資料頁寫入至磁碟;它並非一次性的將所有記憶體中的資料頁全部寫入到磁碟中,因為這會阻塞在寫入時其他的資料庫操作,因此它採用小批量(Small batches)寫入。

當資料庫崩潰時,並非所有Redo Log中的事務(包括已提交和未提交的)所修改的資料頁都已經寫入到了物理磁碟中,我們把那些還未寫入的資料頁叫做髒頁(Dirty Pages)。

在崩潰恢復時,需要找到所有的這些髒頁,並利用Redo Log進行重放,也需要找出所有未提交的事務,利用Undo Log進行回滾。

由於Fuzzy Checkpointing只是小批量寫入,因此並非所有已提交事務的資料頁都寫入至磁碟中;同時由於多個事務(包括已提交和未提交的)會修改同一個資料頁,這會導致在資料頁寫入時可能將未提交事務的資料也寫入到磁碟中了;所以在每一次Fuzzy Checkpointing之後,會把該次Fuzzy Checkpointing時未提交的事務列表和髒頁列表形成為一個CheckPoint日誌,儲存到Redo Log中。

在崩潰恢復過程中,InnoDB引擎會找到Redo Log中最近一次CheckPoint日誌,獲取到未提交的事務列表和髒頁列表,並以該日誌為起點遍歷至Redo Log末尾;在遍歷過程中,如果遇到事務提交,將其從未提交事務列表中移除,如果遇到新事務開始,將它加入到未提交事務列表;同時對遍歷到的所有Physiological Log,都新增到髒頁列表;最後會形成一個最終的未提交事務列表和髒頁列表。

對髒頁列表,在Redo Log中找到最早的那個髒頁所對應的日誌,並以此為起點進行Redo Log重放。此時可能會遇到的Redo Log對應的資料頁實際已經寫入至磁碟中了,不過即使再次重放也沒有關係,因為Redo Log是冪等的。

對所有未提交的事務列表,找到其對應的Undo Log,並進行回滾操作。

相關文章