現在的位置: 首頁 > 資料庫 > 正文

MySQL 是如何實現 ACID 中的 D 的?

2020年02月21日 資料庫 ⁄ 共 3103字 ⁄ 字型大小 評論關閉

  假設你執行了一條 sql 語句:

  update user set age = 18 where user_id = 345981

  MySQL 會直接去磁碟修改數據嗎?

  明顯不會,磁碟IO太慢了,如果每個請求過來 MySQL 都要寫磁碟,磁碟肯定扛不住。

  那就寫內存?把數據從磁碟load到內存,然後修改內存里的數據。

  也不行,萬一掉電了,內存就沒了,數據就再也找不回來了。

  這其實是很多中間件都會遇到的問題,一個中間件做的再怎麼分散式,怎麼高可靠,都會遇到這個問題:

  數據來了,寫磁碟,還是寫內存?

  寫磁碟,嫌太慢?寫內存,又不安全?

  MySQL 的解決方案是:既寫磁碟又寫內存。

  數據寫內存,另外再往磁碟寫 redo log.

  redo log 是什麼

  在執行上面這條 sql 語句時,MySQL 會判斷內存中有沒有 user_id = 345981 的數據,沒有,則去磁碟找到這條數據所在的「頁」,把整頁數據都載入到內存,然後找到 user_id = 345981 的 row 數據,把內存中這行數據的 age 設置為 18。

  這時,內存的數據是新的、正確的,而磁碟數據是舊的、過時的,所以我們稱這時的磁碟對應的頁數據,為「臟頁」。

  這裡補充一個知識點:MySQL 是按頁為單位來讀取數據的,一個頁裡面有很多行記錄,從內存刷數據到磁碟,也是以頁為單位來刷。

  這時候如果掉電了,數據就沒了,於是 MySQL 把你對頁修改了什麼內容,記錄了下來,保存到磁碟,也就是redo log。

  寫完 redo log,MySQL 就認為事務提交成功了,數據持久化了(ACID的D),然後在空閑的時候,再把內存的數據刷到磁碟。

  如果在內存數據刷到磁碟之前,MySQL 掉電了,怎麼辦?

  這時只需要在重啟後,把「臟頁」load 到內存,然後應用 redo log,臟頁就變成「乾淨頁」了。

  你會說,萬一我寫內存成功,但是把 redo log 寫到磁碟失敗了呢?這點後面我們在討論「兩階段提交」時再討論。

  你還會說,redo log 還是要寫磁碟,那不還是很慢?

  並不是,把 redo log 寫到磁碟,比一般的寫磁碟要快,原因有:

  一般我們寫磁碟,都是「隨機寫」,而 redo log,是「順序寫」。

  MySQL 在寫 redo log 上做了優化,比如「組提交」。

  redo log 怎麼存儲

  我們大致可以get到:

  redo log 記錄了 sql 語句以及其他 api,對錶數據產生的變化,也就是說,redo log 記錄的是數據的物理變化,這也是它和後面要講的 binlog 一個最大的區別,binlog 記錄的是數據的邏輯變化,這也是為什麼 redo log 可以用來 crash recovery,而 binlog 不能的原因之一。

  redo log 是存儲在磁碟的,用於 crash recovery 後修正數據的,也就是我們常說的故障恢復,比如掉電,宕機等等。

  redo log 默認有兩個文件。

  redo log 是循環寫的(circular)。

  另外還有兩個參數:

  innodb_log_file_size:設置每個 redo log 文件的大小,默認是 50331648 byte,也就是 48 MB。

  innodb_log_files_in_group:設置 redo log 文件的數量,默認是 2,最大值是 100。

  我們常說事務具有 ACID 四個特性,其中 D(durability),數據持久性,意味著,一旦事務提交,它的狀態就必須保持提交,不能回滾,哪怕你系統宕機了、奔潰了,你也要想辦法把事務做到提交,把數據給我保存進去。

  這確實很嚴格,但看起來也很好實現,就是每次都把數據寫到磁碟,然後再告訴客戶端,事務提交成功了。

  但如果我要追求高性能呢?我要把數據寫到內存呢?

  所以我們說,innodb 在實現高性能寫數據的同時,利用 redo log,實現了事務的持久性。

  binlog

  講完了 redo log,我們再來聊聊 binlog。

  還是這一條 update 語句:

  update user set age = 18 where user_id = 345981

  在這條 update 語句執行的時候,除了生成 redo log,還會生成 binlog。

  binlog 和 redo log 有很多不同,有一點是一定要知道的,就是 redo log 只是 innodb 存儲引擎的功能,而 binlog 是 MySQL server 層的功能,也就是說,redo log 只在使用了 innodb 作為存儲引擎的 MySQL 上才有,而 binlog,只要你是 MySQL,就會有。

  binlog 裡頭記錄了什麼呢?上面有提到,和 redo log 記錄數據的物理變化不同,binlog 記錄的是數據的邏輯變化,比如上面這條 update 語句,你可以簡單的認為,binlog 裡頭就是記錄了這樣一條 sql 語句,當然,它還會記錄當前是在哪個資料庫下的等等。

  binlog 有什麼作用呢?

  MySQL 之所以把 binlog 放在了 server 層,說明 binlog 提供了一些通用的能力,比如:數據還原。

  DBA 總說,他能把 MySQL 的數據還原到任意時刻,怎麼還原?

  假設你在周三晚上八點的時候,不小心把一張表的數據都清空了,怎麼辦?

  這時候 DBA 就會找到最近的一次「全量備份」,然後重放從最近一次全量備份,到周三晚上八點,這段時間的 binlog,於是你的數據就還原回來了。

  binlog 還有另一個作用:主從複製,主庫把 binlog 發給從庫,從庫把 binlog 保存了下來,然後去執行它,這樣就實現了主從同步。

  當然,我們還能讓自己的業務應用,去監聽主庫的 binlog,當資料庫的數據發生變動時,去做特定的事情,比如進行數據實時統計。

  兩階段提交

  最後,那麼當我執行一條 update 語句時,redo log 和 binlog 是在什麼時候被寫入的呢?這就有了我們常說的「兩階段提交」:

  寫入:redo log(prepare)。

  寫入:binlog。

  寫入:redo log(commit)。

  為什麼 redo log 要分兩個階段: prepare 和 commit ?redo log 就不能一次寫入嗎?

  1、先寫 redo log,再寫 binlog

  這樣會出現 redo log 寫入到磁碟了,但是 binlog 還沒寫入磁碟,於是當發生 crash recovery 時,恢復後,主庫會應用 redo log,恢複數據,但是由於沒有 binlog,從庫就不會同步這些數據,主庫比從庫「新」,造成主從不一致

  2、先寫 binlog,再寫 redo log

  跟上一種情況類似,很容易知道,這樣會反過來,造成從庫比主庫「新」,也會造成主從不一致。

  而兩階段提交,就解決這個問題,crash recovery 時:

  如果 redo log 已經 commit,那毫不猶豫的,把事務提交。

  如果 redo log 處於 prepare,則去判斷事務對應的 binlog 是不是完整的。

  是,則把事務提交。

  否,則事務回滾。

  兩階段提交,其實是為了保證 redo log 和 binlog 的邏輯一致性。

  總結一下:

  redo log: innodb 在實現高性能寫數據的同時,利用 redo log,實現了事務 ACID 中的D,持久性。

  binlog:MySQL 的數據還原、主從複製,都依賴 binlog 來實現。

  兩階段提交:為了保證 redo log 和 binlog 的一致性。

  看似一條簡單的 update 語句,MySQL 在這背後其實做了很多事情。

  MySQL 是一個把單機性能發揮到極致的資料庫,這也是為什麼出現了那麼多分散式資料庫,MySQL 依然是很多公司的首選的原因吧。

抱歉!評論已關閉.