詳解一條 SQL 的執行過程

如下文章來源於碼海 ,做者碼海

詳解一條 SQL 的執行過程

每天和數據庫打交道,一天能寫上幾十條 SQL 語句,但你知道咱們的系統是如何和數據庫交互的嗎?MySQL 如何幫咱們存儲數據、又是如何幫咱們管理事務?....是否是感受真的除了寫幾個 「select * from dual」外基本腦子一片空白?這篇文章就將帶你走進 MySQL 的世界,讓你完全瞭解系統究竟是如何和 MySQL 交互的,MySQL 在接受到咱們發送的 SQL 語句時又分別作了哪些事情。java

MySQL 驅動

 

咱們的系統在和 MySQL 數據庫進行通訊的時候,總不多是無緣無故的就能接收和發送請求,就算是你沒有作什麼操做,那總該是有其餘的「人」幫咱們作了一些事情,基本上使用過 MySQL 數據庫的程序員多多少少都會知道 MySQL 驅動這個概念的。就是這個 MySQL  驅動在底層幫咱們作了對數據庫的鏈接,只有創建了鏈接了,纔可以有後面的交互。看下圖表示程序員

 

 

這樣的話,在系統和 MySQL 進行交互以前,MySQL 驅動會幫咱們創建好鏈接,而後咱們只須要發送 SQL 語句就能夠執行 CRUD 了。一次 SQL 請求就會創建一個鏈接,多個請求就會創建多個鏈接,那麼問題來了,咱們系統確定不是一我的在使用的,換句話說確定是存在多個請求同時去爭搶鏈接的狀況。咱們的 web 系統通常都是部署在 tomcat 容器中的,而  tomcat  是能夠併發處理多個請求的,這就會致使多個請求會去創建多個鏈接,而後使用完再都去關閉,這樣會有什麼問題呢?以下圖web

 

 

java 系統在經過 MySQL 驅動和 MySQL 數據庫鏈接的時候是基於 TCP/IP 協議的,因此若是每一個請求都是新建鏈接和銷燬鏈接,那這樣勢必會形成沒必要要的浪費和性能的降低,也就說上面的多線程請求的時候頻繁的建立和銷燬鏈接顯然是不合理的。必然會大大下降咱們系統的性能,可是若是給你提供一些固定的用來鏈接的線程,這樣是否是不須要反覆的建立和銷燬鏈接了呢?相信懂行的朋友會會心一笑,沒錯,說的就是數據庫鏈接池。數據庫

數據庫鏈接池:維護必定的鏈接數,方便系統獲取鏈接,使用就去池子中獲取,用完放回去就能夠了,咱們不須要關心鏈接的建立與銷燬,也不須要關心線程池是怎麼去維護這些鏈接的。緩存

 

 

常見的數據庫鏈接池有 Druid、C3P0、DBCP,鏈接池實現原理在這裏就不深刻討論了,採用鏈接池大大節省了不斷建立與銷燬線程的開銷,這就是有名的「池化」思想,無論是線程池仍是 HTTP 鏈接池,都能看到它的身影。tomcat

 

數據庫鏈接池

 

到這裏,咱們已經知道的是咱們的系統在訪問  MySQL  數據庫的時候,創建的鏈接並非每次請求都會去建立的,而是從數據庫鏈接池中去獲取,這樣就解決了由於反覆的建立和銷燬鏈接而帶來的性能損耗問題了。不過這裏有個小問題,業務系統是併發的,而 MySQL 接受請求的線程呢,只有一個?服務器

其實 MySQL 的架構體系中也已經提供了這樣的一個池子,也是數據庫連池。雙方都是經過數據庫鏈接池來管理各個鏈接的,這樣一方面線程以前不須要是爭搶鏈接,更重要的是不須要反覆的建立的銷燬鏈接。網絡

 

 至此係統和 MySQL 數據庫之間的鏈接問題已經說明清楚了。那麼 MySQL 數據庫中的這些鏈接是怎麼來處理的,又是誰來處理呢?多線程

網絡鏈接必須由線程來處理

對計算基礎稍微有一點了解的的同窗都是知道的,網絡中的鏈接都是由線程來處理的,所謂網絡鏈接說白了就是一次請求,每次請求都會有相應的線程去處理的。也就是說對於 SQL 語句的請求在 MySQL  中是由一個個的線程去處理的。架構

 

 那這些線程會怎麼去處理這些請求?會作哪些事情?

 

SQL 接口

MySQL 中處理請求的線程在獲取到請求之後獲取 SQL 語句去交給 SQL 接口去處理。

 

查詢解析器

 

假如如今有這樣的一個 SQL

SELECT stuName,age,sex FROM students WHERE id=1

可是這個 SQL 是寫給咱們人看的,機器哪裏知道你在說什麼?這個時候解析器就上場了。他會將  SQL  接口傳遞過來的 SQL 語句進行解析,翻譯成 MySQL 本身能認識的語言,至於怎麼解析的就不須要在深究了,無非是本身一套相關的規則。

 

 

 

如今 SQL 已經被解析成  MySQL  認識的樣子的,那下一步是否是就是執行嗎?理論上是這樣子的,可是 MySQL 的強大遠不止於此,他還會幫咱們選擇最優的查詢路徑。

什麼叫最優查詢路徑?就是 MySQL 會按照本身認爲的效率最高的方式去執行查詢

具體是怎麼作到的呢?這就要說到  MySQL  的查詢優化器了

MySQL 查詢優化器

MySQL 查詢優化器

查詢優化器內部具體怎麼實現的咱們不須要是關心,我須要知道的是  MySQL  會幫我去使用他本身認爲的最好的方式去優化這條  SQL  語句,並生成一條條的執行計劃,好比你建立了多個索引,MySQL 會依據成本最小原則來選擇使用對應的索引,這裏的成本主要包括兩個方面, IO 成本和 CPU 成本

IO 成本: 即從磁盤把數據加載到內存的成本,默認狀況下,讀取數據頁的 IO 成本是 1,MySQL 是以頁的形式讀取數據的,即當用到某個數據時,並不會只讀取這個數據,而會把這個數據相鄰的數據也一塊兒讀到內存中,這就是有名的程序局部性原理,因此 MySQL 每次會讀取一整頁,一頁的成本就是 1。因此 IO 的成本主要和頁的大小有關

CPU 成本:將數據讀入內存後,還要檢測數據是否知足條件和排序等 CPU 操做的成本,顯然它與行數有關,默認狀況下,檢測記錄的成本是 0.2。

MySQL 優化器 會計算 「IO 成本 + CPU」 成本最小的那個索引來執行

畫外音:索引成本具體怎麼計算,請參考?這篇文章

 

 優化器執行選出最優索引等步驟後,會去調用存儲引擎接口,開始去執行被  MySQL  解析過和優化過的 SQL 語句

存儲引擎

查詢優化器會調用存儲引擎的接口,去執行  SQL,也就是說真正執行  SQL  的動做是在存儲引擎中完成的。數據是被存放在內存或者是磁盤中的(存儲引擎是一個很是重要的組件,後面會詳細介紹)

本篇文章你們先對存儲引擎有一個大體的認識就能夠了。後續專門文章來詳細介紹的。

執行器

執行器是一個很是重要的組件,由於前面那些組件的操做最終必須經過執行器去調用存儲引擎接口才能被執行。執行器最終最根據一系列的執行計劃去調用存儲引擎的接口去完成  SQL  的執行

 

 

初識存儲引擎

咱們以一個更新的SQL語句來講明,SQL 以下

UPDATE students SET stuName = '小強' WHERE id = 1

當咱們系統發出這樣的查詢去交給 MySQL 的時候,MySQL 會按照咱們上面介紹的一系列的流程最終經過執行器調用存儲引擎去執行,流程圖就是上面那個。在執行這個 SQL 的時候 SQL 語句對應的數據要麼是在內存中,要麼是在磁盤中,若是直接在磁盤中操做,那這樣的隨機IO讀寫的速度確定讓人沒法接受的,因此每次在執行 SQL 的時候都會將其數據加載到內存中,這塊內存就是 InnoDB 中一個很是重要的組件:緩衝池 Buffer Pool

Buffer Pool

Buffer Pool (緩衝池)是 InnoDB 存儲引擎中很是重要的內存結構,顧名思義,緩衝池其實就是相似  Redis  同樣的做用,起到一個緩存的做用,由於咱們都知道 MySQL 的數據最終是存儲在磁盤中的,若是沒有這個 Buffer Pool  那麼咱們每次的數據庫請求都會磁盤中查找,這樣必然會存在 IO 操做,這確定是沒法接受的。可是有了 Buffer Pool 就是咱們第一次在查詢的時候會將查詢的結果存到  Buffer Pool 中,這樣後面再有請求的時候就會先從緩衝池中去查詢,若是沒有再去磁盤中查找,而後在放到  Buffer Pool 中,以下圖

 

 

按照上面的那幅圖,這條 SQL 語句的執行步驟大體是這樣子的

  1. innodb 存儲引擎會在緩衝池中查找 id=1 的這條數據是否存在
  2. 發現不存在,那麼就會去磁盤中加載,並將其存放在緩衝池中
  3. 該條記錄會被加上一個獨佔鎖(總不能你在修改的時候別人也在修改吧,這個機制本篇文章不重點介紹,之後會專門寫文章來詳細講解)
undo 日誌文件

undo 日誌文件:記錄數據被修改前的樣子

undo 顧名思義,就是沒有作,沒發生的意思。undo log  就是沒有發生事情(本來事情是什麼)的一些日誌

咱們剛剛已經說了,在準備更新一條語句的時候,該條語句已經被加載到 Buffer pool 中了,實際上這裏還有這樣的操做,就是在將該條語句加載到 Buffer Pool 中的時候同時會往 undo 日誌文件中插入一條日誌,也就是將 id=1 的這條記錄的原來的值記錄下來。

這樣作的目的是什麼?

Innodb 存儲引擎的最大特色就是支持事務,若是本次更新失敗,也就是事務提交失敗,那麼該事務中的全部的操做都必須回滾到執行前的樣子,也就是說當事務失敗的時候,也不會對原始數據有影響,看圖說話

 

 

這裏說句額外話,其實 MySQL  也是一個系統,就比如咱們平時開發的 java 的功能系統同樣,MySQL  使用的是本身相應的語言開發出來的一套系統而已,它根據本身須要的功能去設計對應的功能,它即然能作到哪些事情,那麼必然是設計者們當初這麼定義或者是根據實際的場景變動演化而來的。因此你們放平心態,把 MySQL 看成一個系統去了解熟悉他。

到這一步,咱們的執行的 SQL 語句已經被加載到 Buffer Pool 中了,而後開始更新這條語句,更新的操做實際是在Buffer Pool中執行的,那問題來了,按照咱們平時開發的一套理論緩衝池中的數據和數據庫中的數據不一致時候,咱們就認爲緩存中的數據是髒數據,那此時 Buffer Pool 中的數據豈不是成了髒數據?沒錯,目前這條數據就是髒數據,Buffer Pool 中的記錄是小強 數據庫中的記錄是旺財 ,這種狀況 MySQL是怎麼處理的呢,繼續往下看

redo 日誌文件:記錄數據被修改後的樣子

redo 日誌文件:記錄數據被修改後的樣子

除了從磁盤中加載文件和將操做前的記錄保存到 undo 日誌文件中,其餘的操做是在內存中完成的,內存中的數據的特色就是:斷電丟失。若是此時 MySQL 所在的服務器宕機了,那麼 Buffer Pool 中的數據會所有丟失的。這個時候 redo 日誌文件就須要來大顯神通了

畫外音:redo 日誌文件是 InnoDB 特有的,他是存儲引擎級別的,不是 MySQL 級別的

redo 記錄的是數據修改以後的值,無論事務是否提交都會記錄下來,例如,此時將要作的是update students set stuName='小強' where id=1; 那麼這條操做就會被記錄到 redo log buffer 中,啥?怎麼又出來一個 redo log buffer ,很簡單,MySQL 爲了提升效率,因此將這些操做都先放在內存中去完成,而後會在某個時機將其持久化到磁盤中。

 

 

截至目前,咱們應該都熟悉了 MySQL 的執行器調用存儲引擎是怎麼將一條 SQL 加載到緩衝池和記錄哪些日誌的,流程以下:

  1. 準備更新一條 SQL 語句
  2. MySQL(innodb)會先去緩衝池(BufferPool)中去查找這條數據,沒找到就會去磁盤中查找,若是查找到就會將這條數據加載到緩衝池(BufferPool)中
  3. 在加載到 Buffer Pool 的同時,會將這條數據的原始記錄保存到 undo 日誌文件中
  4. innodb 會在 Buffer Pool 中執行更新操做
  5. 更新後的數據會記錄在 redo log buffer 中

上面說的步驟都是在正常狀況下的操做,可是程序的設計和優化並不只是爲了這些正常狀況而去作的,也是爲了那些臨界區和極端狀況下出現的問題去優化設計的

這個時候若是服務器宕機了,那麼緩存中的數據仍是丟失了。真煩,居然數據老是丟失,那能不能不要放在內存中,直接保存到磁盤呢?很顯然不行,由於在上面也已經介紹了,在內存中的操做目的是爲了提升效率。

此時,若是 MySQL 真的宕機了,那麼不要緊的,由於 MySQL 會認爲本次事務是失敗的,因此數據依舊是更新前的樣子,並不會有任何的影響。

好了,語句也更新好了那麼須要將更新的值提交啊,也就是須要提交本次的事務了,由於只要事務成功提交了,纔會將最後的變動保存到數據庫,在提交事務前仍然會具備相關的其餘操做

將  redo Log Buffer 中的數據持久化到磁盤中,就是將 redo log buffer 中的數據寫入到 redo log 磁盤文件中,通常狀況下,redo log Buffer 數據寫入磁盤的策略是當即刷入磁盤(具體策略狀況在下面小總結出會詳細介紹),上圖

 

 

若是 redo log Buffer 刷入磁盤後,數據庫服務器宕機了,那咱們更新的數據怎麼辦?此時數據是在內存中,數據豈不是丟失了?不,此次數據就不會丟失了,由於 redo log buffer 中的數據已經被寫入到磁盤了,已經被持久化了,就算數據庫宕機了,在下次重啓的時候 MySQL 也會將 redo 日誌文件內容恢復到 Buffer Pool 中(這邊個人理解是和  Redis  的持久化機制是差很少的,在  Redis  啓動的時候會檢查 rdb 或者是 aof 或者是二者都檢查,根據持久化的文件來將數據恢復到內存中)

到此爲止,從執行器開始調用存儲引擎接口作了哪些事情呢?

1.準備更新一條 SQL 語句

2.MySQL(innodb)會先去緩衝池(BufferPool)中去查找這條數據,沒找到就會去磁盤中查找,若是查找到就會將這條數據加載

到緩衝池(BufferPool)中 3.在加載到 Buffer Pool 的同時,會將這條數據的原始記錄保存到 undo 日誌文件中

4.innodb 會在 Buffer Pool 中執行更新操做

5.更新後的數據會記錄在 redo log buffer 中

---到此是前面已經總結過的---

6.MySQL 提交事務的時候,會將 redo log buffer 中的數據寫入到 redo 日誌文件中 刷磁盤能夠經過 innodb_flush_log_at_trx_commit 參數來設置

值爲 0 表示不刷入磁盤

值爲 1 表示當即刷入磁盤

值爲 2 表示先刷到 os cache

7.myslq 重啓的時候會將 redo 日誌恢復到緩衝池中

截止到目前位置,MySQL  的執行器調用存儲引擎的接口去執行【執行計劃】提供的 SQL 的時候 InnoDB 作了哪些事情也就基本差很少了,可是這還沒完。下面還須要介紹下 MySQL 級別的日誌文件 bin log

bin log 日誌文件:記錄整個操做過程

上面介紹到的redo log是  InnoDB  存儲引擎特有的日誌文件,而bin log屬因而  MySQL  級別的日誌。redo log記錄的東西是偏向於物理性質的,如:「對什麼數據,作了什麼修改」。bin log是偏向於邏輯性質的,相似於:「對 students 表中的 id 爲 1 的記錄作了更新操做」 二者的主要特色總結以下:

性質 redo Log bin Log
文件大小 redo log 的大小是固定的(配置中也能夠設置,通常默認的就足夠了) bin log 可經過配置參數max_bin log_size設置每一個bin log文件的大小(可是通常不建議修改)。
實現方式 redo logInnoDB引擎層實現的(也就是說是 Innodb  存儲引發過獨有的) bin log是  MySQL  層實現的,全部引擎均可以使用 bin log日誌
記錄方式 redo log 採用循環寫的方式記錄,當寫到結尾時,會回到開頭循環寫日誌。 bin log 經過追加的方式記錄,當文件大小大於給定值後,後續的日誌會記錄到新的文件上
使用場景 redo log適用於崩潰恢復(crash-safe)(這一點其實很是相似與 Redis 的持久化特徵) bin log 適用於主從複製和數據恢復

bin log文件是如何刷入磁盤的?

bin log 的刷盤是有相關的策略的,策略能夠經過sync_bin log來修改,默認爲 0,表示先寫入 os cache,也就是說在提交事務的時候,數據不會直接到磁盤中,這樣若是宕機bin log數據仍然會丟失。因此建議將sync_bin log設置爲 1 表示直接將數據寫入到磁盤文件中。

刷入 bin log 有如下幾種模式

一、 STATMENT

基於 SQL 語句的複製(statement-based replication, SBR),每一條會修改數據的 SQL 語句會記錄到 bin log 中

【優勢】:不須要記錄每一行的變化,減小了 bin log 日誌量,節約了 IO , 從而提升了性能

【缺點】:在某些狀況下會致使主從數據不一致,好比執行sysdate()、slepp()等

二、ROW

基於行的複製(row-based replication, RBR),不記錄每條SQL語句的上下文信息,僅需記錄哪條數據被修改了

【優勢】:不會出現某些特定狀況下的存儲過程、或 function、或 trigger 的調用和觸發沒法被正確複製的問題

【缺點】:會產生大量的日誌,尤爲是 alter table 的時候會讓日誌暴漲

三、MIXED

基於 STATMENT 和 ROW 兩種模式的混合複製( mixed-based replication, MBR ),通常的複製使用 STATEMENT 模式保存 bin log ,對於 STATEMENT 模式沒法複製的操做使用 ROW 模式保存 bin log

那既然bin log也是日誌文件,那它是在什麼記錄數據的呢?

其實 MySQL 在提交事務的時候,不只僅會將 redo log buffer  中的數據寫入到redo log 文件中,同時也會將本次修改的數據記錄到 bin log文件中,同時會將本次修改的bin log文件名和修改的內容在bin log中的位置記錄到redo log中,最後還會在redo log最後寫入 commit 標記,這樣就表示本次事務被成功的提交了。

 

 

若是在數據被寫入到bin log文件的時候,剛寫完,數據庫宕機了,數據會丟失嗎?

首先能夠肯定的是,只要redo log最後沒有 commit 標記,說明本次的事務必定是失敗的。可是數據是沒有丟失了,由於已經被記錄到redo log的磁盤文件中了。在 MySQL 重啓的時候,就會將 redo log 中的數據恢復(加載)到Buffer Pool中。

好了,到目前爲止,一個更新操做咱們基本介紹得差很少,可是你有沒有感受少了哪件事情尚未作?是否是你也發現這個時候被更新記錄僅僅是在內存中執行的,哪怕是宕機又恢復了也僅僅是將更新後的記錄加載到Buffer Pool中,這個時候 MySQL 數據庫中的這條記錄依舊是舊值,也就是說內存中的數據在咱們看來依舊是髒數據,那這個時候怎麼辦呢?

其實 MySQL 會有一個後臺線程,它會在某個時機將咱們Buffer Pool中的髒數據刷到 MySQL 數據庫中,這樣就將內存和數據庫的數據保持統一了。

 

 

 

本文總結

到此,關於Buffer Pool、Redo Log Buffer 和undo log、redo log、bin log 概念以及關係就基本差很少了。

咱們再回顧下 

  1. Buffer Pool 是 MySQL 的一個很是重要的組件,由於針對數據庫的增刪改操做都是在 Buffer Pool 中完成的

  2. Undo log 記錄的是數據操做前的樣子

  3. redo log 記錄的是數據被操做後的樣子(redo log 是 Innodb 存儲引擎特有)

  4. bin log 記錄的是整個操做記錄(這個對於主從複製具備很是重要的意義)

從準備更新一條數據到事務的提交的流程描述

  1. 首先執行器根據 MySQL 的執行計劃來查詢數據,先是從緩存池中查詢數據,若是沒有就會去數據庫中查詢,若是查詢到了就將其放到緩存池中

  2. 在數據被緩存到緩存池的同時,會寫入 undo log 日誌文件

  3. 更新的動做是在 BufferPool 中完成的,同時會將更新後的數據添加到 redo log buffer 中

  4. 完成之後就能夠提交事務,在提交的同時會作如下三件事

  5. (第一件事)將redo log buffer中的數據刷入到 redo log 文件中

  6. (第二件事)將本次操做記錄寫入到 bin log文件中

  7. (第三件事)將 bin log 文件名字和更新內容在 bin log 中的位置記錄到redo log中,同時在 redo log 最後添加 commit 標記

至此表示整個更新事務已經完成

結束語

到此爲止,系統是如何和 MySQL 數據庫打交道,提交一條更新的 SQL 語句到 MySQL,MySQL 執行了哪些流程,作了哪些事情從宏觀上都已經講解完成了。更多的 Buffer Pool 的細節將會在以後的文章中詳解

相關文章
相關標籤/搜索