洞悉MySQL底層架構:遊走在緩衝與磁盤之間

提起MySQL,其實網上已經有一大把教程了,爲何我還要寫這篇文章呢,大概是由於網上不少網站都是比較零散,並且描述不夠直觀,不能系統對MySQL相關知識有一個系統的學習,致使不能造成知識體系。爲此我撰寫了這篇文章,試圖讓這些底層架構相關知識更加直觀易懂:php

  • 儘可能以圖文的方式描述技術原理;
  • 涉及到關鍵的技術,附加官網或者技術書籍來源,方便你們進一步擴展學習;
  • 涉及到的背景知識儘量作一個交代,好比討論到log buffer的刷盤方式,延伸一下IO寫磁盤相關知識點。

好了,MySQL從不會到精通系列立刻就要開始了(看完以後仍是不會的話..請忽略這句話)。html

img

可能會有同窗問:爲啥不直接學更加先進的TiDB,或者是強大的OceanBase。mysql

其實,MySQL做爲老牌的應用場景普遍的關係型開源數據庫,其底層架構是很值得咱們學習的,吸取其設計精華,那麼咱們在平時的方案設計工做中也能夠借鑑,若是項目中用的是MySQL,那麼就可以把數據庫用的更好了,瞭解了MySQL底層的執行原理,對於調優工做也是有莫大幫助的。本文我重點講述MySQL底層架構,涉及到:linux

  • 內存結構buffer poollog bufferchange buffer,buffer pool的頁淘汰機制是怎樣的;
  • 磁盤結構系統表空間獨立表空間通用表空間undo表空間redo log
  • 以及IO相關底層原理、查詢SQL執行流程、數據頁結構行結構描述、彙集索引輔助索引的底層數據組織方式、MVCC多版本併發控制的底層實現原理,以及可重複讀讀已提交是怎麼經過MVCC實現的。

image-20200530231121252

看完文本文,您將瞭解到:算法

  1. **總體架構:**InnoDB存儲架構是怎樣的 (一、MySQL架構)
  2. **工做原理:**查詢語句的底層執行流程是怎樣的 (二、查詢SQL執行流程)
  3. **IO性能:**文件IO操做寫磁盤有哪幾種方式,有什麼IO優化方式 (3.1.二、關於磁盤IO的方式)
  4. 緩存:InnoDB緩存(buffer pool, log buffer)的刷新方式有哪些(3.1.2.二、innodb_flush_method)
  5. **緩存:**log buffer是在何時寫入到磁盤的(3.10.二、如何保證數據不丟失 - 其中第四步log buffer持久化到磁盤的時機爲)
  6. **緩存:**爲何redo log prepare狀態也要寫磁盤?(3.10.二、如何保證數據不丟失 - 爲何第二步redo log prepare狀態也要寫磁盤?)
  7. **緩存:**髒頁寫盤通常發生在何時(3.10.二、如何保證數據不丟失 - 其中第五步:髒頁刷新到磁盤的時機爲)
  8. **緩存:**爲何惟一索引的更新不能夠藉助change buffer(3.二、Change Buffer)
  9. 緩存:log buffer的日誌刷盤控制參數innodb_flush_log_at_trx_commit對寫性能有什麼影響(3.4.一、配置參數)
  10. **緩存:**buffer pool的LRU是如何實現的,爲何要這樣實現(3.1.一、緩衝池LRU算法)
  11. **表存儲:**系統表空間的結構,MySQL InnoDB磁盤存儲格式,各類表空間(系統表空間,獨立表空間,通用表空間)的做用和優缺點是什麼,ibdataibdfrm文件分別是幹嗎的(3.五、表空間)
  12. **行字段存儲:**底層頁和行的存儲格式(3.六、InnoDB底層邏輯存儲結構)
  13. 行字段存儲:varcharnull底層是如何存儲的,最大可用存儲多大的長度(3.6.3.一、MySQL中varchar最大長度是多少)
  14. **行字段存儲:**行記錄太長了,一頁存不下,該怎麼存儲?(3.6.3.二、行記錄超過頁大小如何存儲)
  15. **索引:**數據庫索引的組織方式是怎樣的,明白爲何要採用B+樹,而不是哈希表、二叉樹或者B樹(3.七、索引 - 爲何MySQL使用B+樹)
  16. **索引:**索引組織方式是怎樣的,爲何大字段會影響表性能(查詢性能,更新性能)(3.七、索引)
  17. 索引:覆蓋索引聯合索引什麼狀況下會生效(3.7.二、輔助索引)
  18. **索引:**什麼是索引下推,索引下推減小了哪方面的開銷?(3.7.二、輔助索引 - 索引條件下推)
  19. 索引:Change Buffer對二級索引DML語句有什麼優化(3.二、Change Buffer)
  20. **數據完整性:**MySQL是如何保證數據完整性的,redo logundo logbuffer pool數據完整性的關鍵做用分別是什麼(3.10.二、如何保證數據不丟失)
  21. MVCC:MVCC底層是怎麼實現的,可重複讀和讀已提交是怎麼實現的(3.11.二、MVCC實現原理)
  22. 雙寫緩衝區有什麼做用(3.九、Doublewrite Buffer)
  23. Redo Log在一個事務中是在何時寫入的?binlog和Redo Log有什麼區別?(3.10.一、Redo Log在事務中的寫入時機)

一、MySQL架構

以下圖爲MySQL架構涉及到的經常使用組件:sql

image-20200517155127246

二、查詢SQL執行流程

有以下表格:數據庫

image-20200517183833415

咱們執行如下sql:編程

select * from t_user where user_id=10000;
複製代碼

2.一、MySQL客戶端與服務器創建鏈接

以下圖,創建過程:後端

  • 客戶端經過mysql命令發起鏈接請求;
  • 通過三次握手後與服務端創建TCP鏈接;
  • 鏈接器接收到請求以後使用用戶密碼進行身份驗證;
  • 驗證經過以後,獲取用戶的權限信息緩存起來,該鏈接後面都是基於該緩存中的權限執行sql

image-20200517194803475

對於Java應用程序來講,通常會把創建好的鏈接放入數據庫鏈接池中進行復用,只要這個鏈接不關閉,就會一直在MySQL服務端保持着,能夠經過show processlist命令查看,以下:數組

image-20200517185119239

注意,這裏有個Time,表示這個鏈接多久沒有動靜了,上面例子是656秒沒有動靜,默認地,若是超過8個小時尚未動靜,鏈接器就會自動斷開鏈接,能夠經過wait_timeout參數進行控制。

2.二、執行SQL

以下圖,執行sql:

image-20200517231413373

  • 服務端接收到客戶端的查詢sql以後,先嚐試從查詢緩存中查詢該sql是否已經有緩存的結果了,若是有則直接返回結果,若是沒有則執行下一步;
  • 分析器拿到sql以後會嘗試對sql語句進行詞法分析和語法分析,校驗語法的正確性,經過以後繼續往下執行;
  • 優化器拿到分析器的sql以後,開始繼續解析sql,判斷到須要走什麼索引,根據實際狀況重寫sql,最終生成執行計劃;
  • 執行器根據執行計劃執行sql,執行以前會先進行操做權限校驗;而後根據表存儲引擎調用對飲接口進行查詢數據,這裏的掃描行數就是指的接口返回的記錄數,執行器拿到返回記錄以後進一步加工,如本例子:
    • 執行器拿到select * from t_user where user_id=10000的全部記錄,在依次判斷user_name是否是等於"arthinking",獲取到匹配的記錄。

三、InnoDB引擎架構

以下圖,爲存儲引擎的架構:

image-20200530224136957

其實內存中的結構不太好直接觀察到,不過磁盤的仍是能夠看到的,咱們找到磁盤中MySQL的數據文件夾看看:

cd innodb_data_home_dir 查看MySQL 數據目錄:

|- ib_buffer_pool  // 保存緩衝池中頁面的表空間ID和頁面ID,用於重啓恢復緩衝池
|- ib_logfile0  // redo log 磁盤文件1
|- ib_logfile1  // redo log 磁盤文件2,默認狀況下,重作日誌存在磁盤的這兩個文件中,循環的方式寫入重作日誌
|- ibdata1  // 系統表空間文件
|- ibtmp1  // 默認臨時表空間文件,可經過innodb_temp_data_file_path屬性指定文件位置
|- mysql/
|- mysql-bin.000001  // bin log文件
|- mysql-bin.000001  // bin log文件
...
|- mysql-bin.index  // bin log文件索引
|- mysqld.local.err  // 錯誤日誌
|- mysqld.local.pid  // mysql進程號
|- performance_schema/  // performance_schema數據庫
|- sys/  // sys數據庫
|- test/  // 數據庫文件夾
    |- db.opt  // test數據庫配置文件,包含數據庫字符集屬性
    |- t.frm  // 數據表元數據文件,無論是使用獨立表空間仍是系統表空間,每一個表都對應有一個
    |- t.ibd  // 數據庫表獨立表空間文件,若是使用的是獨立表空間,則一個表對應一個ibd文件,不然保存在系統表空間文件中
複製代碼

innodb_data_home_dir[1]

ib_buffer_pool[2]

ib_logfile0[3]

ibtmp1[4]

db.opt[5]

接下來咱們逐一來介紹。

3.一、buffer pool

image-20200530224418964

buffer pool緩衝池)是主內存中的一個區域,在InnoDB訪問表數據索引數據的時候,會順便把對應的數據頁緩存到緩衝池中。若是直接從緩衝池中直接讀取數據將會加快處理速度。在專用服務器上,一般將80%左右的物理內存分配給緩衝池。

爲了提升緩存管理效率,緩衝池把頁面連接爲列表,使用改進版的LRU算法將不多使用的數據從緩存中老化淘汰掉。

3.1.一、緩衝池LRU算法

經過使用改進版的LRU算法來管理緩衝池列表。

當須要把新頁面存儲到緩衝池中的時候,將淘汰最近最少使用的頁面,並將新頁面添加到舊子列表的頭部。

image-20200519225450188

該算法運行方式:

  • 默認 3/8緩衝池用於舊子列表;
  • 當新頁面如緩衝池時,首先將其插入舊子列表頭部
  • 重複訪問舊子列表的頁面,將使其移動至新子列表的頭部;
  • 隨着數據庫的運行,頁面逐步移至列表尾部,緩衝池中未被方位的頁面最終將被老化淘汰。

相關優化參數:

  • innodb_old_blocks_pct:控制LRU列表中舊子列表的百分比,默認是37,也就是3/8,可選範圍爲5~95;
  • innodb_old_blocks_time :指定第一次訪問頁面後的時間窗口,該時間窗口內訪問頁面不會使其移動到LRU列表的最前面。默認是1000,也就是1秒。

innodb_old_blocks_time很重要,有了這1秒,對於全表掃描,因爲是順序掃描的,通常同一個數據頁的數據都是在一秒內訪問完成的,不會升級到新子列表中,一直在舊子列表淘汰數據,因此不會影響到新子列表的緩存。

3.1.二、關於磁盤IO的方式

image-20200530224453194

O_DIRECTinnodb_flush_method參數的一個可選值。

這裏先介紹下和數據庫性能密切相關的文件IO操做方法

3.1.2.一、文件IO操做方法

數據庫系統是基於文件系統的,其性能和設備讀寫的機制有密切的關係。

open:打開文件[6]
int open(const char *pathname, int flags);
複製代碼

系統調用Open會爲該進程一個文件描述符fd,經常使用的flags以下:

  • O_WRONLY:表示咱們以"寫"的方式打開,告訴內核咱們須要向文件中寫入數據;
  • O_DSYNC:每次write都等待物理I/O完成,可是若是寫操做不影響讀取剛寫入的數據,則不等待文件屬性更新;
  • O_SYNC:每次write都等到物理I/O完成,包括write引發的文件屬性的更新;
  • O_DIRECT:執行磁盤IO時繞過緩衝區高速緩存(內核緩衝區),從用戶空間直接將數據傳遞到文件或磁盤設備,稱爲直接IO(direct IO)。由於沒有了OS cache,因此會O_DIRECT下降文件的順序讀寫的效率。
write:寫文件[7]
ssize_t write(int fd, const void *buf, size_t count);
複製代碼

使用open打開文件獲取到文件描述符以後,能夠調用write函數來寫文件,具體表現根據open函數參數的不一樣而不一樣弄。

fsync & fdatasync:刷新文件[8]
#include <unistd.h>

int fsync(int fd);

int fdatasync(int fd);
複製代碼
  • fdatasync:操做完write以後,咱們能夠調用fdatasync將文件數據塊flush到磁盤,只要fdatasync返回成功,則能夠認爲數據已經寫到磁盤了;
  • fsync:與O_SYNC參數相似,fsync還會更新文件metadata到磁盤;
  • sync:sync只是將修改過的塊緩衝區寫入隊列,而後就返回,不等實際寫磁盤操做完成;

爲了保證文件更新成功持久化到硬盤,除了調用write方法,還須要調用fsync。

大體交互流程以下圖:

image-20200520224027300

更多關於磁盤IO的相關內容,能夠閱讀:On Disk IO, Part 1: Flavors of IO[9]

**fsync性能問題:**除了刷髒頁到磁盤,fsync還會同步文件metadata,而文件數據和metadata一般存放在磁盤不一樣地方,因此fsync至少須要兩次IO操做。

對fsync性能的優化建議:因爲以上性能問題,若是可以減小metadata的更新,那麼就可使用fdatasync了。所以須要確保文件的尺寸在write先後沒有發生變化。爲此,能夠建立固定大小的文件進行寫,寫完則開啓新的文件繼續寫。

3.1.2.二、innodb_flush_method

innodb_flush_method定義用於將數據刷新InnoDB數據文件日誌文件的方法,這可能會影響I/O吞吐量。

如下是具體參數說明:

屬性
命令行格式 --innodb-flush-method=value
系統變量 innodb_flush_method
範圍 全局
默認值(Windows) unbuffered
默認值(Unix) fsync
有效值(Windows) unbuffered, normal
有效值(Unix) fsync, O_DSYNC, littlesync, nosync, O_DIRECT, O_DIRECT_NO_FSYNC

比較經常使用的是這三種:

fsync

默認值,使用fsync()系統調用來flush數據文件和日誌文件到磁盤;

O_DSYNC

因爲open函數的O_DSYNC參數在許多Unix系統上都存中問題,所以InnoDB不直接使用O_DSYNC。

InnoDB用於O_SYNC 打開和刷新日誌文件,fsync()刷新數據文件。

表現爲:寫日誌操做是在write函數完成,數據文件寫入是經過fsync()系統調用來完成;

O_DIRECT

使用O_DIRECT (在Solaris上對應爲directio())打開數據文件,並用於fsync()刷新數據文件和日誌文件。此選項在某些GNU/Linux版本,FreeBSD和Solaris上可用。

表現爲:數據文件寫入直接從buffer pool到磁盤,不通過操做系統緩衝,日誌仍是須要通過操做系統緩存;

O_DIRECT_NO_FSYNC

在刷新I/O期間InnoDB使用O_DIRECT,而且每次write操做後跳過fsync()系統調用。

此設置適用於某些類型的文件系統,但不適用於其餘類型的文件系統。例如,它不適用於XFS。若是不肯定所使用的文件系統是否須要fsync()(例如保留全部文件元數據),請改用O_DIRECT。

以下圖所示:

image-20200520232414096

爲何使用了O_DIRECT配置後還須要調用fsync()?

參考MySQL的這個bug:Innodb calls fsync for writes with innodb_flush_method=O_DIRECT[10]

Domas進行的一些測試代表,若是沒有fsync,某些文件系統(XFS)不會同步元數據。若是元數據會更改,那麼您仍然須要使用fsync(或O_SYNC來打開文件)。

例如,若是在啓用O_DIRECT的狀況下增大文件大小,它仍將寫入文件的新部分,可是因爲元數據不能反映文件的新大小,所以若是此刻系統發生崩潰,文件尾部可能會丟失。

爲此:當重要的元數據發生更改時,請繼續使用fsync或除O_DIRECT以外,也能夠選擇使用O_SYNC。

MySQL從v5.6.7起提供了O_DIRECT_NO_FSYNC選項來解決此類問題。

3.二、Change Buffer

change buffer是一種特殊的數據結構,當二級索引頁(非惟一索引)不在緩衝池中時,它們會緩存這些更改 。當頁面經過其餘讀取操做加載到緩衝池中時,再將由INSERTUPDATEDELETE操做(DML)產生的change buffer合併到buffer pool的數據頁中。

爲何惟一索引不可使用chage buffer?

針對惟一索引,若是buffer pool不存在對應的數據頁,仍是須要先去磁盤加載數據頁,才能判斷記錄是否重複,這一步避免不了。

而普通索引是非惟一的,插入的時候以相對隨機的順序發生,刪除和更新也會影響索引樹中不相鄰的二級索引樹,經過使用合併緩衝,避免了在磁盤產生大量的隨機IO訪問獲取普通索引頁。

問題

當有許多受影響的行和許多輔助索引要更新時,change buffer合併可能須要幾個小時,在此期間,I/O會增長,可能會致使查詢效率大大下降,即便在事務提交以後,或者服務器重啓以後,change buffer合併操做也會繼續發生。相關閱讀:Section 14.22.2, 「Forcing InnoDB Recovery」

3.三、自適應哈希索引

自適應哈希索引功能由innodb_adaptive_hash_index變量啓用 ,或在服務器啓動時由--skip-innodb-adaptive-hash-index禁用。

3.四、Log Buffer

log buffer(日誌緩衝區)用於保存要寫入磁盤上的log file(日誌文件)的數據。日誌緩存區的內容會按期刷新到磁盤。

日誌緩衝區大小由innodb_log_buffer_size變量定義 。默認大小爲16MB。較大的日誌緩衝區可讓大型事務在提交以前無需將redo log寫入磁盤。

若是您有更新,插入或者刪除多行的事務,嘗試增大日誌緩衝區的大小能夠節省磁盤I/O。

3.4.一、配置參數

innodb_flush_log_at_trx_commit

innodb_flush_log_at_trx_commit 變量控制如何將日誌緩衝區的內容寫入並刷新到磁盤。

該參數控制是否嚴格存儲ACID仍是嘗試獲取更高的性能,能夠經過該參數獲取更好的性能,可是會致使在系統崩潰的過程當中致使數據丟失。

可選參數:

  • 0,事務提交以後,日誌只記錄到log buffer中,每秒寫一第二天志到緩存並刷新到磁盤,還沒有刷新的日誌可能會丟失;
  • 1,要徹底符合ACID,必須使用該值,表示日誌在每次事務提交時寫入緩存並刷新到磁盤;
  • 2,每次事務提交以後,日誌寫到page cache,每秒刷一次到磁盤,還沒有刷新的日誌可能會丟失;

innodb_flush_log_at_timeout

innodb_flush_log_at_timeout 變量控制日誌刷新頻率。可以讓您將日誌刷新頻率設置爲*N秒(其中N*爲1 ... 2700,默認值爲1)

爲了保證數據不丟失,請執行如下操做:

  • 若是啓用了binlog,則設置:sync_binlog=1;
  • innodb_flush_log_at_trx_commit=1;

配置效果以下圖所示:

image-20200523161540057

3.五、表空間

一個InnoDB表及其索引能夠在建在系統表空間中,或者是在一個 獨立表空間 中,或在 通用表空間

表空間概覽圖:

image-20200523161854294

表空間涉及的文件

相關文件默認在磁盤中的innodb_data_home_dir目錄下:

|- ibdata1  // 系統表空間文件
|- ibtmp1  // 默認臨時表空間文件,可經過innodb_temp_data_file_path屬性指定文件位置
|- test/  // 數據庫文件夾
    |- db.opt  // test數據庫配置文件,包含數據庫字符集屬性
    |- t.frm  // 數據表元數據文件,無論是使用獨立表空間仍是系統表空間,每一個表都對應有一個
    |- t.ibd  // 數據庫表獨立表空間文件,若是使用的是獨立表空間,則一個表對應一個ibd文件,不然保存在系統表空間文件中
複製代碼

frm文件

建立一個InnoDB表時,MySQL 在數據庫目錄中建立一個.frm文件。frm文件包含MySQL表的元數據(如表定義)。每一個InnoDB表都有一個.frm文件。

與其餘MySQL存儲引擎不一樣, InnoDB它還在系統表空間內的自身內部數據字典中編碼有關表的信息。MySQL刪除表或數據庫時,將刪除一個或多個.frm文件以及InnoDB數據字典中的相應條目。

所以,在InnoDB中,您不能僅經過移動.frm 文件來移動表。有關移動InnoDB 表的信息,請參見官方文檔14.6.1.4 Moving or Copying InnoDB Tables

ibd文件

對於在獨立表空間建立的表,還會在數據庫目錄中生成一個 .ibd表空間文件。

通用表空間中建立的表在現有的常規表空間 .ibd文件中建立。常規表空間文件能夠在MySQL數據目錄內部或外部建立。有關更多信息,請參見官方文檔14.6.3.3 General Tablespaces

ibdata文件

系統表空間文件,在 InnoDB系統表空間中建立的表在ibdata中建立。

3.5.一、系統表空間

系統表空間由一個或多個數據文件(ibdata文件)組成。其中包含與InnoDB相關對象有關的元數據(InnoDB 數據字典 data dictionary),以及更改緩衝區change buffer), 雙寫緩衝區doublewrite buffer)和撤消日誌undo logs)的存儲區 。

InnoDB 若是表是在系統表空間中建立的,則系統表空間中也包含表的表數據和索引數據。

系統表空間的問題

在MySQL 5.6.7以前,默認設置是將全部InnoDB表和索引保留 在系統表空間內,這一般會致使該文件變得很是大。由於系統表空間永遠不會縮小,因此若是先加載而後刪除大量臨時數據,則可能會出現存儲問題。

在MySQL 5.7中,默認設置爲 獨立表空間模式,其中每一個表及其相關索引存儲在單獨的 .ibd文件中。此默認設置使使用**Barracuda文件格式的InnoDB功能更容易使用,例如表壓縮**,頁外列的有效存儲以及大索引鍵前綴(innodb_large_prefix)。

將全部表數據保留在系統表空間或單獨的 .ibd文件中一般會對存儲管理產生影響。

InnoDB在MySQL 5.7.6中引入了通用表空間[11],這些表空間也由.ibd文件表示 。通用表空間是使用CREATE TABLESPACE語法建立的共享表空間。它們能夠在MySQL數據目錄以外建立,可以容納多個表,並支持全部行格式的表。

3.5.二、獨立表空間

MySQL 5.7中,配置參數:innodb_file_per_table,默認處於啓用狀態,這是一個重要的配置選項,會影響InnoDB文件存儲,功能的可用性和I/O特性等。

啓用以後,每一個表的數據和索引是存放在單獨的.ibd文件中的,而不是在系統表空間的共享ibdata文件中。

優勢

  • 您能夠更加靈活的選擇數據壓縮[12]的行格式,如:
    • 默認狀況下(innodb_page_size=16K),前綴索引[13]最多包含768個字節。若是開啓innodb_large_prefix,且Innodb表的存儲行格式爲 DYNAMIC 或 COMPRESSED,則前綴索引最多可包含3072個字節,前綴索引也一樣適用;
  • TRUNCATE TABLE執行的更快,而且回收的空間不會繼續保留,而是讓操做系統使用;
  • 能夠在單獨的存儲設備上建立每表文件表空間數據文件,以進行I / O優化,空間管理或備份。請參見 14.6.1.2 Creating Tables Externally

缺點

  • 獨立表空間中的未使用空間只能由同一個表使用,若是管理不當,會形成空間浪費;
  • 多個表須要刷盤,只能執行屢次fsync,沒法合併多個表的寫操做,這可能會致使更多的fsync操做總數;
  • mysqld必須爲每一個表文件空間保留一個打開的文件句柄,若是表數量多,可能會影響性能;
  • 每一個表都須要本身的數據文件,須要更多的文件描述符;

即便啓用了innodb_file_per_table參數,每張表空間存放的只是數據、索引和插入緩存Bitmap頁,其餘數據如回滾信息、插入緩衝索引頁、系統事務信息、二次寫緩衝等仍是存放在原來的共享表空間中。

3.5.三、通用表空間

通用表空間使用CREATE TABLESPACE語法建立。

相似於系統表空間,通用表空間是共享表空間,能夠存儲多個表的數據。

通用表空間比獨立表空間具備潛在的內存優點,服務器在表空間的生存期內將表空間元數據保留在內存中。一個通用表空間一般能夠存放多個表數據,消耗更少的表空間元數據內存。

數據文件能夠放置在MySQL數據目錄或獨立於MySQL數據目錄。

3.5.四、undo表空間

undo表空間包含undo log。

innodb_rollback_segments變量定義分配給每一個撤消表空間的回滾段的數量。

undo log能夠存儲在一個或多個undo表空間中,而不是系統表空間中

在默認配置中,撤消日誌位於系統表空間中。SSD存儲更適合undo log的I/O模式,爲此,能夠把undo log存放在有別於系統表空間的ssd硬盤中。

innodb_undo_tablespaces 配置選項控制undo表空間的數量。

3.5.五、臨時表空間

由用戶建立的非壓縮臨時表和磁盤內部臨時表是在共享臨時表空間中建立的。

innodb_temp_data_file_path 配置選項指定零時表空間文件的路徑,若是未指定,則默認在 innodb_data_home_dir目錄中建立一個略大於12MB 的自動擴展數據文件ibtmp1

使用ROW_FORMAT=COMPRESSED屬性建立的壓縮臨時表,是在獨立表空間中的臨時文件目錄中建立的 。

服務啓動的時候建立臨時表空間,關閉的時候銷燬臨時表空間。若是臨時表空間建立失敗,則意味着服務啓動失敗。

3.六、InnoDB底層邏輯存儲結構

在介紹索引以前,咱們有必要了解一下InnoDB底層的邏輯存儲結構,由於索引是基於這個底層邏輯存儲結構建立的。截止到目前,咱們所展現的都僅僅是物理磁盤中的邏輯視圖,接下來咱們就來看看底層的視圖。

3.6.一、ibd文件組織結構

如今咱們打開一個表空間ibd文件,看看裏面都是如何組織數據的?

以下圖,表空間由段(segment)、區(extent)、頁(page)組成。

InnoDB最小的存儲單位是頁,默認每一個頁大小是16k。

而InnoDB存儲引擎是面向行的(row-oriented),數據按行進行存放,每一個頁規定最多容許存放的行數=16k/2 - 200,即7992行。

image-20200523185138539

段:如數據段、索引段、回滾段等。InnoDB存儲引擎是B+樹索引組織的,因此數據即索引,索引即數據。B+樹的葉子節點存儲的都是數據段的數據。

3.6.二、數據頁結構[14]

名稱 佔用空間 描述
Fil Header 38 byte 頁的基本信息,如所屬表空間,上一頁和下一頁指針。
Page Header 56 byte 數據頁專有的相關信息
Infimun + Supremum 26 byte 兩個虛擬的行記錄,用於限定記錄的邊界
User Records 動態分配 實際存儲的行記錄內容
Free Space 動態調整 還沒有使用的頁空間
Page Directory 動態調整 頁中某些記錄的相對位置
Fil Trailer 8 byte 校驗頁是否完整

關於Infimun和Supremum:首次建立索引時,InnoDB會在根頁面中自動設置一個最小記錄和一個最高記錄,而且永遠不會刪除它們。最低記錄和最高記錄能夠視爲索引頁開銷的一部分。最初,它們都存在於根頁面上,可是隨着索引的增加,最低記錄將存在於第一或最低葉子頁上,最高記錄將出如今最後或最大關鍵字頁上。

image-20200530120303599

3.6.三、行記錄結構描述[15]

先來說講Compact行記錄格式,Compact是MySQL5.0引入的,設計目標是高效的存儲數據,讓一個頁可以存放更多的數據,從而實現更快的B+樹查找。

名稱 描述
變長字段長度列表 字段大小最多用2個字節表示,也就是最多限制長度:2^16=65535個字節;字段大小小於255字節,則用1個字節表示;
NULL標誌位 記錄該行哪些位置的字段是null值
記錄頭信息 記錄頭信息信息,固定佔用5個字節
列1數據 實際的列數據,NULL不佔用該部分的空間
列2數據
...

記錄頭用於將連續的記錄連接在一塊兒,並用於行級鎖定。

每行數據除了用戶定義的列外,還有兩個隱藏列:

  • 6個字節的事務ID列;
  • 7個字節的回滾指針列;
  • 若是InnoDB沒有指定主鍵,還會增長一個6個字節的rowid列;

而記錄頭信息包[16]含以下內容:

名稱 大小(bit) 描述
() 1 未知
() 1 未知
deleted_flag 1 該行是否已被刪除
min_rec_flag 1 若是該記錄是預約義的最小記錄,則爲1
n_owned 4 該記錄擁有的記錄數
heap_no 13 索引堆中該條記錄的排序號
record_type 3 記錄類型:000 普通,001 B+樹節點指針,010 Infimum,011 Supremum,1xx 保留
next_record 16 指向頁中下一條記錄

image-20200530152925354

更詳細的頁結構參考官網:22.2 InnoDB Page Structure

更詳細的行結構參考官網:22.1 InnoDB Record Structure

更詳細的行格式參考官網:14.11 InnoDB Row Formats

根據以上格式,能夠得出數據頁內的記錄組織方式:

image-20200530122458403

3.6.3.一、MySQL中varchar最大長度是多少

上面表格描述咱們知道,一個字段最長限制是65535個字節,這是存儲長度的限制。

而MySQL中對存儲是有限制的,具體參考:8.4.7 Limits on Table Column Count and Row Size

  • MySQL對每一個表有4096列的硬限制,可是對於給定的表,有效最大值可能會更少;
  • MySQL表的每行行最大限制爲65,535字節,這是邏輯的限制;實際存儲的時候,表的物理最大行大小略小於頁面的一半。若是一行的長度少於一頁的一半,則全部行都將存儲在本地頁面內。若是它超過一頁的一半,那麼將選擇可變長度列用於外部頁外存儲,直到該行大小控制在半頁以內爲止。

而實際可以存儲的字符是跟編碼有關的。

背景知識:

  • MySQL 4.0版本如下,varchar(10),表明10個字節,若是存放UTF8漢字,那麼只能存3個(每一個漢字3字節);

  • MySQL 5.0版本以上,varchar(10),指的是10個字符,不管存放的是數字、字母仍是UTF8漢字(每一個漢字3字節),均可以存放10個,最大大小是65532字節

所以,Mysql5根據編碼不一樣,存儲大小也不一樣。

那麼假設咱們使用的是utf8編碼,那麼每一個字符最多佔用3個字節,也就是最多定義varchar(21845)個字符,若是是ascii編碼,一個字符至關於一個字節,最多定義varchar(65535)個字符,下面咱們驗證下。

咱們嘗試建立一個這樣的字段:

CREATE TABLE `t10` ( `id` int(11) NOT NULL,
                  `a` int(11) NOT NULL,
                  PRIMARY KEY (`id`)
                 ) ENGINE=InnoDB CHARSET=ascii ROW_FORMAT=Compact;


alter table t10 add `str` varchar(21845) DEFAULT NULL;

alter table t10 add `str` varchar(65535) DEFAULT NULL;
複製代碼

發現提示這個錯誤:

mysql> alter table t10 add `str` varchar(65535) DEFAULT NULL;
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs
複製代碼

緣由是按照以上的行格式介紹,變長字段長度列表記錄也須要佔用空間,佔用2個字節,另外這裏是容許爲空字段,在8位以內,因此NULL標誌位佔用1個字節,因此咱們總共能夠存儲的字符數是:

65535 - 2 - 2 - 4 - 4=65534

其中 -2 個字節表示變長字段列表,-1表示NULL標誌位,兩個-4表示兩個int類型字段佔用大小

因此實際上可以容納的varchar大小爲:65524,咱們驗證下:

image-20200524121242172

3.6.3.二、行記錄超過頁大小如何存儲

MySQL表的內部表示具備65,535字節的最大行大小限制。InnoDB 對於4KB,8KB,16KB和32KB innodb_page_size 設置,表的最大行大小(適用於本地存儲在數據庫頁面內的數據)略小於頁面的一半 。若是包含 可變長度列InnoDB 行超過最大行大小,那麼將選擇可變長度列用於外部頁外存儲。

可變長度列因爲太長而沒法容納在B樹頁面上,這個時候會把可變長度列存儲在單獨分配的磁盤頁面上,這些頁面稱爲溢出頁面,這些列稱爲頁外列。頁外列的值存儲在由溢出頁面構成的單連接列表中。

InnoDB存儲引擎支持四種行格式:REDUNDANTCOMPACTDYNAMIC,和COMPRESSED。不一樣的行格式,對溢出的閾值和處理方式有所區別,詳細參考:14.11 InnoDB Row Formats

COMPACT行格式處理方式

使用COMPACT行格式的表將前768個字節的變長列值(VARCHARVARBINARYBLOBTEXT類型)存儲在B樹節點內的索引記錄中,其他的存儲在溢出頁上。

若是列的值等於或小於768個字節,則不使用溢出頁,所以能夠節省一些I / O。

若是查過了768個字節,那麼會按照以下方式進行存儲:

image-20200524154903041

DYNAMIC行格式處理方式

DYNAMIC行格式提供與COMPACT行格式相同的存儲特性,但改進了超長可變長度列的存儲能力和支持大索引鍵前綴。

InnoDB 能夠徹底在頁外存儲過長的可變長度列值(針對 VARCHARVARBINARYBLOBTEXT類型),而彙集索引記錄僅包含指向溢出頁的20字節指針。大於或等於768字節的固定長度字段被編碼爲可變長度字段。

image-20200524155015374

表中大字段引起的問題

若是一個表中有過多的可變長度大字段,致使一行記錄太長,而整個時候使用的是COMPACT行格式,那麼就可能會插入數據報錯。

如,頁面大小事16k,根據前面描述咱們知道,MySQL限制一頁最少要存儲兩行數據,若是不少可變長度大字段,在使用COMPACT的狀況下,仍然會把大字段的前面768個字節存在索引頁中,能夠算出最多支持的大字段:1024 * 16 / 2 / 768 = 10.67,那麼超過10個可變長度大字段就會插入失敗了。

這個時候能夠把row format改成:DYNAMIC。

3.七、索引

前面咱們瞭解了InnoDB底層的存儲結構,即:以B+樹的方式組織數據頁。另外瞭解了數據頁中的數據行的存儲方式。

而構建B+樹索引的時候必需要選定一個或者多個字段做爲索引的值,若是索引選擇的是主鍵,那麼咱們就稱爲彙集索引,不然就是二級索引。

爲何MySQL使用B+樹?

  • 哈希表雖然能夠提供O(1)的單行數據操做性能,但卻不能很好的支持排序和範圍查找,會致使全表掃描;
  • B樹能夠再非葉子節點存儲數據,可是這可能會致使查詢連續數據的時候增長更多的I/O操做;
  • 而B+樹數據都存放在葉子節點,葉子節點經過指針相互鏈接,能夠減小順序遍歷時產生的額外隨機I/O

更新詳細解釋: 爲何 MySQL 使用 B+ 樹[17]

3.7.一、彙集索引

瞭解到上面的底層邏輯存儲結構以後,咱們進一步來看看InnoDB是怎麼經過B+樹來組織存儲數據的。

首先來介紹下彙集索引。

彙集索引

主鍵索引的InnoDB術語。

下面咱們建立一張測試表,並插入數據,來構造一顆B+樹:

CREATE TABLE t20 (
id int NOT NULL,
a int NOT NULL,
b int,
c int,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

insert into t20 values(20, 1, 2, 1);
insert into t20 values(40, 1, 2, 5);
insert into t20 values(30, 3, 2, 4);
insert into t20 values(50, 3, 6, 2);
insert into t20 values(10, 1, 1, 1);
複製代碼

能夠看到,雖然咱們是id亂序插入的,可是插入以後查出來的確是排序好的:

image-20200524162034526

這個排序就是B+索引樹構建的。

咱們能夠經過這個在線的動態演示工具來看看B+樹的構造過程,最終結果以下:

image-20200524162043982

實際存放在數據庫中的模型因頁面大小不同而有所不一樣,這裏爲了簡化模型,咱們按照B+樹的通用模型來解釋數據的存儲結構。

相似的,咱們的數據也是這種組織形式的,該B+樹中,咱們以主鍵爲索引進行構建,而且把完整的記錄存到對應的頁下面:

image-20200524170732546

其中藍色的是索引頁,橙色的是數據頁。

每一個頁的大小默認爲16k,若是插入新的數據行,這個時候就要申請新的數據頁了,而後挪動部分數據過去,從新調整B+樹,這個過程稱爲頁分裂,這個過程會影響性能。

相反的,若是InnoDB索引頁的填充因子降低到之下MERGE_THRESHOLD,默認狀況下爲50%(若是未指定),則InnoDB嘗試收縮索引樹以釋放頁面。

自增主鍵的插入是遞增順序插入的,每次添加記錄都是追加的,不涉及到記錄的挪動,不會觸發葉子節點的分裂,而通常業務字段作主鍵,每每都不是有序插入的,寫成本比較高,因此咱們更傾向於使用自增字段做爲主鍵。

彙集索引注意事項

  • 當在表上面定義了PRIMARY KEY以後,InnoDB會把它做爲彙集索引。爲此,爲你的每一個表定義一個PRIMARY KEY。若是沒有惟一而且非空的字段或者一組列,那麼請添加一個自增列;
  • 若是您沒有爲表定義PRIMARY KEY,則MySQL會找到第一個不帶null值的UNIQUE索引,並其用做彙集索引;
  • 若是表沒有PRIMARY KEY或沒有合適的UNIQUE索引,則InnoDB 內部會生成一個隱藏的彙集索引GEN_CLUST_INDEX,做爲行ID,行ID是一個6字節的字段,隨着數據的插入而自增。

彙集索引查找

根據索引進行查找id=50的記錄,以下圖,沿着B+樹一直往下尋找,最終找到第四頁**,而後把該頁加載到buffer pool中,在緩存中遍歷對比查找**,因爲裏面的行記錄是順序組織的,因此很快就能夠定位到記錄了。

image-20200524232033153

3.7.二、輔助索引

除了彙集索引以外的全部索引都稱爲輔助索引(二級索引)。在InnoDB中,輔助索引中每一個記錄都包含該行的主鍵列以及爲輔助索引指定的列。

在輔助索引中查找到記錄,能夠獲得記錄的主鍵索引ID,而後能夠經過這個主鍵索引ID去彙集索引中搜索具體的記錄,這個過程稱爲回表操做。

若是主鍵較長,則輔助索引將使用更多空間,所以具備短的主鍵是有利的。

下面咱們給剛剛的表添加一個組合聯合索引

-- 添加多一個字段
alter table t20 add column d varchar(20) not null default '';
-- 添加一個聯合索引
alter table t20 add index idx_abc(a, b, c);
複製代碼

添加以後組合索引B+樹以下,其中索引key爲abc三個字段的組合,索引存儲的記錄爲主鍵ID:

image-20200525000141742

覆蓋索引(Using index)

InnoDB存儲引擎支持覆蓋索引,即從輔助索引中就能夠獲得查詢的記錄,而不須要回表去查詢彙集索引中的記錄,從而減小大量的IO操做。下面的查詢既是用到了覆蓋索引 idx_abc:

select a, b from t20 where a > 2;
複製代碼

執行結果以下:

image-20200525000655179

能夠發現,Extra這一列提示Using index,使用到了覆蓋索引,掃描的行數爲2。注意:這裏的掃描行數指的是MySQL執行器從引擎取到兩條記錄,引擎內部可能會遍歷到多條記錄進行條件比較。

最左匹配原則

因爲InnoDB索引式B+樹構建的,所以能夠利用索引的「最左前綴」來定位記錄。

也就是說,不只僅是用到索引的所有定義字段會走索引,只要知足最左前綴,就能夠利用索引來加速檢索。這個最左前綴能夠是聯合索引的最左n個字段。

索引條件下推(Using index condition)

索引條件下推 Index Condition Pushdown (ICP),是針對MySQL使用索引從表中檢索行的狀況的一種優化。

爲何叫下推呢,就是在知足要求的狀況下,把索引的條件丟給存儲引擎去判斷,而不是把完整的記錄傳回MySQL Server層去判斷。

ICP支持range, ref, eq_ref, 和 ref_or_null類型的查找,支持MyISAM和InnoDB存儲引擎。

不能將引用子查詢的條件下推,觸發條件不能下推。詳細規則參考:Index Condition Pushdown

若是不使用ICP,則存儲引擎將遍歷索引以在彙集索引中定位行,並將結果返回給MySQL Server層,MySQL Server層繼續根據WHERE條件進行篩選行。

啓用ICP後,若是WHERE能夠僅使用索引中的列來評估部分條件,則MySQL Server層會將這部分條件壓入WHERE條件降低到存儲引擎。而後,存儲引擎經過使用索引條目來判斷索引條件,在知足條件的狀況下,纔回表去查找記錄返回給MySQL Server層。

ICP的目標是減小回表掃描的行數,從而減小I / O操做。對於InnoDB表,ICP僅用於二級索引。

使用索引下推的時候,執行計劃中的Extra會提示:Using index condition,而不是Using index,由於必須回表查詢整行數據。Using index表明使用到了覆蓋索引。

3.八、InnoDB Data Directory

InnoDB數據字典(Data Directory)存放於系統表空間中,主要包含元數據,用於追蹤表、索引、表字段等信息。因爲歷史的緣由,InnoDB數據字典中的元數據與.frm文件中的元數據重複了。

3.九、Doublewrite Buffer

雙寫緩衝區(Doublewrite Buffer)是一個存儲區,是InnoDB在tablespace上的128個頁(2個區),大小是2MB[18]

版本區別:在MySQL 8.0.20以前,doublewrite緩衝區存儲區位於InnoDB系統表空間中。從MySQL 8.0.20開始,doublewrite緩衝區存儲區位於doublewrite文件中。

本文基於MySQL 5.7編寫。

操做系統寫文件是以4KB爲單位的,那麼每寫一個InnoDB的page到磁盤上,操做系統須要寫4個塊。若是寫入4個塊的過程當中出現系統崩潰,那麼會致使16K的數據只有一部分寫是成功的,這種狀況下就是partial page write(部分頁寫入)問題。

InnoDB這個時候是無法經過redo log來恢復的,由於這個時候頁面的Fil Trailer(Fil Trailer 主要存放FIL_PAGE_END_LSN,主要包含頁面校驗和以及最後的事務)中的數據是有問題的。

爲此,每當InnoDB將頁面寫入到數據文件中的適當位置以前,都會首先將其寫入雙寫緩衝區。只有將緩衝區安全地刷新到磁盤後,InnoDB纔會將頁面寫入最終的數據文件。

image-20200526221407586

若是在頁面寫入過程當中發生操做系統或者mysqld進程崩潰,則InnoDB能夠在崩潰恢復期間從雙寫緩衝區中找到頁面的無缺副本用於恢復。恢復時,InnoDB掃描雙寫緩衝區,併爲緩衝區中的每一個有效頁面檢查數據文件中的頁面是否完整。

若是系統表空間文件(「 ibdata文件 」)位於支持原子寫的Fusion-io設備上,則自動禁用雙寫緩衝,而且將Fusion-io原子寫用於全部數據文件。

3.十、Redo Log

重作日誌(Redo Log)主要適用於數據庫的崩潰恢復,用於實現數據的完整性。

重作日誌由兩部分組成:

  • 重作日誌緩衝區 Log Buffer;
  • 重作日誌文件,重作日誌文件在磁盤上由兩個名爲ib_logfile0ib_logfile1的物理文件表示。

image-20200526222744741

爲了實現數據完整性,在髒頁刷新到磁盤以前,必須先把重作日誌寫入到磁盤。除了數據頁,彙集索引、輔助索引以及Undo Log都須要記錄重作日誌。

3.10.一、Redo Log在事務中的寫入時機

在事務中,除了寫Redo log,還須要寫binlog,爲此,咱們先來簡單介紹下binlog。

3.10.1.一、binlog

全寫:Binary Log,二進制log。二進制日誌是一組日誌文件。其中包含有關對MySQL服務器實例進行的數據修改的信息。

Redo Log是InnoDB引擎特有的,而binlog是MySQL的Server層實現的,全部引擎均可以使用。

Redo Log的文件是循環寫的,空間會用完,binlog日誌是追加寫的,不會覆蓋之前的日誌。

binlog主要的目的:

  • 主從同步,主服務器將二進制日誌中包含的事件發送到從服務器,從服務器執行這些事件,以保持和主服務器相同的數據更改;
  • 某些數據恢復操做須要使用二進制日誌,還原到某一個備份點。

binlog主要是用於主從同步和數據恢復,Redo Log主要是用於實現事務數據的完整性,讓InnoDB具備不會丟失數據的能力,又稱爲crash-safe。

binlog日誌的兩種記錄形式:

  • 基於SQL的日誌記錄:事件包含產生數據更改(插入,新增,刪除)的SQL語句;
  • 基於行的日誌記錄:時間描述對單個行的更改。

混合日誌記錄默認狀況下使用基於語句的日誌記錄,但根據須要自動切換到基於行的日誌記錄。

3.10.1.二、Redo Log在事務中的寫入時機

簡單的介紹完binlog,咱們再來看看Redo Log的寫入流程。

假設咱們這裏執行一條sql

update t20 set a=10 where id=1;
複製代碼

執行流程以下:

image-20200529000923116

3.10.二、如何保證數據不丟失

前面咱們介紹Log Buffer的時候,提到過,爲了保證數據不丟失,咱們須要執行如下操做:

  • 若是啓用了binlog,則設置:sync_binlog=1;
  • innodb_flush_log_at_trx_commit=1;
  • sync_binlog=0:表示每次提交事務都只 write,不 fsync;
  • sync_binlog=1:表示每次提交事務都會執行 fsync;
  • sync_binlog=N(N>1) :表示每次提交事務都 write,但累積 N 個事務後才 fsync。

這兩個的做用至關於在上面的流程最後一步,提交事務接口返回Server層以前,把binlog cache和log buffer都fsync到磁盤中了,這樣就保證了數據的落盤,不會丟失,即便奔潰了,也能夠經過binlog和redo log恢復數據相關流程以下:

image-20200529001035132

在磁盤和內存中的處理流程以下面編號所示:

image-20200529001256504

其中第四步log buffer持久化到磁盤的時機爲:

  • log buffer佔用的空間即將達到innodb_log_buffer_size一半的時候,後臺線程主動寫盤;
  • InnoDB後臺有個線程,每隔1秒會把log buffer刷到磁盤;
  • 因爲log buffer是全部線程共享的,當其餘事務線程提交時也會致使已寫入log buffer但還未提交的事務的redo log一塊兒刷新到磁盤

其中第五步:髒頁刷新到磁盤的時機爲:

  • 系統內存不足,須要淘汰髒頁的時候,要把髒頁同步回磁盤;
  • MySQL空閒的時候;
  • MySQL正常關閉的時候,會把髒頁flush到磁盤。

參數innodb_max_dirty_pages_pct是髒頁比例上限,默認值是 75%。

爲何第二步 redo log prepare狀態也要寫磁盤?

由於這裏先寫了,才能確保在把binlog寫到磁盤後崩潰,可以恢復數據:若是判斷到redo log是prepare狀態,那麼查看是否存XID對應的binlog,若是存在,則表示事務成功提交,須要用prepare狀態的redo log進行恢復。

這樣即便崩潰了,也能夠經過redo log來進行恢復了,恢復流程以下:

Redo Log是循環寫的,以下圖:

  • writepos記錄了當前寫的位置,一邊寫位置一邊往前推動,當writepos與checkpoint重疊的時候就表示logfile寫滿了,綠色部分表示是空閒的空間,紅色部分是寫了redo log的空間;
  • checkpoint處標識了當前的LSN,每當系統崩潰重啓,都會從當前checkpoint這個位置執行重作日誌,根據重作日誌逐個確認數據頁是否沒問題,有問題就經過redo log進行修復。

image-20200528235418486

LSN Log Sequence Number的縮寫。表明日誌序列號。在InnoDB中,LSN佔用8個字節,單調遞增,LSN的含義:

  • 重作日誌寫入的總量;
  • checkpoint的位置;
  • 頁的版本;

除了重作日誌中有LSN,每一個頁的頭部也是有存儲了該頁的LSN,咱們前面介紹頁面格式的時候有介紹過。

在頁中LSN表示該頁最後刷新時LSN的大小。[19]

3.十一、Undo Logs

上面說的redo log記錄了事務的行爲,能夠經過其對頁進行重作操做,可是食物有時候須要進行回滾,這時候就須要undo log了[20]

**關於Undo Log的存儲:**InnoDB中有回滾段(rollback segment),每一個回滾段記錄1024個undo log segment,在每一個undo log segment段中進行申請undo頁。系統表空間偏移量爲5的頁記錄了全部的rollback segment header所在的頁。

image-20200530123839227

3.11.一、undo log的格式

根據行爲不一樣分爲兩種:

insert undo log

insert undo log:只對事務自己可見,因此insert undo log在事務提交後可直接刪除,無需執行purge操做;

insert undo log主要記錄了:

next 記錄下一個undo log的位置
type_cmpl undo的類型:insert or update
*undo_no 記錄事務的ID
*table_id 記錄表對象
*len1, col1 記錄列和值
*len2, col2 記錄列和值
... ...
start 記錄undo log的開始位置

假設在事務1001中,執行如下sql,t20的table_id爲10:

insert into t20(id, a, b, c, d) values(12, 2, 3, 1, "init")
複製代碼

那麼對應會生成一條undo log:

image-20200530165013967

update undo log

update undo log:執行update或者delete會產生undo log,會影響已存在的記錄,爲了實現MVCC(後邊介紹),update undo log不能再事務提交時馬上刪除,須要將事務提交時放入到history list上,等待purge線程進行最後的刪除操做。

update undo log主要記錄了:

next 記錄下一個undo log的位置
type_cmpl undo的類型:insert or update
*undo_no undo日誌編號
*table_id 記錄表對象
info_bits
*DATA_TRX_ID 事務的ID
*DATA_ROLL_PTR 回滾指針
*len1, i_col1 n_unique_index
*len2, i_col2
...
n_update_fields 如下是update vector信息,表示update操做致使發送改變的列
*pos1, *len1, u_old_col1
*pos2, *len2, u_old_col2
...
n_bytes_below
*pos, *len, col1
*pos, *len, col2
...
start 記錄undo log的開始位置

假設在事務1002中,執行如下sql,t20的table_id爲10:

update t20 set d="update1" where id=60;
複製代碼

那麼對應會生成一條undo log:

image-20200530171944498

如上圖,每回退應用一個undo log,就回退一個版本,這就是MVCC(Multi versioning concurrency control)的實現原理。

下面咱們在執行一個delete sql:

delete from t20 where id=60;
複製代碼

對應的undo log變爲以下:

image-20200530172640974

如上圖,實際的行記錄不會馬上刪除,而是在行記錄頭信息記錄了一個deleted_flag標誌位。最終會在purge線程purge undo log的時候進行實際的刪除操做,這個時候undo log也會清理掉。

3.11.二、MVCC實現原理

如上圖所示,MySQL只會有一個行記錄,可是會把每次執行的sql致使行記錄的變更,經過undo log的形式記錄起來,undo log經過回滾指針鏈接在一塊兒,這樣咱們想回溯某一個版本的時候,就能夠應用undo log,回到對應的版本視圖了。

咱們知道InnoDB是支持RC(Read Commit)和RR(Repeatable Read)事務隔離級別的,而這個是經過一致性視圖(consistent read view)實現的。

一個事務開啓瞬間,全部活躍的事務(未提交)構成了一個視圖數組,InnoDB就是經過這個視圖數組來判斷行數據是否須要undo到指定的版本:

image-20200530213342612

RR事務隔離級別

假設咱們使用了RR事務隔離級別。咱們看個例子:

以下圖,假設id=60的記錄a=1

image-20200530215747142

事務C啓動的瞬間,活躍的事務以下圖黃色部分所示:

image-20200530222622858

也就是對於事務A、事務B、事務C,他們可以看到的數據只有是行記錄中的最大事務IDDATA_TRX_ID<=11的,若是大於,那麼只能經過undo進行回滾了。若是TRX_ID=當前事務id,也能夠看到,即看到本身的改動。

另外有一個須要注意的:

  • 在RR隔離級別下,當事務更新事務的時候,只能用當前讀來獲取最新的版本數據來更新,若是當前記錄的行鎖被其餘事務佔用,就須要進入所等待;
  • 在RC隔離級別下,每一個語句執行都會計算出新的一致性視圖。

因此咱們分析上面的例子的執行流程:

  • 事務C執行update,執行當前讀,拿到的a=1,而後+1,最終a=2,同時添加一個TRX_ID=11的undo log;
    • image-20200530221027891
  • 事務B執行select,使用快照讀,記錄的DATA_TRX_ID > 11,因此須要經過undo log回滾到DATA_TRX_ID=11的版本,因此拿到的a是1;
  • 事務B執行update,須要使用當前讀,拿到最新的記錄,a=2,而後加1,最終a=3;
    • image-20200530221734284
  • 事務B執行select,拿到當前最新的版本,爲本身的事務id,因此獲得a=3;
  • 事務A執行select,使用快照讀,記錄的DATA_TRX_ID > 11,因此須要經過undo log回滾到DATA_TRX_ID=11的版本,因此拿到的a是1。
  • 若是是RC隔離級別,執行select的時候會計算出新的視圖,新的視圖可以看到的最大事務ID=14,因爲事務B還沒提交,事務C提交了,因此能夠獲得a=2:
    • image-20200530223051094

總結

  • 數據完整性依靠:redo log
  • 事務隔離級別的實現依靠MVCC,MVCC依靠undo log實現
  • IO性能提高方式:buffer pool加快查詢效率和普通索引更新的效率,log buffer對日誌寫的性能提高
  • 查詢性能提高依賴於索引,底層用頁存儲,字段越小頁存儲越多行記錄,查詢效率越快;自增字段做爲彙集索引能夠加快插入操做;
  • 故障恢復:雙寫緩衝區、redo log
  • 主從同步:binlog

本文內容比較多,看完以後須要多梳理,最後你們能夠對照着這個思惟導圖回憶一下,這些內容是否都記住了:

image-20200531191959183

image-20200531180757758


這篇文章的內容就差很少介紹到這裏了,可以閱讀到這裏的朋友真的是頗有耐心,爲你點個贊。

本文爲arthinking基於相關技術資料和官方文檔撰寫而成,確保內容的準確性,若是你發現了有何錯漏之處,煩請高擡貴手幫忙指正,萬分感激。

你們能夠關注個人博客:itzhai.com 獲取更多文章,我將持續更新後端相關技術,涉及JVM、Java基礎、架構設計、網絡編程、數據結構、數據庫、算法、併發編程、分佈式系統等相關內容。

若是您以爲讀完本文有所收穫的話,能夠關注個人帳號,或者點贊吧,碼字不易,您的支持就是我寫做的最大動力,再次感謝!

關注個人公衆號,及時獲取最新的文章。

更多文章

  • JVM系列專題:公衆號發送 JVM

本文做者: arthinking

博客連接: www.itzhai.com/database/in…

洞悉MySQL底層架構:遊走在緩衝與磁盤之間

版權聲明: BY-NC-SA許可協議:創做不易,如需轉載,請務必附加上博客連接,謝謝!


References


  1. innodb_data_home_dir. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_data_home_dir ↩︎

  2. ib_buffer_pool. Retrieved from https://dev.mysql.com/doc/refman/5.6/en/innodb-preload-buffer-pool.html ↩︎

  3. ib_logfile0. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/innodb-redo-log.html ↩︎

  4. ibtmp1. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/innodb-temporary-tablespace.html ↩︎

  5. db.opt. Retrieved from https://dev.mysql.… ↩︎

  6. Linux Programmer's Manual - OPEN(2). (2020-02-09). Retrieved from http://man7.org/linux/man-pages/man2/open.2.html ↩︎

  7. man-pages.write. (2019-10-10). Retrieved from http://man7.org/l… ↩︎

  8. man-pages.fdatasync. (2019-03-06). Retrieved from http://man7.org/l… ↩︎

  9. On Disk IO, Part 1: Flavors of IO. medium.com. Retrieved from https://medium.com/databasss/on-disk-io-part-1-flavours-of-io-8e1ace1de017 ↩︎

  10. Innodb calls fsync for writes with innodb_flush_method=O_DIRECT. Retrieved from https://bugs.mysql.com/bug.php?id=45892 ↩︎

  11. 14.6.3.3 General Tablespaces. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/general-tablespaces.html ↩︎

  12. MYSQL INNODB表壓縮. (2018-03-09). Retrieved from https://cloud.tencent.com/developer/article/1056453 ↩︎

  13. 前綴索引,一種優化索引大小的解決方案. (2015-03-03). Retrieved from https://www.cnblogs.com/studyzy/p/4310653.html ↩︎

  14. MySQL Internals Manual - innodb page structure[EB/OL]. (2020-05-04). Retrieved 2020-0530, from https://dev.mysql.com/doc/internals/en/innodb-page-structure.html ↩︎

  15. official.MySQL Internals Manual - innodb record structure[EB/OL]. (2020-… ↩︎

  16. 姜承堯. MySQL技術內幕-InnoDB存儲引擎第二版[M]. 機械工業出版社, 2013-5:104. ↩︎

  17. 爲何 MySQL 使用 B+ 樹. draveness.me. (2019-12-11). Retrieved from https://draveness.me/whys-the-design-mysql-b-plus-tree/ ↩︎

  18. InnoDB DoubleWrite Buffer as Read Cache using SSDs∗. Retrieved from https://www.usenix.org/legacy/events/fast12/poster_descriptions/Kangdescription2-12-12.pdf ↩︎

  19. 姜承堯. MySQL技術內幕-InnoDB存儲引擎第二版[M]. 機械工業出版社, 2013-5:302-303. ↩︎

  20. 姜承堯. MySQL技術內幕-InnoDB存儲引擎第二版[M]. 機械工業出版社, 2013-5:306. ↩︎

相關文章
相關標籤/搜索