![]() 作者 | 小白 出品 | 小白debug(ID:xiaobaidebug) 你是一個程序員,你做了一個網站應用,站點里的用戶數據,需要存到某個地方,方便隨時讀寫。 很容易想到可以將數據存到文件里。 但如果數據量很大,想從大量文件數據中查找某部分數據,并更新,是一件很痛苦的事情。 那么問題就來了,有辦法可以解決這個問題嗎? 好辦,沒有什么是加一層中間層不能解決的,如果有,那就再加一層。 這次我們要加的中間層是 mysql。 ![]() 什么是 mysqlMysql數據庫,是一款存放和管理數據的軟件, 它介于應用和數據之間,通過一些設計,將大量數據,變成一張張像 excel 的數據表。為應用提供創建(Create), 讀取(Read), 更新(Update), 刪除(Delete)等核心操作。 ![]() 我們來看下它是怎么實現的。 數據頁mysql 將數據組織成 excel 表的樣子。 excel 文件在磁盤上是個xls 文件,mysql 的數據表也類似,在磁盤上則是個ibd 后綴的文件。 ![]() 數據表越大,磁盤上的 ibd 文件也就越大。 直接讀寫一個大文件里的全部數據會很慢,所以 MySQL 將數據拆成一個個數據頁,每頁大小 16KB。這樣我們讀寫部分表數據的時候,就只需要讀取磁盤里的幾個數據頁就好。 ![]() 索引但數據頁那么多,查某條數據時,怎么知道要讀哪些數據頁? 好辦,可以為每個數據頁加入頁號,再為每行數據加個序號,這個序號其實就是所謂的主鍵。 按主鍵大小排序,將每個數據頁里最小的主鍵序號和所在頁的頁號提出來,放入到一個新生成的數據頁中,并且給數據頁加入層級的概念。 這樣我們就可以通過上層的數據頁快速縮小查找范圍,加速查找數據頁的過程。 現在頁跟頁之間看起來就像是一棵倒過來的樹,這棵可以加速查找數據頁的樹,就是我們常說的B+樹索引。 ![]() 上面提到的是針對主鍵的索引,也就是主鍵索引。 ![]() 按同樣的思路,也可以為其他數據表的列去建立索引,比如用戶表的名稱字段,這樣我們就能快速查找到名字為 xx 的用戶有哪些,這就是所謂的輔助索引。 ![]() Buffer Pool但就算有了索引,數據也還是在磁盤上。每次都讀磁盤太慢了。有辦法提升下性能嗎? 有!在磁盤數據和應用之間,加一層進程內緩存,緩存里裝的就是前面提到的 16KB 數據和索引頁, 它就是所謂的 Buffer Pool。 ![]() 讀數據的時候優先讀 Buffer Pool,有數據就返回,沒數據才去磁盤里讀取,減少了讀磁盤的次數,大大提升了性能。 但問題就來了,我們知道,文件讀取,默認會先將文件數據加載到操作系統的文件緩存中,同樣都是緩存,為什么還要整 Buffer Pool 這死出? 這是因為進程自己維護的 Buffer Pool ,可以定制更多緩存策略,還能實現加鎖等各種數據表高級特性。 也正是因為已經有了 Buffer Pool,所以也就沒必要使用操作系統的文件緩存了,所以 Buffer Pool 通過'直接 I/O' 模式, 繞過操作系統的緩存機制,直接從磁盤讀寫數據。 ![]() 自適應 hash 索引就算有了 buffer pool,要查到某個數據頁,也依然要查找 B+樹,查詢復雜度 O(lgn)。能更快嗎? 能!可以使用查詢復雜度為 **O(1)**的 hash 表進行優化。 記錄每個數據頁的查詢頻率,對于熱點數據頁,我們以查詢的值為 key,數據頁地址為 value,構建 hash 表。 比如name為 ![]() 這個 hash 表,就是所謂的自適應哈希索引,Adaptive Hash Index。 ![]() Change Buffer有了自適應 hash 索引的加持,讀性能提高了。那寫性能也能優化嗎? 能! 大部分數據表,除了主鍵索引外,我們還會加一些輔助索引。比如對用戶名加個輔助索引。 那對于這類數據表的寫操作,更新完主鍵索引的數據頁之后,還需要更新輔助索引頁。這樣讀取輔助索引頁的磁盤 IO 必然少不了。 ![]() 怎么辦呢?我們可以先將要寫入的數據收集到一塊內存里,等哪天磁盤里的索引頁正好被讀入 Buffer pool 的時候,再將寫入數據應用到索引頁中。 通過這個方式減少大量的磁盤 IO,提升性能。 而這個將寫操作收集起來的地方,就是所謂的 Change Buffer,它其實是 Buffer pool 的一部分。 ![]() Undo Log在數據庫中,有一個叫事務的概念。不了解沒關系,說白了,就是可以讓多行數據,要么同時更新成功,要么同時更新失敗。也就是所謂的原子性。 ![]() 為了實現這一點,我們就需要知道寫數據時每行數據原來長啥樣,方便對更新后的數據行,進行回滾,因此就有了 Undo Log。 ![]() 更新 buffer pool 數據頁的時候, 會用舊數據生成 undo log 記錄,存儲在 Buffer Pool 中的特殊 undo log 內存頁中。 并隨著 buffer pool 的刷盤機制,不定時寫入到磁盤的 undo log 文件中。 ![]() Redo Log上面提到的都是 buffer pool 相關的內容,它們本質上都是內存。 如果內存數據只寫了一半到磁盤中,數據庫進程就崩了,那一個事務里的多行數據就沒能做到'同時更新成功'。 怎么辦呢? 好辦,我們將事務中更新數據行的操作都寫入到 redo log buffer 內存中,然后在事務提交的時候進行 redo log 刷磁盤,將數據固化到 redo log 文件中。 數據庫進程崩潰重啟后,就能通過 redo log file 找到歷史操作記錄,重做數據。保證了事務里的多行數據變更,要么都成功,要么都失敗。 ![]() 這時候問題就來了,我有這功夫更新 redo log file 文件,直接將 buffer pool 的數據寫入到磁盤不香嗎? ![]() 不太一樣,redo log file 是順序寫入的,buffer pool 的內存數據是隨機分散在磁盤各處的,順序寫磁盤性能是隨機寫的幾十倍,所以很多存儲系統在寫數據時都會搞個日志來記錄操作,方便服務重啟后進行數據對賬,確保數據的一致性和完整性,這類操作就是所謂的 Write-Ahead Logging (WAL) 。 ![]() 但問題又來了,redo log buffer 也是內存,buffer pool 也是內存,如果 redo log buffer 里的數據還沒來得及寫入到 redo log,數據庫進程就崩了,那 redo log buffer 里的數據不也丟了嗎? 是的,所以 redo log 的作用并不是保證所有數據不丟失,而是確保已提交事務的變更不會丟失。但因為 redo log 刷盤頻率很高,所以丟失數據的概率很低。 redo log 本質上是寫入性能和數據完整性折中的產物,做架構就是這樣,做到最后總是需要通過犧牲某些東西去換取另一樣東西,果然,程序員才是真正的煉金術師。 Innodb 是什么我們將上面提到的內容,分為內存和磁盤兩部分,一部分是內存里的自適應哈希,buffer pool,以及 redo log buffer。另一部分是磁盤里存放行數據和索引的.ibd 文件, 以及 undo log, redo log 等文件。它們共同構成了 innodb 存儲引擎。并對外提供一系列函數接口。 比如操作數據行的 write_row(), update_row(),以及操作數據表的 create(), drop()等等接口。 我們平時寫的 SQL 語句,最終都會轉換成 InnoDB 提供的這些接口函數調用。 ![]() 比如:
但問題就來了,我們平時讀寫 mysql 用的 sql 語句,是怎么轉成存儲引擎的函數接口的呢? 那就需要介紹 Server 層了。 Server 層是什么Server 層,本質上是 sql 語句 和 innodb 存儲引擎之間的中間層。 ![]() 在 Server 層內提供一個連接管理模塊,用于管理來自應用的網絡連接。 并提供一個分析器,用于判斷 SQL 語句有沒有語法錯誤,比如 select,是不是少打了一個 再提供一個優化器,用于根據一定的規則選擇該用什么索引,生成執行計劃。 之后,提供一個執行器,根據執行計劃去調用Innodb 存儲引擎的接口函數。 ![]() server 層和存儲引擎層共同構成了一個完整的數據庫,它就是我們常說的 Mysql 數據庫。 ![]() 并且,server 層和存儲引擎層是通過接口函數進行解耦的,換句話說就是,只要實現了上面這些接口函數,就能作為存儲引擎與 Server 層對接。 ![]() 比如,mysql 早期用的是 myisam 存儲引擎,后來才支持的 innodb。 ![]() binlog 是什么你聽說過刪庫跑路吧,為了防止數據庫表被刪除帶來的影響, server 層會將歷史上所有變更操作記錄到磁盤上的日志文件中,這個日志文件就是所謂的 binlog。一旦誤刪表,就可以利用 binlog 來恢復數據。 那么問題就來了,innodb 有一個 redo log 也做類似的事情,為什么還要多此一舉?評論區告訴我答案。 這是因為 redo log 是環狀寫入的,后面寫的內容最終會覆蓋前面的內容,也就是不會記錄所有歷史寫操作,而 binlog 卻會記錄所有歷史變更。并且 binlog 位于 server 層,這樣不管底層的存儲引擎是什么,都能復用這部分能力。 ![]() Mysql 主從架構由于 binlog 記錄了一個 mysql 的所有變更操作,因此我們還可以利用 binlog 數據,'復制'一個新的 mysql 出來。原來的 master 叫主數據庫,復制出來的則是從數據庫,主數據庫負責承接寫流量,從數據庫負責讀流量,這樣就可以讓 mysql 承接更高的讀寫流量。它就是經典的 mysql 主從同步架構。 數據庫查詢更新流程接下來我們用實際例子將上面提到的內容串起來。首先不管是查詢還是更新操作,客戶端都會先跟 mysql 建立網絡連接,并將 sql 發送到 server 層,經過分析器解析 sql 語法、優化器選擇索引生成執行計劃,最終給到執行器調用 InnoDB 的函數接口。
![]() 現在大家通了嗎? 總結
如果你覺得這篇文章對你有幫助,記得轉發給你那不成器的兄弟。最后遺留一個問題,你聽說過 HDFS 嗎?你知道它的架構是怎么樣的嗎? ![]() |
|
來自: 格瑞思n5c5alhf > 《mysql》