本文轉載自:Draveness,略有修改html
原文連接:『淺入淺出』MySQL 和 InnoDB · 面向信仰編程mysql
做爲一名開發人員,在平常的工做中會難以免地接觸到數據庫,不管是基於文件的 sqlite 仍是工程上使用很是普遍的 MySQL、PostgreSQL,可是一直以來也沒有對數據庫有一個很是清晰而且成體系的認知,因此最近兩個月的時間看了幾本數據庫相關的書籍而且閱讀了 MySQL 的官方文檔,但願對各位瞭解數據庫的、不瞭解數據庫的有所幫助。git
本文中對於數據庫的介紹以及研究都是在 MySQL 上進行的,若是涉及到了其餘數據庫的內容或者實現會在文中單獨指出。github
不少開發者在最開始時其實都對數據庫有一個比較模糊的認識,以爲數據庫就是一堆數據的集合,可是實際卻比這複雜的多,數據庫領域中有兩個詞很是容易混淆,也就是數據庫和實例:算法
數據庫:物理操做文件系統或其餘形式文件類型的集合;sql
實例:MySQL 數據庫由後臺線程以及一個共享內存區組成;數據庫
對於數據庫和實例的定義都來自於 MySQL 技術內幕:InnoDB 存儲引擎 一書,想要了解 InnoDB 存儲引擎的讀者能夠閱讀這本書籍。編程
在 MySQL 中,實例和數據庫每每都是一一對應的,而咱們也沒法直接操做數據庫,而是要經過數據庫實例來操做數據庫文件,能夠理解爲數據庫實例是數據庫爲上層提供的一個專門用於操做的接口。緩存
在 Unix 上,啓動一個 MySQL 實例每每會產生兩個進程,mysqld
就是真正的數據庫服務守護進程,而 mysqld_safe
是一個用於檢查和設置 mysqld
啓動的控制程序,它負責監控 MySQL 進程的執行,當 mysqld
發生錯誤時,mysqld_safe
會對其狀態進行檢查並在合適的條件下重啓。安全
MySQL 從第一個版本發佈到如今已經有了 20 多年的歷史,在這麼多年的發展和演變中,整個應用的體系結構變得愈來愈複雜:
最上層用於鏈接、線程處理的部分並非 MySQL 『發明』的,不少服務都有相似的組成部分;第二層中包含了大多數 MySQL 的核心服務,包括了對 SQL 的解析、分析、優化和緩存等功能,存儲過程、觸發器和視圖都是在這裏實現的;而第三層就是 MySQL 中真正負責數據的存儲和提取的存儲引擎,例如:InnoDB、MyISAM 等,文中對存儲引擎的介紹都是對 InnoDB 實現的分析。
在整個數據庫體系結構中,咱們可使用不一樣的存儲引擎來存儲數據,而絕大多數存儲引擎都以二進制的形式存儲數據;這一節會介紹 InnoDB 中對數據是如何存儲的。
在 InnoDB 存儲引擎中,全部的數據都被邏輯地存放在表空間中,表空間(tablespace)是存儲引擎中最高的存儲邏輯單位,在表空間的下面又包括段(segment)、區(extent)、頁(page):
同一個數據庫實例的全部表空間都有相同的頁大小;默認狀況下,表空間中的頁大小都爲 16KB,固然也能夠經過改變 innodb_page_size
選項對默認大小進行修改,須要注意的是不一樣的頁大小最終也會致使區大小的不一樣:
從圖中能夠看出,在 InnoDB 存儲引擎中,一個區的大小最小爲 1MB,頁的數量最少爲 64 個。
MySQL 使用 InnoDB 存儲表時,會將表的定義和數據索引等信息分開存儲,其中前者存儲在 .frm
文件中,後者存儲在 .ibd
文件中,這一節就會對這兩種不一樣的文件分別進行介紹。
不管在 MySQL 中選擇了哪一個存儲引擎,全部的 MySQL 表都會在硬盤上建立一個 .frm
文件用來描述表的格式或者說定義;.frm
文件的格式在不一樣的平臺上都是相同的。
CREATE TABLE test_frm( column1 CHAR(5), column2 INTEGER );
當咱們使用上面的代碼建立表時,會在磁盤上的 datadir
文件夾中生成一個 test_frm.frm
的文件,這個文件中就包含了表結構相關的信息:
MySQL 官方文檔中的 11.1 MySQL .frm File Format 一文對於
.frm
文件格式中的二進制的內容有着很是詳細的表述,在這裏就不展開介紹了。
InnoDB 中用於存儲數據的文件總共有兩個部分,一是系統表空間文件,包括 ibdata1
、ibdata2
等文件,其中存儲了 InnoDB 系統信息和用戶數據庫表數據和索引,是全部表公用的。
當打開 innodb_file_per_table
選項時,.ibd
文件就是每個表獨有的表空間,文件存儲了當前表的數據和相關的索引數據。
與現有的大多數存儲引擎同樣,InnoDB 使用頁做爲磁盤管理的最小單位;數據在 InnoDB 存儲引擎中都是按行存儲的,每一個 16KB 大小的頁中能夠存放 2-7992 行的記錄。(至少是2條記錄,最可能是7992條記錄)
當 InnoDB 存儲數據時,它可使用不一樣的行格式進行存儲;MySQL 5.7 版本支持如下格式的行存儲方式:
Antelope 是 InnoDB 最開始支持的文件格式,它包含兩種行格式 Compact 和 Redundant,它最開始並無名字;Antelope 的名字是在新的文件格式 Barracuda 出現後才起的,Barracuda 的出現引入了兩種新的行格式 Compressed 和 Dynamic;InnoDB 對於文件格式都會向前兼容,而官方文檔中也對以後會出現的新文件格式預先定義好了名字:Cheetah、Dragon、Elk 等等。
兩種行記錄格式 Compact 和 Redundant 在磁盤上按照如下方式存儲:
Compact 和 Redundant 格式最大的不一樣就是記錄格式的第一個部分;在 Compact 中,行記錄的第一部分倒序存放了一行數據中列的長度(Length),而 Redundant 中存的是每一列的偏移量(Offset),從整體上上看,Compact 行記錄格式相比 Redundant 格式可以減小 20% 的存儲空間。
當 InnoDB 使用 Compact 或者 Redundant 格式存儲極長的 VARCHAR 或者 BLOB 這類大對象時,咱們並不會直接將全部的內容都存放在數據頁節點中,而是將行數據中的前 768 個字節存儲在數據頁中,後面會經過偏移量指向溢出頁。
可是當咱們使用新的行記錄格式 Compressed 或者 Dynamic 時都只會在行記錄中保存 20 個字節的指針,實際的數據都會存放在溢出頁面中。
固然在實際存儲中,可能會對不一樣長度的 TEXT 和 BLOB 列進行優化,不過這就不是本文關注的重點了。
想要了解更多與 InnoDB 存儲引擎中記錄的數據格式的相關信息,能夠閱讀 InnoDB Record Structure
頁是 InnoDB 存儲引擎管理數據的最小磁盤單位,而 B-Tree 節點就是實際存放表中數據的頁面,咱們在這裏將要介紹頁是如何組織和存儲記錄的;首先,一個 InnoDB 頁有如下七個部分:
每個頁中包含了兩對 header/trailer:內部的 Page Header/Page Directory 關心的是頁的狀態信息,而 Fil Header/Fil Trailer 關心的是記錄頁的頭信息。
在頁的頭部和尾部之間就是用戶記錄和空閒空間了,每個數據頁中都包含 Infimum 和 Supremum 這兩個虛擬的記錄(能夠理解爲佔位符),Infimum 記錄是比該頁中任何主鍵值都要小的值,Supremum 是該頁中的最大值:
User Records 就是整個頁面中真正用於存放行記錄的部分,而 Free Space 就是空餘空間了,它是一個鏈表的數據結構,爲了保證插入和刪除的效率,整個頁面並不會按照主鍵順序對全部記錄進行排序,它會自動從左側向右尋找空白節點進行插入,行記錄在物理存儲上並非按照順序的,它們之間的順序是由 next_record
這一指針控制的。
B+ 樹在查找對應的記錄時,並不會直接從樹中找出對應的行記錄,它只能獲取記錄所在的頁,將整個頁加載到內存中,再經過 Page Directory 中存儲的稀疏索引和 n_owned
、next_record
屬性取出對應的記錄,不過由於這一操做是在內存中進行的,因此一般會忽略這部分查找的耗時。
InnoDB 存儲引擎中對數據的存儲是一個很是複雜的話題,這一節中也只是對錶、行記錄以及頁面的存儲進行必定的分析和介紹,雖然做者相信這部分知識對於大部分開發者已經足夠了,可是想要真正消化這部份內容還須要不少的努力和實踐。
索引是數據庫中很是很是重要的概念,它是存儲引擎可以快速定位記錄的祕密武器,對於提高數據庫的性能、減輕數據庫服務器的負擔有着很是重要的做用;索引優化是對查詢性能優化的最有效手段,它可以輕鬆地將查詢的性能提升幾個數量級。
在上一節中,咱們談了行記錄的存儲和頁的存儲,在這裏咱們就要從更高的層面看 InnoDB 中對於數據是如何存儲的;InnoDB 存儲引擎在絕大多數狀況下使用 B+ 樹創建索引,這是關係型數據庫中查找最爲經常使用和有效的索引,可是 B+ 樹索引並不能找到一個給定鍵對應的具體值,它只能找到數據行對應的頁,而後正如上一節所提到的,數據庫把整個頁讀入到內存中,並在內存中查找具體的數據行。
B+ 樹是平衡樹,它查找任意節點所耗費的時間都是徹底相同的,比較的次數就是 B+ 樹的高度;在這裏,咱們並不會深刻分析或者動手實現一個 B+ 樹,只是對它的特性進行簡單的介紹。
數據庫中的 B+ 樹索引能夠分爲彙集索引(clustered index)和輔助索引(secondary index),它們之間的最大區別就是,彙集索引中存放着一條行記錄的所有信息,而輔助索引中只包含索引列和一個用於查找對應行記錄的『書籤』。
InnoDB 存儲引擎中的表都是使用索引組織的,也就是按照鍵的順序存放;彙集索引就是按照表中主鍵的順序構建一顆 B+ 樹,並在葉節點中存放表中的行記錄數據。
CREATE TABLE users( id INT NOT NULL, first_name VARCHAR(20) NOT NULL, last_name VARCHAR(20) NOT NULL, age INT NOT NULL, PRIMARY KEY(id), KEY(last_name, first_name, age) KEY(first_name) );
若是使用上面的 SQL 在數據庫中建立一張表,B+ 樹就會使用 id
做爲索引的鍵,並在葉子節點中存儲一條記錄中的全部信息。
圖中對 B+ 樹的描述與真實狀況下 B+ 樹中的數據結構有一些差異,不過這裏想要表達的主要意思是:彙集索引葉節點中保存的是整條行記錄,而不是其中的一部分。
彙集索引與表的物理存儲方式有着很是密切的關係,全部正常的表應該有且僅有一個彙集索引(絕大多數狀況下都是主鍵),表中的全部行記錄數據都是按照彙集索引的順序存放的。
當咱們使用匯集索引對錶中的數據進行檢索時,能夠直接得到彙集索引所對應的整條行記錄數據所在的頁,不須要進行第二次操做。
數據庫將全部的非彙集索引都劃分爲輔助索引,可是這個概念對咱們理解輔助索引並無什麼幫助;輔助索引也是經過 B+ 樹實現的,可是它的葉節點並不包含行記錄的所有數據,僅包含索引中的全部鍵和一個用於查找對應行記錄的『書籤』,在 InnoDB 中這個書籤就是當前記錄的主鍵。
輔助索引的存在並不會影響彙集索引,由於彙集索引構成的 B+ 樹是數據實際存儲的形式,而輔助索引只用於加速數據的查找,因此一張表上每每有多個輔助索引以此來提高數據庫的性能。
一張表必定包含一個彙集索引構成的 B+ 樹以及若干輔助索引的構成的 B+ 樹。
若是在表 users
中存在一個輔助索引 (first_name, age)
,那麼它構成的 B+ 樹大體就是上圖這樣,按照 (first_name, age)
的字母順序對錶中的數據進行排序,當查找到主鍵時,再經過彙集索引獲取到整條行記錄。
上圖展現了一個使用輔助索引查找一條表記錄的過程:經過輔助索引查找到對應的主鍵,最後在彙集索引中使用主鍵獲取對應的行記錄,這也是一般狀況下行記錄的查找方式。
索引的設計實際上是一個很是重要的內容,同時也是一個很是複雜的內容;索引的設計與建立對於提高數據庫的查詢性能相當重要,不過這不是本文想要介紹的內容,有關索引的設計與優化能夠閱讀 數據庫索引設計與優化 一書,書中提供了一種很是科學合理的方法可以幫助咱們在數據庫中創建最適合的索引,固然做者也可能會在以後的文章中對索引的設計進行簡單的介紹和分析。
咱們都知道鎖的種類通常分爲樂觀鎖和悲觀鎖兩種,InnoDB 存儲引擎中使用的就是悲觀鎖,而按照鎖的粒度劃分,也能夠分紅行鎖和表鎖。
樂觀鎖和悲觀鎖其實都是併發控制的機制,同時它們在原理上就有着本質的差異;
雖然樂觀鎖和悲觀鎖在本質上並非同一種東西,一個是一種思想,另外一個是一種真正的鎖,可是它們都是一種併發控制機制。
樂觀鎖不會存在死鎖的問題,可是因爲更新後驗證,因此當衝突頻率和重試成本較高時更推薦使用悲觀鎖,而須要很是高的響應速度而且併發量很是大的時候使用樂觀鎖就能較好的解決問題,在這時使用悲觀鎖就可能出現嚴重的性能問題;在選擇併發控制機制時,須要綜合考慮上面的四個方面(衝突頻率、重試成本、響應速度和併發量)進行選擇。
對數據的操做其實只有兩種,也就是讀和寫,而數據庫在實現鎖時,也會對這兩種操做使用不一樣的鎖;InnoDB 實現了標準的行級鎖,也就是共享鎖(Shared Lock)和互斥鎖(Exclusive Lock);共享鎖和互斥鎖的做用其實很是好理解:
而它們的名字也暗示着各自的另一個特性,共享鎖之間是兼容的,而互斥鎖與其餘任意鎖都不兼容:
稍微對它們的使用進行思考就能想明白它們爲何要這麼設計,由於共享鎖表明了讀操做、互斥鎖表明了寫操做,因此咱們能夠在數據庫中並行讀,可是隻能串行寫,只有這樣才能保證不會發生線程競爭,實現線程安全。
不管是共享鎖仍是互斥鎖其實都只是對某一個數據行進行加鎖,InnoDB 支持多種粒度的鎖,也就是行鎖和表鎖;爲了支持多粒度鎖定,InnoDB 存儲引擎引入了意向鎖(Intention Lock),意向鎖就是一種表級鎖。
與上一節中提到的兩種鎖的種類類似的是,意向鎖也分爲兩種:
隨着意向鎖的加入,鎖類型之間的兼容矩陣也變得越發複雜:
意向鎖其實不會阻塞全表掃描以外的任何請求,它們的主要目的是爲了表示是否有人請求鎖定表中的某一行數據。
有的人可能會對意向鎖的目的並非徹底的理解,咱們在這裏能夠舉一個例子:若是沒有意向鎖,當已經有人使用行鎖對錶中的某一行進行修改時,若是另一個請求要對全表進行修改,那麼就須要對全部的行是否被鎖定進行掃描,在這種狀況下,效率是很是低的;不過,在引入意向鎖以後,當有人使用行鎖對錶中的某一行進行修改以前,會先爲表添加意向互斥鎖(IX),再爲行記錄添加互斥鎖(X),在這時若是有人嘗試對全表進行修改就不須要判斷表中的每一行數據是否被加鎖了,只須要經過等待意向互斥鎖被釋放就能夠了。
到目前爲止已經對 InnoDB 中鎖的粒度有必定的瞭解,也清楚了在對數據庫進行讀寫時會獲取不一樣的鎖,在這一小節將介紹鎖是如何添加到對應的數據行上的,咱們會分別介紹三種鎖的算法:Record Lock、Gap Lock 和 Next-Key Lock。
記錄鎖(Record Lock)是加到索引記錄上的鎖,假設咱們存在下面的一張表 users
:
CREATE TABLE users( id INT NOT NULL AUTO_INCREMENT, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255), age INT, PRIMARY KEY(id), KEY(last_name), KEY(age) );
若是咱們使用 id
或者 last_name
做爲 SQL 中 WHERE
語句的過濾條件,那麼 InnoDB 就能夠經過索引創建的 B+ 樹找到行記錄並添加索引,可是若是使用 first_name
做爲過濾條件時,因爲 InnoDB 不知道待修改的記錄具體存放的位置,也沒法對將要修改哪條記錄提早作出判斷就會鎖定整個表。
記錄鎖是在存儲引擎中最爲常見的鎖,除了記錄鎖以外,InnoDB 中還存在間隙鎖(Gap Lock),間隙鎖是對索引記錄中的一段連續區域的鎖;當使用相似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
的 SQL 語句時,就會阻止其餘事務向表中插入 id = 15
的記錄,由於整個範圍都被間隙鎖鎖定了。
間隙鎖是存儲引擎對於性能和併發作出的權衡,而且只用於某些事務隔離級別。
雖然間隙鎖中也分爲共享鎖和互斥鎖,不過它們之間並非互斥的,也就是不一樣的事務能夠同時持有一段相同範圍的共享鎖和互斥鎖,它惟一阻止的就是其餘事務向這個範圍中添加新的記錄。
Next-Key 鎖相比前二者就稍微有一些複雜,它是記錄鎖和記錄前的間隙鎖的結合,在 users
表中有如下記錄:
+------|-------------|--------------|-------+ | id | last_name | first_name | age | |------|-------------|--------------|-------| | 4 | stark | tony | 21 | | 1 | tom | hiddleston | 30 | | 3 | morgan | freeman | 40 | | 5 | jeff | dean | 50 | | 2 | donald | trump | 80 | +------|-------------|--------------|-------+
若是使用 Next-Key 鎖,那麼 Next-Key 鎖就能夠在須要的時候鎖定如下的範圍:
(-∞, 21] (21, 30] (30, 40] (40, 50] (50, 80] (80, ∞)
既然叫 Next-Key 鎖,鎖定的應該是當前值和後面的範圍,可是實際上卻不是,Next-Key 鎖鎖定的是當前值和前面的範圍。
當咱們更新一條記錄,好比 SELECT * FROM users WHERE age = 30 FOR UPDATE;
,InnoDB 不只會在範圍 (21, 30]
上加 Next-Key 鎖,還會在這條記錄後面的範圍 (30, 40]
加間隙鎖,因此插入 (21, 40]
範圍內的記錄都會被鎖定。
Next-Key 鎖的做用實際上是爲了解決幻讀的問題,咱們會在下一節談事務的時候具體介紹。
既然 InnoDB 中實現的鎖是悲觀的,那麼不一樣事務之間就可能會互相等待對方釋放鎖形成死鎖,最終致使事務發生錯誤;想要在 MySQL 中製造死鎖的問題其實很是容易:
兩個會話都持有一個鎖,而且嘗試獲取對方的鎖時就會發生死鎖,不過 MySQL 也能在發生死鎖時及時發現問題,並保證其中的一個事務可以正常工做,這對咱們來講也是一個好消息。
在介紹了鎖以後,咱們再來談談數據庫中一個很是重要的概念 —— 事務;相信只要是一個合格的軟件工程師就對事務的特性有所瞭解,其中被人常常提起的就是事務的原子性,在數據提交工做時,要麼保證全部的修改都可以提交,要麼就全部的修改所有回滾。
可是事務還遵循包括原子性在內的 ACID 四大特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability);文章不會對這四大特性所有展開進行介紹,相信你可以經過 Google 和數據庫相關的書籍輕鬆得到有關它們的概念,本文最後要介紹的就是事務的四種隔離級別。
事務的隔離性是數據庫處理數據的幾大基礎之一,而隔離級別其實就是提供給用戶用於在性能和可靠性作出選擇和權衡的配置項。
ISO 和 ANIS SQL 標準制定了四種事務隔離級別,而 InnoDB 遵循了 SQL:1992 標準中的四種隔離級別:READ UNCOMMITED
、READ COMMITED
、REPEATABLE READ
和 SERIALIZABLE
;每一個事務的隔離級別其實都比上一級多解決了一個問題:
RAED UNCOMMITED
:使用查詢語句不會加鎖,可能會讀到未提交的行(Dirty Read);
READ COMMITED
:只對記錄加記錄鎖,而不會在記錄之間加間隙鎖,因此容許新的記錄插入到被鎖定記錄的附近,因此再屢次使用查詢語句時,可能獲得不一樣的結果(Non-Repeatable Read);
REPEATABLE READ
:屢次讀取同一範圍的數據會返回第一次查詢的快照,不會返回不一樣的數據行,可是可能發生幻讀(Phantom Read);
SERIALIZABLE
:InnoDB 隱式地將所有的查詢語句加上共享鎖,解決了幻讀的問題;
MySQL 中默認的事務隔離級別就是 REPEATABLE READ
,可是它經過 Next-Key 鎖也可以在某種程度上解決幻讀的問題。
接下來,咱們將數據庫中建立以下的表並經過個例子來展現在不一樣的事務隔離級別之下,會發生什麼樣的問題:
CREATE TABLE test( id INT NOT NULL, UNIQUE(id) );
在一個事務中,讀取了其餘事務未提交的數據。
當事務的隔離級別爲 READ UNCOMMITED
時,咱們在 SESSION 2
中插入的未提交數據在 SESSION 1
中是能夠訪問的。
在一個事務中,同一行記錄被訪問了兩次卻獲得了不一樣的結果。
當事務的隔離級別爲 READ COMMITED
時,雖然解決了髒讀的問題,可是若是在 SESSION 1
先查詢了一行數據,在這以後 SESSION 2
中修改了同一行數據而且提交了修改,在這時,若是 SESSION 1
中再次使用相同的查詢語句,就會發現兩次查詢的結果不同。
不可重複讀的緣由就是,在 READ COMMITED
的隔離級別下,存儲引擎不會在查詢記錄時添加行鎖,鎖定 id = 3
這條記錄。
在一個事務中,同一個範圍內的記錄被讀取時,其餘事務向這個範圍添加了新的記錄。
從新開啓了兩個會話 SESSION 1
和 SESSION 2
,在 SESSION 1
中咱們查詢全表的信息,沒有獲得任何記錄;在 SESSION 2
中向表中插入一條數據並提交;因爲 REPEATABLE READ
的緣由,再次查詢全表的數據時,咱們得到到的仍然是空集,可是在向表中插入一樣的數據卻出現了錯誤。
這種現象在數據庫中就被稱做幻讀,雖然咱們使用查詢語句獲得了一個空的集合,可是插入數據時卻獲得了錯誤,好像以前的查詢是幻覺同樣。
在標準的事務隔離級別中,幻讀是由更高的隔離級別 SERIALIZABLE
解決的,可是它也能夠經過 MySQL 提供的 Next-Key 鎖解決:
REPERATABLE READ
和 READ UNCOMMITED
實際上是矛盾的,若是保證了前者就看不到已經提交的事務,若是保證了後者,就會致使兩次查詢的結果不一樣,MySQL 爲咱們提供了一種折中的方式,可以在 REPERATABLE READ
模式下加鎖訪問已經提交的數據,其自己並不能解決幻讀的問題,而是經過文章前面提到的 Next-Key 鎖來解決。
文章中的內容大都來自於 高性能 MySQL、MySQL 技術內幕:InnoDB 存儲引擎、數據庫索引設計與優化以及 MySQL 的 官方文檔。
因爲篇幅所限僅能對數據庫中一些重要內容進行簡單的介紹和總結,文中內容不免有所疏漏,若是對文章內容的有疑問,能夠在博客下面評論留言。