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

李帆1998發表於2021-06-24

引言

在上篇文章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官方文件,若有不明白或錯漏之處,歡迎留言與我討論。

相關文章