之前的文章簡單的介紹了 MySQL 的事務隔離級別,它們分別是:讀未提交、讀已提交、可重複讀、序列化。這篇文章我們就來探索一下 MySQL 事務隔離級別的底層原理。
本篇文章針對 InnoDB 儲存引擎
多版本併發控制
我們知道,讀未提交會造成髒讀、幻讀、不可重複讀,讀已提交會造成幻讀、不可重複讀,可重複讀可能會有幻讀,和序列化就不會有這些問題。
那 InnoDB 到底是怎麼解決這些問題的呢?又或者,你有沒有想過造成髒讀、幻讀、不可重複讀的底層最根本的原因是什麼呢?
這就是今天要聊的主角——MVCC(Multi-Version Concurrent Controll),也叫多版本併發控制。InnoDB 是一個支援多事務併發的儲存引擎,它能讓資料庫中的讀-寫操作能夠併發的進行,避免由於加鎖而導致讀阻塞。
正是由於有了 MVCC,在事務B更新 id=1
的資料時,事務A讀取 id=1
的操作才不會被阻塞。而不阻塞的背後則是不加鎖的一致性讀。那什麼是一致性讀?
一致性讀
簡單來講,當進行 query 查詢時,InnoDB 會對當前時間點的資料庫建立一個快照,快照建立完之後,當前查詢就只能感知到快照建立之前提交的事務改動,在快照建立之後再提交的事務就不會被當前query感知。
當然,當前事務自己更新的資料是個例外。當前事務修改過的行,再次讀取時是能夠拿到最新的資料的。而對於其他行,讀取的仍然是打快照時的版本。
而這個快照就是 InnoDB 實現事務隔離級別的關鍵。
在讀已提交(Read Committed)的隔離級別下,事務中的每一次的一致性讀都會重新生成快照。而在可重複讀(Repeatable Read)的隔離級別下,事務中所有的一致性讀都只會使用第一次一致性讀生成的快照。
這也就是為什麼,在上圖中事務B提交了事務之後,讀已提交的隔離級別下能看到改動,可重複讀的隔離級別看不到改動,本質上就是因為讀已提交又重新生成了快照。
在讀已提交、可重複讀的隔離級別下,SELECT
語句都會預設走一致性讀,並且在一致性讀的場景下,不會加任何的鎖。其他的修改操作也可以同步的進行,大大的提升了 MySQL 的效能。而這也就是MVCC多版本併發控制的實現原理。這種讀還有個名字叫 快照讀 。
那如果我在事務中想要立馬看到其他的事務的提交怎麼辦?有兩種方法:
使用讀已提交隔離級別 對 SELECT
加鎖,共享鎖和排他鎖都行,再具體點就是FOR SHARE
和FOR UPDATE
當然,第二種方法如果對應的記錄加的鎖和 SELECT
加的鎖互斥,SELECT
就會被阻塞,這種讀也有個別名叫 當前讀。
瞭解完上面的解釋,下次再有人問你 MVCC 是怎麼實現的,你就能從一致性讀(快照讀)和當前讀來進行解釋了,並且把不同的隔離級別下對一致性讀快照的重新整理機制也講清楚。
但是我覺得還不夠,應該還需要繼續往下深入瞭解。因為我們只知道個快照,其底層到底是怎麼實現的呢?其實還是不知道的。
深入一致性讀原理
從常理來說,不同的一致性讀可能會讀到不同版本的資料,那麼這些肯定都儲存在 MySQL 中的,否則不可能被讀取到。是的,這些資料都儲存在 InnoDB 的表空間內,再具體點這些資料儲存在 Undo 表空間內。
InnoDB 內實現 MVCC 的關鍵其實就是三個欄位,並且資料表中每一行都有這三個欄位:
DB_TRX_ID 該欄位有6個位元組,用於儲存上次插入或者更新該行資料的事務的唯一標識。你可能會問,只有插入和更新嗎?那刪除呢?其實在InnoDB的內部,刪除其實就是更新操作,只不過會更新該行中一個特定的比標誌位,將其標記為刪除。 DB_ROLL_PTR 該欄位有7個位元組,你可以叫它回滾指標,該指標指向了儲存在回滾段中的一條具體的Undo Log。即使當前這行資料被更新了,我們同樣的可以通過回滾指標,拿到更新之前的歷史版本資料。 DB_ROW_ID 該欄位有6個位元組,InnoDB給該行資料的唯一標識,該唯一標識會在有新資料插入的時候單調遞增,就跟我們平時定義表結構的時候定義的 primary key
的時候單調遞增是一樣的。DB_ROW_ID會被包含在聚簇索引中,其他的非聚簇索引則不會包含。
通過 DB_ROLL_PTR
可以拿到最新的一條 Undo Log,然後每一個對應的 Undo Log 指向其上一個 Undo Log,這樣一來,不同的版本就可以連線起來形成連結串列,不同的事務根據需求和規則,從連結串列中選擇不同的版本進行讀取,從而實現多版本的併發控制,就像這樣:
可能有人對 Undo Log 沒啥概念,記住這個就好了:
Undo Log 記錄的是此次事務開始前的資料狀態,就有點類似於 Git 中的某個 commit,你提交了某個 commit, 然後開始做一個及其複雜的需求,然後做著做著心態就崩了,就不想要這些改動了,你就可以直接
git reset --hard $last_commit_id
回退,上個 commit 你就可以理解為 Undo Log,感興趣的可以去看看 基於Redo Log和Undo Log的MySQL崩潰恢復流程
Undo Log 的組成
可能也有人會有疑問,說 Undo Log 不是應該在事務提交之後就被刪除了嗎?為什麼我通過 MVCC 還能查到之前的資料呢?
實際上在 InnoDB 中,Undo Log 被分成了兩部分,分別是
Insert Undo Log Update Undo Log
對於 Insert Undo Log 來說,它只會用於在事務中發生錯誤的回滾,因為一旦事務提交了,Insert Undo Log 就完全沒用了,所以在事務提交之後 Insert Undo Log 就會被刪除。
而 Update Undo Log 不同,其可以用於 MVCC 的一致性讀,為不同版本的請求提供資料來源。那這樣一來,是不是 Update Undo Log 就完全沒法移除了?因為你不清楚啥時候就會有個一致性讀請求過來,然後導致其佔用的空間越來越大。
對,但也不完全對。
一致性讀本質上是要處理多事務併發時,需要按需給不同的事務以不同的資料版本,所以如果當前沒有事務存在了,Update Undo Log 就可以被幹掉了。
MySQL 的官方建議有點皮,建議大家定期提交事務,這樣機器上的 Undo Logs 就可以被定期的清理。我尋思,不提交事務整個 DB 不就 hang 住了,那不完犢子了嗎..
EOF
本篇文章就先到這裡,至於怎麼 Update Undo Log 怎麼被幹掉的,之後有空專門寫篇文章來聊聊。
本篇文章已放到我的 Github github.com/sh-blog 中,歡迎 Star。微信搜尋關注【SH的全棧筆記】,回覆【佇列】獲取MQ學習資料,包含基礎概念解析和RocketMQ詳細的原始碼解析,持續更新中。
如果你覺得這篇文章對你有幫助,還麻煩點個贊,關個注,分個享,留個言。