MySQL 核心三劍客 —— 索引、鎖、事務

一、常見存儲引擎

1.1 InnoDB

InnoDB 是 MySQL 5.5 之後默認的存儲引擎,它具有高可靠、高性能的特點,主要具備以下優勢:

  • DML 操作完全遵循 ACID 模型,支持事務,支持崩潰恢復,能夠極大地保護用戶的數據安全;
  • 支持多版本併發控制,它會保存數據的舊版本信息,從而可以支持併發和事務的回滾;
  • 支持行級鎖,支持類似 Oracle 的一致性讀的特性,從而可以承受高併發地訪問;
  • InnoDB 組織數據時默認按照主鍵進行聚簇,從而可以提高主鍵查找的效率。對於頻繁訪問的數據,InnoDB 還會爲其建立哈希索引,從而提高等值查詢的效率,這也稱爲自適應哈希索引;
  • InnoDB 基於磁盤進行存儲,所有存儲記錄按 的方式進行管理。爲彌補 CPU 速度與磁盤速度之間的鴻溝,InnoDB 引用緩存池 (Buffer Pool) 來提高數據的整體性能。查詢時,會將目標頁讀入緩存中;修改時,會先修改緩衝池中的頁,然後再遵循 CheckPoint 機制將頁刷回磁盤。所有緩存頁通過最近最少使用原則 ( LRU ) 來進行定期清理。
  • InnoDB 支持兩次寫 (DoubleWrite) ,從而可以保證數據的安全,提高系統的可靠性。

一個 InnoDB 引擎完整的內存結構和磁盤結構如下圖所示:

1.2 MyISAM

MyISAM 是 MySQL 5.5 之前默認的存儲引擎。創建 MyISAM 表時會創建兩個同名的文件:

  • 擴展名爲 .MYDMYData):用於存儲表數據;
  • 擴展名爲 .MYIMYIndex): 用於存儲表的索引信息。

在 MySQL 8.0 之後,只會創建上述兩個同名文件,因爲 8.0 後表結構的定義存儲在 MySQL 數據字典中,但在 MySQL 8.0 之前,還會存在一個擴展名爲 .frm 的文件,用於存儲表結構信息。MyISAM 與 InnoDB 主要的區別其只支持表級鎖,不支持行級鎖,不支持事務,不支持自動崩潰恢復,但可以使用內置的 mysqlcheck 和 myisamchk 工具來進行檢查和修復。

1.3 MEMORY

MEMORY 存儲引擎(又稱爲 HEAP 存儲引擎)通常用於將表中的數據存儲在內存中,它具有以下特徵:

  • MEMORY 表的表定義信息存儲在 MySQL 數據字典中,而實際的數據則存儲在內存空間中,並以塊爲單位進行劃分;因此當服務器重啓後,表本身並不會被刪除,只是表中的所有數據都會丟失。
  • MEMORY 存儲引擎支持 HASH 索引和 BTREE 索引,默認採用 HASH 索引。
  • MEMORY 表使用固定長度的行存儲格式,即便是 VARCHAR 類型也會使用固定長度進行存儲。
  • MEMORY 支持 AUTO_INCREMENT 列,但不支持 BLOB 列或 TEXT 列。
  • MEMORY 表和 MySQL 內部臨時表的區別在於:兩者默認都採用內存進行存儲,但 MEMORY 表不受存儲轉換的影響,而內部臨時表則會在達到閾值時自動轉換爲磁盤存儲。

基於以上特性,MEMORY 表主要適合於存儲臨時數據 ,如會話狀態、實時位置等信息。

1.4 CSV

CSV 存儲引擎使用逗號分隔值的格式將數據存儲在文本文件中。創建 CSV 表時會同時創建兩個同名的文件:

  • 一個擴展名爲 csv ,負責存儲表的數據,其文件格式爲純文本,可以通過電子表格應用程序 (如 Microsoft Excel ) 進行修改,對應的修改操作也會直接反應在數據庫表中。
  • 另一個擴展名爲 CSM,負責存儲表的狀態和表中存在的行數。

1.5 ARCHIVE

ARCHIVE 存儲引擎默認採用 zlib 無損數據壓縮算法進行數據壓縮,能夠利用極小的空間存儲大量的數據。創建ARCHIVE 表時,存儲引擎會創建與表同名的 ARZ 文件,用於存儲數據。它還具有以下特點:

  • ARCHIVE 引擎支持 INSERT,REPLACE 和 SELECT,但不支持 DELETE 或 UPDATE。
  • ARCHIVE 引擎支持 AUTO_INCREMENT 屬性,並支持在其對應的列上建立索引,如果嘗試在不具有 AUTO_INCREMENT 屬性的列上建立索引,則會拋出異常。
  • ARCHIVE 引擎不支持分區操作。

1.6 MEGRE

MERGE 存儲引擎,也稱爲 MRG_MyISAM 引擎,是一組相同 MyISAM 表的集合。 ”相同” 表示所有表必須具有相同的列數據類型和索引信息。可以通過 UNION = (list-of-tables) 選項來創建 MERGE 表,如下:

mysql> CREATE TABLE t1 ( a INT NOT NULL AUTO_INCREMENT PRIMARY KEY, message CHAR(20)) ENGINE=MyISAM;
mysql> CREATE TABLE t2 ( a INT NOT NULL AUTO_INCREMENT PRIMARY KEY, message CHAR(20)) ENGINE=MyISAM;
mysql> INSERT INTO t1 (message) VALUES ('Testing'),('table'),('t1');
mysql> INSERT INTO t2 (message) VALUES ('Testing'),('table'),('t2');
mysql> CREATE TABLE total (a INT NOT NULL AUTO_INCREMENT,message CHAR(20), INDEX(a))
       ENGINE=MERGE UNION=(t1,t2) INSERT_METHOD=LAST;
       

創建表時可以通過 INSERT_METHOD 選項來控制 MERGE 表的插入:使用 FIRSTLAST 分別表示在第一個或最後一個基礎表中進行插入;如果未指定 INSERT_METHOD 或者設置值爲 NO ,則表示不允許在 MERGE 表上執行插入操作。MERGE 表支持 SELECT,DELETE,UPDATE 和 DELETE 語句,示例如下:

mysql>  SELECT * FROM total;
+---+---------+
| a | message |
+---+---------+
| 1 | Testing |
| 2 | table   |
| 3 | t1      |
| 1 | Testing |
| 2 | table   |
| 3 | t2      |
+---+---------+

二、索引

2.1 B+ tree 數據結構

如果沒有特殊說明,通常大多數數據庫採用的索引都是 B+ tree 索引,它是基於 B+ tree 這種數據結構構建的。爲什麼採用 B+ tree 而不是平衡二叉樹 (AVL) 或紅黑樹等數據結構?這裏假設索引爲 1-16 的自增數據,各類數據結構的表現如下:

平衡二叉樹數據結構

紅黑樹數據結構

Btree 數據結構

B+ Tree 數據結構

以上圖片均通過數據結構可視化網站 Data Structure Visualizations 自動生成,感興趣的小夥伴也可自行嘗試。

從上面的圖示中我們可以看出 B+ Tree 樹具有以下優點:

  • B+ Tree 樹的所有非葉子節點 (如 003,007),都會在葉子節點冗餘一份,所有葉子節點都按照鏈表的方式進行組織,這樣帶來的好處是在範圍查詢中,只需要通過遍歷葉子節點就可以獲取到所有的索引信息。
  • B+ Tree 的所有非葉子節點都可以存儲多個數據值,這取決於節點的大小,在 MySQL 中每個節點的大小爲 16K ,因此其具備更大的出度,即在相同的數據量下,其樹的高度更低。
  • 所有非葉子節點都只存儲索引值,不存儲實際的數據,只有葉子節點纔會存儲指針信息或數據信息。按照每個節點爲 16K 的大小計算,對於千萬級別的數據,其樹的高度通常都在 3~6 左右 (取決於索引值的字節數),因此其查詢性能非常優異。
  • 葉子節點存儲的數據取決於不同數據庫的實現,對於 MySQL 來說,取決於使用的存儲引擎和是否是主鍵索引。

2.2 B+ tree 索引

對於 InnoDB ,因爲主鍵索引是聚集索引,所以其葉子節點存儲的就是實際的數據。而非主鍵索引存儲的則是主鍵的值 :

對於 MyISAM,因爲主鍵索引是非聚集索引,所以其葉子節點存儲的只是指向數據位置的指針:

綜上所述,B+ tree 結構普遍適用於範圍查找,優化排序和分組等操作。B+ tree 是基於字典序進行構建的,因此其適用於以下查詢:

  • 全值匹配:以索引爲條件進行精確查找。如 emp_no 字段爲索引,查詢條件爲 emp_no = 10008
  • 前綴匹配:以聯合索引的前綴爲查找條件。如 emp_nodept_no 爲聯合索引,查找條件爲 emp_no = 10008
  • 列前綴匹配:匹配索引列的值的開頭部分。如 dept_no 爲索引,查詢條件爲 dept_no like "d1%"。前綴匹配和列前綴匹配都是索引前綴性的體現,在某些時候也稱爲前綴索引。
  • 匹配範圍值:按照索引列匹配一定範圍內的值。如 emp_no 字段爲索引,查詢條件爲 emp_no > 10008
  • 只訪問索引的查詢:如 emp_no 字段爲索引,查詢語句爲 select emp_no from employees,此時 emp_no 索引被稱爲本次查詢的覆蓋索引,即只需要從索引上就可以獲取全部的查詢信息,而不必訪問實際的表中的數據。
  • 精確匹配某一列並範圍匹配某一列:如 emp_nodept_no 爲聯合索引,查找條件爲 dept_no = "d004" and emp_no < 10020,這種情況下索引順序必須爲 ( emp_no,dept_no ),這樣才能基於 emp_no 的字典序排序進行範圍查找。

2.3 哈希索引

使用哈希索引時,存儲引擎會對索引列的值進行哈希運算,並將計算出的哈希值和指向該行數據的指針存儲在索引中,因此它更適用於等值比較查詢,而不是範圍查詢,同樣也不能用於優化排序和分組等操作。在建立哈希索引時,需要選取選擇性比較高的列,即列上的數據不容易重複 (如身份證號),這樣可以儘量避免哈希衝突。因爲哈希索引並不需要存儲索引列的數據,所以其結構比較緊湊,對應的查詢速度也比較快。

InnoDB 引擎有一個名爲 “自適應哈希索引 (adaptive hash index)” 的功能,當某些索引值被頻繁使用時,它會在內存中基於 B+ tree 索引再創建一個哈希索引,從而讓 B-Tree 索引具備哈希索引快速查找的優點。

2.4 索引的優點

  • 索引極大減少了服務器需要掃描的數據量;
  • 索引可以幫助服務器避免排序和臨時表;
  • 索引可以將隨機 IO 轉換爲順序 IO。

2.5 使用策略

  • 在查詢時,應該避免在索引列上使用函數或者表達式。
  • 對於多列索引,應該按照使用頻率由高到低的順序建立聯合索引。
  • 儘量避免創建冗餘的索引。如存在索引 (A,B),接着又創建了索引 A,因爲索引 A 是索引 (A,B) 的前綴索引,從而出現冗餘。
  • 建立索引時,應該考慮查詢時候的排序和分組的需求。只有當索引列的順序和 ORDER BY 字句的順序完全一致,並且遵循同樣的升序或降序規則時候,MySQL 纔會使用索引來對結果做排序。

三、鎖

3.1 共享鎖與排它鎖

InnoDB 存儲引擎支持以下兩種標準的行級鎖:

  • 共享鎖 (S Lock,又稱讀鎖) :允許加鎖事務讀取數據;
  • 排它鎖 (X Lock,又稱寫鎖) :允許加鎖事務刪除或者修改數據。

排它鎖和共享鎖的兼容情況如下:

X X
X 不兼容 不兼容
S 不兼容 兼容

3.2 意向共享鎖與意向排它鎖

爲了說明意向鎖的作用,這裏先引入一個案例:假設事務 A 利用 S 鎖鎖住了表中的某一行,讓其只能讀不能寫。之後事務 B 嘗試申請整個表的寫鎖,如果事務 B 申請成功,那麼理論上它就應該能修改表中的任意一行,這與事務 A 持有的行鎖是衝突的。想要解決這個問題,數據庫必須知道表中某一行已經被鎖定,從而在事務 B 嘗試申請整個表的寫鎖時阻塞它。想要知道表中某一行被鎖定,可以對錶的每一行進行遍歷,這種方式可行但是性能比較差,所以 InnoDB 引入了意向鎖。

  • 意向共享鎖 (IS Lock) :當前表中某行或者某幾行數據存在共享鎖;
  • 意向排它鎖 (LX Lock) :當前表中某行或者某幾行數據存在排它鎖。

按照意向鎖的規則,當上面的事務 A 給表中的某一行加 S 鎖時,會同時給表加上 IS 鎖,之後事務 B 嘗試獲取表的 X 鎖時,由於 X 鎖與 IS 鎖並不兼容,所以事務 B 會被阻塞。

X IX S IS
X 不兼容 不兼容 不兼容 不兼容
IX 不兼容 兼容 不兼容 兼容
S 不兼容 不兼容 兼容 兼容
IS 不兼容 兼容 兼容 兼容

3.3 一致性讀

1. 一致性非鎖定讀

一致非鎖定讀 (consistent nonlocking read) 是指在 InnoDB 存儲引擎下,如果將要讀取的行正在執行 DELETE 或 UPDATE 操作,此時不必去等待行上鎖的釋放,而是去讀取 undo 日誌上該行的快照數據,具體如下:

  • 在 READ COMMITTED 事務隔離級別下,讀取被鎖定行的最新一份快照數據;
  • 在 REPEATABLE READ 事務隔離級別下,讀取事務開始時所處版本的數據。

基於多版本併發控制和一致性非鎖定讀,可以避免獲取鎖的等待,從而提高併發訪問下的性能。

2. 一致性鎖定度

一致性鎖定讀則允許用戶按照自己的需求在進行 SELECT 操作時手動加鎖,通常有以下兩種方式:

  • SELECT ... FOR SHARE:在讀取行上加 S 鎖;
  • SELECT ... FOR UPDATE:在讀取行上加 X 鎖。

3.4 鎖的算法

InnoDB 存儲引擎支持以下三種鎖的算法:

Record Lock:行鎖,用於鎖定單個行記錄。示例如下:

-- 利用行鎖可以防止其他事務更新或刪除該行
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;

Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄本身,主要用於解決幻讀問題,示例如下:

-- 利用間隙鎖可以阻止其他事務將值15插入列 t.c1
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;

Next-Key Lock:等價於 行鎖+間隙鎖,既鎖定範圍,也鎖定記錄本身。可以用於解決幻讀中的 ”當前讀“ 的問題。

四、事務

4.1 ACID 定義

InnoDB 存儲引擎完全支持 ACID 模型:

1. 原子性(Atomicity)

事務是不可分割的最小工作單元,事務的所有操作要麼全部提交成功,要麼全部失敗回滾,不存在部分成功的情況。

2. 一致性(Consistency)

數據庫在事務執行前後都保持一致性狀態,數據庫的完整性沒有被破壞。

3. 隔離性(Isolation)

允許多個併發事務同時對數據進行操作,但一個事務所做的修改在最終提交以前,對其它事務是不可見的。

4. 持久性(Durability)

一旦事務提交,則其所做的修改將會永遠保存到數據庫中。即使宕機等故障,也不會丟失。

4.2 事務的實現

數據庫隔離性由上一部分介紹的鎖來實現,而原子性、一致性、持久性都由 undo log 和 redo log 來實現。

  • undo log:存儲在 undo 表空間或全局臨時表空間的 undo 日誌段 (segment) 上,用於記錄數據修改前的狀態,主要用於幫助事務回滾以及實現 MVCC 功能 (如一致性非鎖定讀)。
  • redo log:負責記錄數據修改後的值,主要用於保證事務的持久化。

4.3 併發問題

在併發環境下,數據的更改通常會產生下面四種問題:

1.丟失更新

一個事務的更新操作被另外一個事務的更新操作鎖覆蓋,從而導致數據不一致:

2. 髒讀

在不同的事務下,一個事務讀取到其他事務未提交的數據:

3. 不可重複讀

在同一個事務的兩次讀取之間,由於其他事務對數據進行了修改,導致對同一條數據兩次讀到的結果不一致:

4.幻讀

在同一個事務的兩次讀取之間,由於其他事務對數據進行了修改,導致第二次讀取到第一次不存在數據,或第一次原本存在的數據,第二次卻讀取不到,就好像之前的讀取是 “幻覺” 一樣:

4.4 隔離級別

想要解決以上問題,可以通過設置隔離級別來實現:InnoDB 支持以下四個等級的隔離級別,默認隔離級別爲可重複讀:

  • 讀未提交:在此級別下,一個事務中的修改,即便沒有提交,對其他事務也是可見的。
  • 讀已提交:在此級別下,一個事務中的修改只有已經提交的情況下,對其他事務纔是可見的。
  • 可重複讀:保證在同一個事務中多次讀取同樣數據的結果是一樣的。
  • 串行化:所有事務強制串行執行,由於已經不存在並行,所以上述所有併發問題都不會出現。

在每個級別下,併發問題是否可能出現的情況如下:

隔離級別 髒讀 不可重複讀 幻讀
讀未提交(READ UNCOMMITTED) 可能出現 可能 可能
讀已提交(READ COMMITTED) 不可能出現 可能 可能
可重複讀(REPEATABLE READ) 不可能 不可能 可能
串行化(SERIALIZABLE) 不可能 不可能 不可能

就數據庫層面而言,當前任何隔離級別下都不會發生丟失更新的問題,以 InnoDB 存儲引擎爲例,如果你想要更改表中某行數據,該行數據上必然會加上 X 鎖,而對應的表上則會加上 IX 鎖,其他任何事務必須等待獲取該鎖才能進行修改操作。

五、數據庫設計範式

數據庫設計當中常用的三範式如下:

第一範式:屬性不可分

要求表中的每一列都是不可再細分的原子項。這是最低的範式要求,通常都能夠被滿足。

第二範式:屬性完全依賴於主鍵

要求非主鍵列必須完全依賴於主鍵列,而不能存在部分依賴。示例如下:

mechanism_id (組織機構代碼) employee_id (僱員編號) ename (僱員名稱) mname (機構名稱)
28193182 10001 heibaiying XXXX公司

以上是一張全市在職人員統計表,主鍵爲:機構編碼 + 僱員編號。表中的僱員名稱完全依賴於此聯合主鍵,但機構名稱卻只依賴於機構編碼,這就是部分依賴,因此違背了第二範式。此時常用的解決方式是建立一張組織機構與組織名稱的字典表。

第三範式:避免傳遞依賴

非主鍵列不能依賴於其他非主鍵列,如果其他非主鍵列又依賴於主鍵列,此時就出現了傳遞依賴。示例如下:

employee_id (僱員編號) ename (僱員名稱) dept_no (部門編號) dname(部門名稱)
10001 heibaiying 06 開發部

以上是一張僱員表,僱員名稱和所屬的部門編號都依賴於主鍵 employee_id ,但部門名稱卻依賴於部門編號,此時就出現了非主鍵列依賴於其他非主鍵列,這就違背的第三範式。此時常用的解決方式是建立一張部門表用於維護部門相關的信息。

反範式設計

從上面的例子中我們也可以看出,想要完全遵循三範式設計,可能需要額外增加很多表來進行維護。所以在日常開發中,基於其他因素的綜合考量,可能並不會完全遵循範式設計,甚至可能違反範式設計,這就是反範式設計。

參考資料

  1. 官方文檔:The InnoDB Storage EngineOptimization and IndexesInnoDB Locking and Transaction Model
  2. 姜承堯 . MySQL技術內幕:InnoDB存儲引擎(第2版) . MySQL技術內幕 . 2013-05
  3. 施瓦茨 (Baron Schwartz) / 扎伊採夫 (Peter Zaitsev) / 特卡琴科 (Vadim Tkachenko) . 高性能mysql(第3版) . 電子工業出版社 . 2013-05-01
  4. InnoDB 數據頁解析
  5. MySQL索引背後的數據結構及算法原理
  6. MYSQL-B+TREE索引原理
  7. Innodb中的事務隔離級別和鎖的關係
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章