1. 程式人生 > 實用技巧 >MySQL架構與執行流程

MySQL架構與執行流程

前言:

  MySQL資料庫自己用了也有兩三年了,基本上只是掌握增刪改查的sql語句,從沒有思考過MySQL的內部到底是怎麼根據sql查詢資料的,包括索引的原理,只知道加了索引查的就快,不知道為什麼加上索引效率就會提升,包括索引的限制和優化也知之甚少,所以決定開一專題來學習與記錄MySQL。

MySQL語句的執行流程

  下圖是一條查詢sql語句的執行流程:

1.1 通訊協議

  我們的程式或者工具要操作資料庫,第一步要做什麼事情?當然是跟資料庫建立連線。首先,MySQL 必須要執行一個服務,監聽預設的 3306 埠。在我們開發系統跟第三方對接的時候,必須要弄清楚的有兩件事。第一個就是通訊協議,比如我們是用 HTTP 還是 WebService 還是 TCP?第二個是訊息格式,比如我們用 XML 格式,還是 JSON 格式,還是定長格式?報文頭長度多少,包含什麼內容,每個欄位的詳細含義。

1.1.1 簡單理解MySQL中的通訊協議

  MySQL 是支援多種通訊協議的,可以使用同步/非同步的方式,支援長連線/短連線。這裡我們拆分來看。第一個是通訊型別。

通訊型別:

同步或者非同步

  1、同步通訊依賴於被呼叫方,受限於被呼叫方的效能。也就是說,應用操作資料庫,執行緒會阻塞,等待資料庫的返回。   2、一般只能做到一對一,很難做到一對多的通訊。

非同步跟同步相反:

  2、如果非同步存在併發,每一個 SQL 的執行都要單獨建立一個連線,避免資料混亂。但是這樣會給服務端帶來巨大的壓力(一個連線就會建立一個執行緒,執行緒間切換會佔用大量 CPU 資源)。另外非同步通訊還帶來了編碼的複雜度,所以一般不建議使用。如果要非同步,必須使用連線池,排隊從連線池獲取連線而不是建立新連線。一般來說我們連線資料庫都是同步連線。

連線方式:長連線或者短連線

  MySQL 既支援短連線,也支援長連線。短連線就是操作完畢以後,馬上 close 掉。長連線可以保持開啟,減少服務端建立和釋放連線的消耗,後面的程式訪問的時候還可 以使用這個連線。一般我們會在連線池中使用長連線。保持長連線會消耗記憶體。長時間不活動的連線,MySQL 伺服器會斷開。
show global variables like 'wait_timeout'; -- 非互動式超時時間,如 JDBC 程式 
show global variables like 'interactive_timeout'; -- 互動式超時時間,如資料庫工具
預設都是 28800 秒,8 小時。   我們可以用 show status 命令檢視 MySQL 當前有多少個連線。
show global status like
'Thread%'; Threads_cached:快取中的執行緒連線數。 Threads_connected:當前開啟的連線數。 Threads_created:為處理連線建立的執行緒數。 Threads_running:非睡眠狀態的連線數,通常指併發連線數。
  每產生一個連線或者一個會話,在服務端就會建立一個執行緒來處理。反過來,如果要殺死會話,就是 Kill 執行緒。可以使用 SHOW PROCESSLIST; (root 使用者)檢視 SQL 的執行狀態。一些常見的狀態:
  MySQL 服務允許的最大連線數是多少呢?在 5.7 版本中預設是 151 個,最大可以設定成 16384(2^14)。
show variables like 'max_connections';
show 的引數說明: 1、級別:會話 session 級別(預設);全域性 global 級別 2、動態修改:set,重啟後失效;永久生效,修改配置檔案/etc/my.cnf
set global max_connections = 1000;

使用TCP/IP 協議連線MySQl:

mysql -h192.168.8.211 -uroot -p123456
我們的程式語言的連線模組都是用 TCP 協議連線到 MySQL 伺服器的,比如mysql-connector-java-x.x.xx.jar。

MySQl的通訊方式:

  • 單工

    在兩臺計算機通訊的時候,資料的傳輸是單向的。生活中的類比:遙控器。
  • 半雙工

    在兩臺計算機之間,資料傳輸是雙向的,你可以給我傳送,我也可以給你傳送,但是在這個通訊連線裡面,同一時間只能有一臺伺服器在傳送資料,也就是你要給我發   的話,也必須等我發給你完了之後才能給我發。生活中的類比:對講機。
  • 全雙工

    資料的傳輸是雙向的,並且可以同時傳輸。生活中的類比:打電話。      MySQL 使用了半雙工的通訊方式,要麼是客戶端向服務端傳送資料,要麼是服務端向客戶端傳送資料,這兩個動作不能同時發生。所以客戶端傳送 SQL 語句給服務端的時候,(在一次連線裡面)資料是不能分成小塊傳送的,不管你的 SQL 語句有多大,都是一次性發送。比如我們用 MyBatis 動態 SQL 生成了一個批量插入的語句,插入 10 萬條資料,values後面跟了一長串的內容,或者 where 條件 in 裡面的值太多,會出現問題。這個時候我們必須要調整 MySQL 伺服器配置 max_allowed_packet 引數的值(預設是 4M),把它調大,否則就會報錯。

  另一方面,對於服務端來說,也是一次性發送所有的資料,不能因為你已經取到了想要的資料就中斷操作,這個時候會對網路和記憶體產生大量消耗。所以,我們一定要在程式裡面避免不帶 limit 的這種操作,比如一次把所有滿足條件的資料全部查出來,一定要先 count 一下。如果資料量的話,可以分批查詢。

1.2 查詢快取

  MySQL 內部自帶了一個快取模組。快取的作用我們應該很清楚了,把資料以 KV 的形式放到記憶體裡面,可以加快資料的讀取速度,也可以減少伺服器處理的時間。在 MySQL 8.0 中,查詢快取已經被移除了。主要是因為 MySQL 自帶的快取的應用場景有限,第一個是它要求 SQL 語句必須一模一樣,中間多一個空格,字母大小寫不同都被認為是不同的的 SQL。第二個是表裡面任何一條資料發生變化的時候,這張表所有快取都會失效,所以對於有大量資料更新的應用,也不適合。

1.3語法解析和預處理(Parser & Preprocessor)

  一直很好奇為什麼我的一條 SQL 語句能夠被識別呢?假如我隨便執行一個字串 penyuyan,伺服器報了一個 1064 的錯,它是怎麼知道我輸入的內容是錯誤的?這個就是 MySQL 的 Parser 解析器和 Preprocessor 預處理模組。這一步主要做的事情是對語句基於 SQL 語法進行詞法和語法分析和語義的解析。

  1. 詞法解析

    詞法解析就是把一個完整的 SQL 語句打碎成一個個的單詞。比如一個簡單的 SQL 語句:
select name from user where id = 1
    它會打碎成 8 個符號,每個符號是什麼型別,從哪裡開始到哪裡結束。

  2. 語法解析

    第二步就是語法分析,語法分析會對 SQL 做一些語法檢查,比如單引號有沒有閉合,然後根據 MySQL 定義的語法規則,根據 SQL 語句生成一個數據結構。這個資料   結構我們把它叫做解析樹(select_lex)。

  1.3 前處理器

    問題:如果我寫了一個詞法和語法都正確的 SQL,但是表名或者欄位不存在,會在哪裡報錯?是在資料庫的執行層還是解析器?比如:
select * from penyuyan;
    解析器可以分析語法,但是它怎麼知道資料庫裡面有什麼表,表裡面有什麼欄位呢?實際上還是在解析的時候報錯,解析 SQL 的環節裡面有個前處理器。它會檢查生   成的解析樹,解決解析器無法解析的語義。比如,它會檢查表和列名是否存在,檢查名字和別名,保證沒有歧義。預處理之後得到一個新的解析樹。

  1.4 優化器與執行計劃

    得到解析樹之後,是不是執行 SQL 語句了呢?這裡我們有一個問題,一條 SQL 語句是不是隻有一種執行方式?或者說資料庫最終執行的 SQL 是不是就是我們傳送的   SQL?這個答案是否定的。一條 SQL 語句是可以有很多種執行方式的,最終返回相同的結果,他們是等價的。但是如果有這麼多種執行方式,這些執行方式怎麼得到的?   最終選擇哪一種去執行?根據什麼判斷標準去選擇?這個就是 MySQL 的查詢優化器的模組(Optimizer)。查詢優化器的目的就是根據解析樹生成不同的執行計劃   (Execution Plan),然後選擇一種最優的執行計劃,MySQL 裡面使用的是基於開銷(cost)的優化器,那種執行計劃開銷最小,就用哪種。   可以使用這個命令檢視查詢的開銷:
show status like 'Last_query_cost';
  執行計劃:
  優化器最終會把解析樹變成一個查詢執行計劃,查詢執行計劃是一個數據結構。當然,這個執行計劃是不是一定是最優的執行計劃呢?不一定,因為 MySQL 也有可 能覆蓋不到所有的執行計劃。我們怎麼檢視 MySQL 的執行計劃呢?比如多張表關聯查詢,先查詢哪張表?在執行查詢的時候可能用到哪些索引,實際上用到了什麼索引? MySQL 提供了一個執行計劃的工具。我們在 SQL 語句前面加上 EXPLAIN,就可以看到執行計劃的資訊。
EXPLAIN select name from user where id=1;

  1.5 儲存引擎

    得到執行計劃以後,SQL 語句是不是終於可以執行了?問題又來了:   1、從邏輯的角度來說,我們的資料是放在哪裡的,或者說放在一個什麼結構裡面?   2、執行計劃在哪裡執行?是誰去執行?
  • 儲存引擎基本介紹
    我們先回答第一個問題:在關係型資料庫裡面,資料是放在什麼結構裡面的?(放在表 Table 裡面的)我們可以把這個表理解成 Excel 電子表格的形式。所以我們的表   在儲存資料的同時,還要組織資料的儲存結構,這個儲存結構就是由我們的儲存引擎決定的,所以我們也可以把儲存引擎叫做表型別。     在 MySQL 裡面,我們建立的每一張表都可以指定它的儲存引擎,而不是一個數據庫只能使用一個儲存引擎。儲存引擎的使用是以表為單位的。而且,建立表之後還可   以修改儲存引擎。我們說一張表使用的儲存引擎決定我們儲存資料的結構,那在伺服器上它們是怎麼儲存的呢?我們先要找到資料庫存放資料的路徑:
show variables like 'datadir';

    預設情況下,每個資料庫有一個自己資料夾,任何一個儲存引擎都有一個 frm 檔案,這個是表結構定義檔案。

    不同的儲存引擎存放資料的方式不一樣,產生的檔案也不一樣,innodb 是 1 個,memory 沒有,myisam 是兩個。 主要介紹一下InnoDB:

    mysql 5.7 中的預設儲存引擎。InnoDB 是一個事務安全(與 ACID 相容)的 MySQL儲存引擎,它具有提交、回滾和崩潰恢復功能來保護使用者資料。InnoDB 行級鎖和   Oracle 風格的一致非鎖讀提高了多使用者併發性和效能。InnoDB 將使用者資料儲存在聚集索引中,以減少基於主鍵的常見查詢的 I/O。為了保持資料   完整性,InnoDB 還支援外來鍵引用完整性約束。

  特點:

  支援事務,支援外來鍵,因此資料的完整性、一致性更高。   支援行級別的鎖和表級別的鎖。   支援讀寫併發,寫不阻塞讀(MVCC)。   特殊的索引存放方式,可以減少 IO,提升查詢效率。   適合:經常更新的表,存在併發讀寫或者有事務處理的業務系統。

  1.6 執行引擎,返回結果

    執行引擎利用儲存引擎提供的相應的 API 來完成操作。為什麼我們修改了表的儲存引擎,操作方式不需要做任何改變?因為不同功能的儲存引擎實現的 API 是相同的。

  最後把資料返回給客戶端,即使沒有結果也要返回。

MySQL體系結構總結:

  基於上面分析的流程,我們一起來梳理一下 MySQL 的內部模組。

2.1 模組詳解

1、 Connector:用來支援各種語言和 SQL 的互動,比如 PHP,Python,Java 的JDBC; 2、 Management Serveices & Utilities:系統管理和控制工具,包括備份恢復、MySQL 複製、叢集等等; 3、 Connection Pool:連線池,管理需要緩衝的資源,包括使用者密碼許可權執行緒等等; 4、 SQL Interface:用來接收使用者的 SQL 命令,返回使用者需要的查詢結果; 5、 Parser:用來解析 SQL 語句; 6、 Optimizer:查詢優化器; 7、 Cache and Buffer:查詢快取,除了行記錄的快取之外,還有表快取,Key 快取,許可權快取等等; 8、 Pluggable Storage Engines:外掛式儲存引擎,它提供 API 給服務層使用,跟具體的檔案打交道。

2.2架構分層

  總體上,我們可以把 MySQL 分成三層,跟客戶端對接的連線層,真正執行操作的服務層,和跟硬體打交道的儲存引擎層(參考 MyBatis:介面、核心、基礎)。


2.1.1.連線層

  我們的客戶端要連線到 MySQL 伺服器 3306 埠,必須要跟服務端建立連線,那麼管理所有的連線,驗證客戶端的身份和許可權,這些功能就在連線層完成。

2.1.2.服務層

  連線層會把 SQL 語句交給服務層,這裡面又包含一系列的流程:比如查詢快取的判斷、根據 SQL 呼叫相應的介面,對我們的 SQL 語句進行詞法和語法的解析(比如關鍵字怎麼識別,別名怎麼識別,語法有沒有錯誤等等)。然後就是優化器,MySQL 底層會根據一定的規則對我們的 SQL 語句進行優化,最後再交給執行器去執行。

2.1.3.儲存引擎

  儲存引擎就是我們的資料真正存放的地方,在 MySQL 裡面支援不同的儲存引擎。再往下就是記憶體或者磁碟。

更新語句的執行流程:

  講完了查詢流程,我們是不是再講講更新流程、插入流程和刪除流程?更新流程和查詢流程有什麼不同呢?基本流程也是一致的,也就是說,它也要經過解析器、優化器的處理,最後交給執行器。區別就在於拿到符合條件的資料之後的操作。

3.1. 緩衝池 Buffer Pool

  首先,InnnoDB 的資料都是放在磁碟上的,InnoDB 操作資料有一個最小的邏輯單位,叫做頁(索引頁和資料頁)。我們對於資料的操作,不是每次都直接操作磁碟,因 為磁碟的速度太慢了。InnoDB 使用了一種緩衝池的技術,也就是把磁碟讀到的頁放到一塊記憶體區域裡面。這個記憶體區域就叫 Buffer Pool。

  下一次讀取相同的頁,先判斷是不是在緩衝池裡面,如果是,就直接讀取,不用再次訪問磁碟。

  修改資料的時候,先修改緩衝池裡面的頁。記憶體的資料頁和磁碟資料不一致的時候,我們把它叫做髒頁。InnoDB 裡面有專門的後臺執行緒把 Buffer Pool 的資料寫入到磁碟, 每隔一段時間就一次性地把多個修改寫入磁碟,這個動作就叫做刷髒。   Buffer Pool 是 InnoDB 裡面非常重要的一個結構,它的內部又分成幾塊區域。這裡我們趁機到官網來認識一下 InnoDB 的記憶體結構和磁碟結構。

3.2. InnoDB 記憶體結構和磁碟結構

3.3.1.記憶體結構

  Buffer Pool 主要分為 3 個部分: Buffer Pool、Change Buffer、Adaptive HashIndex,另外還有一個(redo)log buffer。

1、Buffer Pool   Buffer Pool 快取的是頁面資訊,包括資料頁、索引頁。 2、Change Buffer 寫緩衝   如果這個資料頁不是唯一索引,不存在資料重複的情況,也就不需要從磁碟載入索引頁判斷資料是不是重複(唯一性檢查)。這種情況下可以先把修改記錄在記憶體的緩衝 池中,從而提升更新語句(Insert、Delete、Update)的執行速度。這一塊區域就是 Change Buffer。5.5 之前叫 Insert Buffer 插入緩衝,現在也能支援 delete 和 update。 最後把 Change Buffer 記錄到資料頁的操作叫做 merge。什麼時候發生 merge?有幾種情況:在訪問這個資料頁的時候,或者通過後臺執行緒、或者資料庫 shut down、redo log 寫滿時觸發。如果資料庫大部分索引都是非唯一索引,並且業務是寫多讀少,不會在寫資料後立刻讀取,就可以使用 Change Buffer(寫緩衝)。寫多讀少的業務,調大這個值:
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';
3、Adaptive Hash Index   雜湊的索引的記憶體塊。 4、(redo)Log Buffer   思考一個問題:如果 Buffer Pool 裡面的髒頁還沒有刷入磁碟時,資料庫宕機或者重啟,這些資料丟失。如果寫操作寫到一半,甚至可能會破壞資料檔案導致資料庫不可用。為了避免這個問題,InnoDB 把所有對頁面的修改操作專門寫入一個日誌檔案,並且在資料庫啟動時從這個檔案進行恢復操作(實現 crash-safe)——用它來實現事務的持 久性。

3.3.2.磁碟結構

  表空間可以看做是 InnoDB 儲存引擎邏輯結構的最高層,所有的資料都存放在表空間中。InnoDB 的表空間分為 5 大類。

系統表空間 system tablespace

  在預設情況下 InnoDB 儲存引擎有一個共享表空間(對應檔案/var/lib/mysql/ibdata1),也叫系統表空間。InnoDB 系統表空間包含 InnoDB 資料字典和雙寫緩衝區,Change Buffer 和 Undo Logs),如果沒有指定 file-per-table,也包含使用者建立的表和索引資料。 1、undo 在後面介紹,因為有獨立的表空間。 2、資料字典:由內部系統表組成,儲存表和索引的元資料(定義資訊)。 3、雙寫緩衝(InnoDB 的一大特性):   InnoDB 的頁和作業系統的頁大小不一致,InnoDB 頁大小一般為 16K,作業系統頁大小為 4K,InnoDB 的頁寫入到磁碟時,一個頁需要分 4 次寫。

  如果儲存引擎正在寫入頁的資料到磁碟時發生了宕機,可能出現頁只寫了一部分的情況,比如只寫了 4K,就宕機了,這種情況叫做部分寫失效(partial page write),可

能會導致資料丟失。   我們不是有 redo log 嗎?但是有個問題,如果這個頁本身已經損壞了,用它來做崩潰恢復是沒有意義的。所以在對於應用 redo log 之前,需要一個頁的副本。如果出現了 寫入失效,就用頁的副本來還原這個頁,然後再應用 redo log。這個頁的副本就是 doublewrite,InnoDB 的雙寫技術。通過它實現了資料頁的可靠性。跟 redo log 一樣,double write 由兩部分組成,一部分是記憶體的 double write,一個部分是磁碟上的 double write。因為 double write 是順序寫入的,不會帶來很大的開銷。在預設情況下,所有的表共享一個系統表空間,這個檔案會越來越大,而且它的空間不會收縮。

獨佔表空間 file-per-table tablespaces

  我們可以讓每張表獨佔一個表空間。這個開關通過 innodb_file_per_table 設定,預設開啟。
SHOW VARIABLES LIKE 'innodb_file_per_table';
開啟後,則每張表會開闢一個表空間,這個檔案就是資料目錄下的 ibd 檔案(例如/var/lib/mysql/gupao/user_innodb.ibd),存放表的索引和資料。但是其他類的資料,如回滾(undo)資訊,插入緩衝索引頁、系統事務資訊,二次寫緩衝(Double write buffer)等還是存放在原來的共享表空間內。

通用表空間 general tablespaces

  通用表空間也是一種共享的表空間,跟 ibdata1 類似。可以建立一個通用的表空間,用來儲存不同資料庫的表,資料路徑和檔案可以自定義。語法:
create tablespace ts2673 add datafile '/var/lib/mysql/ts2673.ibd' file_block_size=16K engine=innodb;
在建立表的時候可以指定表空間,用 ALTER 修改表空間可以轉移表空間。
create table t2673(id integer) tablespace ts2673;
  不同表空間的資料是可以移動的。刪除表空間需要先刪除裡面的所有表:
drop table t2673; 

drop tablespace ts2673;

臨時表空間 temporary tablespaces

  儲存臨時表的資料,包括使用者建立的臨時表,和磁碟的內部臨時表。對應資料目錄下的 ibtmp1 檔案。當資料伺服器正常關閉時,該表空間被刪除,下次重新產生。

Redo log

  磁碟結構裡面的 redo log,在前面已經介紹過了。

undo log tablespace

  undo log(撤銷日誌或回滾日誌)記錄了事務發生之前的資料狀態(不包括 select)。如果修改資料時出現異常,可以用 undo log 來實現回滾操作(保持原子性)。 在執行 undo 的時候,僅僅是將資料從邏輯上恢復至事務之前的狀態,而不是從物理頁面上操作實現的,屬於邏輯格式的日誌。redo Log 和 undo Log 與事務密切相關,統稱為事務日誌。undo Log 的資料預設在系統表空間 ibdata1 檔案中,因為共享表空間不會自動收縮,也可以單獨建立一個 undo 表空間。   有了這些日誌之後,我們來總結一下一個更新操作的流程,這是一個簡化的過程。name 原值是 qingshan。
update user set name = 'penyuyan' where id=1;
  1、事務開始,從記憶體或磁碟取到這條資料,返回給 Server 的執行器;   2、執行器修改這一行資料的值為 penyuyan;   3、記錄 name=qingshan 到 undo log;   4、記錄 name=penyuyan 到 redo log;   5、呼叫儲存引擎介面,在記憶體(Buffer Pool)中修改 name=penyuyan;   6、事務提交。   記憶體和磁碟之間,工作著很多後臺執行緒。

3.3.3Binlog

  binlog 以事件的形式記錄了所有的 DDL 和 DML 語句(因為它記錄的是操作而不是資料值,屬於邏輯日誌),可以用來做主從複製和資料恢復。跟 redo log 不一樣,它的檔案內容是可以追加的,沒有固定大小限制。在開啟了 binlog 功能的情況下,我們可以把 binlog 匯出成 SQL 語句,把所有的操作重放一遍,來實現資料的恢復。binlog 的另一個功能就是用來實現主從複製,它的原理就是從伺服器讀取主伺服器的 binlog,然後執行一遍。

  更新語句的整體流程圖:

  例如一條語句:update teacher set name='盆魚宴' where id=1;

  1、先查詢到這條資料,如果有快取,也會用到快取。   2、把 name 改成盆魚宴,然後呼叫引擎的 API 介面,寫入這一行資料到記憶體,同時記錄 redo log。這時 redo log 進入 prepare 狀態,然後告訴執行器,執行完成了,可     以隨時提交。   3、執行器收到通知後記錄 binlog,然後呼叫儲存引擎介面,設定 redo log為 commit狀態。   4、更新完成。