Mysql以其還不錯的性能,簡單易用、免費開源的優點深受開發人員喜好。網上有不少關於Mysql的優秀文章,其中有不少主題值得深刻討論,好比:索引、事物、鎖、數據表拆分等等。在這些主題背後都有不少知識理論,筆者認爲這些知識理論都和Mysql的數據的存儲有關係,理解Mysql的數據存儲很是重要,是未來深刻學習探索Mysql的高級特性的基礎,因此整理了這篇文章,筆者能力有限,有理解不到位的地方,歡迎你們留言指正。mysql
爲了實現數據的持久化存儲,mysql將數據存儲到了磁盤上,因此客戶端對數據庫中數據進行查詢,mysql須要將磁盤上的相關數據加載到內存中(這就是所謂的磁盤IO),根據查詢sql篩選出數據返回給客戶端。操做系統讀寫磁盤的基本單位是扇區,而文件系統的基本單位是簇(不明白「扇區」和「簇」概念的讀者不要着急,下邊會進行介紹的),也就是說操做系統將磁盤上的數據文件加載到內存中是一整塊一整塊的讀取的,即使僅須要查詢一塊數據中的一個字節,也須要將這塊數據全加載到內存中來。mysql數據庫5.5版本以後,默認以InnoDB做爲存儲引擎,InnoDB就是以數據頁來存儲數據的,數據頁的大小默認爲16KB,操做系統將數據庫中的數據加載到內存也是以數據頁爲基本單位。sql
下面咱們來看一下磁盤是怎樣存取數據的,下面是一個磁盤的物理結構示意圖:數據庫
能夠看出磁盤有多個盤面套在心軸上,每一個盤的正反面有一個磁頭用於讀寫數據,磁盤在工做時,盤面的高速旋轉引發空氣動力,使得磁頭懸浮在盤面上,與盤面距離不到1微米,能夠在極短的時間內精肯定位到計算機指令指定的磁道上。下面咱們來了解一下磁道、扇區、柱面和簇的概念。緩存
每一個盤片的每一個盤面被劃分紅多個狹窄的同心圓環,數據就是存儲在這樣的同心圓環上,咱們將這樣的圓環稱爲磁道(Track),每一個盤面能夠劃分多個磁道。在每一個盤面的最外圈,離盤心最遠的地方是「0」磁道,向盤心方向依次增加爲1磁道,2磁道,等等。硬盤數據的存放就是從最外圈開始。磁頭只能沿着盤面的徑向移動,移動到要讀取數據的磁道上方,這段時間稱爲尋道時間,bash
磁盤的盤面上磁道數有成千上萬個。每一個磁道上能夠存儲數KB的數據,但計算機並不須要一次讀寫這麼多數據,基於此又把每一個磁道劃分紅若干弧段,每段稱爲一個扇區(Sector)。扇區是硬盤上存儲的物理單位(從DOS時代起,每一個扇區存儲512字節的數據,已經成爲業界不成文的規定)。扇區的編號是從1開始的,而不是0。磁頭到達指定磁道後,盤片經過旋轉,使得要讀取的扇區轉到讀寫磁頭的下方,這段時間稱爲旋轉延遲時間(rotational latencytime)。性能
柱面實際上是咱們抽象出來的一個邏輯概念,前面說過,離盤心最遠的磁道爲0磁道,依此往裏爲1磁道,2磁道,3磁道....,不一樣盤面上相同磁道編號則組成了一個圓柱面,即所稱的柱面(Cylinder)。磁盤數據的讀寫都是按照柱面進行的,即磁頭讀寫數據時首先在同一柱面內從0磁頭開始進行操做,依次向下在同一柱面的不一樣盤面(即磁頭上)進行操做,只有在同一柱面全部的磁頭所有讀寫完畢後磁頭才轉移到下一柱面,由於選取磁頭只需經過電子切換便可,而選取柱面則必須經過機械切換。電子切換比從在機械上磁頭向鄰近磁道移動快得多。所以,數據的讀寫按柱面進行,而不按盤面進行。 讀寫數據都是按照這種方式進行,儘量提升了硬盤讀寫效率。學習
物理相鄰的若干個扇區稱爲了一個簇,文件系統的基本單位是簇(Cluster)。 簇通常有這幾類大小 4K,8K,16K,32K,64K等。簇越大存儲性能越好,但空間浪費嚴重。簇越小性能相對越低,但空間利用率高。ui
從上邊對磁盤結構分析咱們知道:磁盤讀取數據時,磁頭經過盤面徑向移動到磁道上,而後盤面旋轉到目標扇區的時候開始讀取數據,若是數據都存在相鄰的扇區,相比於數據存儲在不一樣的磁道的扇區上來說,磁盤讀取數據的效率就會高不少(不須要尋道時間,只須要不多的旋轉時間),這就是順序I/O性能優於隨機I/O的緣由。編碼
Mysql數據庫查詢數據效率的瓶頸在於磁盤I/O(這是一件很耗時的工做)。數據庫中數據存儲的基本單位是一張表中的一條記錄,記錄是按照行爲單位存儲的,可是數據庫加載磁盤數據到內存並非以行爲單位(這樣的話每讀取一條記錄都須要一次磁盤I/O,效率很是低。),前邊說過,Mysql新版本默認存儲引擎是InnoDB,InnoDB按照頁來存儲數據,頁的大小默認爲16KB。(可經過下面命令行查看:)spa
mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.02 sec)
複製代碼
所以數據庫加載數據到內存,不論讀一條記錄,仍是讀多條記錄,都是將這些記錄所在的頁進行加載。也便是說,數據庫管理存儲空間的基本單位是頁(Page)。
咱們平時向Mysql中插入數據以「記錄」爲基本單位,記錄在磁盤上的存儲方式被稱爲「行格式」或「記錄格式」。InnoDB存儲引擎支持不一樣的行格式(Compact、Redundant、Dynamic和Compressed),它們的原理基本上是相同的。咱們以Compact行格式爲例,下邊是其存儲示意圖:
一條記錄數據的存儲能夠分爲兩部分:「額外信息」和「真實數據」。 額外信息用來描述記錄,分爲變長字段列表、NULL值列表和記錄頭信息,這部分信息很是重要,咱們分別來看一下:
咱們在項目使用到char類型,好比使用char(32)來存儲用戶的密碼,那麼使用char類型的字段會被添加到變長字段列表中嗎?答案是有可能會!由於項目中數據表使用的字符集是不肯定的,最經常使用的utf8mb4使用1~4個變長字節來編碼數據,中文和英文所佔用的字節數是不一樣的。
對於項目中絕大多數使用存儲量小的字段,好比說varchar(32),tinyint(4)等,假設使用utf8字符集,這種字段存儲的真實數據長度必定不會超過255,使用一個字節表示就能夠了。可是若是字段的真實數據可能會超過了255該怎麼表示呢?分爲兩種狀況:當真實數據字節數小於127的時候,用1個字節表示,大於127的時候使用2個字節表示,也就是說字節的最高位表示該字節表示的是一個變長數據的一部分仍是所有。
問:變長字段長度列表、NULL值列表中的信息之因此按照列的順序逆序存放?答:這樣可使記錄中位置靠前的字段和它們對應的字段長度信息加載到內存中時,位置距離更近,可能會提升高速緩存的命中率。
下面是對記錄頭標誌位信息的詳細描述:
名稱 | 佔用bit數 | 描述 |
---|---|---|
預留位1 | 1 | 暫時未使用到 |
預留位2 | 1 | 暫時未使用到 |
delete_mask | 1 | 標記該記錄是否被刪除 |
min_rec_mask | 1 | B+樹的每層非葉子節點中的最小記錄都會添加該標記 |
n_owned | 4 | 當前槽擁有的記錄數 |
heap_no | 13 | 當前記錄在記錄堆的位置 |
record_type | 3 | 表示當前記錄的類型,0表示普通記錄,1表示B+樹非葉子節點記錄,2表示最小記錄,3表示最大記錄 |
next_record | 16 | 下一條記錄的相對位置 |
記錄頭信息很是多,可是這些信息在數據查找和存儲的過程當中很是有用,就好比說next_record記錄着當前記錄下一條記錄的相對位置,對於數據查找很是方便。delete_mark標示記錄是否被刪除,由此咱們能夠知道,數據庫的物理刪除並非直接從磁盤上物理刪除,而是經過一個標誌位標示,等未來要用到這一部分磁盤空間的時候再釋放。其餘的標誌位信息也比較重要,咱們下邊用到的時候會詳細說明。
咱們再來看記錄的真實數據部分,記錄的真實數據中除了咱們數據表中自定義的一些字段外,數據庫會額外的添加一些隱藏列用於完成數據快速查找、事物提交、回滾等操做。隱藏列信息以下:
列名 | 真實名稱 | 是否必須 | 佔用空間 | 描述 |
---|---|---|---|---|
roll_id | DB_ROW_ID | 否 | 6字節 | 惟一標示一條記錄的行ID |
transaction_id | DB_TRX_ID | 是 | 6字節 | 事物ID |
roll_pointer | DB_ROLL_PTR | 是 | 7字節 | 回滾指針 |
這些隱藏列的信息很是重要,咱們來逐一說明一下:
DB_ROW_ID:咱們知道它是無關緊要的,這跟Innodb主鍵的生成策略有關。Innodb優先使用用戶自定義主鍵做爲主鍵,若是用戶沒有定義主鍵,則選取一個Unique鍵做爲主鍵,若是表中連Unique鍵都沒有定義的話,則InnoDB會爲表默認添加一個名爲row_id的隱藏列做爲主鍵。爲何InnoDB非要生成主鍵呢?由於數據庫爲了更好的進行範圍查找和數據匹配,老是將記錄按照主鍵從小到大存儲的,有序對於數據查找很是重要,記錄存儲有序了,就能高效使用二分查找策略了。
DB_TRX_ID:咱們知道InnoDB和MySIAM一個重要的區別是,InnoDB支持事物,事物要保證數據操做的ACID,InnoDB爲數據表中的每條記錄都生成一個transaction_id列,當記錄被事物使用到時,使用它來存儲事物id。
InnoDB支持行鎖,不少人認爲事物必需要配合InnoDB的行鎖才能完成工做,這種觀點是錯誤的!事物自己有本身的隔離級別,隔離級別不一樣,對數據一致性的要求也不一樣,事物自己有本身的一套MCVV(版本鏈控制),不必定使用到行鎖。
真實數據存儲,還有一點很是重要,Innodb將記錄存在默認大小爲16KB的數據頁中,而咱們真實存儲的數據記錄,好比text類型,超過16KB也是有可能的,前邊咱們也提到,記錄存儲自己還有一些額外的信息須要存儲,這樣咱們一個數據頁中每每會存不下這些大記錄。這個時候就會發生頁分裂,多出來的部分數據會被存儲到溢出頁中。
InnoDB默認採用Dynamic的行格式儲存記錄,dynamic行格式中列存儲是否放到off-page頁(溢出頁),主要取決於行大小,它會把行中最長的那一列放到off-page頁,直到數據頁能存放下兩行。
以上就是Innodb引擎關於行記錄的介紹,下邊咱們再來看一直強調的數據頁的結構,以及記錄是怎樣組織起來存儲到數據頁中的。
Innodb爲了實現不一樣的目的設置了不少種頁,咱們上邊提到的儲存記錄數據的頁叫作數據頁,此外還有一些其餘的頁來完成不一樣的工做:好比存放INODE信息的頁,存放undo日誌信息的頁等等,本節咱們主要探討數據頁的存儲結構,其餘類型的頁在「表空間」中會簡單提到,不做爲重點。
爲了實現數據的快速檢索和存儲的科學性,16KB大小的數據頁又被大體分紅了7個部分:
先簡單介紹一下File Header、FileTailer,而後再重點詳細說明行記錄是怎樣組織存儲到數據頁中的。File Header 和 File Tailer 是全部類型的頁都通用的結構。Mysql是以頁爲單位將磁盤數據加載到內存中,而後進行查詢、修改,以後再以頁爲單位,將修改過的數據刷到磁盤上。爲了保證數據的完整性,InnoDB在每一個頁的尾部都加了一個File Trailer部分,這個部分由8個字節組成,存儲頁面的校驗和(4字節)、日誌序列位置(LSN)(4字節),同FileHeader中的校驗和、LSN相對應,用來校驗文件數據同步的完整性。FileHeader還存儲了另一些頁的通用信息,好比頁屬於哪一個表空間(下一節咱們會說什麼是表空間),頁的上一頁、下一頁信息,這是很是重要的,數據頁基於File Header中記錄的上一頁、下一頁信息構成一個雙向鏈表:
數據頁之間經過雙向鏈表鏈接意味着在物理空間存儲上,數據頁之間並不必定是連續的,可是Mysql儘量的會去保證數據頁在物理空間上的連續,由於Mysql常常進行範圍查找,物理空間連續意味着可能更多的使用到順序IO,更詳細的細節咱們下一節會具體講到。
數據記錄會存儲到User Records部分,一開始數據頁中User Records部分並不存在,不佔據任何存儲空間。隨着插入數據的增多,User Records存儲的用戶記錄愈來愈多,Free Space的空間愈來愈少(像海綿同樣自由壓縮),直到沒有了Free Space,用戶數據記錄插入時,申請新的數據頁進行插入。咱們前邊提到,Innodb會爲每一條記錄生成主鍵,若是定義的數據表中沒有主鍵,就會生成一個隱藏的row_id列,在User Records存儲區存儲的記錄是按照由小到大的順序排列的,爲了更好的管理數據記錄,InnoDB定義了兩條僞記錄:最小記錄與最大記錄(infimun+suprenum)。這兩條記錄的構造十分簡單,都是由5字節大小的記錄頭信息和8字節大小的一個固定的部分組成,一共佔用26個字節。上述描述的數據插入過程以下圖所示:
咱們再回過頭來講說記錄行格式中的頭部信息。記錄行的頭部信息40個字節是固定的表示記錄的屬性信息,咱們上邊提到的最小記錄和最大記錄的record_type分別爲二、3,而咱們用戶字節插入的數據記錄record_type爲0。咱們還知道next_record表示從當前記錄的真實數據到下一條記錄的真實數據的地址偏移量。 這塊咱們要重點理解一下,next_record存儲的是當前記錄到下一條真實數據的偏移量而不是到到下一條記錄的偏移量,由於記錄還包含有額外的信息。記錄在數據頁的User Records區按照主鍵大小排序儲存,這樣經過記錄的next_record信息,數據頁中的數據記錄之間就造成了一條單向鏈表:
最小記錄和最大記錄的頭部信息中heap_no是最小的0和1,在數據頁中是排在最前邊的,最大記錄的next_record值爲0,從最小記錄到最大記錄經過next_record存儲的偏移量構成了邏輯上的單向鏈表(圖中將每條記錄分開表示是爲了展現記錄之間的關聯,真實數據存儲中,next_record存儲的是偏移量,數據記錄在物理空間存儲上是連續的)。記錄中的delete_mask都是0,表示記錄並無被刪除。假如用戶記錄2被刪除,會發生什麼事情呢?mysql會將用戶記錄的記錄頭部信息的delete_mask置爲1(並無立刻清理存儲空間),同時將指向它的上一條記錄的next_record改成指向它下一條記錄數據起始的偏移量,依舊維護着一條單向鏈表:
記錄頭標誌中的n_owned存儲的是當前槽擁有的記錄數,槽是什麼?它和數據頁部分的Page Directory又有什麼樣的關係呢?爲了弄明白這兩個問題,咱們先來看看在一個數據頁中查找一條用戶記錄是怎樣實現的。咱們知道用戶記錄在數據頁中按照主鍵大小順序存儲,根據主鍵id查找記錄,可使用二分查找提升效率。二分查找的次數和記錄數量有關係,數據頁中存儲的用戶記錄可能成百上千條,有沒有什麼辦法能加快二分查找的速度呢?另外咱們知道二分查找在數據量大的狀況下,很是的高效,在用戶記錄只有比較少(0-8)個的狀況下,也許並無順序查找來的簡單高效。因此InnoDB在數據頁中爲了加快數據查找速度,將用戶記錄作了分組,每一個組中最後一條記錄(也就是組中最大用戶記錄)的頭信息中n_owned標誌位記錄着當前組擁有的記錄數量;同時,將每一個組中最後一條記錄的地址偏移量單獨提取出來存儲到Page Directory處,造成了所謂的頁目錄。頁目錄中這些地址偏移量被稱爲槽。正如剛剛所說的,組中分配記錄數量的多少是有考量的。InnoDB有這樣的規則:最小記錄的分組只能有1條記錄(就是它本身,最小記錄),最大分組擁有的記錄數是1~8條(包括用戶記錄和它自己),其餘分組中記錄的條數在4~8條。咱們來分析下隨着用戶記錄的插入,分組和「槽」變化的過程:
有了數據頁的目錄部分(Page Directory),咱們再來梳理一下在一個數據頁中查找一條記錄的過程。分爲兩步:第一步:首先經過二分法肯定該記錄所在的槽,並找到該槽所在分組中主鍵值最小的那條記錄。第二步:經過記錄的next_record屬性遍歷該槽所在的組中的各個記錄。到這裏咱們就清楚了InnoDB數據頁的結構,也明白了怎樣在一個數據頁中查找一條指定的記錄了,下面咱們從更大維度,更廣闊的視角來看一看Mysql的數據存儲的設計。
爲了更好的管理數據頁,Innodb引入了表空間的概念。(Oracle的存儲結構是按照表空間進行管理的,Innodb模仿了Oracle的數據存儲方式)。
咱們先來區分一下Mysql的安裝目錄和數據目錄的區別:在操做系統上安裝完Mysql以後,咱們就獲得了Mysql的安裝目錄,其中安裝目錄下的bin文件夾下有管理Mysql的可執行文件,例如mysql、mysqld、mysqld_safe等等。而Mysql在運行期間產生的數據倒是在數據目錄下存放的,能夠經過命令查看mysql的數據目錄:
mysql> show variables like 'datadir';
+---------------+------------------------+
| Variable_name | Value |
+---------------+------------------------+
| datadir | /usr/local/mysql/data/ |
+---------------+------------------------+
1 row in set (0.04 sec)
複製代碼
咱們再來看看數據庫和數據表在操做系統上是如何表示的:每一個數據庫對應mysql數據目錄下的一個同名文件。在咱們使用create database建立數據庫的時候,若是使用Innodb引擎,會在數據庫同名文件下建立一個名爲db.opt的文件,該文件包含了數據庫的字集、比較規則等屬性信息,它是一個二進制文件,筆者使用cat file查看信息時是一些亂碼信息。和數據庫同樣,數據表的存儲也須要一個文件存儲表的屬性信息,例如:表中的列、每一個列類型(int,varchar之類的)、字符集、使用了哪些索引等等。**在Innodb引擎中,建立表的時候會生成一個.frm的數據表同名文件來表示數據表結構。**咱們知道Innodb以頁爲單位存儲數據,爲了更好的管理數據,Innodb引擎將數據表中的數據存儲到表空間下,表空間是一個抽象的概念,它對應着文件系統上的不少文件,表空間下有許多許多頁,咱們的表數據就存儲在這些頁中。Mysql爲Innodb引擎設計了不少表空間,例如:系統表空間、獨立表空間、通用表空間(general tablespace)、undo表空間(undo tablespace)、臨時表空間(temporary tablespace)等等。咱們就來簡單介紹一下系統表空間和獨立表空間:
和Innodb不一樣的是,MyISAM並無表空間,表數據都放在對應的數據庫子目錄下,使用MyISAM建立數據表以後會生成三個文件:表.frm、表.MYD、表.MYI,其中表.frm和Innodb同樣,存儲數據表的屬性信息,表.MYI存儲表的索引文件,表.MYD存儲表的數據文件。這點和Innodb不一樣,咱們說Innodb將表數據和索引都存儲到表空間下,由於Innodb索引的設計和MyISAM是不一樣的,Innodb中數據即索引,索引即數據。
下邊對錶空間的存儲結構作簡單介紹:
數據頁的結構咱們已經很清楚了,爲何好端端的又提出了一個區的概念呢?咱們前邊說過,Mysql儘量的將數據順序儲存來儘量高的使用順序IO提升磁盤加載速度,一個頁默認大小16KB,是能夠存放更多記錄,這些記錄已經順序存儲了,還不夠,因此表空間又添加了區的概念,一個區默認由64個數據頁組成,一個數據頁默認大小是16KB,一個區也就是1M。這樣1M的數據順序存儲了,在插入大量數據的時候,Innodb還能夠申請多個連續的區來存放用戶數據,這樣,多個區就又順序存儲了,這些細節的考慮對於Mysql磁盤IO加載數據性能的提高是不可忽略的。
區存儲數據的粒度仍是太粗了,在數據量比較少的狀況下,區中每每有不少空間不能被有效利用,基於此,又將區分爲了四種:空閒的區、有剩餘空間的碎片區、沒有剩餘空間的碎片區、附屬於某個段的區。這樣在數據表存儲數據比較少的狀況下,就可使用有剩餘空間的碎片區存儲數據了。
那段呢?段又是什麼?Innodb引擎是將數據和索引都存儲成一個B+樹結構,也就是索引和數據是放在一塊兒的(這是聚簇索引的概念),存儲索引的頁和存儲數據的頁是不一樣類型的,在表空間中,索引和數據也是分開存儲的,另外還有關於事物的處理,也有不一樣的段用來存儲數據。爲了更好的區分這些,Innodb使用段來將不一樣的數據作區分,存儲用戶數據記錄的叫數據段,存儲索引的叫索引段。一個段由不少附屬於它的區和一些其餘區的零散的頁組成。(附屬於一個段的區存儲的數據都會這個段的相關數據),這樣就是實現了對特定數據的區分管理。
本節絕大部份內容都是在探討Innodb引擎的數據存儲的,從記錄的存儲到數據頁的設計,後邊簡單談了一下表空間概念,按部就班的幫助你們瞭解了Mysql數據存儲的概貌。文中提到了不少細節,好比數據記錄都有哪些隱藏列,數據頁的Page Directoy區是怎樣設計的等等,理解這些很是重要,Mysql的索引、事物有不少原理都是基於這些基礎的數據存儲結構設計實現的,但願本文能給想要深刻學習Mysql的讀者帶來一些收穫,謝謝你們!