【转】2.2【MySQL】运行原理(二):InnoDB 内存结构、磁盘结构及update sql执行过程分析
前一篇講完了查詢流程,我們是不是再講講更新流程、插入流程和刪除流程?在數(shù)據(jù)庫里面,我們說的update操作其實包括了更新、插入和刪除。如果大家有看過MyBatis的源碼,應(yīng)該知道Executor里面也只有doQuery()和doUpdate()的方法,沒有doDelete()和doInsert()。
更新流程和查詢流程有什么不同呢?基本流程也是一致的,也就是說,它也要經(jīng)過解析器、優(yōu)化器的處理,最后交給執(zhí)行器。區(qū)別就在于拿到符合條件的數(shù)據(jù)之后的操作。
緩沖池 Buffer Pool
首先,InnnoDB的數(shù)據(jù)都是放在磁盤上的,InnoDB操作數(shù)據(jù)有一個最小的邏輯單位,叫做頁(索引頁和數(shù)據(jù)頁)。我們對于數(shù)據(jù)的操作,不是每次都直接操作磁盤,因為磁盤的速度太慢了。 InnoDB使用了一種緩沖池的技術(shù),也就是把磁盤讀到的頁放到一塊內(nèi)存區(qū)域里面。這個內(nèi)存區(qū)域就叫Buffer Pool。
下一次讀取相同的頁,先判斷是不是在緩沖池里面,如果是,就直接讀取,不用再次訪問磁盤。
修改數(shù)據(jù)的時候,先修改緩沖池里面的頁。內(nèi)存的數(shù)據(jù)頁和磁盤數(shù)據(jù)不一致的時候,我們把它叫做臟頁。InnoDB里面有專門的后臺線程把Buffer Pool的數(shù)據(jù)寫入到磁盤,每隔一段時間就一次性地把多個修改寫入磁盤,這個動作就叫做刷臟。
BufferPool是InnoDB里面非常重要的一個結(jié)構(gòu),它的內(nèi)部又分成幾塊區(qū)域。這里我們趁機到官網(wǎng)來認識一下InnoDB的內(nèi)存結(jié)構(gòu)和磁盤結(jié)構(gòu)。
1.Innodb 內(nèi)存結(jié)構(gòu)
Innodb的內(nèi)存結(jié)構(gòu)主要分為 3 個部分: Buffer Pool、Change Buffer、Adaptive HashIndex,另外還有一個(redo)log buffer。
1.1 Buffer Pool
Buffer Pool緩存的是頁面信息,包括數(shù)據(jù)頁、索引頁。
SHOW STATUS LIKE '%innodb_buffer_pool%'; -- 查看服務(wù)器狀態(tài)中 Buffer Pool 相關(guān)信息 SHOW VARIABLES like '%innodb_buffer_pool%'; -- 查看參數(shù)(系統(tǒng)變量)這些狀態(tài)都可以在官網(wǎng)查到詳細的含義,用搜索功能。 Buffer Pool默認大小是128M(134217728字節(jié)),可以調(diào)整。
內(nèi)存的緩沖池寫滿了怎么辦?(Redis 設(shè)置的內(nèi)存滿了怎么辦?)InnoDB 用 LRU算法來管理緩沖池(鏈表實現(xiàn),不是傳統(tǒng)的LRU,分成了young和old),經(jīng)過淘汰的數(shù)據(jù)就是熱點數(shù)據(jù)。
內(nèi)存緩沖區(qū)對于提升讀寫性能有很大的作用。思考一個問題:當需要更新一個數(shù)據(jù)頁時,如果數(shù)據(jù)頁在BufferPool中存在,那么就直接更新好了。否則的話就需要從磁盤加載到內(nèi)存,再對內(nèi)存的數(shù)據(jù)頁進行操作。也就是說,如果沒有命中緩沖池,至少要產(chǎn)生一次磁盤IO,有沒有優(yōu)化的方式呢?
1.2 Change Buffer(寫緩沖)
如果這個數(shù)據(jù)頁不是唯一索引(注:唯一索引就是在同一字段下不能有相同值),也就不需要從磁盤加載索引頁判斷數(shù)據(jù)是不是重復(fù)(唯一性檢查)。這種情況下可以先把修改記錄在內(nèi)存的緩沖池中,從而提升更新語句(Insert、Delete、Update)的執(zhí)行速度。這一塊區(qū)域就是 Change Buffer。5.5 之前叫Insert Buffer 插入緩沖,現(xiàn)在也能支持delete和update。最后把 Change Buffer 記錄到數(shù)據(jù)頁的操作叫做 merge。
什么時候發(fā)生 merge?有幾種情況:在訪問這個數(shù)據(jù)頁的時候,或者通過后臺線程、或者數(shù)據(jù)庫shut down、redo log寫滿時觸發(fā)。
如果數(shù)據(jù)庫大部分索引都是非唯一索引,并且業(yè)務(wù)是寫多讀少,不會在寫數(shù)據(jù)后立刻讀取,就可以使用Change Buffer(寫緩沖)。寫多讀少的業(yè)務(wù),調(diào)大這個值:
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';max_size 代表Change Buffer占Buffer Pool的比例,默認25%。
1.3 Adaptive Hash Index
索引應(yīng)該是放在磁盤的,為什么要專門把一種哈希的索引放到內(nèi)存?這里就不說了,把問題的答案放在詳談索引存儲結(jié)構(gòu)推演過程…
1.4 Log Buffer(Redo log)
MySQL 在更新數(shù)據(jù)時,為了減少磁盤的隨機 IO,因此并不會直接更新磁盤上的數(shù)據(jù),而是先更新 Buffer Pool 中緩存頁的數(shù)據(jù),等到合適的時間點,再將這個緩存頁持久化到磁盤。而 Buffer Pool 中所有緩存頁都是處于內(nèi)存當中的,當 MySQL 宕機或者機器斷電,內(nèi)存中的數(shù)據(jù)就會丟失,因此 MySQL 為了防止緩存頁中的數(shù)據(jù)在更新后出現(xiàn)數(shù)據(jù)丟失的現(xiàn)象,引入了 redo log 機制。
當進行增刪改操作時,MySQL 會在更新 Buffer Pool 中的緩存頁數(shù)據(jù)時,會記錄一條對應(yīng)操作的 redo log 日志,這樣如果出現(xiàn) MySQL 宕機或者斷電時,如果有緩存頁的數(shù)據(jù)還沒來得及刷入磁盤,那么當 MySQL 重新啟動時,可以根據(jù) redo log 日志文件,進行數(shù)據(jù)重做,將數(shù)據(jù)恢復(fù)到宕機或者斷電前的狀態(tài),保證了更新的數(shù)據(jù)不丟失,因此 redo log 又叫做重做日志。它的本質(zhì)是保證事務(wù)提交后,更新的數(shù)據(jù)不丟失。——用它來實現(xiàn)事務(wù)的持久性。
這種日志和磁盤配合的整個過程,其實就是 MySQL 里的 WAL 技術(shù)(Write-Ahead Logging),它的關(guān)鍵點就是先寫日志,再寫磁盤。
同樣是寫磁盤,為什么不直接寫db file,而是先寫日志?
我們先來了解一下隨機I/O和順序I/O的概念。磁盤的最小組成單元是扇區(qū),通常是512個字節(jié)。操作系統(tǒng)和內(nèi)存打交道,最小的單位是頁Page。操作系統(tǒng)和磁盤打交道,讀寫磁盤,最小的單位是塊Block。
如果我們所需要的數(shù)據(jù)是隨機分散在不同頁的不同扇區(qū)中,那么找到相應(yīng)的數(shù)據(jù)需要等到磁臂旋轉(zhuǎn)到指定的頁,然后盤片尋找到對應(yīng)的扇區(qū),才能找到我們所需要的一塊數(shù)據(jù),一次進行此過程直到找完所有數(shù)據(jù),這個就是隨機IO,讀取數(shù)據(jù)速度較慢。
假設(shè)我們已經(jīng)找到了第一塊數(shù)據(jù),并且其他所需的數(shù)據(jù)就在這一塊數(shù)據(jù)后邊,那么就不需要重新尋址,可以依次拿到我們所需的數(shù)據(jù),這個就叫順序IO。
刷盤是隨機I/O,而記錄日志是順序I/O,順序I/O效率更高。所以,先不斷把內(nèi)存中bufferpool的數(shù)據(jù)修改寫入日志,保證不會丟失,然后等到一個適當?shù)臅r機(系統(tǒng)比較空閑)將操作記錄更新到磁盤里面。達到延遲刷盤時機的目的,進而提升系統(tǒng)吞吐。redo log存在的意義主要就是降低對數(shù)據(jù)頁刷盤的要求。
redo log(重做日志)有什么特點?
redo log是InnoDB存儲引擎實現(xiàn)的,并不是所有存儲引擎都有。
不是記錄數(shù)據(jù)頁更新之后的狀態(tài)(某一行或某幾行修改成怎樣怎樣),而是記錄當前頁做了什么改動,屬于物理日志。它用來恢復(fù)提交后的物理數(shù)據(jù)頁(恢復(fù)數(shù)據(jù)頁,且只能恢復(fù)到最后一次提交的位置)
日志分為物理日志和邏輯日志。
- 物理日志就是直接記錄數(shù)據(jù),記錄被修改的page的偏移量,優(yōu)點就是不依賴原page的內(nèi)容,用日志的內(nèi)容可以直接覆蓋到磁盤上面,而缺點就是占用的空間太多,比如新增一個btree索引或者一個update操作。
- 邏輯日志只是記錄關(guān)系表上面的元操作,比如update一行數(shù)據(jù),delete一行數(shù)據(jù)等,優(yōu)點就是比較簡潔而且占用的空間要小,缺點就是需要依賴原page內(nèi)容,而且會有部分執(zhí)行和操作一致性的問題。
redo log 分成內(nèi)存和磁盤兩部分:
- 內(nèi)存部分:Log Buffer,存在刷盤操作。
- 磁盤部分:對應(yīng)于 /var/lib/mysql/目錄下的ib_logfile0和ib_logfile1,每個48M。
因為redo log 實際上記錄數(shù)據(jù)頁的變更,而這種變更記錄是沒必要全部保存,所以 redo log 的大小是固定的,前面的內(nèi)容會被覆蓋。如下圖所示:
再強調(diào)一次,redo log的內(nèi)容主要是用于崩潰恢復(fù)。磁盤的數(shù)據(jù)文件,數(shù)據(jù)來自 buffer pool。redo log 寫入磁盤,不是寫入數(shù)據(jù)文件。
redo log 的內(nèi)存部分:Log Buffer
當然redo log也不是每一次都直接寫入磁盤,在Buffer Pool里面有一塊內(nèi)存區(qū)域(Log Buffer)專門用來保存即將要寫入日志文件的數(shù)據(jù),Innodb_log_buffer默認大小為8M,它一樣可以節(jié)省磁盤IO。
那么,Log Buffer什么時候?qū)懭雔og file,或者說什么日志什么時候刷盤?
在我們寫入數(shù)據(jù)到磁盤的時候,操作系統(tǒng)本身是有緩存的。flush就是把操作系統(tǒng)緩沖區(qū)寫入到磁盤。log buffer寫入磁盤的時機,由一個參數(shù)控制,默認是1。
| 0(延遲寫) | logbuffer 將每秒一次地寫入 logfile 中,并且 logfile 的 flush 操作同時進行。 該模式下,在事務(wù)提交的時候,不會主動觸發(fā)寫入磁盤的操作。 |
| 1(默認,實時寫,實時刷) | 每次事務(wù)提交時 MySQL 都會把 logbuffer 的數(shù)據(jù)寫入 logfile,并且刷到磁盤中去。 |
| 2(實時寫,延遲刷) | 每次事務(wù)提交時 MySQL 都會把 logbuffer 的數(shù)據(jù)寫入 logfile。但是 flush 操作并不會同時進行。該模式下,MySQL 會每秒執(zhí)行一次 flush 操作。 |
以上就是MySQL的內(nèi)存結(jié)構(gòu)。總結(jié)一下,分為:Buffer pool、change buffer、Adaptive Hash Index、 log buffer。
2.Innodb 磁盤結(jié)構(gòu)(表空間)
磁盤結(jié)構(gòu)里面主要是各種各樣的表空間,叫做Table space。表空間可以看做是 InnoDB 存儲引擎邏輯結(jié)構(gòu)的最高層,所有的數(shù)據(jù)都存放在表空間中。InnoDB的表空間分為5大類。
2.1 system tablespace(系統(tǒng)表空間)
在默認情況下 InnoDB 存儲引擎有一個共享表空間(對應(yīng)文件/var/lib/mysql/ibdata1),也叫系統(tǒng)表空間。
InnoDB系統(tǒng)表空間包含InnoDB數(shù)據(jù)字典和雙寫緩沖區(qū),ChangeBuffer和UndoLogs),如果沒有指定file-per-table,也包含用戶創(chuàng)建的表和索引數(shù)據(jù)。
- undo在后面介紹,因為有獨立的表空間。
- 數(shù)據(jù)字典:由內(nèi)部系統(tǒng)表組成,存儲表和索引的元數(shù)據(jù)(定義信息)。
- 雙寫緩沖(InnoDB的一大特性):InnoDB的頁和操作系統(tǒng)的頁大小不一致,InnoDB頁大小一般為16K,操作系統(tǒng)頁大小為4K,InnoDB的頁寫入到磁盤時,一個頁需要分4次寫。
如果存儲引擎正在寫入頁的數(shù)據(jù)到磁盤時發(fā)生了宕機,可能出現(xiàn)頁只寫了一部分的情況,比如只寫了4K,就宕機了,這種情況叫做部分寫失效(partial page write),可能會導(dǎo)致數(shù)據(jù)丟失。
show variables like 'innodb_doublewrite';我們不是有 redo log 嗎?但是有個問題,如果這個頁本身已經(jīng)損壞了,用它來做崩潰恢復(fù)是沒有意義的。所以在對于應(yīng)用redo log之前,需要一個頁的副本。如果出現(xiàn)了寫入失效,就用頁的副本來還原這個頁,然后再應(yīng)用redo log。這個頁的副本就是?double write,InnoDB的雙寫技術(shù)。通過它實現(xiàn)了數(shù)據(jù)頁的可靠性。
跟 redo log 一樣,double write 由兩部分組成,一部分是內(nèi)存的double write,一個部分是磁盤上的double write。因為double write是順序?qū)懭氲?#xff0c;不會帶來很大的開銷。在默認情況下,所有的表共享一個系統(tǒng)表空間,這個文件會越來越大,而且它的空間不會收縮。
2.2 file-per-table tablespaces(獨占表空間)
我們可以讓每張表獨占一個表空間。這個開關(guān)通過innodb_file_per_table設(shè)置,默認開啟。
SHOW VARIABLES LIKE 'innodb_file_per_table';開啟后,則每張表會開辟一個表空間,這個文件就是數(shù)據(jù)目錄下的 ibd文件(例如
/var/lib/mysql/forum/user_innodb.ibd),存放表的索引和數(shù)據(jù)。
但是其他類的數(shù)據(jù),如回滾(undo)信息,插入緩沖索引頁、系統(tǒng)事務(wù)信息,二次寫緩沖(Double write buffer)等還是存放在原來的共享表空間內(nèi)。
2.3 general tablespaces(通用表空間)
通用表空間也是一種共享的表空間,跟ibdata1類似。可以創(chuàng)建一個通用的表空間,用來存儲不同數(shù)據(jù)庫的表,數(shù)據(jù)路徑和文件可以自定義。語法:
create table space ts2673 add datafile '/var/lib/mysql/ts2673.ibd' file_block_size=16K engine=innodb;在創(chuàng)建表的時候可以指定表空間,用ALTER修改表空間可以轉(zhuǎn)移表空間。
create table t2673(idinteger) tablespace ts2673;不同表空間的數(shù)據(jù)是可以移動的。刪除表空間需要先刪除里面的所有表:
drop table t2673; drop tablespace ts2673;2.4 temporary tablespaces(臨時表空間)
存儲臨時表的數(shù)據(jù),包括用戶創(chuàng)建的臨時表,和磁盤的內(nèi)部臨時表。對應(yīng)數(shù)據(jù)目錄下的ibtmp1文件。當數(shù)據(jù)服務(wù)器正常關(guān)閉時,該表空間被刪除,下次重新產(chǎn)生。
2.5 undo log tablespace
undo log(撤銷日志或回滾日志)記錄了事務(wù)發(fā)生之前的數(shù)據(jù)狀態(tài)(不包括select) 。用來保證在必要時實現(xiàn)回滾,如果另一個事務(wù)需要在一致性讀操作中查看原始數(shù)據(jù),則從undo日志記錄中檢索未修改的數(shù)據(jù),也就是說MVCC機制也依賴于undo log來實現(xiàn)。在執(zhí)行 undo 的時候,僅僅是將數(shù)據(jù)從邏輯上恢復(fù)至事務(wù)之前的狀態(tài),而不是從物理頁面上操作實現(xiàn)的,屬于邏輯格式的日志。
redo Log 和 undo Log 與事務(wù)密切相關(guān),統(tǒng)稱為事務(wù)日志。但與redo log不同的是:
- redo log 是存儲的是物理日志,undo log存儲的是邏輯日志
- redo log 是重做日志,提供?前滾?操作;undo log 是回退日志,提供?回滾?操作。
undo 是在事務(wù)開始之前保存的被修改數(shù)據(jù)的一個版本,產(chǎn)生undo日志的時候,同樣會伴隨類似于保護事務(wù)持久化機制的 redo log 的產(chǎn)生。
- Redo 記錄某?數(shù)據(jù)塊?被修改?后?的值,可以用來恢復(fù)未寫入 data file 的已成功事務(wù)更新的數(shù)據(jù)。-- 保證事務(wù)持久性
- Undo 記錄某?數(shù)據(jù)?被修改?前?的值,可以用來在事務(wù)失敗時進行 rollback;-- 保證事務(wù)原子性
比如某一時刻數(shù)據(jù)庫 DOWN 機了,有兩個事務(wù),一個事務(wù)已經(jīng)提交,另一個事務(wù)正在處理。數(shù)據(jù)庫重啟的時候就要根據(jù)日志進行前滾及回滾,把已提交事務(wù)的更改寫到數(shù)據(jù)文件,未提交事務(wù)的更改恢復(fù)到事務(wù)開始前的狀態(tài)。
- 當數(shù)據(jù) crash-recovery 時,通過 redo log 將所有已經(jīng)在存儲引擎內(nèi)部提交的事務(wù)應(yīng)用 redo log 恢復(fù)
- 所有已經(jīng) prepared 但是沒有 commit 的 transactions 將會應(yīng)用 undo log 做 roll back
undo Log 的數(shù)據(jù)默認在系統(tǒng)表空間ibdata1 文件中,因為共享表空間不會自動收縮,也可以單獨創(chuàng)建一個undo表空間。
==>update過程分析
有了這些日志之后,我們來總結(jié)一下一個更新操作的流程,這是一個簡化的過程(name原值是zhangsan)。
update user set name='penyuyan' where id=1; 1. 事務(wù)開始,從內(nèi)存或磁盤取到這條數(shù)據(jù),返回給Server 的執(zhí)行器; 2. 執(zhí)行器修改這一行數(shù)據(jù)的值為penyuyan; 3. 記錄 name=zhangsan 到 undo log; 4. 記錄 name=penyuyan 到 redo log; 5. 調(diào)用存儲引擎接口,在內(nèi)存(Buffer Pool)中修改 name=zhangsan; 6. 事務(wù)提交另外,內(nèi)存和磁盤之間,工作著很多后臺線程。后臺線程的主要作用是負責刷新內(nèi)存池中的數(shù)據(jù)和把修改的數(shù)據(jù)頁刷新到磁盤。后臺線程分為:master thread,IO thread,purge thread,page cleaner thread。
- master thread:負責刷新緩存數(shù)據(jù)到磁盤并協(xié)調(diào)調(diào)度其它后臺進程
- IO thread:分為 insert buffer、 log、 read、 write進程。分別用來處理 insert buffer、重做日志、讀寫請求的IO回調(diào)
- purge thread:用來回收undo 頁
- page cleaner thread:用來刷新臟頁
除了 InnoDB 架構(gòu)中的日志文件,MySQL 的 Server 層也有一個日志文件,叫做binlog,它可以被所有的存儲引擎使用。
binlog 以事件的形式記錄了所有的DDL和DML語句(因為它記錄的是操作而不是數(shù)據(jù)值,屬于邏輯日志),可以用來做主從復(fù)制和數(shù)據(jù)恢復(fù)。跟redo log不一樣,它的文件內(nèi)容是可以追加的,沒有固定大小限制。
在開啟了binlog功能的情況下,我們可以把binlog導(dǎo)出成SQL語句,把所有的操作重放一遍,來實現(xiàn)數(shù)據(jù)的恢復(fù)。binlog的另一個功能就是用來實現(xiàn)主從復(fù)制,它的原理就是從服務(wù)器讀取主服務(wù)器的binlog,然后執(zhí)行一遍。
參考鏈接:對比總結(jié)三大日志:重做日志(redo log),回滾日志(undo log),二進制日志(binlog)
有了這兩個日志之后,我們來看一下一條更新語句是怎么執(zhí)行的:
update teacher set name='盆魚宴' where id=1 1. 先查詢到這條數(shù)據(jù),如果有緩存,也會用到緩存。 2. 把name改成盆魚宴,然后調(diào)用引擎的API接口,寫入這一行數(shù)據(jù)到內(nèi)存,同時記錄redo log。這時 redo log 進入prepare 狀態(tài),然后告訴執(zhí)行器,執(zhí)行完成了,可以隨時提交。 3. 執(zhí)行器收到通知后記錄binlog,然后調(diào)用存儲引擎接口, 設(shè)置redolog為commit狀態(tài)。 4. 更新完成。需要特別注意的幾點:
總結(jié)腦圖
總結(jié)
以上是生活随笔為你收集整理的【转】2.2【MySQL】运行原理(二):InnoDB 内存结构、磁盘结构及update sql执行过程分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 500万内最好的家用旗舰SUV!理想L9
- 下一篇: 跑分堪比11年前的AMD集显 兆芯国产独