大家好, 從今天開始我們開啟MySQL底層原理的探索里程,今天是第一篇我們將MySQL從一個黑盒狀態(tài)一點點撥云見日剖析來了解整個MySQL的架構設計和底層組件
把MySQL當作一個黑盒來執(zhí)行SQL
現(xiàn)在我們來看看, 對于研發(fā)工程師來說數(shù)據(jù)庫是什么東西?平時我們做系統(tǒng)開發(fā)時,一般情況下都會連接到一個MySQL數(shù)據(jù)庫上去,去執(zhí)行各種增刪改查語句。如下圖所示:
但是實際上我們在使用MySQL的過程中,總會遇到這樣那樣的一些問題,比如死鎖異常、SQL性能太差、MySQL gone away等等。在遇到MySQL數(shù)據(jù)庫的一些問題時,一般都會上網(wǎng)搜索博客,然后自己嘗試搗鼓著解決一下,最后解決了問題,可能也沒搞明白里面的原理。
因此我們就要去探索MySQL底層原理的方方面面,以及探索在解決MySQL各種生產(chǎn)實戰(zhàn)問題的時候,以及如何基于MySQL底層
原理去進行分析、排查和定位。
如何連接到MySQL:MySQL驅動
大家都知道,如果我們要訪問數(shù)據(jù)庫,必須得跟數(shù)據(jù)庫建立一個網(wǎng)絡連接,那么這個連接由誰來建立呢?答案就是MySQL驅動,它會在底層跟數(shù)據(jù)庫建立網(wǎng)絡連接,有網(wǎng)絡連接,接著才能去發(fā)送請求給數(shù)據(jù)庫服務器!如下圖所示:
數(shù)據(jù)庫連接池
那么我們來思考一個問題,一個golang系統(tǒng)難道只會跟數(shù)據(jù)庫建立一個連接嗎?這樣肯定是不行的, 對于一個稍微有點流量的站點來說,瞬時會有很多請求打過來,這個時候,都去競爭一個數(shù)據(jù)庫連接的話,性能肯定很低下。
那么如果golang在訪問數(shù)據(jù)庫的時候,都創(chuàng)建一個連接,執(zhí)行完后就釋放,這樣行不行?首先要明白的是數(shù)據(jù)庫連接是有上限的,因為每次建立一個數(shù)據(jù)庫連接都很耗時(tcp三次握手),好不容易建立好了連接,執(zhí)行完了SQL語句,你還把數(shù)據(jù)庫連接給銷毀了,下一次再重新建立數(shù)據(jù)庫連接,那肯定是效率很低下的 。
所以一般我們必須要用數(shù)據(jù)庫連接池,也就是說在一個池子里維持多個數(shù)據(jù)庫連接,讓多個進程使用里面的不同的數(shù)據(jù)庫連接去
執(zhí)行SQL語句,然后執(zhí)行完SQL語句之后,不要銷毀這個數(shù)據(jù)庫連接,而是把連接放回池子里,后續(xù)還可以繼續(xù)使用。
基于這樣的一個數(shù)據(jù)庫連接池的機制,就可以解決連接爭搶和效率問題,如圖所示:
MySQL數(shù)據(jù)庫的連接池
講到現(xiàn)在我們已經(jīng)知道,我們任何一個系統(tǒng)都會有一個數(shù)據(jù)庫連接池去訪問數(shù)據(jù)庫,也就是說這個系統(tǒng)會有多個數(shù)據(jù)庫連接,供程序并發(fā)
的使用。同時我們可能會有多個系統(tǒng)同時去訪問一個數(shù)據(jù)庫。
這個時候,我們將目光轉移到MySQL本身,對于多系統(tǒng)要與數(shù)據(jù)庫建立很多連接,那么MySQL必然也要維護與系統(tǒng)之間的多個連接才可以,所以,這里我們開始了解MySQL架構體系中的第一個環(huán)節(jié),就是連接池。
如下圖所示,實際上MySQL中的連接池就是維護了與系統(tǒng)之間的多個數(shù)據(jù)庫連接。除此之外,你的系統(tǒng)每次跟MySQL建立連接的
時候,還會根據(jù)你傳遞過來的賬號和密碼,進行賬號密碼的驗證,庫表權限的驗證。
講到這里,我們還是把MySQL當作一個黑盒在使用,我們只知道執(zhí)行了insert語句之后,在表里會多出來一條數(shù)據(jù);執(zhí)行了update語句之后,會對表里的數(shù)據(jù)進行更改;執(zhí)行了delete語句之后,會把表里的一條數(shù)據(jù)刪除掉;執(zhí)行了select語句之后,會從表里查詢一些數(shù)據(jù)出來。如果語句性能有點差?沒關系,在表里建幾個索引就可以了!
SQL線程:負責監(jiān)聽,讀取請求
從現(xiàn)在開始就要打破這種把數(shù)據(jù)庫當黑盒子的認知程度,要深入底層,去探索數(shù)據(jù)庫的工作原理以及生產(chǎn)問題的優(yōu)化手段!
現(xiàn)在我們的數(shù)據(jù)庫服務器的連接池中的某個連接接收到了網(wǎng)絡請求,假設就是一條SQL語句,那么我們先思考一個問題,
誰負責從這個連接中去監(jiān)聽網(wǎng)絡請求?誰負責從網(wǎng)絡連接里把請求數(shù)據(jù)讀取出來?
其實這個時候,一定得有一個線程去處理網(wǎng)絡連接,并由它來監(jiān)聽請求以及讀取請求數(shù)據(jù),比如從網(wǎng)絡連接中讀取和解析出來一
條我們的系統(tǒng)發(fā)送過去的SQL語句, 如下圖所示:
SQL接口:負責接收SQL語句
那么接下來著我們思考一下,當MySQL內部的工作線程從一個網(wǎng)絡連接中讀取出來一個SQL語句之后,此時會如何來執(zhí)行這個SQL語
句呢?
其實MySQL內部首先提供了一個組件,就是SQL接口(SQL Interface),它是一套執(zhí)行SQL語句的接口,專門用于執(zhí)行我們
發(fā)送給MySQL的那些增刪改查的SQL語句,因此MySQL的工作線程接收到SQL語句之后,就會轉交給SQL接口去執(zhí)行,如下圖。
查詢解析器:讓Mysql能看懂SQL
那么問題來了,SQL接口是如何執(zhí)行SQL語句呢?你直接把SQL語句交給MySQL,他怎么能看懂和理解這些SQL語句呢?
比如我們來舉一個例子,現(xiàn)在我們有這么一個SQL語句:
select id,name,age,sex from `user` where id=1;
我們用人腦是直接就可以處理一下,只要懂SQL語法的人,立馬大家就知道他是什么意思,但是MySQL是一個數(shù)據(jù)庫管理系統(tǒng),他沒法直接理解這些SQL語句!
此時有一個關鍵的組件要出場了,那就是"查詢解析器"
這個查詢解析器(Parser)就是負責對SQL語句進行解析的,比如對上面那個SQL語句進行一下拆解,拆解
成以下幾個部分:
1) 我們現(xiàn)在要從“users”表里查詢數(shù)據(jù)
2) 查詢“id”字段的值等于1的那行數(shù)據(jù)
3) 對查出來的那行數(shù)據(jù)要提取里面的“id,name,age”三個字段。
所謂的SQL解析,就是按照既定的SQL語法,對我們按照SQL語法規(guī)則編寫的SQL語句進行解析,
然后理解這個SQL語句要干什么事情
如下圖所示:
查詢優(yōu)化器:選擇最優(yōu)的查詢路徑
當我們通過SQL解析器理解了SQL語句要干什么之后,接著會找查詢優(yōu)化器(Optimizer)來選擇一個最優(yōu)的查詢路徑。這里我們可以用一個極為通俗簡單的例子,來理解一下所謂的最優(yōu)查詢路徑是什么。
我們現(xiàn)在理解了一個SQL想要干這么一個事兒:我們現(xiàn)在要從"users"表里查詢數(shù)據(jù),查詢id字段的值等于1的那行數(shù)據(jù),對查出來的那行數(shù)據(jù)要提取里面的"id,name,age"三個字段。
事情明白了,但是到底應該怎么來實現(xiàn)呢?這里我們來簡單分析下有以下兩種查詢路徑:
1) 直接定位到"users"表中的id字段等于1的一行數(shù)據(jù),然后查出來那行數(shù)據(jù)的"id,name,age"三個字段
的值就可以了
2) 先把"users"表中的每一行數(shù)據(jù)的"id,name,age"三個字段的值都查出來,然后從這批數(shù)據(jù)里過濾出來
id字段等于1的那行數(shù)據(jù)的"id,name,age"三個字段
其實我們會發(fā)現(xiàn),要完成這個SQL語句的目標,兩個路徑都可以做到,但
是哪一種更好呢?顯然感覺上是第一種查詢路徑更好一些。
所以查詢優(yōu)化器就是干這個的,它會針對你寫的幾十行、幾百行甚至上千行的復雜SQL語句生成查詢路徑樹,然后從里面選擇一條最優(yōu)的查詢路徑出來。相當于他會告訴你,你應該按照一個什么樣的步驟和順序,去執(zhí)行哪些操作,然后一步一步的把SQL語句就給完成了。如下圖所示:
存儲引擎接口,真正執(zhí)行SQL語句
這個時候把查詢優(yōu)化器選擇的最優(yōu)查詢路徑計劃交給底層的存儲引擎去真正的執(zhí)行。這個存儲引擎是MySQL的架構設計中很有”特色”的一個環(huán)節(jié)。
不知道你有沒有思考過,真正在執(zhí)行SQL語句的時候,要么更新數(shù)據(jù),要么查詢數(shù)據(jù),那么數(shù)據(jù)你覺得存放在哪里?
其實數(shù)據(jù)庫就是一個C、C++語言寫出來的系統(tǒng)而已,然后啟動之后也是一個進程,執(zhí)行他里面的各種代碼,也就是我們上面所說的那些東西。所以對數(shù)據(jù)庫而言,我們的數(shù)據(jù)要不然是放在內存里,要不然是放在磁盤文件里,沒什么特殊的地方!
所以我們來思考一下,假設我們的數(shù)據(jù)有的存放在內存里,有的存放在磁盤文件里。那么問題來了,
我們已經(jīng)知道一個SQL語句要如何執(zhí)行了,但是我們現(xiàn)在怎么知道哪些數(shù)據(jù)在內存里?哪些數(shù)據(jù)在磁盤里?
我們執(zhí)行的時候是更新內存的數(shù)據(jù)?還是更新磁盤的數(shù)據(jù)?我們如果更新磁盤的數(shù)據(jù),是先查詢哪個磁盤
文件,再更新哪個磁盤文件?到這里是不是感覺一頭霧水?
所以這個時候就需要"存儲引擎"了,存儲引擎其實就是執(zhí)行SQL語句的,他會按照一定的步驟去查詢內存
緩存數(shù)據(jù),更新磁盤數(shù)據(jù),查詢磁盤數(shù)據(jù),等等,執(zhí)行諸如此類的一系列的操作
在MySQL的架構設計中,SQL接口、SQL解析器、查詢優(yōu)化器其實都是通用的,就是一套組件而已。
但是存儲引擎的話,他是支持各種各樣的存儲引擎的(MySQL支持插件式引擎),比如我們常見的InnoDB、MyISAM、Memory等等,我們是可以選擇使用哪種存儲引擎來負責具體的SQL語句執(zhí)行的。
當然現(xiàn)在MySQL5.7都是使用InnoDB存儲引擎的,至于存儲引擎的原理,后續(xù)我們也會深入一步一步分析的
執(zhí)行器:根據(jù)執(zhí)行計劃調用存儲引擎的接口
這個時候我們再來思考一個問題,既然存儲引擎可以幫助我們去訪問內存以及磁盤上的數(shù)據(jù),那么是誰來調用存儲引擎的接口呢?
我們是不是還漏了一個執(zhí)行器的概念呢?這個執(zhí)行器會根據(jù)優(yōu)化器選擇的執(zhí)行方案,去調用存儲引擎的接口按照一定的順序和步驟,就把SQL語句的邏輯給執(zhí)行了。
這里舉個例子,比如執(zhí)行器可能會先調用存儲引擎的一個接口,去獲取"users"表中的第一行數(shù)據(jù),然后判斷一下這個數(shù)據(jù)的
id字段的值是否等于我們期望的一個值,如果不是的話,那就繼續(xù)調用存儲引擎的接口,去獲取"users"表的下一行數(shù)據(jù)。就是基于上述的思路,執(zhí)行器就會去根據(jù)我們的優(yōu)化器生成的一套執(zhí)行計劃,然后不停的調用存儲引擎的各種接口去完成SQL語句的執(zhí)行計劃,這樣整個流程就串起來了,如下圖所示:
到這里,我們將MySQL內部是如何去執(zhí)行一條SQL的整個流程詳細的剖析了一遍,可以簡單了解了內部各個組件的作用。下面奉上一張極客時間中MySQL45講專欄關于MySQL執(zhí)行的邏輯架構圖:
下一篇我們將會講"從一條SQL更新語句來了解InnoDB存儲引擎的架構設計", 敬請期待。。。。