MySQL數(shù)據(jù)庫

數(shù)據(jù)庫和實例

數(shù)據(jù)庫:物理操作文件系統(tǒng)或其他形式文件類型的集合;
實例:MySQL 數(shù)據(jù)庫由后臺線程以及一個共享內(nèi)存區(qū)組成;

在 MySQL 中,實例和數(shù)據(jù)庫往往都是一一對應的,而我們也無法直接操作數(shù)據(jù)庫,而是要通過數(shù)據(jù)庫實例來操作數(shù)據(jù)庫文件,可以理解為數(shù)據(jù)庫實例是數(shù)據(jù)庫為上層提供的一個專門用于操作的接口。

在 Unix 上,啟動一個 MySQL 實例往往會產(chǎn)生兩個進程,mysqld 就是真正的數(shù)據(jù)庫服務守護進程,而 mysqld_safe 是一個用于檢查和設置 mysqld 啟動的控制程序,它負責監(jiān)控 MySQL 進程的執(zhí)行,當 mysqld 發(fā)生錯誤時,mysqld_safe 會對其狀態(tài)進行檢查并在合適的條件下重啟。

MySQL架構

MySQL.png

最上層用于連接、線程處理的部分并不是 MySQL 『發(fā)明』的,很多服務都有類似的組成部分;第二層中包含了大多數(shù) MySQL 的核心服務,包括了對 SQL 的解析、分析、優(yōu)化和緩存等功能,存儲過程、觸發(fā)器和視圖都是在這里實現(xiàn)的;而第三層就是 MySQL 中真正負責數(shù)據(jù)的存儲和提取的存儲引擎,例如:InnoDB、MyISAM 等;

數(shù)據(jù)的存儲

絕大多數(shù)存儲引擎都以二進制的形式存儲數(shù)據(jù);這一節(jié)會介紹 InnoDB 中對數(shù)據(jù)是如何存儲的。

在 InnoDB 存儲引擎中,所有的數(shù)據(jù)都被邏輯地存放在表空間中,表空間(tablespace)是存儲引擎中最高的存儲邏輯單位,在表空間的下面又包括段(segment)、區(qū)(extent)、頁(page):


InnoDB.png

同一個數(shù)據(jù)庫實例的所有表空間都有相同的頁大小;默認情況下,表空間中的頁大小都為 16KB,當然也可以通過改變 innodb_page_size 選項對默認大小進行修改,需要注意的是不同的頁大小最終也會導致區(qū)大小的不同;

如何存儲表

MySQL 使用 InnoDB 存儲表時,會將表的定義和數(shù)據(jù)索引等信息分開存儲,其中前者存儲在 .frm 文件中,后者存儲在 .ibd 文件中;

.frm 文件

無論在 MySQL 中選擇了哪個存儲引擎,所有的 MySQL 表都會在硬盤上創(chuàng)建一個 .frm 文件用來描述表的格式或者說定義;.frm 文件的格式在不同的平臺上都是相同的。
CREATE TABLE test_frm(
column1 CHAR(5),
column2 INTEGER
);
當我們使用上面的代碼創(chuàng)建表時,會在磁盤上的 datadir 文件夾中生成一個 test_frm.frm 的文件,這個文件中就包含了表結構相關的信息

.ibd 文件

InnoDB 中用于存儲數(shù)據(jù)的文件總共有兩個部分,一是系統(tǒng)表空間文件,包括 ibdata1、ibdata2 等文件,其中存儲了 InnoDB 系統(tǒng)信息和用戶數(shù)據(jù)庫表數(shù)據(jù)和索引,是所有表公用的。
當打開 innodb_file_per_table 選項時,.ibd 文件就是每一個表獨有的表空間,文件存儲了當前表的數(shù)據(jù)和相關的索引數(shù)據(jù)。

如何存儲記錄

與現(xiàn)有的大多數(shù)存儲引擎一樣,InnoDB 使用頁作為磁盤管理的最小單位;數(shù)據(jù)在 InnoDB 存儲引擎中都是按行存儲的,每個 16KB 大小的頁中可以存放 2-200 行的記錄。
當 InnoDB 存儲數(shù)據(jù)時,它可以使用不同的行格式進行存儲;

行溢出數(shù)據(jù)

當 InnoDB 使用 Compact 或者 Redundant 格式存儲極長的 VARCHAR 或者 BLOB 這類大對象時,我們并不會直接將所有的內(nèi)容都存放在數(shù)據(jù)頁節(jié)點中,而是將行數(shù)據(jù)中的前 768 個字節(jié)存儲在數(shù)據(jù)頁中,后面會通過偏移量指向溢出頁。

但是當我們使用新的行記錄格式 Compressed 或者 Dynamic 時都只會在行記錄中保存 20 個字節(jié)的指針,實際的數(shù)據(jù)都會存放在溢出頁面中。

數(shù)據(jù)頁結構

頁是 InnoDB 存儲引擎管理數(shù)據(jù)的最小磁盤單位,而 B-Tree 節(jié)點就是實際存放表中數(shù)據(jù)的頁面,我們在這里將要介紹頁是如何組織和存儲記錄的;首先,一個 InnoDB 頁有以下七個部分:


InnoDB頁.png

每一個頁中包含了兩對 header/trailer:內(nèi)部的 Page Header/Page Directory 關心的是頁的狀態(tài)信息,而 Fil Header/Fil Trailer 關心的是記錄頁的頭信息。

在頁的頭部和尾部之間就是用戶記錄和空閑空間了,每一個數(shù)據(jù)頁中都包含 Infimum 和 Supremum 這兩個虛擬的記錄(可以理解為占位符),Infimum 記錄是比該頁中任何主鍵值都要小的值,Supremum 是該頁中的最大值:


ye.png

User Records 就是整個頁面中真正用于存放行記錄的部分,而 Free Space 就是空余空間了,它是一個鏈表的數(shù)據(jù)結構,為了保證插入和刪除的效率,整個頁面并不會按照主鍵順序?qū)λ杏涗涍M行排序,它會自動從左側向右尋找空白節(jié)點進行插入,行記錄在物理存儲上并不是按照順序的,它們之間的順序是由 next_record 這一指針控制的。

B+ 樹在查找對應的記錄時,并不會直接從樹中找出對應的行記錄,它只能獲取記錄所在的頁,將整個頁加載到內(nèi)存中,再通過 Page Directory 中存儲的稀疏索引和 n_owned、next_record 屬性取出對應的記錄,不過因為這一操作是在內(nèi)存中進行的,所以通常會忽略這部分查找的耗時。

索引

索引的數(shù)據(jù)結構

在這里我們就要從更高的層面看 InnoDB 中對于數(shù)據(jù)是如何存儲的;InnoDB 存儲引擎在絕大多數(shù)情況下使用 B+ 樹建立索引,這是關系型數(shù)據(jù)庫中查找最為常用和有效的索引,但是 B+ 樹索引并不能找到一個給定鍵對應的具體值,它只能找到數(shù)據(jù)行對應的頁,然后正如上一節(jié)所提到的,數(shù)據(jù)庫把整個頁讀入到內(nèi)存中,并在內(nèi)存中查找具體的數(shù)據(jù)行。


B+樹.png

B+ 樹是平衡樹,它查找任意節(jié)點所耗費的時間都是完全相同的,比較的次數(shù)就是 B+ 樹的高度;在這里,我們并不會深入分析或者動手實現(xiàn)一個 B+ 樹,只是對它的特性進行簡單的介紹。

聚集索引和輔助索引

數(shù)據(jù)庫中的 B+ 樹索引可以分為聚集索引(clustered index)和輔助索引(secondary index),它們之間的最大區(qū)別就是,聚集索引中存放著一條行記錄的全部信息,而輔助索引中只包含索引列和一個用于查找對應行記錄的『書簽』。

聚集索引
InnoDB 存儲引擎中的表都是使用索引組織的,也就是按照鍵的順序存放;聚集索引就是按照表中主鍵的順序構建一顆 B+ 樹,并在葉節(jié)點中存放表中的行記錄數(shù)據(jù)。
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 在數(shù)據(jù)庫中創(chuàng)建一張表,B+ 樹就會使用 id 作為索引的鍵,并在葉子節(jié)點中存儲一條記錄中的所有信息。

聚集索引葉節(jié)點中保存的是整條行記錄,而不是其中的一部分。
聚集索引與表的物理存儲方式有著非常密切的關系,所有正常的表應該有且僅有一個聚集索引(絕大多數(shù)情況下都是主鍵),表中的所有行記錄數(shù)據(jù)都是按照聚集索引的順序存放的。
當我們使用聚集索引對表中的數(shù)據(jù)進行檢索時,可以直接獲得聚集索引所對應的整條行記錄數(shù)據(jù)所在的頁,不需要進行第二次操作。

輔助索引
數(shù)據(jù)庫將所有的非聚集索引都劃分為輔助索引,但是這個概念對我們理解輔助索引并沒有什么幫助;輔助索引也是通過 B+ 樹實現(xiàn)的,但是它的葉節(jié)點并不包含行記錄的全部數(shù)據(jù),僅包含索引中的所有鍵和一個用于查找對應行記錄的『書簽』,在 InnoDB 中這個書簽就是當前記錄的主鍵。
輔助索引的存在并不會影響聚集索引,因為聚集索引構成的 B+ 樹是數(shù)據(jù)實際存儲的形式,而輔助索引只用于加速數(shù)據(jù)的查找,所以一張表上往往有多個輔助索引以此來提升數(shù)據(jù)庫的性能。
一張表一定包含一個聚集索引構成的 B+ 樹以及若干輔助索引的構成的 B+ 樹。

查找一條表記錄的過程
使用輔助索引查找一條表記錄的過程:通過輔助索引查找到對應的主鍵,最后在聚集索引中使用主鍵獲取對應的行記錄,這也是通常情況下行記錄的查找方式。

索引的設計

索引的設計其實是一個非常重要的內(nèi)容,同時也是一個非常復雜的內(nèi)容;索引的設計與創(chuàng)建對于提升數(shù)據(jù)庫的查詢性能至關重要,不過這不是本文想要介紹的內(nèi)容,有關索引的設計與優(yōu)化可以閱讀 數(shù)據(jù)庫索引設計與優(yōu)化 一書,書中提供了一種非??茖W合理的方法能夠幫助我們在數(shù)據(jù)庫中建立最適合的索引

我們都知道鎖的種類一般分為樂觀鎖和悲觀鎖兩種,InnoDB 存儲引擎中使用的就是悲觀鎖,而按照鎖的粒度劃分,也可以分成行鎖和表鎖。

并發(fā)控制機制

樂觀鎖和悲觀鎖其實都是并發(fā)控制的機制,同時它們在原理上就有著本質(zhì)的差別;

樂觀鎖是一種思想,它其實并不是一種真正的『鎖』,它會先嘗試對資源進行修改,在寫回時判斷資源是否進行了改變,如果沒有發(fā)生改變就會寫回,否則就會進行重試,在整個的執(zhí)行過程中其實都沒有對數(shù)據(jù)庫進行加鎖;
悲觀鎖就是一種真正的鎖了,它會在獲取資源前對資源進行加鎖,確保同一時刻只有有限的線程能夠訪問該資源,其他想要嘗試獲取資源的操作都會進入等待狀態(tài),直到該線程完成了對資源的操作并且釋放了鎖后,其他線程才能重新操作資源;
雖然樂觀鎖和悲觀鎖在本質(zhì)上并不是同一種東西,一個是一種思想,另一個是一種真正的鎖,但是它們都是一種并發(fā)控制機制。

樂觀鎖不會存在死鎖的問題,但是由于更新后驗證,所以當沖突頻率和重試成本較高時更推薦使用悲觀鎖,而需要非常高的響應速度并且并發(fā)量非常大的時候使用樂觀鎖就能較好的解決問題,在這時使用悲觀鎖就可能出現(xiàn)嚴重的性能問題;在選擇并發(fā)控制機制時,需要綜合考慮上面的四個方面(沖突頻率、重試成本、響應速度和并發(fā)量)進行選擇。

鎖的種類

對數(shù)據(jù)的操作其實只有兩種,也就是讀和寫,而數(shù)據(jù)庫在實現(xiàn)鎖時,也會對這兩種操作使用不同的鎖;InnoDB 實現(xiàn)了標準的行級鎖,也就是共享鎖(Shared Lock)和互斥鎖(Exclusive Lock);共享鎖和互斥鎖的作用其實非常好理解:

共享鎖(讀鎖):允許事務對一條行數(shù)據(jù)進行讀??;
互斥鎖(寫鎖):允許事務對一條行數(shù)據(jù)進行刪除或更新;
而它們的名字也暗示著各自的另外一個特性,共享鎖之間是兼容的,而互斥鎖與其他任意鎖都不兼容:

為什么要這么設計:
因為共享鎖代表了讀操作、互斥鎖代表了寫操作,所以我們可以在數(shù)據(jù)庫中并行讀,但是只能串行寫,只有這樣才能保證不會發(fā)生線程競爭,實現(xiàn)線程安全。

鎖的粒度

無論是共享鎖還是互斥鎖其實都只是對某一個數(shù)據(jù)行進行加鎖,InnoDB 支持多種粒度的鎖,也就是行鎖和表鎖;為了支持多粒度鎖定,InnoDB 存儲引擎引入了意向鎖(Intention Lock),意向鎖就是一種表級鎖。

意向鎖也分為兩種:
意向共享鎖:事務想要在獲得表中某些記錄的共享鎖,需要在表上先加意向共享鎖;
意向互斥鎖:事務想要在獲得表中某些記錄的互斥鎖,需要在表上先加意向互斥鎖;

隨著意向鎖的加入,鎖類型之間的兼容矩陣也變得愈加復雜:
意向鎖其實不會阻塞全表掃描之外的任何請求,它們的主要目的是為了表示是否有人請求鎖定表中的某一行數(shù)據(jù)。

鎖的算法

到目前為止已經(jīng)對 InnoDB 中鎖的粒度有一定的了解,也清楚了在對數(shù)據(jù)庫進行讀寫時會獲取不同的鎖,在這一小節(jié)將介紹鎖是如何添加到對應的數(shù)據(jù)行上的,我們會分別介紹三種鎖的算法:Record Lock、Gap Lock 和 Next-Key Lock。

Record 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 不知道待修改的記錄具體存放的位置,也無法對將要修改哪條記錄提前做出判斷就會鎖定整個表。

Gap Lock
記錄鎖是在存儲引擎中最為常見的鎖,除了記錄鎖之外,InnoDB 中還存在間隙鎖(Gap Lock),間隙鎖是對索引記錄中的一段連續(xù)區(qū)域的鎖;當使用類似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE; 的 SQL 語句時,就會阻止其他事務向表中插入 id = 15 的記錄,因為整個范圍都被間隙鎖鎖定了。

間隙鎖是存儲引擎對于性能和并發(fā)做出的權衡,并且只用于某些事務隔離級別。

雖然間隙鎖中也分為共享鎖和互斥鎖,不過它們之間并不是互斥的,也就是不同的事務可以同時持有一段相同范圍的共享鎖和互斥鎖,它唯一阻止的就是其他事務向這個范圍中添加新的記錄。

Next-Key Lock
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] 范圍內(nèi)的記錄都會被鎖定。

Next-Key 鎖的作用其實是為了解決幻讀的問題;

死鎖的發(fā)生

不同事務之間就可能會互相等待對方釋放鎖造成死鎖,最終導致事務發(fā)生錯誤;

事務與隔離級別

在介紹了鎖之后,我們再來談談數(shù)據(jù)庫中一個非常重要的概念 —— 事務;其中被人經(jīng)常提起的就是事務的原子性,在數(shù)據(jù)提交工作時,要么保證所有的修改都能夠提交,要么就所有的修改全部回滾。

但是事務還遵循包括原子性在內(nèi)的 ACID 四大特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability);

幾種隔離級別

事務的隔離性是數(shù)據(jù)庫處理數(shù)據(jù)的幾大基礎之一,而隔離級別其實就是提供給用戶用于在性能和可靠性做出選擇和權衡的配置項。

ISO 和 ANIS SQL 標準制定了四種事務隔離級別,而 InnoDB 遵循了 SQL:1992 標準中的四種隔離級別:READ UNCOMMITED、READ COMMITED、REPEATABLE READ 和 SERIALIZABLE;每個事務的隔離級別其實都比上一級多解決了一個問題:

RAED UNCOMMITED:使用查詢語句不會加鎖,可能會讀到未提交的行(Dirty Read);
READ COMMITED:只對記錄加記錄鎖,而不會在記錄之間加間隙鎖,所以允許新的記錄插入到被鎖定記錄的附近,所以再多次使用查詢語句時,可能得到不同的結果(Non-Repeatable Read);
REPEATABLE READ:多次讀取同一范圍的數(shù)據(jù)會返回第一次查詢的快照,不會返回不同的數(shù)據(jù)行,但是可能發(fā)生幻讀(Phantom Read);
SERIALIZABLE:InnoDB 隱式地將全部的查詢語句加上共享鎖,解決了幻讀的問題;

MySQL 中默認的事務隔離級別就是 REPEATABLE READ,但是它通過 Next-Key 鎖也能夠在某種程度上解決幻讀的問題。

不可重復讀

當事務的隔離級別為 READ COMMITED 時,雖然解決了臟讀的問題,但是如果在 SESSION 1 先查詢了一個范圍的數(shù)據(jù),在這之后 SESSION 2 中插入一條數(shù)據(jù)并且提交了修改,在這時,如果 SESSION 1 中再次使用相同的查詢語句,就會發(fā)現(xiàn)兩次查詢的結果不一樣。
不可重復讀的原因就是,在 READ COMMITED 的隔離級別下,存儲引擎不會在查詢記錄時添加間隙鎖,鎖定 id < 5 這個范圍。

幻讀

重新開啟了兩個會話 SESSION 1 和 SESSION 2,在 SESSION 1 中我們查詢?nèi)淼男畔ⅲ瑳]有得到任何記錄;在 SESSION 2 中向表中插入一條數(shù)據(jù)并提交;由于 REPEATABLE READ 的原因,再次查詢?nèi)淼臄?shù)據(jù)時,我們獲得到的仍然是空集,但是在向表中插入同樣的數(shù)據(jù)卻出現(xiàn)了錯誤。

這種現(xiàn)象在數(shù)據(jù)庫中就被稱作幻讀,雖然我們使用查詢語句得到了一個空的集合,但是插入數(shù)據(jù)時卻得到了錯誤,好像之前的查詢是幻覺一樣。

在標準的事務隔離級別中,幻讀是由更高的隔離級別 SERIALIZABLE 解決的,但是它也可以通過 MySQL 提供的 Next-Key 鎖解決;

REPERATABLE READ 和 READ UNCOMMITED 其實是矛盾的,如果保證了前者就看不到已經(jīng)提交的事務,如果保證了后者,就會導致兩次查詢的結果不同,MySQL 為我們提供了一種折中的方式,能夠在 REPERATABLE READ 模式下加鎖訪問已經(jīng)提交的數(shù)據(jù),其本身并不能解決幻讀的問題,而是通過文章前面提到的 Next-Key 鎖來解決。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容