面試官:MySQL InnoDB事務中的ACID特性是如何實現的?
不得不說,現在的面試還是比幾年前卷了很多的。
以前的面試官大概率只會問,“說下 MySQL InnoDB 事務中的 ACID 特性各是什么”僅此而已了,根本不會涉及到什么底層實現。
嗯,那就卷起來吧,接下來我們先看看 ACID 特性的定義,然后再延展開來往底層實現上講。
原子性(Atomicity),事務是一個不可分割的最小單位,要么全部執行成功,要么全部失敗回滾。
一致性(Consistency),業務邏輯的一致性,保證事務從一個一致的業務狀態轉換到另一個。
比如轉賬功能,從你的賬戶里扣減10元,必須在我的賬戶里增加10元。
隔離性(Isolation),多個并發執行的事務是相互隔離、互不干擾的。當然,隔離是分等級的,這就是所謂的事務隔離級別。
持久性(Durability),事務提交后就會在硬盤中持久化,數據不會丟失。
原子性(Atomicity)
我們再看一下對于事務原子性的定義,事務是一個不可分割的最小單位,要么全部執行成功,要么全部失敗回滾。
在這里,事務全部執行成功依賴于 Redo Log(重做日志),而事務回滾則依賴于 Undo Log(撤銷日志)。
也就是說,事務的原子性是通過 Redo Log(重做日志)和Undo Log(撤銷日志)實現的。
圖片
Redo Log
當事務被提交的時候,對數據表的寫操作并不是直接刷新磁盤上的數據文件上,而是先被寫入到 Redo Log 中(默認情況下),再通過 Master Thread 適時刷新到磁盤上的數據文件中,其目的是為了減少磁盤頻繁的隨機 IO 操作。
這里在重點說下用來控制 Redo Log 的 innodb_flush_log_at_trx_commit 參數,該參數有0,1,2 三個選項:
圖片
參數為0:事務提交時并不需要將 Redo Log 寫入到磁盤中,僅僅寫入到Log Buffer 中,然后通過 Master Thread 每秒鐘進行一次 Redo Log 的刷盤操作。
等于1:默認值,在事務進行中不斷地寫入到 Redo Log Buffer 中,在事務提交時必須將事務的 Redo Log 刷新到磁盤上。
等于2:事務提交時將僅將 Redo Log 寫入到 OS Buffer 中,然后操作系統每秒鐘進行一次 Redo Log 的刷盤操作。
選擇該選項,如果只是 MySQL 數據庫掛掉了,操作系統沒有問題的情況下,對應的事務數據并沒有丟失。
Undo Log
在事務提交前,MySQL InnoDB 會先將用于回滾操作的 SQL 語句保存到 Undo Log 中,以便于將其恢復到事務開始前的狀態,其屬于邏輯日志。
舉例說明:如果 MySQL InnoDB 進行 insert 操作時,其對應的 Undo Log 為 delete 語句,反之則是 delete 操作對應 Undo Log 的 insert 語句。
如果 MySQL InnoDB 進行 update 操作時,其對應的 Undo Log 為反向 update 語句。
Undo Log 是通過回滾段和 Undo 段來進行存儲的,其中一個事務系統段可保存 256 個回滾段,一個回滾段可保存 1024 個 Undo 段的信息。
三者關系如下:
圖片
BTW:該圖截取自《MySQL 內核:InnoDB存儲引擎》
Undo Log 除了保證原子性,還可以通過其實現 MVCC(多版本并發控制)機制,支持 MySQL InnoDB 的快照讀的操作。
持久性(Durability)
網上的很多資料說,MySQL InnoDB 的持久性是通過具備 WAL (Write-Ahead Logging)機制的 Redo Log 來實現的,要求所有數據庫寫操作在寫入數據文件之前,必須先寫入到日志文件中。
這種說法不能說不對,只是不太全面。
除了 Redo Log 之外,Double Write 機制也是 MySQL InnoDB用來保證數據頁完整性的技術,從而保證了持久性,接下來我們了解一下它的作用。
眾所周知,MySQL InnoDB 的最小 IO 單元為默認 16KB 的 Page,無論其存儲在 Buffer Pool 中還是在磁盤上,而 Linux 的文件系統 Page 只有 4KB。
這也就意味著,如果 MySQL InnoDB 將一個 Page 的數據進行刷盤操作,需要寫四個文件系統的 Page,但這個操作并不是原子性的。
如果在這個過程中遇到系統崩潰或者服務器宕機,就會導致四個文件系統的 Page 沒有全部寫完,從而出現 MySQL InnoDB 的 Page 損壞的情況,而 Double Write 機制則正是為了解決這個問題。
Double Write 機制包含兩個部分,內存中的 Double Write Buffer 和磁盤共享表空間的 128 個數據頁,大小都是 2MB。
其具體運行機制如下圖所示:
圖片
在事務提交時 Redo Log 被刷新到磁盤上,隨后 Master Thread 適時將 Buffer Pool 中的臟頁刷新到 Double Write Buffer 中,最后將 Double Write Buffer 中的數據寫入到 Double Write 共享表空間和數據文件中。
如果在寫入 Double Write 共享表空間的時候出現宕機崩潰的情況,此時數據文件仍然是完整的,可以通過 Redo Log 進行恢復。
反之,如果在寫入 Double Write 共享表空間的時候成功了,卻在寫入數據文件的時候出現宕機崩潰的情況,則如上圖所示:可先通過 Double Write 共享表空間的 Page 對數據文件中的 Page 進行覆蓋,再通過 Redo Log 進行恢復。
隔離性(Isolation)
MySQL InnoDB 的默認事務隔離級別是可重復讀,其隔離性是通過鎖機制和 MVCC(多版本并發控制)來實現的。
先說說寫操作,如下圖所示,MySQL InnoDB 有很多類型的鎖,這些鎖機制可以避免多個事務對同一項數據資源進行修改,從而保證了各事務間相互隔離、互不干擾。
圖片
這里所說的“同一項數據資源”不僅僅是指一行數據,也可以是數據頁級別、表級別甚至是庫級別。
MySQL InnoDB 中的讀操作分為快照讀和當前讀兩種。
快照讀是指在事務開始時將數據的一個副本保存起來,然后在整個事務過程中使用這個副本進行讀取,不受其他并發事務的影響,SQL語句如下:
SELECT * FROM table1;
當前讀是指在事務期間每次讀取數據都返回最新的數據,不使用事務開始時的數據副本,SQL語句如下:
SELECT * FROM table1 FOR UPDATE;
SELECT * FROM table1 LOCK IN SHARE MODE;
當前讀的隔離性仍然是通過鎖機制來實現的,而快照讀則是MVCC(多版本并發控制)。
MVCC 是一種數據庫中用于處理并發讀寫事務的技術,通過維護數據的不同版本的方式,來實現查詢操作在不需要等待其他事務持有的鎖的情況下進行,從而提高了數據庫的并發性。
MVCC 是通過數據行的隱藏字段、Undo Log 和 Read View 來實現的。
其中隱藏字段包括:
- DB_TRX_ID:6 Byte,最近一次插入或修改該行記錄的事務ID。
- DB_ROLL_PTR:7 Byte,回滾指針,指向存儲在Undo Log中的這條記錄的上一個版本。
- DB_ROW_ID:6 Byte,隱藏的自增主鍵,如果數據表中沒有設定主鍵的話,MySQL InnoDB 會自動通過 DB_ROW_ID 產生一個聚簇索引。
再來說說 Read View,當我們發起快照讀的時候,會對該行記錄生成一個 Read View(讀視圖),用來判斷當前事務能夠看到哪個版本的數據,既可能是當前數據表中最新的數據,也可能是該行記錄在 Undo Log 里面的某個版本的數據。
如下圖所示:
圖片
一致性(Consistency)
有的同學在一致性保證這塊提到了 Binlog,我認為不太準確,因為這里說的是實現事務的一致性,而不是主從庫的數據一致性,不一樣的。
事務的一致性特性,是為了保障業務邏輯的一致性,保證事務從一個一致的業務狀態轉換到另一個。
我認為事務的 ACID 之間,并不是像《金字塔原理》一書中提到的 MECE 原則一樣 —— 相互獨立,完全窮盡。
事務的原子性、持久性和隔離性都是為了實現事務的一致性,讓業務狀態可以正常流轉。
圖片