MySQL🐬 InnoDB 教我的事: Index 索引、鎖、資源各司其職

Jayden Lin
程式猿吃香蕉
Published in
9 min readOct 7, 2022

筆者曾任職 Yahoo,現在區塊鏈產業打滾,《軟體需求溝通 ─ 從外商公司學跨部門協作開發》線上課程講師,紛絲團《程式猿吃香蕉🍌

(圖片來源:https://dev.to/aldora/)

在先前的文章,我談到在併發編程下,鎖 (Lock) 資源 (Resource) 的區別,並且以 Java 作為範例:

然而,在系統裡面不僅僅有應用程式 (Application) 這一層,還會有資料庫、快取 (Cache)、訊息隊列 (Message Queue) 等等,它們也各自有鎖」與資源」的設計,並且有著各種不同的特性。在深入理解之後,才能以正確的排列組合對系統做更好地處理。因此,這一次我們來看看 InnoDB 是怎麼設計「鎖」與「資源」的。

▍InnoDB 中的資源 Resource

相較於 Java 是用方法 (method)變數 (variable) 類別 (class) 等方式來定義「資源」,InnoDB 中的「資源」指的是什麼呢? InnoDB 身為資料庫儲存引擎,它所要保護的資源很明顯就是資料庫中的數據了。

InnoDB 的數據有一項很大的特色,就是它的數據是以 B+ tree 形式儲存的(如上圖),我們可以將 B+ tree 理解為樹狀結構的索引,幫助 InnoDB 快速定位資料InnoDB 中預設每張資料表 (Table) 都會有一個索引,稱為「聚簇索引」 (Clustered index),是由資料表的主鍵 (Primary Key) 建立的,如果沒有指定主鍵,會以 row ID 建立,聚簇索引又俗稱為主表,因為它負責存放資料表所有的數據。

假設我們有一張資料表, id 為主鍵,如下圖所示,它的聚簇索引長什麼樣子呢?

下圖為這張資料表聚簇索引的樣子,綠色方塊的部分為資料表的主鍵,分別為 1 到 12,在樹狀結構的葉子部分 (Leaf Node),表示一個頁(Page),每一頁裡面有多筆實際的數據,頁和頁之間由雙向鏈結串列 (Double Linked List) 連結起來。每一個頁中最大的索引值,例如: 3, 6, 9, 12 ,會成為樹狀結構的上一層,依此類推,形成一個平衡的樹 (Balanced Tree)。

當要找尋資料的時候,可以依照樹狀索引的順序快速找到。以下圖為例 ,如果要找主鍵為 8 的數據,就會先從聚簇索引樹的根部「找大於等於 8 的位置」,找到了 9 ,往下一層,同樣繼續找「大於等於 8 的位置」,最後找到 (8, Door, 277) 這一筆數據。找尋的路徑,我以綠色箭頭標示。

除此之外,InnoDB 還允許我們根據業務需求新增其他索引 (index),可以建立唯一索引或是非唯一索引,統稱為「二級索引」 (Secondary index),這些索引也都是以 B+ tree 的形式儲存。

舉個例子,如果我們把上述資料表的 name 欄位建立二級索引,就會變成下圖的樣子。這麼一來,以 name 為條件搜尋時,速度就會比較快。

另外要注意的是,二級索引的葉子節點跟聚簇索引不同,二級索引不會儲存每筆數據完整的內容,而是儲存主鍵值再關聯到聚簇索引,如下圖所示:

當 InnDB 回傳資料時,如果走二級索引搜尋,便會先從二級索引找到對應數據後,再到聚簇索引撈出完整的數據,例如,以下的 SQL 語句:

便會先找到 Door 對應的 id 為 8 ,再到聚簇索引撈出對應的 id, price 的數據回傳。

這樣做有什麼好處呢?當我們搜尋二級索引的欄位時,如果回傳的內容跟主表聚簇索引無關,就可以只搜尋二級索引這顆 B+tree 就好了,效能會比較好,舉例來說,當 SQL 語句為:

這時候 InnoDB 所需要回傳的數據都在二級索引上找得到,就不用再回聚簇索引撈資料了,這樣的情況稱為「覆蓋索引」 (Covering Index) ,這裡請讀者先將覆蓋索引的觀念記著,在下一篇講到 InnoDB 鎖與資源誤用的例子會用到。

以上,便是 InnoDB 對「資源」的設計,那麼 InnoDB 的鎖又是什麼呢?

▍InnoDB 中的鎖 Lock

因為 InnoDB 將資源定義成樹狀結構 (還不只有一棵樹!包含聚簇索引還有二級索引),雖然方便查找數據,但也造成了「鎖」的設計非常地複雜。

要怎麼鎖住樹狀結構呢?

我們看一下 InnoDB 鎖結構的原始碼 (MySQL 8.0) ,如下圖:

(節錄部分,來源:https://github.com/mysql/mysql-server/blob/8.0/storage/innobase/include/lock0priv.h)

根據 InnoDB 的原始碼,定義了 lock_t 為鎖的資料結構,其中紀錄了事務 (Transaction) 訊息 trx ,以及索引訊息 index,因此 InnoDB 的鎖有能力根據不同的事務來建立鎖,並根據索引資訊,精確鎖到某一頁 (Page) 裏面的某一筆資料或多筆資料,在併發的時候控制資源爭搶。

從鎖結構裡面,鎖透過索引找到資源 (數據),InnoDB 定義的鎖跟資源之間的關係就連上了。

我們可以再往下挖 lock_rec_t 的原始碼 (如下圖),可以看到一個鎖確實有能力定位到某一頁 (Page) ,記錄在 page_id 裏面。

(來源:https://github.com/mysql/mysql-server/blob/8.0/storage/innobase/include/lock0priv.h)

當然,InnoDB 鎖結構 lock_t 僅是定義了一個通用的結構,我們在 lock_t 裡面還有看到 type_mode ,它標記目前這個鎖的類型。不同類型的鎖在使用上有更多細緻的操作。

InnoDB 中不同的鎖類型,根據過往的經驗以及學習總結,主要可以分為以下幾類:

❶ 根據操作類型區分

  • 共享鎖 (Shared lock,簡稱 S 鎖)
  • 排他鎖 (Exclusive Lock,簡稱 X 鎖)

​❷ 根據鎖的粒度區分

  • Table Lock:鎖著整張資料表。
  • Record Lock:鎖特定一筆資料。
  • Gap Lock:鎖 B+tree 葉子節點與葉子節點某個範圍的鎖。
  • Next-key Lock:為了達成可重複讀 (Repeatable Read),鎖某個範圍的鎖,可以理解成 Gap Lock + Record Lock 的組合。

❸ 協調鎖與鎖之間關係的鎖

這部分的鎖通常是 InnoDB 自動幫我們加上的,為了協調不同鎖之間的操作。

  • Metadata Lock:協調 DML (Data Manipulation Language) 操作與 DDL (Data Definition Language) 操作加上的鎖。
  • Intention Lock:為了協調不同粒度操作 (Table lock 與 Record Lock) 加上的鎖。
  • Insert Intention Lock:也是一種範圍類型的鎖,為了增加插入的迸發度,在取得 Gap lock 失敗後,會加上這個鎖。

以上是我對 InnoDB 的鎖在概念上的整理,如果對這些鎖的細節知識有興趣,可以參考官網的文件

▍小結

在前一篇文章《併發編程⚡談談鎖與資源》有提到:「鎖是鎖,資源是資源,系統設計時需要把兩者分開思考。」

雖然乍聽之下很好理解,但是:

在真實的環境中,鎖和資源的樣貌是更複雜的。

以 InnoDB 來說,它的「資源」是定義成樹狀結構,而且「鎖」又細分成好幾種類型。

InnDB 裡面的鎖和資源,不像上一篇提到的 Java 範例,可以簡單地使用 synchronized 鎖住資源,並且 InnoDB 的鎖在思考上也不是那麼直觀。然而,計算機科學的有趣之處,就在於我們可以將複雜的想法抽象化,直到看見我們關注的本質。因此,我們同樣可以用資源的抽象化概念來檢視 InnoDB 的設計,讀出其中的趣味。

由於 InnoDB 鎖的複雜性,下一篇我會談談 InnoDB 中鎖與資源誤用的情境,例如:以為鎖了但其實沒鎖住,或是以為沒鎖,但卻被鎖了等等情境。

如果你對 MySQL InnoDB 或是併發編程有興趣,也可以閱讀我的其他文章:

若是喜歡我分享的內容,可以訂閱我的粉絲團《程式猿吃香蕉🍌,在軟體開發的路上,一起分享交流,一起成長。

--

--

Jayden Lin
程式猿吃香蕉

曾在 Yahoo 擔任 Lead Engineer,負責廣告系統,帶團隊做跨國開發,現任職區塊鏈產業。也是《程式猿吃香蕉》團隊創辦人,喜歡將實用的軟體知識以簡單生動的方式講給大家聽 😄😄😄