一個查詢語句通過哪些步驟
此次咱們從MySQL的總體架構來說SQL的執行過程,以下圖:node
在總體分爲兩部分Server和引擎層,這裏引擎層我使用InnoDB去代替,引擎層的設計是插件形式的,能夠任意替代,接下來咱們開始介紹每一個組件的做用:
Server層
鏈接器:鏈接器負責跟客戶端創建鏈接、獲取權限、維持和管理鏈接;
查詢緩存:服務的查詢緩存,若是能找到對應的查詢,則沒必要進行查詢解析,優化,執行等過程,直接返回緩存中的結果集;
解析器:解析器會根據查詢語句,構造出一個解析樹,主要用於根據語法規則來驗證語句是否正確,好比SQL的關鍵字是否正確,關鍵字的順序是否正確;
優化器:解析樹轉化爲查詢計劃,通常狀況下,一條查詢能夠有不少種執行方式,最終返回相同的結果,優化器就是根據成本找到這其中最優的執行計劃;
執行器:執行計劃調用查詢執行引擎,而查詢引擎經過一系列API接口查詢到數據;mysql
InnoDB
後臺線程:負責刷新內存池中的數據,保證緩存池中的內存緩存是最近的數據,將已修改的數據刷新到磁盤文件,同時保證數據庫發生異常的狀況能恢復到正常狀況;
內存池:內存池也能夠叫作緩存池,主要爲彌補磁盤的速度較慢對數據庫產生的影響,查詢的時候,首先將磁盤讀到的頁的數據放在內存池中,下次讀取的時候直接從內存池中讀取數據,修改數據的時候,首先修改內存池中的數據,而後後臺線程按照必定的頻率刷新到磁盤上。
文件:主要是指表空間文件,而外還有一些日誌文件;
以上大體的介紹一下MySQL的總體架構,其中內存池、文件、後臺線程等一些跟細節的東西沒有介紹,後面咱們介紹其餘時候在帶出來其中的詳細的部分,另外在附上一張MySQL5.6總體架構圖:算法
InnoDB如何保存數據
這部份內容是創建在上部分的基礎上,須要對內存池、文件、後臺線程深刻到細節去了解組成,接下咱們仍是分三部分開始講解:sql
文件
文件分爲日誌文件和存儲文件,分爲兩部分講起:數據庫
存儲文件
存儲文件也就是表數據的存儲,總體的存儲結構以下圖:緩存
表空間主要分爲兩類文件,一類是共享表空間,一類是每張表單獨的表空間,單獨的表空間存放的是表中的數據、索引等信息,共享的表空間主要是存儲事務信息、回滾信息等數據;表空間由段(Segment)、區(Extend)、頁(Page)、行(Row)組成,接下來簡單介紹一下這4種結構:
- 段(Segment)
常見的Segment有數據段、索引段、回滾段等, 數據段爲B+樹的葉子節點(Leaf node segment)、索引段爲B+樹的非葉子節點(Non-leaf node segment)。以下圖:

每建立索引就會建立一個索引段,索引段的葉子節點指向數據段,經過這樣的組合來完成咱們查詢數據時候須要,所以建立索引越多,會致使須要構建的索引段就越多,致使插入數據時間就會增長。
- 區(Extend)
區是構成段的基本元素,一個段由若干個區構成,一個區是物理上連續分配的一段空間,每個段至少會有一個區,在建立一個段時會建立一個默認的區。若是存儲數據時,一個區已經不足以放下更多的數據,此時須要從這個段中分配一個新的區來存放新的數據。一個段所管理的空間大小是無限的,能夠一直擴展下去,可是擴展的最小單位就是區。每一個區大小固定爲1MB,區由頁組成,爲保證區中Page的連續性一般InnoDB會一次從磁盤中申請4-5個區。在默認Page的大小爲16KB的狀況下,一個區則由64個連續的Page組成。
- 頁(Page):
頁是構成區的基本單位,是InnoDB磁盤管理的最小單位。在邏輯上(頁面號都是從小到大連續的)及物理上都是連續的。在向表中插入數據時,若是一個頁面已經被寫完,系統會從當前區中分配一個新的空閒頁面處理使用,若是當前區中的64個頁面都被分配完,系統會從當前頁面所在段中分配一個新的區,而後再從這個區中分配一個新的頁面來使用。
- 行(Row):
InnoDB按照行進行存放數據,每一個頁存放的數據有硬性規定,最多存放16KB,當數據大於16KB的時候會發生行溢出,會存儲到而外的頁(Uncompressed BLOB Page)當中。
日誌文件
關於日誌文件這裏主要介紹三種日誌文件,分別爲binlog、redo log、redo log:服務器
binlog
binlog用於記錄數據庫執行的寫入性操做(不包括查詢)信息,以二進制的形式保存在磁盤中。binlog是mysql的邏輯日誌,而且由Server層進行記錄,使用任何存儲引擎的mysql數據庫都會記錄binlog日誌。binlog是經過追加的方式進行寫入的,能夠經過max_binlog_size參數設置每一個binlog文件的大小,當文件大小達到給定值以後,會生成新的文件來保存日誌。架構
binlog日誌格式
- ROW
基於行的複製,不記錄每條sql語句的上下文信息,僅需記錄哪條數據被修改了。
優勢:
不會出現某些特定狀況下的存儲過程、或function、或trigger的調用和觸發沒法被正確複製的問題;
缺點:
由於每行都要記錄日誌,會照成日誌量暴漲;
- STATMENT
基於SQL語句的複製,每一條會修改數據的sql語句會記錄到binlog中。
優勢:
不須要記錄每一行的變化,減小了binlog日誌量,節約了IO, 從而提升了性能;
缺點:
在某些狀況下會致使主從數據不一致,好比執行sysdate()等函數的時候。
- MIXED
基於STATMENT和ROW兩種模式的混合複製,通常的複製使用STATEMENT模式保存binlog,對於STATEMENT模式沒法複製的操做使用ROW模式保存binlog
使用場景
binlog的主要使用場景有兩個,分別是主從複製和數據恢復;併發
- 主從複製
在Master端開啓binlog,而後將binlog發送到各個Slave端,Slave端重放binlog從而達到主從數據一致。
- 數據恢復
恢復到某一時刻的日誌,經過使用mysqlbinlog工具來恢復數據;
刷盤時機
對於InnoDB存儲引擎而言,只有在事務提交時纔會記錄biglog,此時記錄還在內存中,Mysql經過sync_binlog參數控制biglog的刷盤時機,取值範圍是0-N,
N表明多少條之後開始進行刷盤,當設置爲0的時候由系統自行判斷什麼時候寫入磁盤,當設置爲1的時候,至關於每次Commit就進行刷盤一次,可是這個時候要注意與redo log日誌可能存在不一致的狀況,這個時候須要設置innodb_support_xa參數也爲1,這樣就能保證兩個兩份日誌是同步的。app
redo log
redo log包括兩部分:redo log buffer和redo log file,redo log buffer是在內存中,redo log file是在磁盤上,當MySQL執行DML語句的時候,首先寫入redo log buffer,而後按照必定條件順序寫入redo log file,何時會觸發buffer內容寫入到file當中呢?
- InnoDB後臺線程中的主線程,每秒會進行一次將buffer中的數據刷入到磁盤當中;
- 經過設置innodb_flush_log_at_trx_commit參數,來控制刷新的時機,當設置爲 1 的時候,事務每次提交都會將 log buffer 中的日誌寫入 os buffer 並調用 fsync()刷到 log file on disk中。這種方式即便系統崩潰也不會丟失任何數據,可是由於每次提交都寫入磁盤,IO 的性能較差。當設置爲 0 的時候,事務提交時不會將 log buffer 中日誌寫入到 os buffer,而是每秒寫入 os buffer 並調用fsync()寫入到 log file on disk 中。也就是說設置爲 0 時是(大約)每秒刷新寫入到磁盤中的,當系統崩潰,會丟失 1 秒鐘的數據。當設置爲 2 的時候,每次提交都僅寫入到 os buffer,而後是每秒調用 fsync() 將 os buffer 中的日誌寫入到 log file on disk。

redo log日誌格式
redo log記錄數據頁的變動,在設計上redo log採用了大小固定,循環寫入的方式,當寫到結尾時,會回到開頭循環寫日誌,本質上就是一個環狀。
rdo log刷盤完成之後,其實數據最終還沒刷新到真正數據磁盤上,所以還須要刷新到真正的數據磁盤上,本質上redo log的設計就是爲了下降對數據頁刷盤的要求,接下來咱們結合上圖來聊聊是如何刷新到數據文件文件上的,也就是checkpoint機制:
首先看下環,環上有4個ib_logfile_*的文件,該文件就是存儲redo log日誌的文件,能夠經過控制innodb_log_files_in_group的數量來控制文件的個數,經過innodb_log_file_size來控制文件的大小,不介意將文件的設置的太大,若是設置的太大會致使奔潰恢復的時候過於緩慢,也不能設置的過小,這樣可能致使一次事務須要切換屢次日誌文件,此外還會照成頻繁寫入磁盤文件,照成性能抖動;
接下來咱們看兩個端點write pos和check point,write pos到check point之間的部分是redo log空着的部分,用於記錄新的記錄;check point到write pos之間是redo log待落盤的數據頁更改記錄。當write pos追上check point時,會先推進check point向前移動,空出位置再記錄新的日誌。
InnoDB在啓動的時候,無論上次數據庫是否正常關閉,都會嘗試進行恢復操做,分爲兩種狀況:
- checkpoint表示已經完整刷到磁盤上data page上的LSN,所以恢復時僅須要恢復從checkpoint開始的日誌部分,LSN表示寫入日誌的字節的總量,例如,當數據庫在上一次checkpoint的LSN爲10000時宕機,且事務是已經提交過的狀態。啓動數據庫時會檢查磁盤中數據頁的LSN,若是數據頁的LSN小於日誌中的LSN,則會從檢查點開始恢復。
- 在宕機前正處於checkpoint的刷盤過程,且數據頁的刷盤進度超過了日誌頁的刷盤進度。這時候一宕機,數據頁中記錄的LSN就會大於日誌頁中的LSN,在重啓的恢復過程當中會檢查到這一狀況,這時超出日誌進度的部分將不會重作,由於這自己就表示已經作過的事情,無需再重作。
在恢復的過程當中由於redo log記錄的是數據頁的物理變化,所以恢復的時候速度比邏輯日誌(如binlog)要快不少;
使用的場景
MySQL用來確保事務的持久性。redo log記錄事務執行後的狀態,用來恢復未寫入data file的已成功事務更新的數據。防止在發生故障的時間點,尚有髒頁未寫入磁盤,在重啓mysql服務的時候,根據redo log進行重作,從而達到事務的持久性這一特性。
undo log
undo log記錄數據的邏輯變化,用戶事務的回滾操做和MVCC, undo log 存放在共享表空間中,以段(rollback segment)的形式存在。
undo log日誌格式
邏輯格式的日誌,在事務進行回滾的時候,能夠將數據從邏輯上恢復至事務以前的狀態。
使用的場景
保證數據的原子性,保存了事務發生以前的數據的一個版本,能夠用於回滾,同時能夠提供多版本併發控制下的讀(MVCC),也即非鎖定讀。
刷盤時機
當事務提交以後,undo log並不能立馬被刪除,而是放入待清理的鏈表,由Purge線程判斷是否由其餘事務在使用undo段中表的上一個事務以前的版本信息,決定是否能夠清理undo log的日誌空間。
內存池
InnoDB 存儲引擎是基於磁盤存儲的,也就是說數據都是存儲在磁盤上的,因爲 CPU 速度和磁盤速度之間的鴻溝, InnoDB 引擎使用緩衝池技術來提升數據庫的總體性能。內存池簡單來講就是一塊內存區域.在數據庫中進行讀取頁的操做,首先將從磁盤讀到的頁存放在內存池中,下一次讀取相同的頁時,首先判斷該頁是否是在內存池中,若在,稱該頁在內存池中被命中,直接讀取該頁。不然,讀取磁盤上的頁。對於數據庫中頁的修改操做,首先修改在內存池中頁,而後再以必定的頻率刷新到磁盤,並非每次頁發生改變就刷新回磁盤。
內存池中緩存的信息主要有:index page、data page、insert buffer、自適應哈希索引、 lock info、數據字典信息等。索引頁和數據頁佔緩衝池的很大一部分。在InnoDB中,內存池中的頁大小默認爲16KB,和磁盤的頁的大小默認同樣。咱們已經介紹過數據文件的存儲結構相信你們對緩存結構的內容也會有必定理解,咱們就不單獨介紹了,後面只會重點強調一下insert buffer和自適應哈希索引這兩塊內容,以及擴展下內存池的設計原理。
Insert Buffer
Insert Buffer的設計,對於非彙集索引的插入和更新操做,不是每一次直接插入到索引頁中,而是先判斷插入非彙集索引頁是否在緩衝池中,若存在,則直接插入,不存在,則先放入一個Insert Buffer對象中。數據庫這個非彙集的索引已經插到葉子節點,而實際並無,只是存放在另外一個位置。而後再以必定的頻率和狀況進行Insert Buffer和輔助索引頁子節點的merge(合併)操做,這時一般能將多,這就大大提升了對於非彙集索引插入的性能。這個時候可能會照成一種狀況,當MySQL數據庫發生宕機的時候有有大量的Insert Buffer沒有被合併到非彙集索引的頁當中的時候,這個時候MySQL恢復須要很長的時間。
須要知足的條件:
索引是非彙集索引,索引不是惟一的;
對於具體的實現咱們下次再聊;
自適應哈希索引
InnoDB存儲引擎會監控對錶上各索引頁的查詢。若是觀察到創建哈希索引能夠提高速度,這簡歷哈希索引,稱之爲自適應哈希索引。AHI是經過緩衝池的B+樹頁構造而來的。所以創建的速度很是快,且不要對整張表構建哈希索引。InnoDB存儲引擎會自動根據訪問的頻率和模式來自動的爲某些熱點頁創建哈希索引。
後臺線程
Master Thread
這是最核心的一個線程,主要負責將緩衝池中的數據異步刷新到磁盤,保證數據的一致性,包括贓頁的刷新、合併插入緩衝等。
IO Thread
在 InnoDB 存儲引擎中大量使用了異步 IO 來處理寫 IO 請求, IO Thread 的工做主要是負責這些 IO 請求的回調處理。
Purge Thread
事務被提交以後, undo log 可能再也不須要,所以須要 Purge Thread 來回收已經使用並分配的 undo頁. InnoDB 支持多個 Purge Thread, 這樣作能夠加快 undo 頁的回收。
完成總體功能介紹之後,咱們開始聊聊數據如何插入到InnoDB引擎上的:
假設場景以下:
首先咱們建立一張表T,主鍵爲Id,輔助索引爲a
create table T(id int primary key, a int not null, name varchar(16),index (a))engine=InnoDB;
接下來插入一條數據,
insert into t(id,a,name) values(id1,a1,'哈哈'),(id2,a2,'哈哈哈');
咱們介紹過MySQL讀取數據的流程,Server層咱們仍是會通過鏈接器、解析器、優化器、執行器這些東西,這些咱們就不介紹了,咱們主要介紹剩下的操做:
插入數據時候可能有兩種場景:
第一種場景:假設Id1這條數據在內存池中,
- 直接更新Buffer Pool中的Index Page和Data Page;
- 寫入redo log中,處於預提交狀態;
- 寫入binlog中,
- 提交事務,處於commit狀態,兩階段提交;
- 後臺線程寫入到數據文件的索引段和數據段中;
第二種場景假設id2這條數據再也不內存池中,
- 數據寫入到內存池中,非彙集索引寫入到Insert Buffer,其餘數據寫入Data Page中;
- 後續的動做保持和上面剩下的步驟同樣。
擴展閱讀
咱們來聊聊內存池(Buffer Pool)運行原理,能夠從如下3個方面來看:
- 如何管理緩存的頁?
InnoDB爲每個緩存頁都建立了一些控制信息,這些控制信息包括該頁所屬的表空間編號、頁號、頁在Buffer Pool中的地址、LSN等信息,每一個緩存頁對應的控制信息佔用的內存大小是相同的,咱們就把每一個頁對應的控制信息佔用的一塊內存稱爲一個控制塊吧,控制塊和緩存頁是一一對應的,它們都被存放到 Buffer Pool 中,其中控制塊被存放到 Buffer Pool 的前邊,緩存頁被存放到 Buffer Pool 後邊,因此整個Buffer Pool對應的內存空間看起來就是這樣的:

碎片就是空間不夠分配的緩存頁。
當咱們最初啓動MySQL服務器的時候,須要完成對Buffer Pool的初始化過程,就是分配Buffer Pool的內存空間,把它劃分紅若干對控制塊和緩存頁。可是此時並無真實的磁盤頁被緩存到Buffer Pool中,以後隨着程序的運行,會不斷的有磁盤上的頁被緩存到Buffer Pool中,接下來會有一個問題就是怎麼區分Buffer Pool中哪些緩存頁是空閒的,哪些已經被使用?咱們最好在某個地方記錄一下哪些頁是可用的,咱們能夠把全部空閒的頁包裝成一個節點組成一個鏈表,這個鏈表也能夠被稱做Free鏈表。由於剛剛完成初始化的Buffer Pool中全部的緩存頁都是空閒的,因此每個緩存頁都會被加入到Free鏈表中,總體設計以下圖:

從圖中能夠看出,Free鏈表包含着鏈表的頭節點地址,尾節點地址,以及當前鏈表中節點的數量等信息。每一個Free鏈表的節點中都記錄了某個緩存頁控制塊的地址,而每一個緩存頁控制塊都記錄着對應的緩存頁地址,因此至關於每一個Free鏈表節點都對應一個空閒的緩存頁。
每當須要從磁盤中加載一個頁到Buffer Pool中時,就從Free鏈表中取一個空閒的緩存頁,而且把該緩存頁對應的控制塊的信息填上,而後把該緩存頁對應的Free鏈表節點從鏈表中移除,表示該緩存頁已經被使用了。
- 緩存的淘汰?
機器的內存大小是有限的,因此MySQL的InnoDB Buffer Pool的大小一樣是有限的,若是須要緩存的頁佔用的內存大小超過了Buffer Pool大小,InnoDB Buffer Pool採用經典的LRU算法來進行頁面淘汰,以提升緩存命中率。當Buffer Pool中再也不有空閒的緩存頁時,就須要淘汰掉部分最近不多使用的緩存頁。
當咱們須要訪問某個頁時,能夠這樣處理LRU鏈表:
1.若是該頁不在Buffer Pool中,在把該頁從磁盤加載到Buffer Pool中的緩存頁時,就把該緩存頁包裝成節點塞到鏈表的頭部。
2.若是該頁在Buffer Pool中,則直接把該頁對應的LRU鏈表節點移動到鏈表的頭部。
可是這樣作會有一些性能上的問題,好比你的一次全表掃描或一次邏輯備份就把熱數據給衝完了,就會致使致使緩衝池污染問題!Buffer Pool中的全部數據頁都被換了一次血,其餘查詢語句在執行時又得執行一次從磁盤加載到Buffer Pool的操做,而這種全表掃描的語句執行的頻率也不高,每次執行都要把Buffer Pool中的緩存頁刷新一次,這嚴重的影響到其餘查詢對 Buffer Pool 的使用,下降了緩存命中率。
針對這種場景InnoDB存儲引擎對傳統的LRU算法作了一些優化,在InnoDB中加入了midpoint。新讀到的頁,雖然是最新訪問的頁,但並非直接插入到LRU列表的首部,而是插入LRU列表的midpoint位置。這個算法稱之爲midpoint insertion stategy。默認配置插入到列表長度的5/8處。midpoint由參數innodb_old_blocks_pct控制。
midpoint以前的列表稱之爲new列表,以後的列表稱之爲old列表。能夠簡單的將new列表中的頁理解爲最爲活躍的熱點數據。
- 髒頁如何實現刷新?
更新是在緩存池中先進行的,那它就和磁盤上的頁不一致了,這樣的緩存頁也被稱爲髒頁。因此須要考慮這些被修改的頁面何時刷新到磁盤?固然,最簡單的作法就是每發生一次修改就當即同步到磁盤上對應的頁上,可是頻繁的往磁盤中寫數據會嚴重的影響程序的性能。因此每次修改緩存頁後,咱們並不着急當即把修改同步到磁盤上,而是在將來的某個時間點進行同步,由後臺刷新線程依次刷新到磁盤,實現修改落地到磁盤。
可是若是不當即同步到磁盤的話,那以後再同步的時候咱們怎麼知道Buffer Pool中哪些頁是髒頁,哪些頁歷來沒被修改過呢?咱們須要建立一個存儲髒頁的鏈表,凡是在LRU鏈表中被修改過的頁都須要加入這個鏈表中,由於這個鏈表中的頁都是須要被刷新到磁盤上的,因此也叫Flush鏈表,鏈表的構造和Free鏈表差很少,這裏的髒頁修改指的此頁被加載進Buffer Pool後第一次被修改,只有第一次被修改時才須要加入Flush鏈表,若是這個頁被再次修改就不會再放到Flush鏈表了,由於已經存在。須要注意的是,髒頁數據實際還在LRU鏈表中,而Flush鏈表中的髒頁記錄只是經過指針指向LRU鏈表中的髒頁。
結束
歡迎你們點點關注,點點贊,感謝!