在MySQL中,簡單的CURD是很容易上手的。sql
可是,理解CURD的背後發生了什麼,倒是一件特別困難的事情。數據庫
在這一篇的內容中,我將簡單介紹一下MySQL的架構是什麼樣的,分別有什麼樣的功能。而後再簡單介紹一下在咱們執行簡單的查詢和更新指令的時候,背後到底發生了什麼。數組
在這一小節中,我會先簡單的介紹一下各個部分的功能。隨後,將在第2、第三節中詳細介紹。緩存
先來看一張圖:安全
簡單的來說一講:bash
鏈接器負責跟客戶端創建鏈接、獲取權限、維持和管理鏈接。架構
在客戶端輸入了帳號密碼以後,若是此時帳號密碼驗證經過,鏈接器將會和客戶端創建一條TCP鏈接。這個鏈接將會在長時間無請求後被鏈接器自動斷開(默認是8小時)。異步
此外,在鏈接創建後,若是管理員修改了這個帳戶的權限,也不會對當前的鏈接有任何的影響,當前鏈接所擁有的權限仍是以前未修改前的權限。ide
分析器有兩個功能:詞法分析、語法分析。性能
對於一個 SQL 語句,分析器首先進行詞法分析,對sql
語句進行拆分,識別出各個字符串表明的含義。
而後就是語法分析,分析器根據定義的語法規則判斷sql
語句是否知足 MySQL 語法。
因此,若是咱們看到You have an error in your SQL syntax
這麼一段話,就能夠知道這個錯誤是由分析器返回的。
這裏的緩存會保存以前的sql
查詢語句和結果。你能夠理解爲這是一個map
:key
是查詢的sql
語句,value
是查詢的結果。
而且,在官方手冊中,有這麼一句話:
Queries must be exactly the same (byte for byte) to be seen as identical.
也就是說,查詢語句必須得和以前徹底一致,每個字節都同樣,大小寫敏感,甚至不能多一個空格。
可是,這裏的緩存是很是容易失效的。爲了保證查詢的冪等性,當某一張表有數據更新後,這個表的緩存也將失效。
因此,對於更新壓力大的數據庫來講,查詢緩存的命中率會很是低。建議只在讀多寫少的數據庫開啓緩存。
可是,在MySQL8.0之後,已經刪除了緩存功能。
查詢優化器的任務是發現執行SQL查詢的最佳方案。大多數查詢優化器,包括MySQL的查詢優化器,總或多或少地在全部可能的查詢評估方案中搜索最佳方案。
簡單來講,優化器就是尋找一個最快可以查詢到數據的策略。
在經過了上述的過程後,Server
層已經解析出了須要處理的數據是什麼,應該怎麼作。
隨後會進行權限的判斷,若是當前的鏈接擁有目標表的權限,則會調用存儲引擎開放的接口,處理須要處理的數據。
到這裏MySQL的基本架構就講完了。可是由於我省略了大部分的細節,只講了這麼一小部分,可能會致使你的疑問增長了。
不過不要緊,咱們接着往下看,用實際的例子來解釋這裏的每一部分,可能會更容易理解。
咱們從這麼一條sql
講起:
select * from T where ID = 1;
複製代碼
首先,會調用分析器,進行詞法分析。
此時,詞法分析發現這條sql
語句是以select
開頭的,而且在這條語句中沒有任何不肯定的數據,因此會去緩存中查找是否保存了這條語句的結果做爲緩存。
可是關於上面的說法,有我我的推測的部分。我沒有在官方文檔中找到MySQL是什麼時候查找緩存的,究竟是在分析器以前仍是分析器以後。
可是在《高性能MySQL》這本書中提到了 「經過檢查sql語句是否以select」 開頭,因此我推測查找緩存是須要先通過簡單的詞法分析的。
只有通過了詞法分析分析,MySQL才能知道這段語句是不是select
語句,也能知道這條語句中有無一些不肯定的數據(如當前時間等)。
此時,若是緩存未命中,則繼續使用分析器進行語法分析。而後,根據這顆語法樹,來判斷這條sql
語句是否符合MySQL語法的。
注意,關於詞法分析和語法分析,若是你感興趣的話,能夠看一看編譯原理相關的內容。
而後來到了優化器。優化器就是在有多種查找方式的時候,自行選擇一個更好的查詢方式。
例如,若是此時sql
語句裏面有多個索引,會選擇一個合適的索引;又或者在關聯查詢的時候,選擇一個更好的方案。
這一部分的內容我想在之後的文章中介紹,這裏我想重點講講下面的內容,關於MySQL中數據的結構。
在咱們利用最後一步的執行器去進行數據的讀取和寫入的時候,實際上是調用了MySQL中的存儲引擎進行數據的讀寫和寫入。
回到咱們的例子,咱們要找的是在表T
中ID
爲1的數據。可是,存儲引擎並不會返回這麼一條具體的數據,他返回的是包含這條數據的數據頁。
這裏我補充一點點知識:
數據庫使用頁管理,和咱們操做系統是同樣的。由於咱們如今的機器是馮諾依曼結構的,這是是一種將程序指令存儲器和數據存儲器合併在一塊兒的存儲器結構。
在這種結構中,具備一個特性,叫局部性原理。
簡單的來解釋就是若是一行數據被讀取了或者一條指令被執行了,那麼很大機率接下來CPU會繼續讀取或執行這個地址或者這個地址後面的數據和指令。
在MySQL中也是同樣的,若是一次性讀取一個頁,那麼可能在接下來的讀寫中所操做的數據也在這個數據頁內,這樣可使得磁盤IO的次數更少。
回到咱們剛剛說的內容,至於引擎是如何找到這個頁的,我想在後面索引相關的文章中再詳細解釋。這裏咱們先簡單的理解爲引擎可以快速的找到這一行數據所在的頁,而後這一頁返回給執行器。
此時,這一頁數據還會被保存在內存中。在以後還須要用到這些數據的時候,將會直接在內存中進行處理,而且MySQL的內存空間中能夠存放不少個這樣的數據頁。也就是說,這個時候不管是查找仍是修改,均可以在內存中進行,而不須要每次都進行磁盤IO。
最後,會在合適的時候將這一頁數據寫回磁盤。至因而在何時如何寫回磁盤的,咱們接着往下看。
在說完了如何查找數據以後,咱們已經知道了一行數據是如何以頁的形式保存在內存中了。咱們如今要解決的問題是:
update
語句是如何執行這是一個頗有意思的問題,咱們來假設兩種情境:
假設MySQL在更新以後只更新內存中的數據就返回,而後再某一時刻進行IO將數據頁持久化。這樣全部操做都是在內存中,能夠想象此時的MySQL性能是特別高的。可是,若是在更新完內存又尚未進行持久化的這段時間,MySQL宕機了,那麼咱們的數據就丟失了。
再來看另一種狀況:每次MySQL將內存中的頁更新好後,馬上進行IO,只有數據落盤後才返回。此時咱們能夠保證數據必定是正確的。可是,每一次的操做,都要進行IO,此時MySQL的效率變得很是低。
因此咱們來看看MySQL是如何作到保證性能的狀況下,還保證數據不丟的。
如今回到這條語句:
update T set a = a + 1 where ID = 0;
複製代碼
假設這條sql語句是正確的,存在名爲ID
,a
的列在表T
中,且存在ID
爲0的數據。
此時通過鏈接器,分析器,分析器發現這是一條update
語句,因而繼續語法分析,優化器,執行器。執行器判斷有權限,而後開表,引擎找到了包含了ID爲0這行數據的數據頁,將這一頁數據保存在內存中。
你能夠發現,update
語句,一樣也走了這麼一遍流程。
而後重點來了,咱們要介紹一下MySQL是如何保證數據一致性的。
這裏要介紹一個很重要的日誌模塊,稱爲rodo log
(重作日誌)。
注意,重作日誌是InnoDB引擎特有的。
重作日誌在更新數據的時候,會記錄在哪一個數據頁更新了什麼數據,而且只要成功的在重作日誌記錄了此次更新,不須要將內存中的數據頁寫回磁盤,就能夠認爲此次更新已經完成了。
MySQL裏有一個名詞,叫WAL技術,WAL的全稱是Write-Ahead-Logging,它的關鍵點就是先寫日誌,再寫磁盤,也就是說只要保證了日誌的落盤,數據就必定正確。此時只要保存了日誌,就算此時MySQL宕機了,沒有將數據頁寫回磁盤,也能夠在以後利用日誌進行恢復。
可是,InnoDB的redo log
是固定大小的,好比能夠配置爲一組4個文件,每一個文件的大小是1GB。固定大小也就形成了一個問題,redo log
是會被寫滿的。
因此,InnoDB採起了循環寫的方式。注意看,這裏有兩個指針。write_pos
表示當前寫的位置,只要有記錄更新了,write_pos
就會日後移動。而check_point
表示檢查點,只要InnoDB將check_point
指向的修改記錄更新到了磁盤中,check_point
將會日後移動。
換句話說,拿咱們剛剛的update T set a = a + 1 where ID = 0;
舉例,若是咱們把這一行數據所在的內存頁更新好了,而且寫入了rodo log
中,此時將返回修改爲功的提示。而後在rodo log
中表現爲記錄了在某一個內存頁的更新記錄。
注意,此時在磁盤中,數據a
未改變,在內存中,a
改成了a+1
,在rodo log
中記錄了這個內存頁的更新記錄,write_pos
日後移動。
此時,若是要把check_point
日後移,那麼他就應該把記錄中這個內存頁的更新持久化到磁盤中,也就是說要把a+1
寫回磁盤,此時不管是磁盤仍是內存,a
的數據都是a+1
。只有成功的寫回了磁盤,check_point
才能夠日後移動。這個設計,使得rodo log
是能夠無限重複使用的。
那麼問題來了,咱們如今只是知道了write_pos
會在數據更新以後日後移動,那麼check_point
會在何時移動呢?
這裏涉及到了innodb_io_capacity
這個參數,這個參數會告訴InnoDB你的磁盤讀寫速度怎麼樣,而後由他來控制check_point
的移動。至於如何調優,我想在之後的文章中來介紹,在本文你就理解爲,他會按照必定的速度,不斷推動。
而後問題又來了,若是此時數據庫有大量的更新操做,而check_point
推動的速度又是恆定的,那麼write_pos
不斷往前推動,就必定會寫滿。這種狀況是InnoDB要儘可能避免的。由於出現這種狀況的時候,整個系統就不能再接受更新了,全部的更新都會被堵住。若是你從監控上看,這時候更新數會跌爲0。至於如何避免這種狀況,我想等到調優的時候再來聊,這裏咱們只是知道會有這麼一種狀況。
除此以外還有一種狀況我想聊一聊,一樣是大量的更新操做。咱們在前面已經提到過了,全部的操做都會在內存中完成,也就是說若是此時我要操做的數據,他們分佈到了不一樣的數據頁中,那麼此時內存中就存儲了很是多的數據頁。這個時候,內存可能不足了。
咱們這裏補充一個概念,乾淨頁和髒頁。乾淨頁指的是從磁盤讀到內存中,沒有被修改過,你能夠理解爲只被查詢而沒有被更新過的數據頁。而髒頁是和磁盤中數據不同的數據頁,他被修改過。若是此時有大量的查詢或更新操做,那麼就須要有大量的內存空間,而此時內存空間已經有各類各樣的數據頁了。那麼咱們應該怎麼辦呢?
而後問題又雙叕來了,若是此時咱們由於內存空間不足而將這個髒頁寫回了磁盤,可是對這個髒頁的更新卻記錄在了redo log
的不一樣位置,那麼在redo log
須要更新這個頁的時候,怎麼辦呢?咱們需不須要在刷新髒頁的時候,在redo log
中也把對應的記錄刪掉或者怎麼樣呢?
這個問題我但願你能思考一下,若是有了這個疑問我想你就理解了上面我說的關於redo log
和髒頁的問題了。答案是在更新髒頁的時候,是不須要修改redo log
的。redo log
在check_point
往前推動的時候,若是發現這個頁已經被刷回磁盤了,將會跳過這條記錄。
說了這麼多重作日誌,咱們再來聊聊歸檔日誌。
有幾個緣由,redo log
是循環使用的,也就是說新數據必定會覆蓋舊數據,咱們沒辦法拿他來恢復太長時間的記錄。
第二個緣由是由於redo log
是InnoDB引擎特有的,在別的引擎中,就沒有重作日誌了。
因此在這裏咱們聊聊引擎層必有的歸檔日誌binlog
。
歸檔日誌是追加寫的,在一個文件寫滿後就會切換到下一個文件繼續寫,會記錄每一條語句更改了什麼內容。
也就是說,在進行故障恢復的時候,可使用binlog
一條一條的恢復記錄。
那咱們要怎麼保證binlog
必定能保證數據一致性呢,咱們來聊聊MySQL中的兩階段提交。
仍是以update T set a = a + 1 where ID = 0;
爲例:
解釋一下:一直到更新內存中的數據頁,在上面都已經提到過了。而後是將數據頁的更新寫入redo log
中。
注意,這裏寫的redo log
,並非寫入了redo log
的文件中,而是寫入了名爲redo log
的buffer中,也就是說此時並無使用磁盤IO,不會形成性能的下降。
而後,進入了名爲prepare
的階段。
而後,寫入bin log
。注意,這裏說的寫入bin log
,也一樣沒有持久化,也是寫入了buffer中。
只有當這二者都寫入成功了,纔會到提交事務的階段。
而後,有兩個參數很重要。
這兩個參數決定了是否等待直到將redo log
和bin log
持久化以後再返回。
sync_binlog
和innodb_flush_log_at_trx_commit
。
先說說innodb_flush_log_at_trx_commit
:
也就是說,若是咱們設置爲了1,在最後提交的時候,會調用fsync
等待redo log
持久化,才返回。
再說說sync_binlog
:
fsync
。那麼,咱們爲何須要兩階段提交來保證數據的一致性呢?
咱們假設如今寫完了redo log
,進入了prepare階段,可是尚未寫bin log
,此時數據庫宕機,那麼重啓後事務會回滾,不影響數據。
再作一個假設,咱們已經寫完了bin log
,宕機了,再重啓後MySQL會判斷redo log
是否已經有了commit
標識,若是有,則提交;不然的話,去判斷bin log
是否完整,若是是完整的,則提交,不然回滾。
那麼,若是咱們沒有將階段提交,會怎麼樣呢?
假設咱們先提交redo log
,再提交bin log
,此時邏輯和兩階段提交同樣,可是沒有了兩次驗證。那麼若是咱們在redo log
提交完了宕機了,那麼咱們重啓後,能夠根據redo log
來恢復數據。可是由於咱們在bin log
中沒有更新,因此在將來若是使用bin log
進行恢復,或者同步從庫的時候,將會致使數據不一致。(主從同步問題在之後的文章解釋)
再作一個假設,先提交bin log
,再提交redo log
。那麼在恢復的時候這個數據是沒有被更新的,可是在將來使用bin log
的時候,會發現這裏的數據不一致。
因此說,兩階段提交是爲了保證這兩個日誌是能夠一致的。
首先,謝謝你能看到這裏。
但願這篇文章可以給你帶來幫助,讓你對MySQL的瞭解能夠加深一些。固然了,文章篇幅有限,做者水平也有限,文章中不少地方的細節沒有展開講。不少知識點會在從此的文章中不斷進行補充。另外,若是你發現了做者不對的地方,還請不吝指正,謝謝你!
其次,要特別感謝雄哥,給了我不少的幫助!另外,也特別感謝丁奇老師,我是以《MySQL實戰45講》做爲主線進行學習的。
PS:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~