mysql:(六)事務

08 | 事務到底是隔離的還是不隔離的

InnoDB 的行數據有多個版本,每個數據版本有自己的 row trx_id,每個事務或者語句有

自己的一致性視圖。普通查詢語句是一致性讀,一致性讀會根據 row trx_id 和一致性視圖

確定數據版本的可見性。

對於可重複讀,查詢只承認在事務啓動前就已經提交完成的數據;

對於讀提交,查詢只承認在語句啓動前就已經提交完成的數據;

而當前讀,總是讀取已經提交完成的最新版本。

你也可以想一下,爲什麼表結構不支持“可重複讀”?這是因爲表結構沒有對應的行數

據,也沒有 row trx_id,因此只能遵循當前讀的邏輯。

當然,MySQL 8.0 已經可以把表結構放在 InnoDB 字典裏了,也許以後會支持表結構的可

重複讀。

09 | 普通索引和唯一索引,應該怎麼選擇

在前面的基礎篇文章中,我給你介紹過索引的基本概念,相信你已經瞭解了唯一索引和普

通索引的區別。今天我們就繼續來談談,在不同的業務場景下,應該選擇普通索引,還是

唯一索引?

假設你在維護一個市民系統,每個人都有一個唯一的身份證號,而且業務代碼已經保證了

不會寫入兩個重複的身份證號。如果市民系統需要按照身份證號查姓名,就會執行類似這

樣的 SQL 語句:

 

所以,你一定會考慮在 id_card 字段上建索引。

由於身份證號字段比較大,我不建議你把身份證號當做主鍵,那麼現在你有兩個選擇,要

麼給 id_card 字段創建唯一索引,要麼創建一個普通索引。如果業務代碼已經保證了不會

寫入重複的身份證號,那麼這兩個選擇邏輯上都是正確的。

現在我要問你的是,從性能的角度考慮,你選擇唯一索引還是普通索引呢?選擇的依據是

什麼呢?

 

查詢過程

假設,執行查詢的語句是 select id from T where k=5。這個查詢語句在索引樹上查找的

過程,先是通過 B+ 樹從樹根開始,按層搜索到葉子節點,也就是圖中右下角的這個數據

頁,然後可以認爲數據頁內部通過二分法來定位記錄。

對於普通索引來說,查找到滿足條件的第一個記錄 (5,500) 後,需要查找下一個記錄,

直到碰到第一個不滿足 k=5 條件的記錄。

對於唯一索引來說,由於索引定義了唯一性,查找到第一個滿足條件的記錄後,就會停

止繼續檢索。

那麼,這個不同帶來的性能差距會有多少呢?答案是,微乎其微。

InnoDB 的數據是按數據頁爲單位來讀寫的。也就是說,當需要讀一條記錄的

時候,並不是將這個記錄本身從磁盤讀出來,而是以頁爲單位,將其整體讀入內存。在

InnoDB 中,每個數據頁的大小默認是 16KB。

因爲引擎是按頁讀寫的,所以說,當找到 k=5 的記錄的時候,它所在的數據頁就都在內存

裏了。那麼,對於普通索引來說,要多做的那一次“查找和判斷下一條記錄”的操作,就

只需要一次指針尋找和一次計算。

當然,如果 k=5 這個記錄剛好是這個數據頁的最後一個記錄,那麼要取下一個記錄,必須

讀取下一個數據頁,這個操作會稍微複雜一些。

但是,我們之前計算過,對於整型字段,一個數據頁可以放近千個 key,因此出現這種情

況的概率會很低。所以,我們計算平均性能差異時,仍可以認爲這個操作成本對於現在的

CPU 來說可以忽略不計。

更新過程

爲了說明普通索引和唯一索引對更新語句性能的影響這個問題,我需要先跟你介紹一下

change buffer。

當需要更新一個數據頁時,如果數據頁在內存中就直接更新,而如果這個數據頁還沒有在

內存中的話,在不影響數據一致性的前提下,InooDB 會將這些更新操作緩存在 change

buffer 中,這樣就不需要從磁盤中讀入這個數據頁了。在下次查詢需要訪問這個數據頁的

時候,將數據頁讀入內存,然後執行 change buffer 中與這個頁有關的操作。通過這種方

式就能保證這個數據邏輯的正確性。

需要說明的是,雖然名字叫作 change buffer,實際上它是可以持久化的數據。也就是

說,change buffer 在內存中有拷貝,也會被寫入到磁盤上。

將 change buffer 中的操作應用到原數據頁,得到最新結果的過程稱爲 merge。除了訪

問這個數據頁會觸發 merge 外,系統有後臺線程會定期 merge。在數據庫正常關閉

(shutdown)的過程中,也會執行 merge 操作。

顯然,如果能夠將更新操作先記錄在 change buffer,減少讀磁盤,語句的執行速度會得

到明顯的提升。而且,數據讀入內存是需要佔用 buffer pool 的,所以這種方式還能夠避

免佔用內存,提高內存利用率。

merge 的執行流程是這樣的:

1. 從磁盤讀入數據頁到內存(老版本的數據頁);

2. 從 change buffer 裏找出這個數據頁的 change buffer 記錄 (可能有多個),依次應

用,得到新版數據頁;

3. 寫 redo log。這個 redo log 包含了數據的變更和 change buffer 的變更。

什麼條件下可以使用 change buffer 呢?

對於唯一索引來說,所有的更新操作都要先判斷這個操作是否違反唯一性約束。比如,要

插入 (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%。

現在,你已經理解了 change buffer 的機制,那麼我們再一起來看看如果要在這張表中插

入一個新記錄 (4,400) 的話,InnoDB 的處理流程是怎樣的。

第一種情況是,這個記錄要更新的目標頁在內存中。這時,InnoDB 的處理流程如下:

對於唯一索引來說,找到 3 和 5 之間的位置,判斷到沒有衝突,插入這個值,語句執行

結束;

對於普通索引來說,找到 3 和 5 之間的位置,插入這個值,語句執行結束。

這樣看來,普通索引和唯一索引對更新語句性能影響的差別,只是一個判斷,只會耗費微

小的 CPU 時間。

但,這不是我們關注的重點。

第二種情況是,這個記錄要更新的目標頁不在內存中。這時,InnoDB 的處理流程如下:

對於唯一索引來說,需要將數據頁讀入內存,判斷到沒有衝突,插入這個值,語句執行

結束;

對於普通索引來說,則是將更新記錄在 change buffer,語句執行就結束了。

將數據從磁盤讀入內存涉及隨機 IO 的訪問,是數據庫裏面成本最高的操作之一。change

buffer 因爲減少了隨機磁盤訪問,所以對更新性能的提升是會很明顯的。

之前我就碰到過一件事兒,有個 DBA 的同學跟我反饋說,他負責的某個業務的庫內存命

中率突然從 99% 降低到了 75%,整個系統處於阻塞狀態,更新語句全部堵住。而探究其

原因後,我發現這個業務有大量插入數據的操作,而他在前一天把其中的某個普通索引改

成了唯一索引。

change buffer 的使用場景

通過上面的分析,你已經清楚了使用 change buffer 對更新過程的加速作用,也清楚了

change buffer 只限於用在普通索引的場景下,而不適用於唯一索引。那麼,現在有一個

問題就是:普通索引的所有場景,使用 change buffer 都可以起到加速作用嗎?

因爲 merge 的時候是真正進行數據更新的時刻,而 change buffer 的主要目的就是將記

錄的變更動作緩存下來,所以在一個數據頁做 merge 之前,change buffer 記錄的變更

越多(也就是這個頁面上要更新的次數越多),收益就越大。

因此,對於寫多讀少的業務來說,頁面在寫完以後馬上被訪問到的概率比較小,此時

change buffer 的使用效果最好。這種業務模型常見的就是賬單類、日誌類的系統。

反過來,假設一個業務的更新模式是寫入之後馬上會做查詢,那麼即使滿足了條件,將更

新先記錄在 change buffer,但之後由於馬上要訪問這個數據頁,會立即觸發 merge 過

程。這樣隨機訪問 IO 的次數不會減少,反而增加了 change buffer 的維護代價。所以,

對於這種業務模式來說,change buffer 反而起到了副作用。

索引選擇和實踐

回到我們文章開頭的問題,普通索引和唯一索引應該怎麼選擇。其實,這兩類索引在查詢

能力上是沒差別的,主要考慮的是對更新性能的影響。所以,我建議你儘量選擇普通索

引。

如果所有的更新後面,都馬上伴隨着對這個記錄的查詢,那麼你應該關閉 change

buffer。而在其他情況下,change buffer 都能提升更新性能。

在實際使用中,你會發現,普通索引和 change buffer 的配合使用,對於數據量大的表的

更新優化還是很明顯的。

特別地,在使用機械硬盤時,change buffer 這個機制的收效是非常顯著的。所以,當你

有一個類似“歷史數據”的庫,並且出於成本考慮用的是機械硬盤時,那你應該特別關注

這些表裏的索引,儘量使用普通索引,然後把 change buffer 儘量開大,以確保這個“歷

史數據”表的數據寫入速度。

change buffer redo log

分析這條更新語句,你會發現它涉及了四個部分:內存、redo log(ib_log_fileX)、 數

據表空間(t.ibd)、系統表空間(ibdata1)。

這條更新語句做了如下的操作(按照圖中的數字順序):

1. Page 1 在內存中,直接更新內存;

2. Page 2 沒有在內存中,就在內存的 change buffer 區域,記錄下“我要往 Page 2 插

入一行”這個信息

3. 將上述兩個動作記入 redo log 中(圖中 3 和 4)。

做完上面這些,事務就可以完成了。所以,你會看到,執行這條更新語句的成本很低,就

是寫了兩處內存,然後寫了一處磁盤(兩次操作合在一起寫了一次磁盤),而且還是順序

寫的。

同時,圖中的兩個虛線箭頭,是後臺操作,不影響更新的響應時間。

那在這之後的讀請求,要怎麼處理呢?

比如,我們現在要執行 select * from t where k in (k1, k2)。這裏,我畫了這兩個讀請求

的流程圖。

如果讀語句發生在更新語句後不久,內存中的數據都還在,那麼此時的這兩個讀操作就與

系統表空間(ibdata1)和 redo log(ib_log_fileX)無關了。所以,我在圖中就沒畫出

這兩部分。

 

1. 讀 Page 1 的時候,直接從內存返回。有幾位同學在前面文章的評論中問到,WAL 之後

如果讀數據,是不是一定要讀盤,是不是一定要從 redo log 裏面把數據更新以後纔可

以返回?其實是不用的。你可以看一下圖 3 的這個狀態,雖然磁盤上還是之前的數據,

但是這裏直接從內存返回結果,結果是正確的。

2. 要讀 Page 2 的時候,需要把 Page 2 從磁盤讀入內存中,然後應用 change buffer 裏

面的操作日誌,生成一個正確的版本並返回結果。

可以看到,直到需要讀 Page 2 的時候,這個數據頁纔會被讀入內存。

所以,如果要簡單地對比這兩個機制在提升更新性能上的收益的話,redo log 主要節省的

是隨機寫磁盤的 IO 消耗(轉成順序寫),而 change buffer 主要節省的則是隨機讀磁盤

的 IO 消耗。

小結

今天,我從普通索引和唯一索引的選擇開始,和你分享了數據的查詢和更新過程,然後說

明瞭 change buffer 的機制以及應用場景,最後講到了索引選擇的實踐。

由於唯一索引用不上 change buffer 的優化機制,因此如果業務可以接受,從性能角度出

發我建議你優先考慮非唯一索引。

最後,又到了思考題時間。

通過圖 2 你可以看到,change buffer 一開始是寫內存的,那麼如果這個時候機器掉電重

啓,會不會導致 change buffer 丟失呢?change buffer 丟失可不是小事兒,再從磁盤讀

入數據可就沒有了 merge 過程,就等於是數據丟失了。會不會出現這種情況呢?

================

上期問題時間

我在上一篇文章最後留給你的問題是,如果某次寫入使用了 change buffer 機制,之後主

機異常重啓,是否會丟失 change buffer 和數據。

這個問題的答案是不會丟失,留言區的很多同學都回答對了。雖然是隻更新內存,但是在

事務提交的時候,我們把 change buffer 的操作也記錄到 redo log 裏了,所以崩潰恢復

的時候,change buffer 也能找回來。

========================

感謝文章提供

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章