跟面試官侃半小時MySQL事務,說完原子性、一致性、持久性的實現
提到MySQL的事務,我相信對MySQL有了解的同學都能聊上幾句,無論是面試求職,還是日常開發,MySQL的事務都跟我們息息相關。
而事務的ACID(即原子性Atomicity、一致性Consistency、隔離性Isolation、持久性Durability)可以說涵蓋了事務的全部知識點,所以,我們不僅要知道ACID是什麼,還要了解ACID背後的實現,只有這樣,無論在日常開發還是面試求職,都能無往而不利。
上一篇 跟面試官侃半小時MySQL事務隔離性,從基本概念深入到實現 主要圍繞「隔離性」展開,從基本概念,到隔離性的實現,最後以一個實戰案例進行融會貫通。本篇內容將介紹原子性、一致性、持久性相關實現,由於這部分內容可能很多人會相對陌生,因為日常業務開發可能不太會去接觸和深究,但是了解完後,你對MySQL會有更深刻的認識。
1.基本概念
- 原子性。
整個事務是不可分割的最小單位,事務中任何一個語句執行失敗,所有已經執行成功的語句也要回滾,整個數據庫狀態要恢復到執行事務前到狀態。
- 一致性。
事務將數據庫從一種狀態轉變為下一種一致的狀態。在事務的前後,數據庫的完整性約束沒有被破壞。(事務的acid不是完全正交的,尤其是一致性,可能跟原子性、隔離性都有一定關係,後面會看到)
- 持久性。
事務一旦提交,那麼就是永久性的,不會因為宕機等故障導致數據丟失(外力影響不保證,比如磁盤損害)。持久性是保證了數據庫的高可靠性(High Reliability),而不是高可用性(Hign Availability)。高可用性並不能通過事務來保證。
2.持久性的實現
MySQL的innoDB存儲引擎,使用Redo log保證了事務的持久性。
當事務提交時,必須先將事務的所有日誌寫入日誌文件進行持久化,就是我們常說的WAL(write ahead log)機制(這個技術是保障持久性的關鍵技術,在HBase中也扮演重要角色,有興趣的同學可以參考我之前關於HBase的文章)。這樣才能保證斷電或宕機等情況發生後,已提交的事務不會丟失,這個能力稱為 crash-safe。
下面深入聊一聊redo log的機制,給大家更深刻的理解。
Redo log包括兩部分,重做日誌緩衝(redo log buffer)和重做日誌文件(redo log file),前者是易失的緩存,後者是持久化的文件。
舉一個事務的例子:
- 步驟1:begin;
- 步驟2:insert into t1 …r
- 步驟3:insert into t2 …
- 步驟4:commit;
這個事務的寫入過程實際拆解如下:
innodb緩衝池的概念本文就不展開說明了,以後有機會可以展開說一下。
重點關注在這個事務提交前,將 redo log 的寫入拆成了兩個步驟,prepare 和 commit,這就是”兩階段提交」。
為什麼要採用兩階段提交呢?
實際上,兩階段提交是分佈式系統常用的機制。MySQL使用了兩階段提交後,也是為了保證事務的持久性。Redo log 和bingo 有一個共同的數據字段,叫 XID,崩潰恢復的時候,會按順序掃描 redo log。
- 假設在寫入binlog前系統崩潰,那麼數據庫恢復後順序掃描 redo log,碰到只有 parepare、而沒有 commit 的 redo log,就拿着 XID 去 binlog 找對應的事務,而且binlog也沒寫入,所以事務就直接回滾了。
- 假設在寫入binlog之後,事務提交前數據庫崩潰,那麼數據庫恢復後順序掃描 redo log,碰到既有 prepare、又有 commit 的 redo log,就直接提交,保證數據不丟失。
這個事務要往兩個表中插入記錄,插入數據的過程中,生成的日誌都得先寫入redo log buffer ,等到commit的時候,才真正把日誌寫到 redo log 文件。(當然,這裡不絕對,因為redo log buffer可能因為其他原因被迫刷新到redo log)。
而為了確保每次日誌都能寫入日誌文件,在每次將重做日誌緩衝 寫入 重做日誌文件 後,InnoDB存儲引擎都需要調用一次fsync操作,確保寫入了磁盤。
對於redo log的持久化,可以如下圖所示。
1)先寫入redo log buffer,在藍色區域。
2)寫入redo log file,但是還沒有fsync,這時候是處於黃色的位置,處於系統緩存。
3)調用fsync,真正寫入磁盤。
為了控制 redo log 的寫入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 參數,它有三種可能取值:
- 設置為 0 的時候,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中 ;
- 設置為 1 的時候,表示每次事務提交時都將 redo log 直接持久化到磁盤;
- 設置為 2 的時候,表示每次事務提交時都只是把 redo log 寫到 page cache。
binlog的寫入和redo log一樣,也是包括bingo cache和bingo file,同樣跟上面的三色層次類似(當然,binlog是server層的,不是存儲引擎層的),包括log buffer、文件系統page cache、hard disk。
寫入page cache 和 fsync到disk 的時機,是由參數 sync_binlog 控制的:
- sync_binlog=0 的時候,表示每次提交事務都只 寫入文件系統的page cache,不 fsync;
- sync_binlog=1 的時候,表示每次提交事務都會執行 fsync;
- sync_binlog=N(N>1) 的時候,表示每次提交事務都寫入文件系統的page cache,但累積 N 個事務後才 fsync。(如果主機發生異常重啟,會丟失最近 N 個事務的 binlog 日誌)
通常我們說 MySQL 的「雙 1」配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都設置成 1。也就是說,一個事務完整提交前,需要等待兩次刷盤,一次是 redo log(prepare 階段),一次是 binlog。
特別需要區分的是,redo log和binlog的不同。這也是經常在面試中可能會問到的兩種日誌的差異。
注意有這麼幾點:
- 產生位置不同。
redo log是innodb的存儲引擎產生的,而binlog是數據庫的server層實現的。換句話說,如果你使用MySQL,換其他存儲引擎,那麼可能沒有redo log,但是還是會有binlog。
- 日誌記錄的內容形式不同。
binlog是一種邏輯日誌,記錄對應的SQL語句,而redo log記錄了物理日誌,是針對每個數據頁的修改。
- 日誌寫入磁盤時間不同。
binlog只有在事務提交後完成一次寫入,對於一個事物而言,在binlog中只有一條記錄。而redo log在事務進行中不斷被寫入,而且是並發寫入的,不是順序寫入的。
- 保存方式不同。
redo log 是循環寫的,空間固定會用完;binlog 是可以追加寫入的。「追加寫」是指 binlog 文件寫到一定大小後會切換到下一個,並不會覆蓋以前的日誌。
3.原子性的實現
Undo log保證了事務的原子性。
在對數據庫進行修改時,innoDB引擎除了會產生redo log,還會產生undo log。InnoDB實現回滾,靠的是undo log:當事務對數據庫進行修改時,InnoDB會生成對應的undo log;如果事務執行失敗導致事務需要回滾,就利用undo log中的信息將數據回滾到修改之前的樣子。
有人認為undo log是redo log的逆過程,其實是不對的。兩個日誌文件其實都能看作是一種對數據的恢復操作,redo log恢復事務導致的數據頁的修改,而undo log能夠恢複數據記錄到某個特定的版本。
所以redo log是一種物理日誌(數據頁的修改),而undo log是一種邏輯日誌(數據記錄)。
undo log還要另外一個重要作用,就是用於mvcc中,進行多版本控制,也就是實現事務隔離性的基礎,當用戶讀取一行記錄時,如果這個記錄已接被其他事務佔用,那麼當前事務就可以通過undo讀取之前的行版本信息,用來實現非鎖定讀取,就是「快照讀」。(事務隔離性的問題,可以看我上一篇文章 跟面試官侃半小時MySQL事務隔離性,從基本概念深入到實現 )。
4.一致性的實現
就像一開始在定義的時候介紹的,事務的ACID性質不是完全正交的,尤其是一致性,我們可以認為原子性、持久性和隔離性都是為了實現事務的一致性。
當然,這裡的一致性是指數據庫層面的事務一致性。
如果說你在應用層面做一個操作,給轉賬者扣錢,沒給接收者加錢,那麼這個不一致跟事務的不一致是沒有關係的,需要開發人員自己做業務邏輯一致性的保證。
看到這裡了,原創不易,點個關注、點個贊吧,你最好看了~
知識碎片重新梳理,構建Java知識圖譜://github.com/saigu/JavaKnowledgeGraph(歷史文章查閱非常方便)
掃碼關注我的公眾號「阿丸筆記」,第一時間獲取最新更新。同時可以免費獲取海量Java技術棧電子書、各個大廠面試題。