今天簡單聊聊 MySQL 的基礎架構。我們經常說,看一個事兒千萬不要直接陷入細節里,你應該先看看總體,這樣能夠幫助你從高緯度理解問題。同樣,對于 MySQL 的學習也是這樣。平時我們使用數據庫,看到的通常都是一個整體。比如,你有個最簡單的表,表里只有一個 ID 字段,在執行下面這個查詢語句時:
SELECT * FROM A WHERE ID = '404';
我們看到的只是輸入一條語句,返回一個結果,卻不知道這條語句在 MySQL 內部的執行過程。
所以今天我想和你一起把 MySQL 拆解一下,看看里面都有哪些“零件”,希望借由這個拆解過程,讓你對 MySQL 有更深入的理解。這樣當我們碰到 MySQL 的一些異常或者問題時,就能夠直戳本質,更為快速地定位并解決問題。
下面我給出的是 MySQL 的基本架構示意圖,從中你可以清楚地看到 SQL 語句在 MySQL 的各個功能模塊中的執行過程。
大體來說,MySQL 可以分為 Server 層和存儲引擎層兩部分。
Server層包括連接器、查詢緩存、分析器、優化器、執行器等,涵蓋 MySQL的大多數核心服務
能,以及所有的內置函數(如日期、時間、數學和加密函數等),所有跨存儲引擎的功能都在
這一層實現,比如存儲過程、觸發器、視圖等。
而存儲引擎層負責數據的存儲和提取。其架構模式是插件式的,支持 InnoDB、 MyISAM、
Memory等多個存儲引擎。現在最常用的存儲引擎是IoDB,它從 MySQL5.5.5版本開始成
為了默認存儲引擎。
也就是說,你執行 create table建表的時候,如果不指定引孳類型,默認使用的就是 InnoDB。
不過,你也可以通過指定存儲引擎的類型來選擇別的引擎,比如在 create table語句中使用
engine= memory,來指定使用內存引擎創建表。不同存儲引擎的表數據存取方式不同,支持的
功能也不同,在后面的文章中,我們會討論到引擎的選擇
從圖中不難看出,不同的存儲引擎共用—個 Server層,也就是從連接器到執行器的部分。你可以
先對每個組件的名字有個印象,接下來我會結合開頭提到的那條SQL語句,帶你走一遍整個執行
流程,依次看下每個組件的作用
連接器第一步,你會先連接到這個數據庫上,這時候接待你的就是連接、獲取權限、維持和管理連接。命令一般是這么寫的:
mysql -h$ip -P$port -u$user -p
輸完命令之后,你就需要在交互對話里面輸入密碼。雖然密碼也可以直接跟在 -p 后面寫在命令行中,但這樣可能會導致你的密碼泄露。如果你連的是生產服務器,強烈建議你不要這么做。
連接命令中的 mysql 是客戶端工具,用來跟服務端建立連接。在完成經典的 TCP 握手后,連接器就要開始認證你的身份,這個時候用的就是你輸入的用戶名和密碼。
- 如果用戶名或密碼不對,你就會收到一個"Access denied for user"的錯誤,然后客戶端程序結束執行。
- 如果用戶名密碼認證通過,連接器會到權限表里面查出你擁有的權限。之后,這個連接里面的權限判斷邏輯,都將依賴于此時讀到的權限。
這就意味著,一個用戶成功建立連接后,即使你用管理員賬號對這個用戶的權限做了修改,也不會影響已經存在連接的權限。修改完成后,只有再新建的連接才會使用新的權限設置。
連接完成后,如果你沒有后續的動作,這個連接就處于空閑狀態,你可以在 show processlist 命令中看到它。文本中這個圖是 show processlist 的結果,其中的 Command 列顯示為“Sleep”的這一行,就表示現在系統里面有一個空閑連接。
客戶端如果太長時間沒動靜,連接器就會自動將它斷開。這個時間是由參數 wait_timeout 控制的,默認值是 8 小時。
如果在連接被斷開之后,客戶端再次發送請求的話,就會收到一個錯誤提醒: Lost connection to MySQL server during query。這時候如果你要繼續,就需要重連,然后再執行請求了。
數據庫里面,長連接是指連接成功后,如果客戶端持續有請求,則一直使用同一個連接。短連接則是指每次執行完很少的幾次查詢就斷開連接,下次查詢再重新建立一個。
建立連接的過程通常是比較復雜的,所以我建議你在使用中要盡量減少建立連接的動作,也就是盡量使用長連接。
但是全部使用長連接后,你可能會發現,有些時候 MySQL 占用內存漲得特別快,這是因為 MySQL 在執行過程中臨時使用的內存是管理在連接對象里面的。這些資源會在連接斷開的時候才釋放。所以如果長連接累積下來,可能導致內存占用太大,被系統強行殺掉(OOM),從現象看就是 MySQL 異常重啟了。
怎么解決這個問題呢?你可以考慮以下兩種方案。
定期斷開長連接。使用一段時間,或者程序里面判斷執行過一個占用內存的大查詢后,斷開連接,之后要查詢再重連。
如果你用的是 MySQL 5.7 或更新版本,可以在每次執行一個比較大的操作后,通過執行 mysql_reset_connection 來重新初始化連接資源。這個過程不需要重連和重新做權限驗證,但是會將連接恢復到剛剛創建完時的狀態。
查詢緩存
連接建立完成后,你就可以執行 select 語句了。執行邏輯就會來到第二步:查詢緩存。
MySQL 拿到一個查詢請求后,會先到查詢緩存看看,之前是不是執行過這條語句。之前執行過的語句及其結果可能會以 key-value 對的形式,被直接緩存在內存中。key 是查詢的語句,value 是查詢的結果。如果你的查詢能夠直接在這個緩存中找到 key,那么這個 value 就會被直接返回給客戶端。
如果語句不在查詢緩存中,就會繼續后面的執行階段。執行完成后,執行結果會被存入查詢緩存中。你可以看到,如果查詢命中緩存,MySQL 不需要執行后面的復雜操作,就可以直接返回結果,這個效率會很高。
但是大多數情況下我會建議你不要使用查詢緩存,為什么呢?因為查詢緩存往往弊大于利。
查詢緩存的失效非常頻繁,只要有對一個表的更新,這個表上所有的查詢緩存都會被清空。因此很可能你費勁地把結果存起來,還沒使用呢,就被一個更新全清空了。對于更新壓力大的數據庫來說,查詢緩存的命中率會非常低。除非你的業務就是有一張靜態表,很長時間才會更新一次。比如,一個系統配置表,那這張表上的查詢才適合使用查詢緩存。
好在 MySQL 也提供了這種“按需使用”的方式。你可以將參數 query_cache_type 設置成 DEMAND,這樣對于默認的 SQL 語句都不使用查詢緩存。而對于你確定要使用查詢緩存的語句,可以用 SQL_CACHE 顯式指定,像下面這個語句一樣:
mysql> select SQL_CACHE * from T where ID = 10需要注意的是,MySQL 8.0 版本直接將查詢緩存的整塊功能了。
分析器
如果沒有命中查詢緩存,就要開始真正執行語句了。首先,MySQL 需要知道你要做什么,因此需要對 SQL 語句做解析。
分析器先會做“詞法分析”。你輸入的是由多個字符串和空格組成的一條 SQL 語句,MySQL 需要識別出里面的字符串分別是什么,代表什么。
MySQL 從你輸入的"select"這個關鍵字識別出來,這是一個查詢語句。它也要把字符串“T”識別成“表名 T”,把字符串“ID”識別成“列 ID”。
做完了這些識別以后,就要做“語法分析”。根據詞法分析的結果,語法分析器會根據語法規則,判斷你輸入的這個 SQL 語句是否滿足 MySQL 語法。
如果你的語句不對,就會收到“You have an error in your SQL syntax”的錯誤提醒,比如下面這個語句 select 少打了開頭的字母“s”。
mysql> elect * from t where ID=1;ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1
一般語法錯誤會提示第一個出現錯誤的位置,所以你要關注的是緊接“use near”的內容。
優化器
經過了分析器,MySQL 就知道你要做什么了。在開始執行之前,還要先經過優化器的處理。
優化器是在表里面有多個索引的時候,決定使用哪個索引;或者在一個語句有多表關聯(join)的時候,決定各個表的連接順序。比如你執行下面這樣的語句,這個語句是執行兩個表的 join:
mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;這兩種執行方法的邏輯結果是一樣的,但是執行的效率會有不同,而優化器的作用就是決定選擇使用哪一個方案。
優化器階段完成后,這個語句的執行方案就確定下來了,然后進入執行器階段。如果你還有一些疑問,比如優化器是怎么選擇索引的,有沒有可能選擇錯等等,沒關系,我會在后面的文章中單獨展開說明優化器的內容。
執行器
MySQL 通過分析器知道了你要做什么,通過優化器知道了該怎么做,于是就進入了執行器階段,開始執行語句。
開始執行的時候,要先判斷一下你對這個表 T 有沒有執行查詢的權限,如果沒有,就會返回沒有權限的錯誤,如下所示 (在工程實現上,如果命中查詢緩存,會在查詢緩存返回結果的時候,做權限驗證。查詢也會在優化器之前調用 precheck 驗證權限)。
mysql> select * from T where ID=10;ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'
如果有權限,就打開表繼續執行。打開表的時候,執行器就會根據表的引擎定義,去使用這個引擎提供的接口。
比如我們這個例子中的表 T 中,ID 字段沒有索引,那么執行器的執行流程是這樣的:
調用 InnoDB 引擎接口取這個表的第一行,判斷 ID 值是不是 10,如果不是則跳過,如果是則將這行存在結果集中;
調用引擎接口取“下一行”,重復相同的判斷邏輯,直到取到這個表的最后一行。
執行器將上述遍歷過程中所有滿足條件的行組成的記錄集作為結果集返回給客戶端。
至此,這個語句就執行完成了。
對于有索引的表,執行的邏輯也差不多。第一次調用的是“取滿足條件的第一行”這個接口,之后循環取“滿足條件的下一行”這個接口,這些接口都是引擎中已經定義好的。
你會在數據庫的慢查詢日志中看到一個 rows_examined 的字段,表示這個語句執行過程中掃描了多少行。這個值就是在執行器每次調用引擎獲取數據行的時候累加的。
在有些場景下,執行器調用一次,在引擎內部則掃描了多行,因此引擎掃描行數跟 rows_examined 并不是完全相同的。我們后面會專門有一篇文章來講存儲引擎的內部機制,里面會有詳細的說明。
小結
今天我給你介紹了 MySQL 的邏輯架構,希望你對一個 SQL 語句完整執行流程的各個階段有了一個初步的印象。由于篇幅的限制,我只是用一個查詢的例子將各個環節過了一遍。如果你還對每個環節的展開細節存有疑問,也不用擔心,后續在實戰章節中我還會再提到它們。
我給你留一個問題吧,如果表 T 中沒有字段 k,而你執行了這個語句 select * from T where k=1, 那肯定是會報“不存在這個列”的錯誤: “Unknown column ‘k’ in ‘where clause’”。你覺得這個錯誤是在我們上面提到的哪個階段報出來的呢?
感謝你的收聽,歡迎你給我留言,也歡迎分享給更多的朋友一起閱讀。