MySQL 面試常問

今天大部分程序需要處理的數據,都來自數據庫,尤其是關係型數據庫,那麼一條 SQL 提交到數據庫之後,數據庫都做了些什麼?如果不懂這些問題,就無法更好的使用數據庫,更無法回答好面試官的問題。 而現在流行的開源數據庫,非 MySQL 莫屬,面試中 MySQL 也是必問,於是我就學習了專欄《MySQL實戰45講》,今天的文章試着回答以下兩個問題:

1、一條 SQL 語句提交到數據庫之後,數據庫都會執行哪些動作?

2、MySQL 是如何恢復到某一天的某一秒的狀態?

先看下一條讀操作 SQL 的查詢過程:

連接器

客戶端在提交 SQL 語句之前,你需要先連接上數據庫,也就是說要提供用戶名密碼登陸,這便是連接器發揮作用的時候。

連接上去後,MySQL 就創建了一個連接對象放在了內存中,連接對象裏有用戶的相關權限信息,此時如果管理員修改了用戶權限,只要用戶不退出重新連接,就不會被影響。

內存資源是比較昂貴的,不用的話就要被清理。如果不做任何操作,在一定的時間之後(默認是 8 小時),連接器會自動斷開,此時再查詢就會報錯。

一個比較好的方案是使用數據庫連接池。Python 編程可以使用第三方庫 DBUtils 來管理數據庫連接池。

查詢緩存

緩存可以快速返回命中的查詢,在使用上的感受就是同一個 SQL,第二次查詢時結果是立刻顯示的。查詢緩存中以 SQL 語句作爲 KEY,查詢結果作爲 VALUE。

如果你的查詢能夠直接在這個緩存中找到 key,並且具有對該表的相應的權限,那麼這個 value 就會被直接返回給客戶端。

如果沒有找到,會走接下來流程,一旦查到結果,結果還是會保存在查詢緩存中。

分析器

如果沒有命中查詢緩存,SQL 語句就會傳給分析器進行詞法分析,分析是否有語法錯誤,解析中表名,字段名等等,其實不僅僅數據庫有分析器,很多開源的工具也有分析 SQL 的功能,比如 Python
可以使用 python-sqlparse,JAVA 可以使用 druid(阿里巴巴開源)。

解析出表名之後,檢查一下用戶對錶的權限,如果權限符合就進行下一步優化器。

優化器

經過了分析器,MySQL 就知道你要做什麼了。

在開始執行之前,還要先經過優化器的處理。優化器是在表裏面有多個索引的時候,決定使用哪個索引;或者在一個語句有多表關聯(join)的時候,決定各個表的連接順序。

執行器

MySQL 通過分析器知道了你要做什麼,通過優化器知道了該怎麼做,於是就進入了執行器階段,開始執行語句。開始執行的時候,要先判斷一下你對這個表 T 有沒有執行查詢的權限,如果沒有,就會返回沒有權限的錯誤。

也許你會問,權限驗證前面不是已經做了嗎? 爲什麼這裏還要進行權限驗證,因爲除了sql 還可能有存儲引擎,觸發器等,在這些對象中,也可能需要調用其它表去獲取數據,也需要權限驗證,前面的階段對於觸發器,存儲引擎這種對象的執行是做不到的。

比如說:

select * from T where ID=10;

如果 ID 字段沒有索引,那麼執行器的執行流程是這樣的:調用 InnoDB 引擎接口取這個表的第一行,判斷 ID 值是不是 10,如果不是則跳過,如果是則將這行存在結果集中;調用引擎接口取“下一行”,重複相同的判斷邏輯,直到取到這個表的最後一行。執行器將上述遍歷過程中所有滿足條件的行組成的記錄集作爲結果集返回給客戶端。至此,這個語句就執行完成了。

對於有索引的表,執行的邏輯也差不多。第一次調用的是“取滿足條件的第一行”這個接口,之後循環取“滿足條件的下一行”這個接口,這些接口都是引擎中已經定義好的。

說到存儲引擎,MySQl 支持 InnoDB、MyISAM、Memory 等多個存儲引擎。現在最常用的存儲引擎是 InnoDB,它從 MySQL 5.5.5 版本開始成爲了默認存儲引擎。也就是說,你執行 create table 建表的時候,如果不指定引擎類型,默認使用的就是 InnoDB。不過,你也可以通過指定存儲引擎的類型來選擇別的引擎,比如在 create table 語句中使用 engine=memory, 來指定使用內存引擎創建表。不同存儲引擎的表數據存取方式不同,支持的功能也不同。

接下來,看一下寫操作的執行過程,redo log 和 binlog 又起到了什麼作用?

寫操作

首先,可以確定的說,查詢語句的那一套流程,更新語句也是同樣會走一遍。

與查詢流程不一樣的是,更新流程還涉及兩個重要的日誌模塊,它們正是redo log(重做日誌)和 binlog(歸檔日誌)。如果接觸 MySQL,那這兩個詞肯定是繞不過的,redo log 和 binlog 在設計上有很多有意思的地方,這些設計思路也可以用到你自己的程序裏。

以更新操作爲例,假如 SQL 語句爲:

update table_a set count = count + 1 where id = 2
  1. 執行器先找引擎取 id=2 這一行。id 是主鍵,引擎直接用樹搜索找到這一行。如果 id=2 這一行所在的數據頁本來就在內存中,就直接返回給執行器;否則,需要先從磁盤讀入內存,然後再返回。
  2. 執行器拿到引擎給的行數據,把這個值加上 1,比如原來是 N,現在就是 N+1,得到新的一行數據,再調用引擎接口寫入這行新數據。
  3. 引擎將這行新數據更新到內存中,同時將這個更新操作記錄到 redo log 裏面,此時 redo log 處於 prepare 狀態。然後告知執行器執行完成了,隨時可以提交事務。
  4. 執行器生成這個操作的 binlog,並把 binlog 寫入磁盤。
  5. 執行器調用引擎的提交事務接口,引擎把剛剛寫入的 redo log 改成提交(commit)狀態,更新完成。

這裏得說明一下,redo log 和 binlog 都是日誌文件,爲了防止異常重啓、掉電、恢復數據等場景,這些日誌文件都會持久化到磁盤上。爲了防止頻繁的訪問磁盤,寫 redo log 前會先寫到內存中的 redo log buffer,會定期一起寫到磁盤。

但是這兩個 log 文件又有所區別:

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用。
  2. redo log 是物理日誌,記錄的是“在某個數據頁上做了什麼修改”;binlog 是邏輯日誌,記錄的是這個語句的原始邏輯,比如“給 id=2 這一行的 c 字段加 1 ”。
  3. redo log 是循環寫的,空間固定會用完;binlog 是可以追加寫入的。“追加寫”是指 binlog 文件寫到一定大小後會切換到下一個,並不會覆蓋以前的日誌。
  4. redo log 用於回滾,binlog 用於恢復。

如果將 MySQL 恢復到某一天的某一秒

要做到這一點有個前提,就是要對 MySQL 數據庫定期做整庫備份。這裏的定期取決於系統的重要性,可以是一天一備,也可以是一週一備。

當需要恢復到指定的某一秒時,比如某天下午兩點發現中午十二點有一次誤刪表,需要找回數據,那你可以這麼做:

  1. 首先,找到最近的一次全量備份,如果你運氣好,可能就是昨天晚上的一個備份,從這個備份恢復到臨時庫;
  2. 然後,從備份的時間點開始,將備份的 binlog 依次取出來,重放到中午誤刪表之前的那個時刻。這樣你的臨時庫就跟誤刪之前的線上庫一樣了。
  3. 最後,你可以把表數據從臨時庫取出來,按需要恢復到線上庫去。

爲什麼要兩階段提交

前面寫操作中的提到,寫磁盤前先寫 redo log,此時 redo log 狀態爲 prepare,然後再寫 binlog,寫完 binlog 後,再提交,redo log 才處於 commit 狀態。

爲什麼要等 binlog 寫完才能提交呢? 這是因爲假如 binlog 沒寫完就提交,此時如果異常重啓,那麼 binlog 就沒有這條記錄,在後續的主從複製時,將該 binlog 重放之後,從庫的數據與主庫的數據就產生了不一致。

如果先寫 binlog,再寫 redo log,假如寫完 binlog 系統異常重啓,那麼重啓恢復後由於 redo log 還沒有寫,因此事務回滾,但是由於 binlog 已經成功寫入,在後續的主從複製後仍然導致主從不一致。

MySQL 如何回滾與恢復數據的?

前面提到 InnoDB 有個日誌文件叫 redo log,就可以持久化存在磁盤上的,但是在內存中也有一份對應的緩衝區,叫 redo log buffer,爲了應對異常重啓,InnoDB 有一個後臺線程,每隔 1 秒,就會把 redo log buffer 中的日誌,調用 write 寫到文件系統的 page cache,然後調用 fsync 持久化到磁盤。

也就是 redo log buffer -> page cache -> 磁盤 這一過程,每秒都在進行,一旦發生異常重啓,從 redo log 中恢復就可以了。那具體是怎麼恢復的呢?

事務提交之前,先寫入 redo log,狀態是 prepare,表示已經準備好了,隨時可以提交。

事務提交之後,redo log 對應的狀態是 commit,表示已經提交。

如果是 prepare 時發生異常重啓,mysql 在恢復後對狀態爲 prepare 狀態的事務進行回滾。

如果是 commit 狀態,表示本來已經寫完了,重啓也沒關係。

如果是 prepare 之前崩潰了,也無所謂,本來就沒有開始寫數據,重啓也沒有任何損失。

現在有了 redo log,只能保證數據不丟,但還無法保證數據可以恢復到之前的某一時刻的狀態。

這就需要 binlog,binlog 是 mysql 自帶的歸檔日誌。

假如在寫 binlog 前異常重啓,mysql 在恢復後對狀態爲 prepare 狀態的事務進行回滾。

假如在寫 binlog 後異常重啓,則判斷對應的事務 binlog 是否存在並完整:

a. 如果是,則提交事務;
b. 否則,回滾事務。

你可能會問,處於 prepare 階段的 redo log 加上完整 binlog,重啓就能恢復,MySQL 爲什麼要這麼設計?

回答: binlog 寫完以後 MySQL 發生崩潰,這時候 binlog 已經寫入了,之後就會被從庫(或者用這個 binlog 恢復出來的庫)使用。所以,在主庫上也要提交這個事務。採用這個策略,主庫和備庫的數據就保證了一致性。

還有一個問題,就是爲什麼不讓 redo log 也承擔 binlog 的功能?

這是因爲,redo log 是循環寫的,寫完後會從開頭繼續寫,這樣 redo log 就無法記錄一段時間內的完整操作,這樣歷史日誌沒法保留,redo log 也就起不到歸檔的作用。

另一個原因就是就是 MySQL 系統依賴於 binlog。binlog 作爲 MySQL 一開始就有的功能,被用在了很多地方。其中,MySQL 系統高可用的基礎,就是 binlog 複製。還有很多公司有異構系統(比如一些數據分析系統),這些系統就靠消費 MySQL 的 binlog 來更新自己的數據。關掉 binlog 的話,這些下游系統就沒法輸入了。

最後的話

MySQL 的奧妙就在於 redo log 和 binlog 的完美配合,這樣的模式保證了系統可以應對異常重啓,也保證了數據可以恢復到某一天的任意一秒的狀態,當然這是在有完整備份的前提下,其實這樣的設計可以遷移到平時軟件設計上,比如說涉及用戶輸入的系統,在發生異常重啓、掉電的情況下,如何讓用戶的輸入不丟失,系統的配置文件比較複雜被改亂了,如何快速恢復到某一天之前的配置狀態等。

此外 MySQL 是面試必問,找工作的同學建議多準備下,《MySQL實戰45講》我已經全部學完,專欄的質量很高,推薦學習。

如有問題歡迎留言討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章