提起MySQL,其實網上已經有一大把教程了,爲何我還要寫這篇文章呢,大概是由於網上不少網站都是比較零散,並且描述不夠直觀,不能系統對MySQL相關知識有一個系統的學習,致使不能造成知識體系。爲此我撰寫了這篇文章,試圖讓這些底層架構相關知識更加直觀易懂:php
圖文
的方式描述技術原理;官網
或者技術書籍
來源,方便你們進一步擴展學習;背景知識
儘量作一個交代,好比討論到log buffer的刷盤方式,延伸一下IO寫磁盤相關知識點。好了,MySQL從不會到精通系列立刻就要開始了(看完以後仍是不會的話..請忽略這句話)。html
可能會有同窗問:爲啥不直接學更加先進的TiDB,或者是強大的OceanBase。mysql
其實,MySQL做爲老牌的應用場景普遍的關係型開源數據庫,其底層架構是很值得咱們學習的,吸取其設計精華,那麼咱們在平時的方案設計工做中也能夠借鑑,若是項目中用的是MySQL,那麼就可以把數據庫用的更好了,瞭解了MySQL底層的執行原理,對於調優工做也是有莫大幫助的。本文我重點講述MySQL底層架構,涉及到:linux
buffer pool
、log buffer
、change buffer
,buffer pool的頁淘汰機制是怎樣的;系統表空間
、獨立表空間
、通用表空間
、undo表空間
、redo log
;IO
相關底層原理、查詢SQL執行流程
、數據頁結構
和行結構
描述、彙集索引
和輔助索引
的底層數據組織方式、MVCC
多版本併發控制的底層實現原理,以及可重複讀
、讀已提交
是怎麼經過MVCC實現的。看完文本文,您將瞭解到:算法
IO操做
寫磁盤有哪幾種方式,有什麼IO優化方式 (3.1.二、關於磁盤IO的方式)InnoDB緩存
(buffer pool, log buffer)的刷新方式有哪些(3.1.2.二、innodb_flush_method)log buffer
的日誌刷盤控制參數innodb_flush_log_at_trx_commit
對寫性能有什麼影響(3.4.一、配置參數)表空間
(系統表空間,獨立表空間,通用表空間)的做用和優缺點是什麼,ibdata
、ibd
、frm
文件分別是幹嗎的(3.五、表空間)varchar
,null
底層是如何存儲的,最大可用存儲多大的長度(3.6.3.一、MySQL中varchar最大長度是多少)索引
的組織方式是怎樣的,明白爲何要採用B+樹
,而不是哈希表、二叉樹或者B樹(3.七、索引 - 爲何MySQL使用B+樹)大字段
會影響表性能(查詢性能,更新性能)(3.七、索引)覆蓋索引
、聯合索引
什麼狀況下會生效(3.7.二、輔助索引)索引下推
,索引下推減小了哪方面的開銷?(3.7.二、輔助索引 - 索引條件下推)Change Buffer
對二級索引DML語句有什麼優化(3.二、Change Buffer)redo log
、undo log
和buffer pool
數據完整性的關鍵做用分別是什麼(3.10.二、如何保證數據不丟失)MVCC
底層是怎麼實現的,可重複讀和讀已提交是怎麼實現的(3.11.二、MVCC實現原理)以下圖爲MySQL架構涉及到的經常使用組件:sql
有以下表格:數據庫
咱們執行如下sql:編程
select * from t_user where user_id=10000;
以下圖,創建過程:後端
對於Java應用程序來講,通常會把創建好的鏈接放入數據庫鏈接池中進行復用,只要這個鏈接不關閉,就會一直在MySQL服務端保持着,能夠經過show processlist
命令查看,以下:數組
注意,這裏有個Time,表示這個鏈接多久沒有動靜了,上面例子是656秒沒有動靜,默認地,若是超過8個小時尚未動靜,鏈接器就會自動斷開鏈接,能夠經過wait_timeout
參數進行控制。
以下圖,執行sql:
以下圖,爲存儲引擎的架構:
其實內存中的結構不太好直接觀察到,不過磁盤的仍是能夠看到的,咱們找到磁盤中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文件,不然保存在系統表空間文件中
接下來咱們逐一來介紹。
buffer pool
(緩衝池
)是主內存
中的一個區域,在InnoDB訪問表數據
和索引數據
的時候,會順便把對應的數據頁緩存到緩衝池中。若是直接從緩衝池中直接讀取數據將會加快處理速度。在專用服務器上,一般將80%左右的物理內存分配給緩衝池。
爲了提升緩存管理效率,緩衝池把頁面連接爲列表,使用改進版的LRU算法
將不多使用的數據從緩存中老化淘汰掉。
經過使用改進版的LRU算法來管理緩衝池列表。
當須要把新頁面存儲到緩衝池中的時候,將淘汰最近最少使用的頁面,並將新頁面添加到舊子列表的頭部。
該算法運行方式:
相關優化參數:
innodb_old_blocks_pct
:控制LRU列表中舊子列表的百分比,默認是37,也就是3/8,可選範圍爲5~95;innodb_old_blocks_time
:指定第一次訪問頁面後的時間窗口,該時間窗口內訪問頁面不會使其移動到LRU列表的最前面。默認是1000,也就是1秒。innodb_old_blocks_time很重要,有了這1秒,對於全表掃描,因爲是順序掃描的,通常同一個數據頁的數據都是在一秒內訪問完成的,不會升級到新子列表中,一直在舊子列表淘汰數據,因此不會影響到新子列表的緩存。
O_DIRECT
是innodb_flush_method
參數的一個可選值。
這裏先介紹下和數據庫性能密切相關的文件IO操做方法
數據庫系統是基於文件系統的,其性能和設備讀寫的機制有密切的關係。
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下降文件的順序讀寫的效率。ssize_t write(int fd, const void *buf, size_t count);
使用open打開文件獲取到文件描述符以後,能夠調用write函數來寫文件,具體表現根據open函數參數的不一樣而不一樣弄。
#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。
大體交互流程以下圖:
更多關於磁盤IO的相關內容,能夠閱讀:On Disk IO, Part 1: Flavors of IO[9]
fsync性能問題:除了刷髒頁到磁盤,fsync還會同步文件metadata,而文件數據和metadata一般存放在磁盤不一樣地方,因此fsync至少須要兩次IO操做。
對fsync性能的優化建議:因爲以上性能問題,若是可以減小metadata的更新,那麼就可使用fdatasync了。所以須要確保文件的尺寸在write先後沒有發生變化。爲此,能夠建立固定大小的文件進行寫,寫完則開啓新的文件繼續寫。
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()
系統調用來flush數據文件和日誌文件到磁盤;
因爲open函數的O_DSYNC參數在許多Unix系統上都存中問題,所以InnoDB不直接使用O_DSYNC。
InnoDB
用於O_SYNC
打開和刷新日誌文件,fsync()
刷新數據文件。
表現爲:寫日誌操做是在write函數完成,數據文件寫入是經過fsync()
系統調用來完成;
使用O_DIRECT
(在Solaris上對應爲directio()
)打開數據文件,並用於fsync()
刷新數據文件和日誌文件。此選項在某些GNU/Linux版本,FreeBSD和Solaris上可用。
表現爲:數據文件寫入直接從buffer pool到磁盤,不通過操做系統緩衝,日誌仍是須要通過操做系統緩存;
在刷新I/O期間InnoDB
使用O_DIRECT
,而且每次write操做後跳過fsync()
系統調用。
此設置適用於某些類型的文件系統,但不適用於其餘類型的文件系統。例如,它不適用於XFS。若是不肯定所使用的文件系統是否須要fsync()(例如保留全部文件元數據),請改用O_DIRECT。
以下圖所示:
爲何使用了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
選項來解決此類問題。
change buffer是一種特殊的數據結構,當二級索引頁(非惟一索引)不在緩衝池中時,它們會緩存這些更改 。當頁面經過其餘讀取操做加載到緩衝池中時,再將由INSERT
,UPDATE
或DELETE
操做(DML)產生的change buffer合併到buffer pool的數據頁中。
爲何惟一索引不可使用chage buffer?
針對惟一索引,若是buffer pool不存在對應的數據頁,仍是須要先去磁盤加載數據頁,才能判斷記錄是否重複,這一步避免不了。
而普通索引是非惟一的,插入的時候以相對隨機的順序發生,刪除和更新也會影響索引樹中不相鄰的二級索引樹,經過使用合併緩衝,避免了在磁盤產生大量的隨機IO訪問獲取普通索引頁。
問題
當有許多受影響的行和許多輔助索引要更新時,change buffer合併可能須要幾個小時,在此期間,I/O會增長,可能會致使查詢效率大大下降,即便在事務提交以後,或者服務器重啓以後,change buffer合併操做也會繼續發生。相關閱讀:Section 14.22.2, 「Forcing InnoDB Recovery」
自適應哈希索引功能由innodb_adaptive_hash_index
變量啓用 ,或在服務器啓動時由--skip-innodb-adaptive-hash-index
禁用。
log buffer(日誌緩衝區)用於保存要寫入磁盤上的log file(日誌文件)的數據。日誌緩存區的內容會按期刷新到磁盤。
日誌緩衝區大小由innodb_log_buffer_size
變量定義 。默認大小爲16MB。較大的日誌緩衝區可讓大型事務在提交以前無需將redo log寫入磁盤。
若是您有更新,插入或者刪除多行的事務,嘗試增大日誌緩衝區的大小能夠節省磁盤I/O。
innodb_flush_log_at_trx_commit
innodb_flush_log_at_trx_commit
變量控制如何將日誌緩衝區的內容寫入並刷新到磁盤。
該參數控制是否嚴格存儲ACID仍是嘗試獲取更高的性能,能夠經過該參數獲取更好的性能,可是會致使在系統崩潰的過程當中致使數據丟失。
可選參數:
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;
配置效果以下圖所示:
一個InnoDB
表及其索引能夠在建在系統表空間中,或者是在一個 獨立表空間 中,或在 通用表空間。
innodb_file_per_table
啓用時,一般是將表存放在獨立表空間中,這是默認配置;innodb_file_per_table
禁用時,則會在系統表空間中建立表;CREATE TABLE ... TABLESPACE
語法。有關更多信息,請參見官方文檔 14.6.3.3 General Tablespaces。表空間概覽圖:
相關文件默認在磁盤中的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中建立。
系統表空間由一個或多個數據文件(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數據目錄以外建立,可以容納多個表,並支持全部行格式的表。
MySQL 5.7中,配置參數:innodb_file_per_table
,默認處於啓用狀態,這是一個重要的配置選項,會影響InnoDB
文件存儲,功能的可用性和I/O特性等。
啓用以後,每一個表的數據和索引是存放在單獨的.ibd文件中的,而不是在系統表空間的共享ibdata文件中。
數據壓縮
[12]的行格式,如:
前綴索引
[13]最多包含768個字節。若是開啓innodb_large_prefix,且Innodb表的存儲行格式爲 DYNAMIC 或 COMPRESSED,則前綴索引最多可包含3072個字節,前綴索引也一樣適用;TRUNCATE TABLE
執行的更快,而且回收的空間不會繼續保留,而是讓操做系統使用;即便啓用了innodb_file_per_table參數,每張表空間存放的只是數據、索引和插入緩存Bitmap頁,其餘數據如回滾信息、插入緩衝索引頁、系統事務信息、二次寫緩衝等仍是存放在原來的共享表空間中。
通用表空間使用CREATE TABLESPACE
語法建立。
相似於系統表空間,通用表空間是共享表空間,能夠存儲多個表的數據。
通用表空間比獨立表空間具備潛在的內存優點,服務器在表空間的生存期內將表空間元數據保留在內存中。一個通用表空間一般能夠存放多個表數據,消耗更少的表空間元數據內存。
數據文件能夠放置在MySQL數據目錄或獨立於MySQL數據目錄。
undo表空間包含undo log。
innodb_rollback_segments
變量定義分配給每一個撤消表空間的回滾段的數量。
undo log能夠存儲在一個或多個undo表空間中,而不是系統表空間中。
在默認配置中,撤消日誌位於系統表空間中。SSD存儲更適合undo log的I/O模式,爲此,能夠把undo log存放在有別於系統表空間的ssd硬盤中。
innodb_undo_tablespaces
配置選項控制undo表空間的數量。
由用戶建立的非壓縮臨時表和磁盤內部臨時表是在共享臨時表空間中建立的。
innodb_temp_data_file_path
配置選項指定零時表空間文件的路徑,若是未指定,則默認在 innodb_data_home_dir
目錄中建立一個略大於12MB 的自動擴展數據文件ibtmp1
。
使用ROW_FORMAT=COMPRESSED
屬性建立的壓縮臨時表,是在獨立表空間中的臨時文件目錄中建立的 。
服務啓動的時候建立臨時表空間,關閉的時候銷燬臨時表空間。若是臨時表空間建立失敗,則意味着服務啓動失敗。
在介紹索引以前,咱們有必要了解一下InnoDB底層的邏輯存儲結構,由於索引是基於這個底層邏輯存儲結構建立的。截止到目前,咱們所展現的都僅僅是物理磁盤中的邏輯視圖,接下來咱們就來看看底層的視圖。
如今咱們打開一個表空間ibd文件,看看裏面都是如何組織數據的?
以下圖,表空間由段(segment)、區(extent)、頁(page)組成。
InnoDB最小的存儲單位是頁,默認每一個頁大小是16k。
而InnoDB存儲引擎是面向行的(row-oriented),數據按行進行存放,每一個頁規定最多容許存放的行數=16k/2 - 200,即7992行。
段:如數據段、索引段、回滾段等。InnoDB存儲引擎是B+樹索引組織的,因此數據即索引,索引即數據。B+樹的葉子節點存儲的都是數據段的數據。
名稱 | 佔用空間 | 描述 |
---|---|---|
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會在根頁面中自動設置一個最小記錄和一個最高記錄,而且永遠不會刪除它們。最低記錄和最高記錄能夠視爲索引頁開銷的一部分。最初,它們都存在於根頁面上,可是隨着索引的增加,最低記錄將存在於第一或最低葉子頁上,最高記錄將出如今最後或最大關鍵字頁上。
先來說講Compact行記錄格式,Compact是MySQL5.0引入的,設計目標是高效的存儲數據,讓一個頁可以存放更多的數據,從而實現更快的B+樹查找。
名稱 | 描述 |
---|---|
變長字段長度列表 | 字段大小最多用2個字節表示,也就是最多限制長度:2^16=65535個字節;字段大小小於255字節,則用1個字節表示; |
NULL標誌位 | 記錄該行哪些位置的字段是null值 |
記錄頭信息 | 記錄頭信息信息,固定佔用5個字節 |
列1數據 | 實際的列數據,NULL不佔用該部分的空間 |
列2數據 | |
... |
記錄頭用於將連續的記錄連接在一塊兒,並用於行級鎖定。
每行數據除了用戶定義的列外,還有兩個隱藏列:
而記錄頭信息包[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 | 指向頁中下一條記錄 |
更詳細的頁結構參考官網:22.2 InnoDB Page Structure
更詳細的行結構參考官網:22.1 InnoDB Record Structure
更詳細的行格式參考官網:14.11 InnoDB Row Formats
根據以上格式,能夠得出數據頁內的記錄組織方式:
上面表格描述咱們知道,一個字段最長限制是65535個字節,這是存儲長度的限制。
而MySQL中對存儲是有限制的,具體參考:8.4.7 Limits on Table Column Count and Row Size
而實際可以存儲的字符是跟編碼有關的。
背景知識:
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,咱們驗證下:
MySQL表的內部表示具備65,535字節的最大行大小限制。InnoDB
對於4KB,8KB,16KB和32KB innodb_page_size
設置,表的最大行大小(適用於本地存儲在數據庫頁面內的數據)略小於頁面的一半 。若是包含 可變長度列的InnoDB
行超過最大行大小,那麼將選擇可變長度列用於外部頁外存儲。
可變長度列因爲太長而沒法容納在B樹頁面上,這個時候會把可變長度列存儲在單獨分配的磁盤頁面上,這些頁面稱爲溢出頁面
,這些列稱爲頁外列
。頁外列的值存儲在由溢出頁面構成的單連接列表
中。
InnoDB
存儲引擎支持四種行格式:REDUNDANT
,COMPACT
, DYNAMIC
,和COMPRESSED
。不一樣的行格式,對溢出的閾值和處理方式有所區別,詳細參考:14.11 InnoDB Row Formats。
COMPACT行格式處理方式
使用COMPACT
行格式的表將前768個字節的變長列值(VARCHAR
, VARBINARY
和 BLOB
和 TEXT
類型)存儲在B樹節點內的索引記錄中,其他的存儲在溢出頁上。
若是列的值等於或小於768個字節,則不使用溢出頁,所以能夠節省一些I / O。
若是查過了768個字節,那麼會按照以下方式進行存儲:
DYNAMIC行格式處理方式
DYNAMIC
行格式提供與COMPACT
行格式相同的存儲特性,但改進了超長可變長度列的存儲能力和支持大索引鍵前綴。
InnoDB
能夠徹底在頁外存儲過長的可變長度列值(針對 VARCHAR
, VARBINARY
和 BLOB
和 TEXT
類型),而彙集索引記錄僅包含指向溢出頁的20字節指針。大於或等於768字節的固定長度字段被編碼爲可變長度字段。
表中大字段引起的問題
若是一個表中有過多的可變長度大字段,致使一行記錄太長,而整個時候使用的是COMPACT行格式,那麼就可能會插入數據報錯。
如,頁面大小事16k,根據前面描述咱們知道,MySQL限制一頁最少要存儲兩行數據,若是不少可變長度大字段,在使用COMPACT的狀況下,仍然會把大字段的前面768個字節存在索引頁中,能夠算出最多支持的大字段:
1024 * 16 / 2 / 768 = 10.67
,那麼超過10個可變長度大字段就會插入失敗了。這個時候能夠把row format改成:DYNAMIC。
前面咱們瞭解了InnoDB底層的存儲結構,即:以B+樹的方式組織數據頁。另外瞭解了數據頁中的數據行的存儲方式。
而構建B+樹索引的時候必需要選定一個或者多個字段做爲索引的值,若是索引選擇的是主鍵,那麼咱們就稱爲彙集索引,不然就是二級索引。
爲何MySQL使用B+樹?
- 哈希表雖然能夠提供O(1)的單行數據操做性能,但卻不能很好的支持排序和範圍查找,會致使全表掃描;
- B樹能夠再非葉子節點存儲數據,可是這可能會致使查詢連續數據的時候增長更多的I/O操做;
- 而B+樹數據都存放在葉子節點,葉子節點經過指針相互鏈接,能夠減小順序遍歷時產生的額外隨機I/O
更新詳細解釋: 爲何 MySQL 使用 B+ 樹[17]
瞭解到上面的底層邏輯存儲結構以後,咱們進一步來看看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亂序插入的,可是插入以後查出來的確是排序好的:
這個排序就是B+索引樹構建的。
咱們能夠經過這個在線的動態演示工具來看看B+樹的構造過程,最終結果以下:
實際存放在數據庫中的模型因頁面大小不同而有所不一樣,這裏爲了簡化模型,咱們按照B+樹的通用模型來解釋數據的存儲結構。
相似的,咱們的數據也是這種組織形式的,該B+樹中,咱們以主鍵爲索引進行構建,而且把完整的記錄存到對應的頁下面:
其中藍色的是索引頁,橙色的是數據頁。
每一個頁的大小默認爲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中,在緩存中遍歷對比查找,因爲裏面的行記錄是順序組織的,因此很快就能夠定位到記錄了。
除了彙集索引以外的全部索引都稱爲輔助索引(二級索引)。在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:
InnoDB存儲引擎支持覆蓋索引,即從輔助索引中就能夠獲得查詢的記錄,而不須要回表去查詢彙集索引中的記錄,從而減小大量的IO操做。下面的查詢既是用到了覆蓋索引 idx_abc:
select a, b from t20 where a > 2;
執行結果以下:
能夠發現,Extra這一列提示Using index,使用到了覆蓋索引,掃描的行數爲2。注意:這裏的掃描行數指的是MySQL執行器從引擎取到兩條記錄,引擎內部可能會遍歷到多條記錄進行條件比較。
因爲InnoDB索引式B+樹構建的,所以能夠利用索引的「最左前綴」來定位記錄。
也就是說,不只僅是用到索引的所有定義字段會走索引,只要知足最左前綴,就能夠利用索引來加速檢索。這個最左前綴能夠是聯合索引的最左n個字段。
索引條件下推 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
表明使用到了覆蓋索引。
InnoDB數據字典(Data Directory)存放於系統表空間中,主要包含元數據,用於追蹤表、索引、表字段等信息。因爲歷史的緣由,InnoDB數據字典中的元數據與.frm
文件中的元數據重複了。
雙寫緩衝區(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纔會將頁面寫入最終的數據文件。
若是在頁面寫入過程當中發生操做系統或者mysqld進程崩潰,則InnoDB能夠在崩潰恢復期間從雙寫緩衝區中找到頁面的無缺副本用於恢復。恢復時,InnoDB掃描雙寫緩衝區,併爲緩衝區中的每一個有效頁面檢查數據文件中的頁面是否完整。
若是系統表空間文件(「 ibdata文件 」)位於支持原子寫的Fusion-io設備上,則自動禁用雙寫緩衝,而且將Fusion-io原子寫用於全部數據文件。
重作日誌(Redo Log)主要適用於數據庫的崩潰恢復,用於實現數據的完整性。
重作日誌由兩部分組成:
ib_logfile0
和ib_logfile1
的物理文件表示。爲了實現數據完整性,在髒頁刷新到磁盤以前,必須先把重作日誌寫入到磁盤。除了數據頁,彙集索引、輔助索引以及Undo Log都須要記錄重作日誌。
在事務中,除了寫Redo log,還須要寫binlog,爲此,咱們先來簡單介紹下binlog。
全寫:Binary Log,二進制log。二進制日誌是一組日誌文件。其中包含有關對MySQL服務器實例進行的數據修改的信息。
Redo Log是InnoDB引擎特有的,而binlog是MySQL的Server層實現的,全部引擎均可以使用。
Redo Log的文件是循環寫的,空間會用完,binlog日誌是追加寫的,不會覆蓋之前的日誌。
binlog主要的目的:
binlog主要是用於主從同步和數據恢復,Redo Log主要是用於實現事務數據的完整性,讓InnoDB具備不會丟失數據的能力,又稱爲crash-safe。
binlog日誌的兩種記錄形式:
混合日誌記錄默認狀況下使用基於語句的日誌記錄,但根據須要自動切換到基於行的日誌記錄。
簡單的介紹完binlog,咱們再來看看Redo Log的寫入流程。
假設咱們這裏執行一條sql
update t20 set a=10 where id=1;
執行流程以下:
前面咱們介紹Log Buffer
的時候,提到過,爲了保證數據不丟失,咱們須要執行如下操做:
- 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恢復數據相關流程以下:
在磁盤和內存中的處理流程以下面編號所示:
其中第四步log buffer持久化到磁盤的時機爲:
innodb_log_buffer_size
一半的時候,後臺線程主動寫盤;其中第五步:髒頁刷新到磁盤的時機爲:
參數
innodb_max_dirty_pages_pct
是髒頁比例上限,默認值是 75%。
爲何第二步 redo log prepare狀態也要寫磁盤?
由於這裏先寫了,才能確保在把binlog寫到磁盤後崩潰,可以恢復數據:若是判斷到redo log是prepare狀態,那麼查看是否存XID對應的binlog,若是存在,則表示事務成功提交,須要用prepare狀態的redo log進行恢復。
這樣即便崩潰了,也能夠經過redo log來進行恢復了,恢復流程以下:
Redo Log是循環寫的,以下圖:
LSN
,每當系統崩潰重啓,都會從當前checkpoint這個位置執行重作日誌,根據重作日誌逐個確認數據頁是否沒問題,有問題就經過redo log進行修復。LSN Log Sequence Number的縮寫。表明日誌序列號。在InnoDB中,LSN佔用8個字節,單調遞增,LSN的含義:
- 重作日誌寫入的總量;
- checkpoint的位置;
- 頁的版本;
除了重作日誌中有LSN,每一個頁的頭部也是有存儲了該頁的LSN,咱們前面介紹頁面格式的時候有介紹過。
在頁中LSN表示該頁最後刷新時LSN的大小。[19]
上面說的redo log記錄了事務的行爲,能夠經過其對頁進行重作操做,可是食物有時候須要進行回滾,這時候就須要undo log了。[20]
關於Undo Log的存儲:InnoDB中有回滾段(rollback segment),每一個回滾段記錄1024個undo log segment,在每一個undo log segment段中進行申請undo頁。系統表空間偏移量爲5的頁記錄了全部的rollback segment header所在的頁。
根據行爲不一樣分爲兩種:
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:
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:
如上圖,每回退應用一個undo log,就回退一個版本,這就是MVCC(Multi versioning concurrency control)的實現原理。
下面咱們在執行一個delete sql:
delete from t20 where id=60;
對應的undo log變爲以下:
如上圖,實際的行記錄不會馬上刪除,而是在行記錄頭信息記錄了一個deleted_flag
標誌位。最終會在purge線程purge undo log的時候進行實際的刪除操做,這個時候undo log也會清理掉。
如上圖所示,MySQL只會有一個行記錄,可是會把每次執行的sql致使行記錄的變更,經過undo log的形式記錄起來,undo log經過回滾指針鏈接在一塊兒,這樣咱們想回溯某一個版本的時候,就能夠應用undo log,回到對應的版本視圖了。
咱們知道InnoDB是支持RC
(Read Commit)和RR
(Repeatable Read)事務隔離級別的,而這個是經過一致性視圖
(consistent read view)實現的。
一個事務開啓瞬間,全部活躍的事務(未提交)構成了一個視圖數組,InnoDB就是經過這個視圖數組來判斷行數據是否須要undo到指定的版本:
假設咱們使用了RR事務隔離級別。咱們看個例子:
以下圖,假設id=60的記錄a=1
事務C啓動的瞬間,活躍的事務以下圖黃色部分所示:
也就是對於事務A、事務B、事務C,他們可以看到的數據只有是行記錄中的最大事務IDDATA_TRX_ID
<=11的,若是大於,那麼只能經過undo進行回滾了。若是TRX_ID=當前事務id,也能夠看到,即看到本身的改動。
另外有一個須要注意的:
因此咱們分析上面的例子的執行流程:
本文內容比較多,看完以後須要多梳理,最後你們能夠對照着這個思惟導圖回憶一下,這些內容是否都記住了:
這篇文章的內容就差很少介紹到這裏了,可以閱讀到這裏的朋友真的是頗有耐心,爲你點個贊。
本文爲arthinking
基於相關技術資料和官方文檔撰寫而成,確保內容的準確性,若是你發現了有何錯漏之處,煩請高擡貴手幫忙指正,萬分感激。
你們能夠關注個人博客:itzhai.com
獲取更多文章,我將持續更新後端相關技術,涉及JVM、Java基礎、架構設計、網絡編程、數據結構、數據庫、算法、併發編程、分佈式系統等相關內容。
若是您以爲讀完本文有所收穫的話,能夠關注
個人帳號,或者點贊
吧,碼字不易,您的支持就是我寫做的最大動力,再次感謝!
關注個人公衆號,及時獲取最新的文章。
更多文章
本文做者: arthinking
版權聲明:
BY-NC-SA
許可協議:創做不易,如需轉載,請務必附加上博客連接,謝謝!
innodb_data_home_dir. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_data_home_dir ↩︎
ib_buffer_pool. Retrieved from https://dev.mysql.com/doc/refman/5.6/en/innodb-preload-buffer-pool.html ↩︎
ib_logfile0. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/innodb-redo-log.html ↩︎
ibtmp1. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/innodb-temporary-tablespace.html ↩︎
db.opt. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/data-dictionary-file-removal.html ↩︎
Linux Programmer's Manual - OPEN(2). (2020-02-09). Retrieved from http://man7.org/linux/man-pages/man2/open.2.html ↩︎
man-pages.write. (2019-10-10). Retrieved from http://man7.org/linux/man-pages/man2/write.2.html ↩︎
man-pages.fdatasync. (2019-03-06). Retrieved from http://man7.org/linux/man-pages/man2/fdatasync.2.html ↩︎
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 ↩︎
Innodb calls fsync for writes with innodb_flush_method=O_DIRECT. Retrieved from https://bugs.mysql.com/bug.php?id=45892 ↩︎
14.6.3.3 General Tablespaces. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/general-tablespaces.html ↩︎
MYSQL INNODB表壓縮. (2018-03-09). Retrieved from https://cloud.tencent.com/developer/article/1056453 ↩︎
前綴索引,一種優化索引大小的解決方案. (2015-03-03). Retrieved from https://www.cnblogs.com/studyzy/p/4310653.html ↩︎
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 ↩︎
official.MySQL Internals Manual - innodb record structure[EB/OL]. (2020-05-04). Retrieved 2020-0530, from https://dev.mysql.com/doc/internals/en/innodb-record-structure.html ↩︎
姜承堯. MySQL技術內幕-InnoDB存儲引擎第二版[M]. 機械工業出版社, 2013-5:104. ↩︎
爲何 MySQL 使用 B+ 樹. draveness.me. (2019-12-11). Retrieved from https://draveness.me/whys-the-design-mysql-b-plus-tree/ ↩︎
InnoDB DoubleWrite Buffer as Read Cache using SSDs∗. Retrieved from https://www.usenix.org/legacy/events/fast12/poster_descriptions/Kangdescription2-12-12.pdf ↩︎
姜承堯. MySQL技術內幕-InnoDB存儲引擎第二版[M]. 機械工業出版社, 2013-5:302-303. ↩︎
姜承堯. MySQL技術內幕-InnoDB存儲引擎第二版[M]. 機械工業出版社, 2013-5:306. ↩︎