MySQL:一條更新語句是如何執行的


引言

在上篇文章MySQL:一條SQL是如何執行的中我們先講了一條SQL語句是如何執行的,如圖所示:

極客時間林曉斌老師的圖

MySQL 的邏輯架構圖

  1. 客戶端先通過連接器建立連接,連接器自會判斷用戶權限
  2. (如果開啟了查詢緩存並且匹配上key就直接返回結果給客戶端,不執行下面的流程)
  3. 分析器對SQL進行詞法分析與語法分析,明確SQL要做什么
  4. 優化器生成執行計划,選擇索引,明確怎么做
  5. 執行器通過操作存儲引擎讀寫接口來獲取或更新數據,並將執行結果返回給客戶端

但是對於存儲引擎內部的執行流程沒有講到。本文用一條更新語句帶你了解InnoDB的內存結構和磁盤結構,限於篇幅和作者本身知識儲備,或有不詳盡之處,歡迎指出。

本文目的是讓你知道更新過程中涉及到了哪些東西,這個東西的細節和配置就需要你自己去慢慢探索了。

限於篇幅,本文不講解WAL機制(redo log 和 binlog),閱讀此文的朋友最好對此先有所了解

更新流程圖

有點復雜,其實這個是我用來復習的,涉及到的知識點還是挺多的,歡迎取用,下面慢慢講解下圖

上面涉及到了諸多內存與磁盤,下面從InnoDB的內存結構與磁盤結構來理清上述流程。本文主要是想講流程,對InnoDB的內存結構和磁盤結構只會簡單講解,有遺漏或錯誤之處還請指正。

更新流程說明

主要講更新過程中涉及的內存和磁盤,講是什么和會怎么樣,而為什么要這樣放在本文末尾拓展知識里,這里主要先讓你形成更新脈絡。

第一步:更新數據

當我們要更新 id = 2 這條數據時,會先去判斷該記錄是否在數據頁內存中。

  • 如果在,那么更新數據頁內存,此時數據頁內存中的數據和磁盤上數據不一致,我們叫他臟頁
  • 如果不在,先要判斷是否是唯一索引
    • 如果不是,那么將更新內容寫入到Change Buffer中,結束
    • 如果是唯一索引,那么將 id = 2 這條記錄讀入數據頁內存中(干凈頁),然后更新數據頁內存(變臟頁

數據頁內存

數據頁內存是什么

數據頁內存是InnoDB buffer pool的一塊內存區域,存儲的單位是數據頁,例如id = 2 這行記錄和所在的頁數據。

擴展:InnoDB 的數據是按數據頁為單位來讀寫的。也就是說,當需要讀一條記錄的時候,並不是將這個記錄本身從磁盤讀出來,而是以頁為單位,將其整體讀入內存。在 InnoDB 中,每個數據頁的大小默認是 16KB。

為什么需要數據頁內存

我們要知道緩存機制是為了解決CPU高算力和I/O讀寫能力的差距。這里也不例外,對於查詢來說,如果記錄在數據頁內存中那么查詢要快的多,內存讀寫比磁盤讀寫快多了,更新也是一樣的道理,不管是更新到數據頁內存中還是更新到Change Buffer中,都是讀寫內存。

Change Buffer

Change Buffer 是什么

是InnoDB buffer pool的另一塊內存區域,他和數據頁內存不同的是他存儲的是更新內容,例如 把id = 2的這條記錄中某個列的值 從 1 改為 2。下次查詢id = 2這條記錄時,讓磁盤中的記錄(此時值為1) 執行 change buffer中的更新操作,得到列值2,這個過程叫merge,merge后的結果會放入數據頁內存中。

注意這里要判斷是不是唯一索引,只有非唯一索引的更新操作才可以使用change buffer。

為什么非唯一索引才能使用change buffer

對於非唯一索引,往往會有多條記錄,這些記錄往往是隨機存儲的,不在一個數據頁上,假設 id = 2 有 1000條記錄,分散在10個數據頁上,那么就要10次I/O讀,而寫入Change Buffer 是內存寫,所以Change Buffer對更新性能的提升是很明顯的。

為了使用change buffer 提升更新性能,我們是不是可以更多的選擇 普通索引 呢

innodb_change_buffer_max_size 變量允許將更改緩沖區的最大大小配置為緩沖池總大小的百分比。默認情況下, innodb_change_buffer_max_size設置為 25。最大設置為 50。

注意雖然名字叫作 change buffer,實際上它是可以持久化的數據。也就是說,change buffer 在內存中有拷貝,也會被寫入到磁盤上。

change buffer的前身是insert buffer,只能對insert 操作優化;后來升級了,增加了update/delete的支持,名字也改叫change buffer.

第二步:緩存日志內容

  • 當我們對數據頁內存或Change Buffer更新以后,會將更新記錄寫入 redo log buffer。
    • 如果是更新數據頁,那么redo log buffer 記入 update ... id = 2
    • 如果是寫入change buffer, 那么記入 new change buffer item('update ... id = 2')
  • 同時我們會將SQL語句寫入到binlog cache中

磁盤I/O是數據庫里面成本最高的操作之一,前面將數據更新都落在內存上,大大減少了磁盤I/O次數,那么對於寫日志,也有同樣的機制來避免直接對磁盤讀寫。

redo log buffer

在一個事務的更新過程中,日志是要寫多次的。例如如下語句

begin;
insert into t1 ...
insert into t2 ...
commit;

這個事務要往兩個表中插入記錄,插入數據的過程中,生成的日志都得先保存起來,但又不能在還沒 commit 的時候就直接寫到 redo log 文件里。所以,redo log buffer 就是一塊內存,用來先存 redo 日志的。也就是說,在執行第一個 insert 的時候,數據的內存被修改了,redo log buffer 也寫入了日志。執行第二個 insert的時候,再次往redo log buffer中寫入一條日志並更新數據頁內存。

但是,真正把日志寫到 redo log 文件(文件名是 ib_logfile+ 數字),是在執行 commit 語句的時候做的。

binlog cache

binlog 的寫入邏輯比較簡單:事務執行過程中,先把日志寫到 binlog cache,事務提交的時候,再把 binlog cache 寫到 binlog 文件中。

一個事務的 binlog 是不能被拆開的,因此不論這個事務多大,也要確保一次性寫入。
這就涉及到了 binlog cache 的保存問題。

系統給 binlog cache 分配了一片內存,每個線程一個,參數 binlog_cache_size 用於控制單個線程內 binlog cache 所占內存的大小。如果超過了這個參數規定的大小,就要暫存到磁盤。事務提交的時候,執行器把 binlog cache 里的完整事務寫入到 binlog 中,並清空 binlog cache。

每個線程有自己 binlog cache,但是共用同一份 binlog 文件。

第三步:日志寫入磁盤

到了這一步,就要准備寫日志到磁盤了。不管是redo log 還是 binlog,在事務執行過程中,都會先寫入到內存中,只有在事務提交的時候才會寫磁盤。

我們先來看看磁盤讀寫相關的知識點

  • write:指的就是指把日志寫入到文件系統的 page cache,並沒有把數據持久化到磁盤,所以速度比較快。
  • fsync:才是將數據持久化到磁盤的操作。一般情況下,我們認為 fsync 才占磁盤的 IOPS。

再來看MySQL對binlog的寫入策略配置sync_binlog

  • sync_binlog=0 的時候,表示每次提交事務都只 write,不 fsync;
  • sync_binlog=1 的時候,表示每次提交事務都會執行 fsync;
  • sync_binlog=N(N>1) 的時候,表示每次提交事務都 write,但累積 N 個事務后才 fsync。

為了控制 redo log 的寫入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 參數,它有三種可能取值:

  • 設置為 0 的時候,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中 ;
  • 設置為 1 的時候,表示每次事務提交時都將 redo log 直接持久化到磁盤;
  • 設置為 2 的時候,表示每次事務提交時都只是把 redo log 寫到 page cache。

由於本文重點不是探討WAL,所以這里就不重點分析上述配置對異常恢復的影響了,這里我們把兩個參數都設置為1。然后向你介紹組提交機制。

前面說到fsync才占IOPS,那么我們可以盡量延后fsync的執行時間,這樣一次I/O寫入的數據更多,減少了I/O次數。就像redo log buffer一樣,只不過這里把 write 的 page cache 當做了緩沖池。

我們先來看正常情況下原來redo log 和 binlog 的寫入策略:

  1. redo log prepere write
  2. redo log prepere fsync
  3. binlog wirte
  4. binlog fsync
  5. redo log commit write
  6. redo log commit fsync

優化的思路是減少磁盤I/O次數,那么可以讓 fsync 的動作慢一慢,組提交機制如下所示:

  1. redo log prepere write
  2. binlog wirte
  3. redo log prepere fsync
  4. binlog fsync
  5. redo log commit write

兩者對比有兩點差別:

  • 原第二步和第三步調換,也就是說將 redo log prepere fsync 放在 binlog wirte 之后,這樣binlog write 可以緩存更多內容
  • 原第六步 redo log commit fsync 去掉了,這是因為redo log prepere fsyncbinlog fsync 執行完已經能夠滿足異常恢復,內中原因請了解 WAL。

組提交從字面意思很好理解,多組一起提交。

並發場景下,對於 redo log 來說, 多個事務日志都在 redo log buffer中,有一個刷盤了其他事務的日志也會跟着一起刷盤,假設有3個事務同時執行,同時結束,那么提交時只需要fsync一次而不是3次。

對於binlog來說,雖然每個線程都有自己的binlog cache 緩存,但是都共享一個binlog文件,即使我們將 sync_binlog 設置為1,由於組提交機制在 binlog wirte 和 binlog fsync 插入了 redo log fsync,那么在並發場景下,binlog page cache也是可能存在多個事務日志的,這樣也減少了刷盤次數。

如果將 sync_binlog 設為 100,當累計100個事務才fsync,大大減少了IOPS。這樣你可能更容易理解組提交機制了,但要注意如果機器宕機,那么這一百個事務的binlog就丟失了。

兩個JOB:臟頁落盤 和 redo log Buffer 落盤

臟頁落盤

前面我們說過,更新操作要么落在數據頁內存上要么落在Change Buffer上,並不會立刻寫到磁盤上。即使事務提交,我們從上圖可以看到事務提交的核心是對redo log和binlog的操作,並不強調把數據頁中的臟頁刷到磁盤上。那么,數據頁內存中的臟頁是誰把他寫到磁盤上了呢?

這就要說到數據頁的刷盤機制了,正常情況下,系統會在“空閑”的時候自動落盤,除此之外,發生以下情況也會觸發數據頁內存落盤。

  • redo log 滿了
  • MySQL 正常關閉
  • Buffer Pool 內存不足

那么如果系統異常關閉了呢?

在崩潰恢復場景中,InnoDB 如果判斷到一個數據頁可能在崩潰恢復的時候丟失了更新,就會將它讀到內存,然后讓 redo log 更新內存內容。更新完成后,內存頁變成臟頁,就回到了第一種情況的狀態。

redo log 落盤

前面說到 redo log 會先寫到 redo log buffer 中,然后在事務提交的時候刷到磁盤,但是要注意redo log 在事務沒有提交的時候也是會刷到磁盤的,MySQL有個JOB每隔1秒就會把redo log buffer 刷到磁盤。注意redo log buffer 是在語句執行時就寫入了,所以redo log 落盤時可能事務還沒有提交。

注意Binlog不會在事務未提交前落盤,Binlog只會在事務提交后才刷新到磁盤。

實際上,除了后台線程每秒一次的輪詢操作外,還有兩種場景會讓一個沒有提交的事務的 redo log 寫入到磁盤中。

  • 一種是,redo log buffer 占用的空間即將達到 innodb_log_buffer_size 一半的時候,后台線程會主動寫盤。注意,由於這個事務並沒有提交,所以這個寫盤動作只是 write,而沒有調用 fsync,也就是只留在了文件系統的 page cache。
  • 另一種是,並行的事務提交的時候,順帶將這個事務的 redo log buffer 持久化到磁盤。假設一個事務 A 執行到一半,已經寫了一些 redo log 到 buffer 中,這時候有另外一個線程的事務 B 提交,如果 innodb_flush_log_at_trx_commit 設置的是 1,那么按照這個參數的邏輯,事務 B 要把 redo log buffer 里的日志全部持久化到磁盤。這時候,就會帶上事務 A 在 redo log buffer 里的日志一起持久化到磁盤。

總結

這篇文章寫得馬馬虎虎,有諸多不詳盡之處,因為涉及到的內存結構和磁盤結構過多,還有WAL機制,實在不是一篇文章能夠講完的。所以本文主要是想講是什么,讓你知道更新過程中涉及到哪些東西,至於為什么要這樣,buffer pool 還有哪些細節,很多點都沒有挑明,建議參考MySQL官方文檔,若有不明白或錯漏之處,歡迎留言與我討論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM