MySQL 入門(1):查詢和更新的內部實現

摘要

在MySQL中,簡單的CURD是很容易上手的。sql

可是,理解CURD的背後發生了什麼,倒是一件特別困難的事情。數據庫

在這一篇的內容中,我將簡單介紹一下MySQL的架構是什麼樣的,分別有什麼樣的功能。而後再簡單介紹一下在咱們執行簡單的查詢和更新指令的時候,背後到底發生了什麼。數組

1 MySQL結構

在這一小節中,我會先簡單的介紹一下各個部分的功能。隨後,將在第2、第三節中詳細介紹。緩存

先來看一張圖:安全

簡單的來說一講:bash

1.1 鏈接器

鏈接器負責跟客戶端創建鏈接、獲取權限、維持和管理鏈接。架構

在客戶端輸入了帳號密碼以後,若是此時帳號密碼驗證經過,鏈接器將會和客戶端創建一條TCP鏈接。這個鏈接將會在長時間無請求後被鏈接器自動斷開(默認是8小時)。異步

此外,在鏈接創建後,若是管理員修改了這個帳戶的權限,也不會對當前的鏈接有任何的影響,當前鏈接所擁有的權限仍是以前未修改前的權限。ide

1.2 分析器

分析器有兩個功能:詞法分析、語法分析。性能

對於一個 SQL 語句,分析器首先進行詞法分析,對sql語句進行拆分,識別出各個字符串表明的含義。

而後就是語法分析,分析器根據定義的語法規則判斷sql語句是否知足 MySQL 語法。

因此,若是咱們看到You have an error in your SQL syntax這麼一段話,就能夠知道這個錯誤是由分析器返回的。

1.3 緩存

這裏的緩存會保存以前的sql查詢語句和結果。你能夠理解爲這是一個mapkey是查詢的sql語句,value是查詢的結果。

而且,在官方手冊中,有這麼一句話:

Queries must be exactly the same (byte for byte) to be seen as identical.

也就是說,查詢語句必須得和以前徹底一致,每個字節都同樣,大小寫敏感,甚至不能多一個空格。

可是,這裏的緩存是很是容易失效的。爲了保證查詢的冪等性,當某一張表有數據更新後,這個表的緩存也將失效。

因此,對於更新壓力大的數據庫來講,查詢緩存的命中率會很是低。建議只在讀多寫少的數據庫開啓緩存。

可是,在MySQL8.0之後,已經刪除了緩存功能。

1.4 優化器

查詢優化器的任務是發現執行SQL查詢的最佳方案。大多數查詢優化器,包括MySQL的查詢優化器,總或多或少地在全部可能的查詢評估方案中搜索最佳方案。

簡單來講,優化器就是尋找一個最快可以查詢到數據的策略。

1.5 執行器

在經過了上述的過程後,Server層已經解析出了須要處理的數據是什麼,應該怎麼作。

隨後會進行權限的判斷,若是當前的鏈接擁有目標表的權限,則會調用存儲引擎開放的接口,處理須要處理的數據。

到這裏MySQL的基本架構就講完了。可是由於我省略了大部分的細節,只講了這麼一小部分,可能會致使你的疑問增長了。

不過不要緊,咱們接着往下看,用實際的例子來解釋這裏的每一部分,可能會更容易理解。

2 查詢

咱們從這麼一條sql講起:

select * from T where ID = 1;
複製代碼

2.1 查找緩存

首先,會調用分析器,進行詞法分析。

此時,詞法分析發現這條sql語句是以select開頭的,而且在這條語句中有任何不肯定的數據,因此會去緩存中查找是否保存了這條語句的結果做爲緩存。

可是關於上面的說法,有我我的推測的部分。我沒有在官方文檔中找到MySQL是什麼時候查找緩存的,究竟是在分析器以前仍是分析器以後。

可是在《高性能MySQL》這本書中提到了 「經過檢查sql語句是否以select」 開頭,因此我推測查找緩存是須要先通過簡單的詞法分析的。

只有通過了詞法分析分析,MySQL才能知道這段語句是不是select語句,也能知道這條語句中有無一些不肯定的數據(如當前時間等)。

2.2 緩存未命中

此時,若是緩存未命中,則繼續使用分析器進行語法分析。而後,根據這顆語法樹,來判斷這條sql語句是否符合MySQL語法的。

注意,關於詞法分析和語法分析,若是你感興趣的話,能夠看一看編譯原理相關的內容。

而後來到了優化器。優化器就是在有多種查找方式的時候,自行選擇一個更好的查詢方式。

例如,若是此時sql語句裏面有多個索引,會選擇一個合適的索引;又或者在關聯查詢的時候,選擇一個更好的方案。

這一部分的內容我想在之後的文章中介紹,這裏我想重點講講下面的內容,關於MySQL中數據的結構。

2.3 數據的結構

在咱們利用最後一步的執行器去進行數據的讀取和寫入的時候,實際上是調用了MySQL中的存儲引擎進行數據的讀寫和寫入。

回到咱們的例子,咱們要找的是在表TID爲1的數據。可是,存儲引擎並不會返回這麼一條具體的數據,他返回的是包含這條數據的數據頁

這裏我補充一點點知識:

數據庫使用頁管理,和咱們操做系統是同樣的。由於咱們如今的機器是馮諾依曼結構的,這是是一種將程序指令存儲器和數據存儲器合併在一塊兒的存儲器結構。

在這種結構中,具備一個特性,叫局部性原理。

  • 時間局部性(Temporal Locality):若是一個信息項正在被訪問,那麼在近期它極可能還會被再次訪問。程序循環、堆棧等是產生時間局部性的緣由。
  • 空間局部性(Spatial Locality):在最近的未來將用到的信息極可能與正在使用的信息在空間地址上是臨近的。
  • 順序局部性(Order Locality):在典型程序中,除轉移類指令外,大部分指令是順序進行的。順序執行和非順序執行的比例大體是5:1。此外,對大型數組訪問也是順序的。指令的順序執行、數組的連續存放等是產生順序局部性的緣由。

簡單的來解釋就是若是一行數據被讀取了或者一條指令被執行了,那麼很大機率接下來CPU會繼續讀取或執行這個地址或者這個地址後面的數據和指令。

在MySQL中也是同樣的,若是一次性讀取一個頁,那麼可能在接下來的讀寫中所操做的數據也在這個數據頁內,這樣可使得磁盤IO的次數更少。

回到咱們剛剛說的內容,至於引擎是如何找到這個頁的,我想在後面索引相關的文章中再詳細解釋。這裏咱們先簡單的理解爲引擎可以快速的找到這一行數據所在的頁,而後這一頁返回給執行器。

此時,這一頁數據還會被保存在內存中。在以後還須要用到這些數據的時候,將會直接在內存中進行處理,而且MySQL的內存空間中能夠存放不少個這樣的數據頁。也就是說,這個時候不管是查找仍是修改,均可以在內存中進行,而不須要每次都進行磁盤IO。

最後,會在合適的時候將這一頁數據寫回磁盤。至因而在何時如何寫回磁盤的,咱們接着往下看。

3 更新

在說完了如何查找數據以後,咱們已經知道了一行數據是如何以頁的形式保存在內存中了。咱們如今要解決的問題是:

  • update語句是如何執行
  • 如何將執行後的新數據持久化在磁盤中

這是一個頗有意思的問題,咱們來假設兩種情境:

假設MySQL在更新以後只更新內存中的數據就返回,而後再某一時刻進行IO將數據頁持久化。這樣全部操做都是在內存中,能夠想象此時的MySQL性能是特別高的。可是,若是在更新完內存又尚未進行持久化的這段時間,MySQL宕機了,那麼咱們的數據就丟失了。

再來看另一種狀況:每次MySQL將內存中的頁更新好後,馬上進行IO,只有數據落盤後才返回。此時咱們能夠保證數據必定是正確的。可是,每一次的操做,都要進行IO,此時MySQL的效率變得很是低。

因此咱們來看看MySQL是如何作到保證性能的狀況下,還保證數據不丟的。

如今回到這條語句:

update T set a = a + 1 where ID = 0;
複製代碼

假設這條sql語句是正確的,存在名爲IDa的列在表T中,且存在ID爲0的數據。

此時通過鏈接器,分析器,分析器發現這是一條update語句,因而繼續語法分析,優化器,執行器。執行器判斷有權限,而後開表,引擎找到了包含了ID爲0這行數據的數據頁,將這一頁數據保存在內存中。

你能夠發現,update語句,一樣也走了這麼一遍流程。

而後重點來了,咱們要介紹一下MySQL是如何保證數據一致性的。

3.1 重作日誌

這裏要介紹一個很重要的日誌模塊,稱爲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 logcheck_point往前推動的時候,若是發現這個頁已經被刷回磁盤了,將會跳過這條記錄。

3.2 歸檔日誌

說了這麼多重作日誌,咱們再來聊聊歸檔日誌。

有幾個緣由,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 logbin log持久化以後再返回。

sync_binloginnodb_flush_log_at_trx_commit

先說說innodb_flush_log_at_trx_commit

  • 當設置參數爲1時,(默認爲1),表示事務提交時必須調用一次 fsync 操做,最安全的配置,保障持久性。
  • 當設置參數爲2時,則在事務提交時只作 write 操做,只保證將redo log buffer寫到系統的頁面緩存中,不進行fsync操做,所以若是MySQL數據庫宕機時,不會丟失事務,但操做系統宕機則可能丟失事務。
  • 當設置參數爲0時,表示事務提交時不進行寫入redo log操做,這個操做僅在master thread 中完成,而在master thread中每1秒進行一次重作日誌的fsync操做,所以實例 crash 最多丟失1秒鐘內的事務。(master thread是負責將緩衝池中的數據異步刷新到磁盤,保證數據的一致性)。

也就是說,若是咱們設置爲了1,在最後提交的時候,會調用fsync等待redo log持久化,才返回。

再說說sync_binlog

  • sync_binlog=0的時候,表示每次提交事務都只write,不fsync。
  • sync_binlog=1的時候,表示每次提交事務都會執行fsync。
  • sync_binlog=N(N>1)的時候,表示每次提交事務都write,但累積N個事務後才fsync。但若是宕機了可能會丟失最後的N條語句。 也就是說,若是咱們設置爲了1,最後提交的時候會和上面說到的同樣,等待系統的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:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~

相關文章
相關標籤/搜索