對于后端開發來說,數據庫是我們日常開發中幾乎都會使用到的。而且對于許多大型應用來說,往往數據庫就是限制其性能的瓶頸所在。在以前的大多數時間里面,對數據庫的認知,始終停留在一個比較淺的層面里。遂決定翻閱相關的書籍、博客和官方文檔,讓自己對數據庫有一個全面的了解。
MySQL架構
以下是MySQL大體的組件結構
如果只關心和聚焦于客戶端向MySQL發送一條sql語句大致主要會涉及到的Server層的組件,可以查看下圖:
上面這張圖的箭頭標識和注釋也說得比較清楚了。如果想要更加詳細的說明,可以自行查閱相關文獻資料。需要說明的是在MySQL8.0中,已經將查詢緩存整個去掉了。
MySQL內置存儲引擎介紹
上圖介紹的MySQL架構大致可以分成Server層和存儲引擎層的。MySQL提供的基于插件式的存儲引擎,使得我們可以根據不同的需要選擇不同的引擎,甚至是在同一個schema中的不同表,也可以使用不同的存儲引擎。而實際的數據,也是存儲在存儲引擎中的。不同的存儲引擎的架構和對數據的組織方式也有所不同。正是這些不同,決定了這些存儲引擎提供了不同的特性和功能。
內置存儲引擎
我們可以使用show engines來查看當前mysql內置了哪些存儲引擎
三種常見的存儲引擎區別及介紹
InnoDB
InnoDB從5.1版本以后,已經取代了MyISAM,成為MySQL默認的存儲引擎了。可以從上表上看到,InnoDB是唯一支持事務、XA和Savepoints的內置存儲引擎。同時,它還支持行鎖、外鍵約束等。InnoDB表基于聚簇索引建立,并且采用MVCC來支持高并發,同時實現了ANSI SQL92定義的四種隔離級別,并在引擎內部實現了redo log和undo log。這些特性組合在一起,使得InnoDB成為了一個適合處理大量數據的高性能事務引擎。對于DBA來說,結合server層的bin log組成的日志系統機制,使得使用InnoDB作為數據存儲引擎的數據庫具備安全的崩潰恢復能力和快速穩定的復制性能,這些都是其它存儲引擎所不具備的。所以在Oracle收購MySQL以后,沒有了版權問題,InnoDB就毫無爭議地成為了MySQL的默認存儲引擎了。
MyISAM
在MySQL5.1及之前的版本,MyISAM都是默認的存儲引擎。雖然不支持事務、不支持行級鎖,崩潰后無法安全恢復。但是還MyISAM還是提供了許多其它的特性,包括全文索引、壓縮、空間函數(GIS)等。這些特性在某些場景下的性能是高于其它存儲引擎的,比如需要存儲和批量查詢歸檔日志數據,MyISAM引擎能提供較高的處理效率。
Memory
Memory存儲引擎將表中的數據存儲到內存中,為查詢和引用其他表數據提供快速訪問。因為是存在內存中,所以數據訪問的速度一般也要快于其它存儲引擎,同時Memory支持Hash索引,因此在單值查找的速度非常快。同時,MySQL在執行查詢的過程中需要使用臨時表來保存中間結果,內部使用的臨時表就是Memory表(如果結果集大小超出Memory表的限制,則會轉換成MyISAM表)。
使用哪一種引擎需要靈活選擇,一個數據庫中多個表可以使用不同引擎以滿足各種性能和實際需求,使用合適的存儲引擎,將會提高整個數據庫的性能
功 能 | MYISAM | Memory | InnoDB |
---|---|---|---|
存儲限制 | 256TB | RAM | 64TB |
支持哈希索引 | No | Yes | No |
支持全文索引 | Yes | No | No |
支持數索引 | Yes | Yes | Yes |
支持數據緩存 | No | N/A | Yes |
支持外鍵 | No | No | Yes |
InnoDB內存/磁盤結構及存儲邏輯結構
InnoDB總體架構
上面這張圖是InnoDB存儲引擎在內存和磁盤上的對應結構。
這里分別取兩個最常操作的update和select操作大致描述一下內部的流轉機制(在默認的可重復讀級別下, 同時略去了所有有關加鎖釋放鎖的操作):
1.select * from xxx where id=1 語句:
(1)引擎接收到執行計劃,會創建一個trx_id。
(2)查詢是否在這個內存中,如果在,則返回行。如果在change buffer中或者不在內存中,則從磁盤中讀入內存(在change buffer中還要涉及merge操作更新內存中的數據)。引擎層拿到數據,返回給到server層的執行器。
(3)如果Innodb發現某二級索引被頻繁訪問,會對該索引上創建一個哈希索引。下次同樣的查詢過來,會直接走這個自適應哈希索引。
2.update xxx set xxx where id=1 語句:
(1)引擎接收到執行計劃,會為該事務創建一個trx_id。
(2)查詢是否在這個內存中,如果在,則返回行。如果在change buffer中或者不在內存中,則從磁盤中讀入內存(在change buffer中還要涉及merge操作更新內存中的數據)。引擎層拿到數據,返回給到server層的執行器。
(3)執行器更新這行的相關列數據,再通過API接口調用引擎層,更新修改后的行數據更新到內存中。同時寫入redo log,此時處于prepare階段。
(4)引擎層告知執行器已經已經執行完成,隨時可以提交事務。
(5)server層的執行器將該操作寫入bin log
(6)執行期通過API接口告知引擎提交事務,引擎把剛剛寫入的redo log標記為commit狀態,更新完成。(兩階段提交)
上面就是5.5版本下的文件組織,可以看到,每個schema都有一個文件夾,文件夾里面有 db.opt和*.frm格式的文件。db.opt存放的是字符集和字符集排序規則信息(字符文件,可以打開),frm文件存的是表結構等信息,官方同時也給出說明,frm里的信息和InnoDB數據字典有所重疊,是出于歷史遺留原因(參見)。另外如果你沒有設置innodb_file_per_tale=ON那么所有的數據文件都將存儲在ibdata1文件中。所以這個文件會隨著數據的增長而增長。同時我們也可以看到,InnoDB結構圖中5.7相對與5.5最大的變化,就是對于ibdata1的拆分,它把原本共享表空間,undo表空間和臨時表空間從ibdata1中分了出來。好處當然就是結構更加清晰,更加方便獨立管理,不會出現之前臨時表空間不再使用釋放了,ibdata1文件還是那么大等情況。
InnoDB的數據邏輯結構
從上面InnoDB的架構圖里面的右半部分可以知道,無論是索引還是數據,InnoDB都把它們存在.idb后綴(或者ibdata1)的文件中。而在MyISAM中,索引和數據是分別存儲在MYI和MYD文件中的。
下圖是InnoDB的數據組織形式
從圖中可以看出,InnoDB數據其實就是保存在聚簇索引的葉子節點中的,并且按照主鍵列順序存儲在數據文件中的。
相比之下,MyISAM的數據則是按照插入的順序存儲在磁盤上的,其索引的葉子節點存儲的是可以定位到實際數據的行號(或者是可以找到其物理位置的地址,這里隱藏了頁的物理細節)。
這兩種數據組織形式,使得下面兩種引擎有如下區別:
1.由于使用聚簇索引,所以無法同時把數據行存放在兩個地方,所以一個表只能有一個聚簇索引。而InnoDB的二級索引的葉子節點也只能存儲聚簇索引上的主鍵值,從而導致二級索引(除覆蓋索引以外)在查詢數據的時候需要回表。
2.也由于MyISAM的索引不存在聚簇索引,葉子節點存儲的是實際數據的行號,所以對MyISAM而言,主鍵索引和其它索引一樣,不存在定位實際數據塊上的性能差異。
這里有一個有意思的問題,如果InnoDB的二級索引的葉子節點和MyISAM一樣,存儲的是可以直接找到實際數據的行號,那豈不是可以避免了回表的問題。我個人感覺確實是這樣的。但是那樣會存在一個問題,那就是行鎖和間隙鎖的加鎖問題。我們知道,InnoDB的行鎖其實質就是加在索引上面加的鎖,只要訪問到該索引,就會在對應的索引上面加鎖(包含回表的加鎖)如果不回表的話,那么不同索引上面加鎖并且相互獨立,那么行鎖和間隙鎖就毫無意義了。反過來說,InnoDB為什么會做一個回表這樣的邏輯,其實是在犧牲部分二級索引定位數據頁的性能,來換取更細粒度的鎖帶來的顯著性能提升。另外,如果在二級索引上存儲的是實際數據的行號,那在數據頁調整的時候,也要對這些二級索引進行更新,這同時也會導致寫性能的下降。
InnoDB的行鎖和間隙鎖
前面有提到,InnoDB的鎖是加在索引上的,行鎖的引入減少了鎖競爭的情況,從而提高了并發度。在不同隔離級別的不同語句下,加鎖情況也是不一樣的。從上圖可以知道,有一些查詢甚至不需要加鎖,通過基于MVCC實現的一致性讀就可以達到對應的隔離級別,這里又進一步提高了并發度。
總結
其實當我們大概了解了InnoDB架構組件中各個組件的作用,以及其數據存儲的邏輯結構。也就大概明白了為什么InnoDB提供了這么多其它存儲引擎不能提供的相關特性。例如:
1.redo log及對應的兩階段提交協議的引入,使得引擎可以提供在系統崩潰的時候提供安全崩潰恢復機制。
2.undo log機制的引入,每一事務都有一個單調遞增的trx_id,使得Innodb可以基于MVCC對相關的事務做一致性讀(或者稱為快照讀)。
3.InnoDB基于聚簇索引的數據組織形式,多數情況下通過行鎖,間隙鎖和快照讀實現了四種隔離級別。這種方式相比與直接加表鎖,性能更高,更加適合高并發場景。
通過根據其作用推出它能提供的特性,反過來也加深我們對各個組件的作用的理解。同時,通過思考這些組件的設計思想來實現對應的特性,這本身就是一個很有趣的過程。