假設一個用戶管理系統,每一個人註冊都有一個惟一的手機號,並且業務代碼已經保證了不會寫入兩個重複的手機號。若是用戶管理系統須要按照手機號查姓名,就會執行相似這樣的 SQL 語句:mysql
select name from users where mobile = '15202124529';
sql
一般會考慮在 mobile 字段上建索引。因爲手機號字段相對較大,一般基本不會把手機號當作主鍵,那麼如今就有兩個選擇:數據庫
1. 給 id_card 字段建立惟一索引 2. 建立一個普通索引
若是業務代碼已經保證了不會寫入重複的身份證號,那麼這兩個選擇邏輯上都是正確的。數組
如圖:假設字段 k 上的值都不重複
緩存
接下來,就從這兩種(ID,k)索引對查詢語句和更新語句的性能影響來進行分析異步
假設,執行查詢的語句是 select id from T where k=5。這個查詢語句在索引樹上查找的過程,先是經過 B+ 樹從樹根開始,按層搜索到葉子節點,也就是圖中右下角的這個數據頁,而後能夠認爲數據頁內部經過二分法來定位記錄(數據頁內部經過有序數組保存節點。數據頁之間經過雙向鏈表串接)。性能
那麼,這個不一樣帶來的性能差距會有多少呢?答案是,微乎其微。優化
緣由:除非 Key 的列很是大,有連續多個 Key 佔滿了一個 page,纔會引發一次 page 的 IO,這樣纔會產生比較明顯的性能差別,從均攤上看,差別幾乎能夠不算。spa
InnoDB 的數據是按數據頁爲單位來讀寫的。也就是說,當須要讀一條記錄的時候,並非將這個記錄自己從磁盤讀出來,而是以頁爲單位,將其總體讀入內存。在 InnoDB 中,每一個數據頁的大小默認是 16KB。線程
爲了說明普通索引和惟一索引對更新語句性能的影響這個問題,須要先介紹一下 change buffer
經過這種方式就能保證這個數據邏輯的正確性
須要說明的是,雖然名字叫做 change buffer,實際上它是能夠持久化的數據。也就是說,change buffer 在內存中有拷貝,也會被寫入到磁盤上。
把change buffer中的操做,應用到舊的數據頁,獲得新的數據頁的過程,應該稱爲merge。
Ps. 除了訪問這個數據頁會觸發 merge 外,系統有後臺線程會按期 merge。在數據庫正常關閉(shutdown)的過程當中,也會執行 merge 操做。
(change buffer的merge操做,先把change buffer的操做更新到內存的數據頁中,此操做寫到redo log中,mysql未宕機,redo log寫滿後須要移動check point點時,經過判斷內存中數據和磁盤是否一致便是否是髒頁來刷新到磁盤中,當mysql宕機後沒有內存即沒有髒頁,經過redo log來恢復。)
顯然,若是可以將更新操做先記錄在 change buffer,減小讀磁盤,語句的執行速度會獲得明顯的提高。
並且,數據讀入內存是須要佔用 buffer pool 的,因此這種方式還可以避免佔用內存,提升內存利用率。
對於惟一索引來講,全部的更新操做都要先判斷這個操做是否違反惟一性約束。
好比,要插入 (4,400) 這個記錄,就要先判斷如今表中是否已經存在 k=4 的記錄,而這必需要將數據頁讀入內存才能判斷。
若是都已經讀入到內存了,那直接更新內存會更快,就不必使用 change buffer 了。
所以,惟一索引的更新就不能使用 change buffer,實際上也只有普通索引可使用。
change buffer 用的是 buffer pool 裏的內存,所以不能無限增大。change buffer 的大小,能夠經過參數 innodb_change_buffer_max_size 來動態設置。這個參數設置爲 50 的時候,表示 change buffer 的大小最多隻能佔用 buffer pool 的 50%。
Ps. 數據庫緩衝池(buffer pool) https://www.jianshu.com/p/f9ab1cb24230
理解了 change buffer 的機制,那麼若是要在這張表中插入一個新記錄 (4,400) 的話,InnoDB 的處理流程是怎樣的
一、第一種狀況是:這個記錄要更新的目標頁在內存中。
這樣看來,普通索引和惟一索引對更新語句性能影響的差異,只是一個判斷,只會耗費微小的 CPU 時間。但,這不是關注的重點
二、第二種狀況是,這個記錄要更新的目標頁不在內存中。這時,InnoDB 的處理流程以下:
將數據從磁盤讀入內存涉及隨機 IO 的訪問,是數據庫裏面成本最高的操做之一。change buffer 由於減小了隨機磁盤訪問,因此對更新性能的提高是會很明顯的。
change buffer主要是將更新操做緩存起來,異步處理. 這樣每次更新過來,直接記下change buffer便可,速度很快,將屢次寫磁盤變爲一次寫磁盤
經過上面的分析,已經清楚了使用 change buffer 對更新過程的加速做用,也清楚了 change buffer 只限於用在普通索引的場景下,而不適用於惟一索引。
由於 merge 的時候是真正進行數據更新的時刻,而 change buffer 的主要目的就是將記錄的變動動做緩存下來,因此在一個數據頁作 merge 以前,change buffer 記錄的變動越多(也就是這個頁面上要更新的次數越多),收益就越大。
所以,對於寫多讀少的業務來講,頁面在寫完之後立刻被訪問到的機率比較小,此時 change buffer 的使用效果最好。這種業務模型常見的就是帳單類、日誌類的系統。(適合寫多讀少的場景,讀多寫少反倒會增長change buffer的維護代價)
反過來,假設一個業務的更新模式是寫入以後立刻會作查詢,那麼即便知足了條件,將更新先記錄在 change buffer,但以後因爲立刻要訪問這個數據頁,會當即觸發 merge 過程。這樣隨機訪問 IO 的次數不會減小,反而增長了 change buffer 的維護代價。因此,對於這種業務模式來講,change buffer 反而起到了反作用。(若是當即對普通索引的更新操做結果執行查詢,就會觸發merge操做,磁盤中的數據會和change buffer 的操做記錄進行合併,產生大量io)
綜上分析,普通索引和惟一索引應該怎麼選擇:
其實,這兩類索引在查詢能力上是沒差異的,主要考慮的是對更新性能的影響。因此,建議儘可能選擇普通索引。
若是全部的更新後面,都立刻伴隨着對這個記錄的查詢,那麼應該關閉 change buffer。
而在其餘狀況下,change buffer 都能提高更新性能。在實際使用中,普通索引和 change buffer 的配合使用,對於數據量大的表的更新優化仍是很明顯的。
Ps. 特別地,在使用機械硬盤時,change buffer 這個機制的收效是很是顯著的。因此,當有一個相似「歷史數據」的庫,應該特別關注這些表裏的索引,儘可能使用普通索引,而後把 change buffer 儘可能開大,以確保這個「歷史數據」表的數據寫入速度。
理解了 change buffer 的原理,可能會聯想到 redo log 和 WAL(Write-Ahead Logging,它的關鍵點就是先寫日誌,再寫磁盤)。
WAL 提高性能的核心機制,也的確是儘可能減小隨機讀寫
在表上執行這個插入語句:
mysql> insert into t(id,k) values(id1,k1),(id2,k2);
假設當前 k 索引樹的狀態,查找到位置後,k1 所在的數據頁在內存 (InnoDB buffer pool) 中,k2 所在的數據頁不在內存中。如圖 是帶 change buffer 的更新狀態圖。
圖3 帶 change buffer 的更新過程
分析這條更新語句,你會發現它涉及了四個部分:
內存、redo log(ib_log_fileX)、 數據表空間(t.ibd)、系統表空間(ibdata1)。
數據表空間:就是一個個的表數據文件,對應的磁盤文件就是「表名.ibd」; 系統表空間:用來放系統信息,如數據字典等,對應的磁盤文件是「ibdata1」
數據表空間 和 系統表空間 彷佛表明的就是B+樹對應的那個複雜的結構
這條更新語句作了以下的操做(按照圖中的數字順序):
作完上面這些,事務就能夠完成了。因此,你會看到,執行這條更新語句的成本很低,就是寫了兩處內存,而後寫了一處磁盤(兩次操做合在一塊兒寫了一次磁盤),並且仍是順序寫的。
change buffer和redo log顆粒度不同,由於change buffer只是針對若是更改的數據所在頁不在內存中才暫時儲存在change buffer中。而redo log會記錄一個事務內進行數據更改的全部操做,即便修改的數據已經在內存中了,那也會記錄下來
同時,圖中的兩個虛線箭頭,是後臺操做,不影響更新的響應時間。
那在這以後的讀請求,要怎麼處理呢?
好比,咱們如今要執行 select * from t where k in (k1, k2)。
若是讀語句發生在更新語句後不久,內存中的數據都還在,那麼此時的這兩個讀操做就與系統表空間(ibdata1)和 redo log(ib_log_fileX)無關了。
圖 4 帶 change buffer 的讀過程
從圖中能夠看到:讀 Page 1 的時候,直接從內存返回。
WAL 以後若是讀數據,是否是必定要讀盤,是否是必定要從 redo log 裏面把數據更新之後才能夠返回?
實際上是不用的。雖然磁盤上仍是以前的數據,可是這裏直接從內存返回結果,結果是正確的。要讀 Page 2 的時候,須要把 Page 2 從磁盤讀入內存中,而後應用 change buffer 裏面的操做日誌,生成一個正確的版本並返回結果。能夠看到,直到須要讀 Page 2 的時候,這個數據頁纔會被讀入內存。
若是要簡單地對比這兩個機制在提高更新性能上的收益的話,redo log 主要節省的是隨機寫磁盤的 IO 消耗(轉成順序寫),而 change buffer 主要節省的則是隨機讀磁盤的 IO 消耗。
一、經過圖 3 能夠看到,change buffer 一開始是寫內存的,那麼若是這個時候機器掉電重啓,會不會致使 change buffer 丟失呢?change buffer 丟失可不是小事兒,再從磁盤讀入數據可就沒有了 merge 過程,就等因而數據丟失了。會不會出現這種狀況呢?
答:
1.change buffer有一部分在內存有一部分在ibdata.
作purge操做,應該就會把change buffer裏相應的數據持久化到ibdata
2.redo log裏記錄了數據頁的修改以及change buffer新寫入的信息
若是掉電,持久化的change buffer數據已經purge,不用恢復。主要分析沒有持久化的數據
狀況又分爲如下幾種:
(1)change buffer寫入,redo log雖然作了fsync但未commit,binlog未fsync到磁盤,這部分數據丟失(2)change buffer寫入,redo log寫入但沒有commit,binlog以及fsync到磁盤,先從binlog恢復redo log,再從redo log恢復change buffer
(3)change buffer寫入,redo log和binlog都已經fsync.那麼直接從redo log裏恢復。