引言
? ?在上篇文章中,我們以《MySQL架構篇》拉開了MySQL
數據庫的的序幕,上篇文章中將MySQL
分層架構中的每一層都進行了詳細闡述。而在本篇中,則會進一步站在一條SQL
的角度,從SQL
的誕生開始,到SQL
執行、數據返回等全鏈路進行分析。
? ?在此之前呢,其實也寫過兩篇類似的姊妹篇,第一篇講的是《一個網絡請求的神奇之旅!》,在其中化身一個網絡請求,切身感受了從瀏覽器發出后,到響應數據的返回。而在更早的時候,《JVM系列》中也有一篇文章,講的是《一個Java對象從誕生到死亡的歷程》,其中聊到了Java
對象是如何誕生的,運行時會經歷什么過程?使用結束后又是如何回收的。
在本篇中,則會在站在數據庫的視角,再次感受“一條
SQL
多姿多彩的歷程”!你如果認真的看完了“一個請求、一個對象、一條SQL
”這三部曲后,相信你對于程序開發又會有一個全新的深刻認知。
一、一條SQL是如何誕生的?
? ?SQL
語句都誕生于客戶端,主要有兩種方式產生一條SQL
,一種是由開發者自己手動編寫,另一種則是相關的ORM
框架自動生成,一般情況下,MySQL
運行過程中收到的大部分SQL
都是由ORM
框架生成的,比如Java
中的MyBatis、Hibernate
框架等。
? ?同時,SQL
生成的時機一般都與用戶的請求有關,當用戶在系統中進行了某項操作,一般都會產生一條SQL
,例如我們在瀏覽器上輸入如下網址:
https://juejin.cn/user/862486453028888/posts
此時,就會先請求掘金的服務器,然后由掘金內部實現中的ORM
框架,根據請求參數生成一條SQL
,類似于下述的偽SQL
:
select * from juejin_article where userid = 862486453028888;
這條SQL
大致描述的意思就是:根據用戶請求的「作者ID」,在掘金數據庫的文章表中,查詢該作者的所有文章信息。
? ?從上述這個案例中可以明顯感受出來,用戶瀏覽器上看到的數據一般都來自于數據庫,而數據庫執行的SQL
則源自于用戶操作,兩者是相輔相成的關系,也包括任何寫操作(增、刪、改),本質上也會被轉換一條條SQL
,也舉個簡單的例子:
# 請求網址(Request URL)
https://www.xxx.com/user/register
# 請求參數(Request Param)
{
user_name : "竹子愛熊貓",
user_pwd : "123456",
user_sex : "男",
user_phone : "18888888888",
......
}
# 這里對于用戶密碼的處理不夠嚴謹,沒有做加密操作不要在意~
比如上述這個用戶注冊的案例,當用戶在網頁上點擊「注冊」按鈕時,會向目標網站的服務器發送一個post
請求,緊接著同樣會根據請求參數,生成一條SQL
,如下:
insert into table_user (user_name,user_pwd,user_sex,user_phone,....)
VALUES ("竹子愛熊貓", "123456", "男", "18888888888", ....);
也就是說,一條SQL
的誕生都源自于一個用戶請求,在開發程序時,SQL
的大體邏輯我們都會由業務層的編碼決定,具體的SQL
語句則是根據用戶的請求參數,以及提前定制好的“SQL
骨架”拼揍而成。當然,在Java
程序或其他語言編寫的程序中,只能生成SQL
,而SQL
真正的執行工作是需要交給數據庫去完成的。
二、一條SQL執行前會經歷的過程
? ?經過上一步之后,一條完整的SQL
就誕生了,為了SQL
能夠正常執行,首先會先去獲取一個數據庫連接對象,上篇關于MySQL
的架構篇曾聊到過,MySQL
連接層中會維護著一個名為「連接池」的玩意兒,但相信大家也都接觸過「數據庫連接池」這個東西,比如Java中的C3P0、Druid、DBCP....
等各類連接池。
那此時在這里可以思考一個問題,為什么數據庫自己維護了連接池的情況下,在
MySQL
客戶端中還需要再次維護一個數據庫連接池呢?接下來一起聊一聊。
2.1、數據庫連接池的必要性
? ?眾所周知,當要在Java
中創建一個數據庫連接時,首先會去讀取配置文件中的連接地址、賬號密碼等信息,然后根據配置的地址信息,發起網絡請求獲取數據庫連接對象。在這個過程中,由于涉及到了網絡請求,那此時必然會先經歷TCP
三次握手的過程,同時獲取到連接對象完成SQL
操作后,又要釋放這個數據庫連接,此時又需要經歷TCP
四次揮手過程。
從上面的描述中可以明顯感知出,在Java中創建、關閉數據庫連接的過程,過程開銷其實比較大,而在程序上線后,又需要頻繁進行數據庫操作。因此如果每次操作數據庫時,都獲取新的連接對象,那整個
Java
程序至少會有四分之一的時間內在做TCP
三次握手/四次揮手工作,這對整個系統造成的后果可想而知....
也正是由于上述原因,因此大名鼎鼎的「數據庫連接池」登場了,「數據庫連接池」和「線程池」的思想相同,會將數據庫連接這種較為珍貴的資源,利用池化技術對這種資源進行維護。也就代表著之后需要進行數據庫操作時,不需要自己去建立連接了,而是直接從「數據庫連接池」中獲取,用完之后再歸還給連接池,以此達到復用的效果。
當然,連接池中維護的連接對象也不會一直都在,當長時間未進行
SQL
操作時,連接池也會銷毀這些連接對象,而后當需要時再次創建,不過何時創建、何時銷毀、連接數限制等等這些工作,都交給了連接池去完成,無需開發者自身再去關注。
在Java中,目前最常用的數據庫連接池就是阿里的Druid
,一般咱們都會用它作為生產環境中的連接池:
[圖片上傳失敗...(image-7aa740-1670309104421)]
目前
Druid
已經被阿里貢獻給Apache
軟件基金會維護了~
OK~,回到前面拋出的問題,有了MySQL
連接池為何還需要在客戶端維護一個連接池?
對于這個問題,相信大家在心里多少都有點答案了,原因很簡單,兩者都是利用池化技術去達到復用資源、節省開銷、提升性能的目的,只不過針對的方向不同。
MySQL
的連接池主要是為了實現復用線程的目的,因為每個數據庫連接在MySQL
中都會使用一條線程維護,而每次為客戶端分配連接對象時,都需要經歷創建線程、分配棧空間....這些繁重的工作,這個過程需要時間,同時資源開銷也不小,所以MySQL
利用池化技術解決了這些問題。
而客戶端的連接池,主要是為了實現復用數據庫連接的目的,因為每次SQL
操作都需要經過TCP
三次握手/四次揮手的過程,過程同樣耗時且占用資源,因此也利用池化技術解決了這個問題。
其實也可以這樣理解,
MySQL
連接池維護的是工作線程,客戶端連接池則維護的是網絡連接。
2.2、SQL執行前會發生的事情
? ?回歸本文主題,當完整的SQL
生成后,會先去連接池中嘗試獲取一個連接對象,那接下來會發生什么事情呢?如下圖:
[圖片上傳失敗...(image-85f48b-1670309104421)]
當嘗試從連接池中獲取連接時,如果此時連接池中有空閑連接,可以直接拿到復用,但如果沒有,則要先判斷一下當前池中的連接數是否已達到最大連接數,如果連接數已經滿了,當前線程則需要等待其他線程釋放連接對象,沒滿則可以直接再創建一個新的數據庫連接使用。
假設此時連接池中沒有空閑連接,需要再次創建一個新連接,那么就會先發起網絡請求建立連接。
首先會經過《TCP的三次握手過程》,對于這塊就不再細聊了,畢竟之前聊過很多次了。當網絡連接建立成功后,也就等價于在MySQL
中創建了一個客戶端會話,然后會發生下圖一系列工作:
[圖片上傳失敗...(image-2d7987-1670309104421)]
- ①首先會驗證客戶端的用戶名和密碼是否正確:
- 如果用戶名不存在或密碼錯誤,則拋出
1045
的錯誤碼及錯誤信息。 - 如果用戶名和密碼驗證通過,則進入第②步。
- 如果用戶名不存在或密碼錯誤,則拋出
- ②判斷
MySQL
連接池中是否存在空閑線程:- 存在:直接從連接池中分配一條空閑線程維護當前客戶端的連接。
- 不存在:創建一條新的工作線程(映射內核線程、分配棧空間....)。
- ③工作線程會先查詢
MySQL
自身的用戶權限表,獲取當前登錄用戶的權限信息并授權。
到這里為止,執行SQL
前的準備工作就完成了,已經打通了執行SQL
的通道,下一步則是準備執行SQL
語句,工作線程會等待客戶端將SQL
傳遞過來。
三、一條SQL語句在數據庫中是如何執行的?
? ?經過連接層的一系列工作后,接著客戶端會將要執行的SQL
語句通過連接發送過來,然后會進行MySQL
服務層進行處理,不過根據用戶的操作不同,MySQL
執行SQL
語句時也會存在些許差異,這里是指讀操作和寫操作,兩者SQL
的執行過程并不相同,下面先來看看select
語句的執行過程。
3.1、一條查詢SQL的執行過程
在分析查詢SQL
的執行流程之前,咱們先模擬一個案例,以便于后續分析:
-- SQL語句
SELECT user_id FROM `zz_user` WHERE user_sex = "男" AND user_name = "竹子④號";
-- 表數據
+---------+--------------+----------+-------------+
| user_id | user_name | user_sex | user_phone |
+---------+--------------+----------+-------------+
| 1 | 竹子①號 | 男 | 18888888888 |
| 2 | 竹子②號 | 男 | 13588888888 |
| 3 | 竹子③號 | 男 | 15688888888 |
| 4 | 熊貓①號 | 女 | 13488888888 |
| 5 | 熊貓②號 | 女 | 18588888888 |
| 6 | 竹子④號 | 男 | 17777777777 |
| 7 | 熊貓③號 | 女 | 16666666666 |
+---------+--------------+----------+-------------+
先上個SQL
執行的完整流程圖,后續再逐步分析每個過程:
[圖片上傳失敗...(image-41304b-1670309104421)]
- ①先將
SQL
發送給SQL
接口,SQL
接口會對SQL
語句進行哈希處理。 - ②
SQL
接口在緩存中根據哈希值檢索數據,如果緩存中有則直接返回數據。 - ③緩存中未命中時會將
SQL
交給解析器,解析器會判斷SQL
語句是否正確:- 錯誤:拋出
1064
錯誤碼及相關的語法錯誤信息。 - 正確:將
SQL
語句交給優化器處理,進入第④步。
- 錯誤:拋出
- ④優化器根據
SQL
制定出不同的執行方案,并擇選出最優的執行計劃。 - ⑤工作線程根據執行計劃,調用存儲引擎所提供的
API
獲取數據。 - ⑥存儲引擎根據
API
調用方的操作,去磁盤中檢索數據(索引、表數據....)。 - ⑦發送磁盤
IO
后,對于磁盤中符合要求的數據逐條返回給SQL
接口。 - ⑧
SQL
接口會對所有的結果集進行處理(剔除列、合并數據....)并返回。
上述是一個簡單的流程概述,一般情況下查詢SQL
的執行都會經過這些步驟,下面再將每一步拆開詳細聊一聊。
SQL接口會干的工作
? ?當客戶端將SQL
發送過來之后,SQL
緊接著會交給SQL
接口處理,首先會對SQL
做哈希處理,也就是根據SQL
語句計算出一個哈希值,然后去「查詢緩存」中比對,如果緩存中存在相同的哈希值,則代表著之前緩存過相同SQL
語句的結果,那此時則直接從緩存中獲取結果并響應給客戶端。
在這里,如果沒有從緩存中查詢到數據,緊接著會將
SQL
語句交給解析器去處理。
SQL
接口除開對SQL
進行上述的處理外,后續還會負責處理結果集(稍后分析)。
解析器中會干的工作
? ?解析器收到SQL
后,會開始檢測SQL
是否正確,也就是做詞法分析、語義分析等工作,在這一步,解析器會根據SQL
語言的語法規則,判斷客戶端傳遞的SQL
語句是否合規,如果不合規就會返回1064
錯誤碼及錯誤信息:
ERROR 1064 (42000): You have an error in your SQL syntax; check....
? ?但如果SQL
語句沒有問題,此時就會對SQL
語句進行關鍵字分析,也就是根據SQL
中的SELECT、UPDATE、DELETE
等關鍵字,先判斷SQL
語句的操作類型,是讀操作還是寫操作,然后再根據FROM
關鍵字來確定本次SQL
語句要操作的是哪張表,也會根據WHERE
關鍵字后面的內容,確定本次SQL
的一些結果篩選條件.....。
總之,經過關鍵字分析后,一條
SQL
語句要干的具體工作就會被解析出來。
解析了SQL
語句中的關鍵字之后,解析器會根據分析出的關鍵字信息,生成對應的語法樹,然后交給優化器處理。
在這一步也就相當于Java中的
.java
源代碼變為.class
字節碼的過程,目的就是將SQL
語句翻譯成數據庫可以看懂的指令。
優化器中會干的工作
? ?經過解析器的工作后會得到一個SQL
語法樹,也就是知道了客戶端的SQL
大體要干什么事情了,接著優化器會對于這條SQL
,給出一個最優的執行方案,也就是告訴工作線程怎么執行效率最高、最節省資源以及時間。
? ?優化器最開始會根據語法樹制定出多個執行計劃,然后從多個執行計劃中選擇出一個最好的計劃,交給工作線程去執行,但這里究竟是如何選擇最優執行計劃的,相信大家也比較好奇,那此時我們結合前面給出的案例分析一下。
SELECT user_id FROM `zz_user` WHERE user_sex = "男" AND user_name = "竹子④號";
先來看看,對于這條SQL
而言,總共有幾種執行方案呢?答案是兩種。
- ①先從表中將所有
user_sex="男"
的數據查出來,再從結果中獲取user_name="竹子④號"
的數據。 - ②先從表中尋找
user_name="竹子④號"
的數據,再從結果中獲得user_sex="男"
的數據。
再結合前面給出的表數據,暫且分析一下上述兩種執行計劃哪個更好呢?
+---------+--------------+----------+-------------+
| user_id | user_name | user_sex | user_phone |
+---------+--------------+----------+-------------+
| 1 | 竹子①號 | 男 | 18888888888 |
| 2 | 竹子②號 | 男 | 13588888888 |
| 3 | 竹子③號 | 男 | 15688888888 |
| 4 | 熊貓①號 | 女 | 13488888888 |
| 5 | 熊貓②號 | 女 | 18588888888 |
| 6 | 竹子④號 | 男 | 17777777777 |
| 7 | 熊貓③號 | 女 | 16666666666 |
+---------+--------------+----------+-------------+
如果按照第①種方案執行,此時會先得到四條user_sex="男"
的數據,然后再從四條數據中查找user_name="竹子④號"
的數據。
如果按照第②中方案執行,此時會直接得到一條user_name="竹子④號"
的數據,然后再判斷一下user_sex
是否為"男",是則直接返回,否則返回空。
相較于兩種執行方案的過程,前者需要掃一次全表,然后再對結果集逐條判斷。而第二種方案掃一次全表后,只需要再判斷一次就可以了,很明顯可以感知出:第②種執行計劃是最優的,因此優化器會給出第②種執行計劃。
? ?經過上述案例的講解后,大家應該能夠對優化器的工作進一步理解。但上述案例僅是為了幫助大家理解,實際的SQL
優化過程會更加復雜,例如多表join
查詢時,怎么查更合適?單表復雜SQL
查詢時,有多條索引可以走,走哪條速度最快....,因此一條SQL
的最優執行計劃,需要結合多方面的優化策略來生成,例如MySQL
優化器的一些優化準則如下:
- ?多條件查詢時,重排條件先后順序,將效率更好的字段條件放在前面。
- ?當表中存在多個索引時,選擇效率最高的索引作為本次查詢的目標索引。
- ?使用分頁
Limit
關鍵字時,查詢到對應的數據條數后終止掃表。 - ?多表
join
聯查時,對查詢表的順序重新定義,同樣以效率為準。 - ?對于
SQL
中使用函數時,如count()、max()、min()...
,根據情況選擇最優方案。-
max()
函數:走B+
樹最右側的節點查詢(大的在右,小的在左)。 -
min()
函數:走B+
樹最左側的節點查詢。 -
count()
函數:如果是MyISAM
引擎,直接獲取引擎統計的總行數。 ......
-
- ?對于
group by
分組排序,會先查詢所有數據后再統一排序,而不是一開始就排序。 - ?
......
總之,根據SQL
不同,優化器也會基于不同的優化準則選擇出最佳的執行計劃。但需要牢記的一點是:MySQL
雖然有優化器,但對于效率影響最大的還是SQL
本身,因此編寫出一條優秀的SQL
,才是提升效率的最大要素。
存儲引擎中會干的工作
? ?經過優化器后,會得到一個最優的執行計劃,緊接著工作線程會根據最優計劃,去依次調用存儲引擎提供的API
,在上篇文章中提到過,存儲引擎主要就是負責在磁盤讀寫數據的,不同的存儲引擎,存儲在本地磁盤中的數據結構也并不相同,但這些底層實現并不需要MySQL
的上層服務關心,因為上層服務只需要負責調用對應的API
即可,存儲引擎的API
功能都是相同的。
? ?工作線程根據執行計劃調用存儲引擎的API
查詢指定的表,最終也就是會發生磁盤IO
,從磁盤中檢索數據,當然,檢索的數據有可能是磁盤中的索引文件,也有可能是磁盤中的表數據文件,這點要根據執行計劃來決定,我們只需要記住,經過這一步之后總能夠得到執行結果即可。
但有個小細節,還記得最開始創建數據庫連接時,對登錄用戶的授權步驟嘛?當工作線程去嘗試查詢某張表時,會首先判斷一下線程自身維護的客戶端連接,其登錄的用戶是否具備這張表的操作權限,如果不具備則會直接返回權限不足的錯誤信息。
? ?不過存儲引擎從磁盤中檢索出目標數據后,并不會將所有數據全部得到后再返回,而是會逐條返回給SQL
接口,然后會由SQL
接口完成最后的數據聚合工作,對于這點稍后會詳細分析。
下來再來看看寫入
SQL
的執行過程,因為讀取和寫入操作之間,也會存在些許差異。
3.2、一條寫入SQL的執行過程
? ?假設此時要執行下述這一條寫入類型的SQL
語句(還是基于之前的表數據):
UPDATE `zz_user` SET user_sex = "女" WHERE user_id = 6;
上面這條SQL
是一條典型的修改SQL
,但除開修改操作外,新增、刪除等操作也屬于寫操作,寫操作的意思是指會對表中的數據進行更改。同樣先上一個完整的流程圖:
[圖片上傳失敗...(image-c5b383-1670309104421)]
從上圖來看,相較于查詢SQL
,寫操作的SQL
執行流程明顯會更復雜一些,這里也先簡單總結一下每一步流程,然后再詳細分析一下其中一些與查詢SQL
中不同的步驟:
- ①先將
SQL
發送給SQL
接口,SQL
接口會對SQL
語句進行哈希處理。 - ②在緩存中根據哈希值檢索數據,如果緩存中有則將對應表的所有緩存全部刪除。
- ③經過緩存后會將
SQL
交給解析器,解析器會判斷SQL
語句是否正確:- 錯誤:拋出
1064
錯誤碼及相關的語法錯誤信息。 - 正確:將
SQL
語句交給優化器處理,進入第④步。
- 錯誤:拋出
- ④優化器根據
SQL
制定出不同的執行方案,并擇選出最優的執行計劃。 - ⑤在執行開始之前,先記錄一下
undo-log
日志和redo-log(prepare狀態)
日志。 - ⑥在緩沖區中查找是否存在當前要操作的行記錄或表數據(內存中):
- 存在:
- ⑦直接對緩沖區中的數據進行寫操作。
- ⑧然后利用
Checkpoint
機制刷寫到磁盤。
- 不存在:
- ⑦根據執行計劃,調用存儲引擎的
API
。 - ⑧發生磁盤
IO
,對磁盤中的數據做寫操作。
- ⑦根據執行計劃,調用存儲引擎的
- 存在:
- ⑨寫操作完成后,記錄
bin-log
日志,同時將redo-log
日志中的記錄改為commit
狀態。 - ⑩將
SQL
執行耗時及操作成功的結果返回給SQL
接口,再由SQL
接口返回給客戶端。
整個寫SQL
的執行過程,前面的一些步驟與查SQL
執行的過程沒太大差異,唯一一點不同的在于緩存哪里,原本查詢時是從緩存中嘗試獲取數據。而寫操作時,由于要對表數據發生更改,因此如果在緩存中發現了要操作的表存在緩存,則需要將整個表的所有緩存清空,確保緩存的強一致性。
OK~,除開上述這點區別外,另外多出了唯一性判斷、一個緩沖區寫入,以及幾個寫入日志的步驟,接下來一起來聊聊這些。
唯一性判斷主要是針對插入、修改語句來說的,因為如果表中的某個字段建立了唯一約束或唯一索引后,當插入/修改一條數據時,就會先檢測一下目前插入/修改的值,是否與表中的唯一字段存在沖突,如果表中已經存在相同的值,則會直接拋出異常,反之會繼續執行。
很簡單哈~,接著再來聊聊緩沖區和日志!
其實在上篇中聊到過,由于CPU
和磁盤之間的性能差距實在過大,因此MySQL
中會在內存中設計一個「緩沖區」的概念,主要目的是在于彌補CPU
與磁盤之間的性能差距。
緩沖區中會做的工作
??在真正調用存儲引擎的API
操作磁盤之前,首先會在「緩沖區」中查找有沒有要操作的目標數據/目標表,如果存在則直接對緩沖區中的數據進行操作,然后MySQL
會在后臺以一種名為Checkpoint
的機制,將緩沖區中更新的數據刷回到磁盤。只有當緩沖區沒有找到目標數據時,才會去真正調用存儲引擎的API
,然后發生磁盤IO
,去對應磁盤中的表數據進行修改。
不過值得注意的一點是:雖然緩沖區中有數據時會先操作緩沖區,然后再通過
Checkpoint
機制刷寫磁盤,但這兩個過程不是連續的!也就是說,當線程對緩沖區中的數據操作完成后,會直接往下走,數據落盤的工作則會交給后臺線程。
不過雖然兩者之間是異步的,但對于人而言,這個過程不會有太大的感知,畢竟CPU
在運行的時候,都是按納秒、微秒級作為單位。
??但不管數據是在緩沖區還是磁盤,本質上數據更改的動作都是發生在內存的,就算是修改磁盤數據,也是將數據讀到內存中操作,然后再將數據寫回磁盤。不過在「寫SQL
」執行的前后都會記錄日志,這點下面詳細聊聊,這也是寫SQL
與讀SQL
最大的區別。
寫操作時的日志
??執行「讀SQL
」一般都不會有狀態,也就是說:MySQL
執行一條select
語句,幾乎不會留下什么痕跡。但這里為什么用幾乎這個詞呢?因為查詢時也有些特殊情況會留下“痕跡”,就是慢查詢SQL
:
慢查詢
SQL
:查詢執行過程耗時較長的SQL
記錄。
在執行查詢SQL
時,大多數的普通查詢MySQL
并不關心,但慢查詢SQL
除外,這類SQL
一般是引起響應緩慢問題的“始作俑者”,所以當一條查詢SQL
的執行時長超過規定的時間限制,就會被“記錄在案”,也就是會記錄到慢查詢日志中。
??與「查詢SQL
」恰恰相反,任何一條寫入類型的SQL
都是有狀態的,也就代表著只要是會對數據庫發生更改的SQL
,執行時都會被記錄在日志中。首先所有的寫SQL
在執行之前都會生成對應的撤銷SQL
,撤銷SQL
也就是相反的操作,比如現在執行的是insert
語句,那這里就生成對應的delete
語句....,然后記錄在undo-log
撤銷/回滾日志中。但除此之外,還會記錄redo-log
日志。
??redo-log
日志是InnoDB
引擎專屬的,主要是為了保證事務的原子性和持久性,這里會將寫SQL
的事務過程記錄在案,如果服務器或者MySQL
宕機,重啟時就可以通過redo_log
日志恢復更新的數據。在「寫SQL
」正式執行之前,就會先記錄一條prepare
狀態的日志,表示當前「寫SQL
」準備執行,然后當執行完成并且事務提交后,這條日志記錄的狀態才會更改為commit
狀態。
除開上述的
redo-log、undo-log
日志外,同時還會記錄bin-log
日志,這個日志和redo-log
日志很像,都是記錄對數據庫發生更改的SQL
,只不過redo-log
是InnoDB
引擎專屬的,而bin-log
日志則是MySQL
自帶的日志。
不過無論是什么日志,都需要在磁盤中存儲,而本身「寫SQL
」在磁盤中寫表數據效率就較低了,此時還需寫入多種日志,效率定然會更低。對于這個問題MySQL
以及存儲引擎的設計者自然也想到了,所以大部分日志記錄也是采用先寫到緩沖區中,然后再異步刷寫到磁盤中。
比如
redo-log
日志在內存中會有一個redo_log
緩沖區中,bin-log
日志也同理,當需要記錄日志時,都是先寫到內存中的緩沖區。
那內存中的日志數據何時會刷寫到磁盤呢?對于這點則是由刷盤策略來決定的,redo-log
日志的刷盤策略由innodb_flush_log_at_trx_commit
參數控制,而bin-log
日志的刷盤策略則可以通過sync_binlog
參數控制:
-
innodb_flush_log_at_trx_commit
:-
0
:間隔一段時間,然后再刷寫一次日志到磁盤(性能最佳)。 -
1
:每次提交事務時,都刷寫一次日志到磁盤(性能最差,最安全,默認策略)。 -
2
:有事務提交的情況下,每間隔一秒時間刷寫一次日志到磁盤。
-
-
sync_binlog
:-
0
:同上述innodb_flush_log_at_trx_commit
參數的2
。 -
1
:同上述innodb_flush_log_at_trx_commit
參數的1
,每次提交事務都會刷盤,默認策略。
-
到這里就大致闡述了一下「寫SQL
」執行時,會寫的一些日志記錄,這些日志在后續做數據恢復、遷移、線下排查時都較為重要,因此后續也會單開一篇詳細講解。
四、一條SQL執行完成后是如何返回的?
? ?一條「讀SQL
」或「寫SQL
」執行完成后,由于SQL
操作的屬性不同,兩者之間也會存在差異性,
4.1、讀類型的SQL返回
? ?前面聊到過,MySQL
執行一條查詢SQL
時,數據是逐條返回的模式,因為如果等待所有數據全部查出來之后再一次性返回,必然會導致撐滿內存。
不過這里的返回,并不是指返回客戶端,而是指返回
SQL
接口,因為從磁盤中檢索出目標數據時,一般還需要對這些數據進行再次處理,舉個例子理解一下。
SELECT user_id FROM `zz_user` WHERE user_sex = "男" AND user_name = "竹子④號";
還是之前那條查詢SQL
,這條SQL
要求返回的結果字段僅有一個user_id
,但在磁盤中檢索數據時,會直接將這個字段單獨查詢出來嗎?并不是的,而是會將整條行數據全部查詢出來,如下:
+---------+--------------+----------+-------------+
| user_id | user_name | user_sex | user_phone |
+---------+--------------+----------+-------------+
| 6 | 竹子④號 | 男 | 17777777777 |
+---------+--------------+----------+-------------+
從行記錄中篩選出最終所需的結果字段,這個工作是在SQL
接口中完成的,也包括多表聯查時,數據的合并工作,同樣也是在SQL
接口完成,其他SQL
亦是同理。
還有一點需要牢記:就算沒有查詢到數據,也會將執行狀態、執行耗時這些信息返回給
SQL
接口,然后由SQL
接口向客戶端返回NULL
。
不過當查詢到數據后,在正式向客戶端返回之前,還會順手將結果集放入到緩存中。
4.2、寫類型的SQL返回
? ?寫SQL
執行的過程會比讀SQL
復雜,但寫SQL
的結果返回卻很簡單,寫類型的操作執行完成之后,僅會返回執行狀態、受影響的行數以及執行耗時,比如:
UPDATE `zz_user` SET user_sex = "女" WHERE user_id = 6;
這條SQL
執行成功后,會返回Query OK, 1 row affected (0.00 sec)
這組結果,最終返回給客戶端的則只有「受影響的行數」,如果寫SQL
執行成功,這個值一般都會大于0
,反之則會小于0
。
4.3、執行結果是如何返回給客戶端的?
? ?對于這個問題的答案其實很簡單,由于執行當前SQL
的工作線程,本身也維護著一個數據庫連接,這個數據庫連接實際上也維持著客戶端的網絡連接,如下:
[圖片上傳失敗...(image-960bac-1670309104421)]
當結果集處理好了之后,直接通過Host
中記錄的地址,將結果集封裝成TCP
數據報,然后返回即可。
數據返回給客戶端之后,除非客戶端主動輸入
exit
等退出連接的命令,否則連接不會立馬斷開。
如果要斷開客戶端連接時,又會經過TCP
四次揮手的過程。
不過就算與客戶端斷開了連接,
MySQL
中創建的線程并不會銷毀,而是會放入到MySQL
的連接池中,等待其他客戶端復用當前連接。一般情況下,一條線程在八小時內未被復用,才會觸發MySQL
的銷毀工作。
五、SQL執行篇總結
? ?看到這里,SQL
執行原理篇也走進了尾聲,其實SQL
語句的執行過程,實際上也就是MySQL
的架構中一層層對其進行處理,理解了MySQL
架構篇的內容后,相信看SQL
執行篇也不會太難,經過這篇文章的學習后,相信大家對數據庫的原理知識也能夠進一步掌握,那我們下篇再見~