MySQL數據庫事務實現原理


在經典的數據庫理論里,本地事務具備四大特征:

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

在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,並進行回滾操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM