SQL語句執行的經過
從用戶發起請求,到服務接口調用MySQL驅動,MySQL服務器執行完SQL語句返回結果中間發生了什么?首先放一張圖來看整個過程使用到的各個組件,然后再對各個過程進行分析。
1. 連接過程
以Openresty服務器為例,Openresty是多進程+I/O多路復用結構(Nginx的I/O模型),可以支撐高的并發,一個Worker就是一個進程,一個進程可以處理多條請求。
我們知道當需要執行SQL語句時需要先于MySQL服務器建立連接,如果每個一個請求都建立一個連接,使用完再關閉連接,如果頻繁的創建和銷毀連接顯然是不合理的,浪費系統資源造成性能下降,這時連接池就出現了。
連接池
連接池會維護多個(長)連接,一個SQL語句執行時分配一個連接,使用完不會銷毀連接,而是放到空閑隊列中等待下次使用,這樣可以在高并發的場景大大減少創建、銷毀連接帶來的性能問題。
連接器
類似Web服務器通過連接池維護與數據庫服務器的連接,MySQL的連接器提供了同樣的功能,也維護了一個連接池,不同的是MySQL連接器同時還有權限驗證的功能。
- 修改密碼不會影響已經建立的鏈接。
- 連接完成后如果沒有操作,改連接就會處于空閑狀態,可以使用
show processlist
命令查看,如果長時間沒有操作連接器會在到達超時時間后斷開它。
長連接
長連接是客戶端持續有請求,使用的是同一個連接,建立連接的過程通常是比較慢的,建議盡量使用長連接。但是長連接累計較多時可能會導致內存過大(內存管理在連接對象里),比系統強行kill,引起MySQL異常重啟,可以使用以下兩種方法解決:
- 定期斷開長連接。
- 如果是MySQL5.7及以上的版本,可以在每次執行一個比較大的操作后執行
mysql_reset_connection
重新初始化連接資源(不會重新建立連接)。
2. 執行過程
查詢緩存
如果是查詢語句,而且開啟了查詢緩存,連接器拿到一個查詢請求后,會先查看查詢緩存是否有(之前執行過這條語句)。緩存key為sql語句,value是查詢結果。
- 不建議開啟查詢緩存,除非是基本不會變的數據表。因為只要對表有更新,該表上的所有查詢緩存都會清空,導致查詢命中率很低。
分析器
分析器的功能就是對SQL語句做詞法分析和語法分析,解析這條語句要干什么,語法錯誤會返回錯誤提醒。
優化器
優化器是在表中有多個索引的時候決定使用哪個索引,或者有多表關聯(join)的時候決定各個表的連接順序。
執行器
通過分析器知道了要做什么,優化器知道了改怎么做,執行器就是真正的語句執行階段。開始執行的時候要先判斷對表是否有權限(在優化器之前也會做預檢查)。執行器會調用存儲引擎提供的接口進行讀寫操作。
3. 更新語句執行過程
查詢語句是只讀的,比較簡單,經過一系列組件最終查詢到結果返回。但是更新語句就相對復雜一些,涉及到兩個日志模塊:redo log和binlog。
redo log(重做日志)
如果每次更新都要刷盤,整個過程磁盤IO成本、查詢成本都比較高,為了提升更新效率,InnoDB引擎提供了redo log(順序寫入速度很快)。
WAL(Write-Ahead Logging):先寫日志,再寫磁盤。當有一條記錄需要更新的時候,InnoDB引擎會先將記錄寫入redo log并更新內存,這時候更新就算完成了,再需要的時候再將這個操作更新到磁盤里。
日志結構:redo log大小是固定的,比如配置一組4個文件,每個文件1G,
就可以記錄總共4G的記錄。從頭開始寫,寫完后又回到開頭循環寫入。
crash-safe:故障安全,redo log除了能提高更新操作的效率,同時還保證了故障安全,在數據庫異常時不會導致數據丟失。
binlog(歸檔日志)
MySQL最開始沒有InnoDB引擎,binlog日志只用于歸檔和復制,只依靠binlog沒有crash-safe能力。
- redo log是InnoDB引擎獨有的,屬于存儲層;binlog是MySQL提供的,屬于server層
- redo log是物理日志,記錄在某個數據頁上做了什么修改;binlog是邏輯日志,記錄SQL語句的原始邏輯
- redo log是循環寫的,空間固定,用完會從頭開始寫;binlog是追加寫的,一定大小后切換到下一個文件,不會覆蓋
Buffer Pool緩沖池
InnoDB重要的內存結構,數據的操作都是在Buffer Pool中操作的,如果數據不在緩沖內存中,會先從磁盤中讀取到數據頁到緩沖池,然后再執行相關操作。
update執行過程
update T set k = k + 2 where id = '1' limit 1
- 執行器調引擎讀接口找id=2這一行,如果數據頁本來在內存就直接返回,否則先從磁盤load到內存中再返回。
- 然后執行器將k值加上2,得到新的一行數據,在調用引擎的寫接口寫入這行新數據。
- 引擎將這行數據更新到內存中,同時將更新操作記錄到redo log,此時redo log處于prepare狀態,然后告知執行器執行完成,可以提交事務。
- 執行器生成這個操作的binlog并寫入磁盤。
- 執行器調用存儲引擎的事務提交接口,引擎把剛寫入redo log改為commit狀態,更新完成。
兩階段提交
保證了crash-safe能力,如果不使用兩階段提交,使用binlog恢復數據庫時會導致與原數據庫狀態不一致。
假如不使用兩階段提交,在寫日志時機器發生故障:
- redo log寫入(比如k,本來為0,執行更新后
k = 2)后發生故障,binlog未寫入。由于redo log寫完之后即使系統崩潰,也會能將數據恢復,恢復后這一行數據k=2。但是binlog沒寫完就crash,binlog沒有記錄這條語句,如果使用binlog來恢復時會少一個事務,恢復后的k=0,原數據庫k=2。 - binlog寫入后發生故障,redo log未寫入。redo log為寫入,崩潰后這個事務無效,k=0。但是binlog已經記錄了更新語句,之后恢復時會多出一個事務,恢復后k=2,原數據庫k=0。
總結
- MySQL連接器使用連接池維護連接,并進行檢查權限,接收一個SQL語句
- 然后通過分析器、優化器知道如何執行SQL語句
- 通過執行器與存儲引擎交互,完成數據的讀寫。
- 數據更新同時會寫入兩個重要的日志文件:redo log和binlog,并通過兩階段提交保證了crash-safe能力。