InnoDB引擎面面觀


自 2010 年 MYSQL 5.5.5 發布以來,InnoDB 已經取代 MyISAM 作為 MYSQL 的默認表類型,這得益於他在新時代前瞻性的功能開發:

☑ 遵循 ACID 模型開發的 DML 操作

☑ 對事務的支持

☑ 支持行級鎖

☑ 支持外鍵

這些特性讓 MYSQL 在新時代仍能夠站在時代之巔,時至今日仍舊不為落后。

InnoDB 架構

下圖顯示了構成InnoDB存儲引擎體系結構的內存中和磁盤上的結構:

1

​ 圖片來自於 MYSQL 官網

如上圖所示,InnoDB 架構分為兩大部分:內存存儲 和 磁盤存儲,每一部分分別有自己的組成部分:

內存存儲部分包括:

  • Buffer Pool
  • Change Buffer
  • Adaptive Hash Index
  • Log Buffer

磁盤存儲包括:

  • Tables
  • Indexes
  • Tablespaces
  • Doublewrite buffer
  • Redo Log
  • Undo Logs

就上面的各個部分我們單獨拿出來一一介紹。

內存架構

Buffer Pool

Buffer Pool 最主要的功能就是加速讀和加速寫。

當讀取數據的時候如果該數據所在的數據頁正好在 Buffer Pool 中,那么就直接從 Buffer Pool 中讀取數據;

當寫入數據的時候,先將該數據放入 Buffer Pool 中,並記錄 redo log 日志,對於寫入操作到這里就算完成了。至於這個頁什么時候會被刷入到磁盤,這就是刷臟的邏輯。

在 InnoDB 中數據是按照 頁/ 塊(默認為 16K )的方式存儲到磁盤,並以同樣的方式讀取文件到 Buffer Pool 中,然后用同樣大小空間做內存映射。既然是預讀數據那么肯定存在冷熱問題,常見的緩存淘汰算法 LRU 在這種場景必然逃不掉,我們先從 Buffer Pool 的結構開始着手。

Buffer Pool 由兩個部分組成:控制塊 和 緩存數據頁:

4

二者一一對應,控制塊中的內容主要是緩存數據頁的頁號、表空間號、數據頁在 Buffer Pool 中的地址等一系列信息。

單個控制塊的大小約為緩存數據頁的 5% 左右,並且控制塊的大小不計入 Buffer Pool 的分配空間內。如果你分配了 10G 的 Buffer Pool,那么實際占用的內存大小可能大於 10G,因為 MySQL 實例啟動的時候,需要記入控制塊的大小。

在 Buffer Pool 中保存的次級模塊 叫做 Buffer chunks。一個 Buffer Pool 中有一個或多個 chunk,每個 chunk 大小默認為 128M,最小為 1M,每個 chunk 中包含一個 buf_block_t 的 blocks 數組,保存的內容即上面我們說的數據頁。

Buffer Pool 肯定不能隨便將數據頁載入內存,所以在 chunk 中包含當面數據頁數組和對應的控制體信息,在代碼中 Buffer Pool 用 buf_pool_t 對象來表述,它包含四個部分:

  • free 鏈表:存儲當前 Buffer Pool 實例中所有的空閑頁面
  • flush_list 鏈表:存儲所有被修改過且需要刷到文件中區的頁面
  • mutex:保護實例,同一時刻只能被一個線程訪問的對象
  • LRU list:chunks,加載到內存中的數據頁塊鏈表。

free list

初始化的時候會申請一定數量的 page,當然這些 page 都是 free page,所以在 free list 中保存的都是 未被使用的頁。

在執行 sql 的過程中,每次拉取數據頁到內存中都會判斷 free list 的頁面是否夠用,如果不夠就 flush LRU 鏈表和 flush_list 鏈表來釋放空頁;如果夠用的情況就從 free list 中刪除對應頁面並將改頁添加入 LRU list,申請的總頁數保持不變。

LRU list

所有新讀取進來的數據頁都在這里。與傳統的 LRU 算法不同的是將鏈表分為冷熱兩個部分,主要是為了防止預讀的數據頁和全表掃描對緩沖區污染。

LRU list 整體空間做了如下划分:

2

​ 圖片來自於 MYSQL 官網

LRU list 有兩個區域,一個是鏈表頭部的 new Sublist 區域(熱數據),另一個是 old Sublist 區域(冷數據)。

默認情況下 LRU List 的 3/8 用於 old sublist,new Sublist 與 old Sublist 的分界線是鏈表的中點字段:midpoint。

如果一個頁之前沒有在 Buffer Pool 中,首次被查詢到會添加到 new Sublist 中,如果一個沒有 where 條件的查詢進行全表掃描那么會將大部分無效數據代入 new Sublist,造成 真正的熱點數據進入 old Sublist 而被回收。

預讀機制

針對預讀造成的數據污染,InnoDB 也做出了相應的措施。最近訪問的頁默認會插入到 LRU list 的 5/8 之后的位置,即 Old Sublist 的頭部位置。

如果有一個新的預讀任務做了全表掃描,讀取出來的頁信息會首先放到 Old Sublist 的頭部,這些頁中可能有某些頁才是本次查詢真正的數據所在頁,那么這些頁在讀取的時候會被加入 new Sublist 的頭部。

老生代停留時窗口

盡管有這樣的預讀機制,對於不會命中索引的 ”like“ 查詢仍然會造成大量的緩沖池污染。 MYSQL 又提供了一個 ”老生代停留時間窗口“ 的機制,配置參數 innodb_old_blocks_time 指定了一個在第一次訪問到實際移動這個數據頁到 new Sublist 頭部的時間窗口,單位為:毫秒。默認 值為 1000 ,即為 1000 毫秒。

假設預讀一批數據插入到 Old Sublist 的頭部,此時設置的 window timewait = 2000,如果在 2000ms 內該數據被訪問那么也不會被移動到 New Sublist 中,只有滿足 被訪問 且 在 Old Sublist 中停留的時間 > window timewait 才會被放入 New Sublist。

flush list

這里用於緩存那些當前發生過更改的數據。需要注意的是在 flush list 中保存的並不是數據頁,而是數據頁對應的控制塊信息。

當某個數據頁在 Buffer Pool 中第一次被更改過,會將它加入到 flush list 鏈表中,鏈表采用頭插法,同時記錄該數據頁的兩個屬性:

oldest_modification:oldest 是指修改該頁面的 mtr 第一次開始時候的 lsn 號

newest_modification:newest 是指最后一次修改該頁面的 mtr 結束時候的 lsn 號

如果該頁再次有改動的時候不會執行插入操作而是更改 newest_modification。

mutex

InnoDB 定義了很多的 Mutex,場景包括數據緩沖區,字典表系統鎖表等等,用來保護有並發競爭的對象。BUffer Pool 中也是一樣,比如對同一個數據頁的更改操作必須要加鎖,否則導致覆蓋。

我們來看一些 Buffer Pool 相關的配置參數, show variables like "Innodb_buffer_pool%"; 可以查看 buffer_pool 相關的配置參數:

mysql> show variables like "Innodb_buffer_pool%";
+-------------------------------------+----------------+
| Variable_name                       | Value          |
+-------------------------------------+----------------+
| innodb_buffer_pool_chunk_size       | 134217728      |
| innodb_buffer_pool_dump_at_shutdown | ON             |
| innodb_buffer_pool_dump_now         | OFF            |
| innodb_buffer_pool_dump_pct         | 25             |
| innodb_buffer_pool_filename         | ib_buffer_pool |
| innodb_buffer_pool_instances        | 1              |
| innodb_buffer_pool_load_abort       | OFF            |
| innodb_buffer_pool_load_at_startup  | ON             |
| innodb_buffer_pool_load_now         | OFF            |
| innodb_buffer_pool_size             | 134217728      |
+-------------------------------------+----------------+

InnoDB_buffer_pool_size

用於設置 InnoDB 緩存池(InnoDB_buffer_pool) 的大小,默認值是 47MB。InnoDB 緩存池的大小對 InnoDB 整體性能影響較大,如果當前的 MySQL 服務器專門用於提供 MySQL 服務,應盡量增加 InnoDB_buffer_pool_size的大小,把頻繁訪問的數據都放到內存中來,盡可能減少 InnoDB 對硬盤的訪問,爭取將 InnoDB 最大化成為一個內存存儲引擎。

InnoDB_buffer_pool_instances

默認值是 1,表示 InnoDB 緩存池被划分到一個區域。適當地增加該參數(例如將該參數值設置為 2,此時 InnoDB 被划分成為兩個區域),可以提升 InnoDB 的並發性能。如果 InnoDB 緩存池被划分成多個區域,建議每個區域不小於 1GB 的空間。

innodb_buffer_pool_dump_pct

可以設置一次讀取的數據頁填充 Buffer Pool 的占比,默認是 25%。如果經常做全表掃描那么緩沖區很可以總是被無效數據填充,所以這時候可以將 innodb_buffer_pool_dump_pct設置的小一些,對於這種大批量掃描就不會對緩沖區做侵入性覆蓋。

查看 Buffer Pool 的運行狀態: show status like "Innodb_buffer_pool%"; :

mysql> show status like "Innodb_buffer_pool%";
+---------------------------------------+--------------------------------------------------+
| Variable_name                         | Value                                            |
+---------------------------------------+--------------------------------------------------+
| Innodb_buffer_pool_dump_status        | Dumping of buffer pool not started               |
| Innodb_buffer_pool_load_status        | Buffer pool(s) load completed at 170819  9:57:57 |
| Innodb_buffer_pool_resize_status      |                                                  |
| Innodb_buffer_pool_pages_data         | 324                                              |
| Innodb_buffer_pool_bytes_data         | 5308416                                          |
| Innodb_buffer_pool_pages_dirty        | 0                                                |
| Innodb_buffer_pool_bytes_dirty        | 0                                                |
| Innodb_buffer_pool_pages_flushed      | 39                                               |
| Innodb_buffer_pool_pages_free         | 7868                                             |
| Innodb_buffer_pool_pages_misc         | 0                                                |
| Innodb_buffer_pool_pages_total        | 8192                                             |
| Innodb_buffer_pool_read_ahead_rnd     | 0                                                |
| Innodb_buffer_pool_read_ahead         | 0                                                |
| Innodb_buffer_pool_read_ahead_evicted | 0                                                |
| Innodb_buffer_pool_read_requests      | 1620                                             |
| Innodb_buffer_pool_reads              | 290                                              |
| Innodb_buffer_pool_wait_free          | 0                                                |
| Innodb_buffer_pool_write_requests     | 515                                              |
+---------------------------------------+--------------------------------------------------+

可以看到一共有多少頁(Innodb_buffer_pool_pages_total),空閑頁數(Innodb_buffer_pool_pages_free),臟頁數(Innodb_buffer_pool_pages_dirty)等等。通過這些狀態可以調整配置來讓緩存盡可能多地命中。

Change Buffer

Change Buffer 的主要目的是將對二級索引的數據操作緩存下來,以此減少二級索引的隨機 IO,並達到操作合並的效果。

在 MySQL 5.5 之前的版本中,由於只支持緩存 insert 操作,所以最初叫做 insert buffer,只是后來的版本中支持了更多的操作類型緩存,才改叫 Change Buffer。

Change Buffer 物理上是一顆 BTree,一條 Change Buffer log 記錄大概包含如下列:

5

在 Change Buffer tree 上 唯一定為一條記錄是通過三列 (space id, page no , counter) 作為主鍵的,其中 counter 是一個遞增值,目的是為了維持不同操作的有序性,例如可以通過 counter 來保證在 merge 時執行如下序列時的循序和用戶操作順序是一致的:INSERT x, DELETE-MARK x, INSERT x。

Adaptive Hash Index

Adaptive Hash index(自適應哈希索引)的特性使得 InnoDB 在不犧牲事務特性或可靠性的前提下,為緩沖池提供適當的工作負載和足夠的內存的時候,能夠表現的更像 in-memory(內存)數據庫。

該特性是通過變量 innodb_adaptive_hash_index 來使用的,可以說 Adaptive Hash index 不 是傳統意義的索引,可以理解為在 Btree 上的 "索引"。

當對某個頁面訪問次數滿足一定條件會將頁面地址存於 Hash 表,下次查詢可以非常快速的找到頁面不需要 Btree 去查。

Log Buffer

Log Buffer (日志緩沖區)是一塊內存區域用來保存要寫入磁盤上的日子文件的數據。 Log Buffer 的大小由innodb_log_buffer_size 變量定義。默認大小為 16MB。Log Buffer 的內容會定期刷到磁盤上。

設置較大的 Log Buffer 讓較大事務能夠運行,而無需在事務提交之前將 redo log 中的數據寫入磁盤。如果你的系統中有較多大事務類型的操作,增加日志緩沖區的大小可以節省磁盤 IO。

Log Buffer 通過 innodb_flush_log_at_trx_commit 參數來控制日志刷入磁盤的頻率:

  • 0:每秒寫入日志並將其刷新到磁盤一次,未刷新日志的事務可能會在崩潰中丟失。
  • 1:是默認值,每次事務提交時寫入日志並刷新到磁盤,確保數據不會丟失,這種方式是最安全的,但同時也是最慢的。
  • 2:在每次事務提交后寫入日志,並每秒刷新一次磁盤。未刷新日志的事務可能會在崩潰中丟失。每次事務提交時 MySQL 都會把 log buffer 的數據寫入 log file,但是 flush(刷到磁盤)操作並不會同時進行。該模式下,MySQL 會每秒執行一次 flush(刷到磁盤)操作。)也就是每次事務提交,該事務都會在 log file 里面,但刷入磁盤的操作是每秒一次的,不是寫入 log file 時一起(同步)的,所以只有操作系統崩潰或斷電的情況下才會丟失上一秒的事務。

上面 InnoDB 整體架構圖中可以看到 Redo log buffer 是 InnoDB 內存區域的一部分。

Redo Log

InnoDB 使用 Redo Log 來保證數據的一致性和可持久性,它采用 WAL 機制,即先寫日志再寫數據。具體來說,InnoDB 進行寫操作時,先將數據操作記錄在 log buffer 中,然后將 log buffer 中的數據刷到磁盤 log file 中,后續數據再落到數據 ibd 文件這一步驟由 checkpoint 來保證。

redo log 包括兩部分:一是內存中的日志緩沖(redo log buffer),該部分日志是易失性的;二是磁盤上的重做日志文件(redo log file),該部分日志是持久的。將 redo log buffer 寫入 redo log file 遵循 innodb_flush_log_at_trx_commit 參數的設定。

redo log 相關的參數設定中有一個參數:innodb_log_files_in_group,表明一個日志組中有多少個日志文件,雖然 MySQL 5.6 開始已經放棄了日志組的概念,但參數名依舊保留了下來以兼容以前的配置。該參數的含義為有多少個 log 文件(最少為 2 個)。所以 redo log 總日志文件個數是有限的,redo log 采用順序寫的方式,在全部文件寫滿之后則會回到第一個文件的起始位置重新寫入。

redo log 以塊為單位進行存儲的,每個塊占 512 字節,稱為 redo log block。所以不管是 log buffer 中還是 os buffer 中以及 redo log file on disk 中,都是這樣以 512 字節的塊存儲的。

每個 redo log block 由 3 部分組成:日志塊頭、日志塊尾和日志主體。其中日志塊頭占用 12 字節,日志塊尾占用 8 字節,所以每個 redo log block 的日志主體部分只有 512-12-8=492 字節。

redo log block 數據格式

log block 中 492 字節的部分是 log body,它由 4 個部分構成:

  • redo_log_type:占用 1 個字節,表示 redo log 的日志類型
  • space:表示表空間的 ID,采用壓縮的方式后,占用的空間可能小於 4 字節
  • page_no:表示頁的偏移量,同樣是壓縮過的
  • Ÿredo_log_body 表示每個重做日志的數據部分,恢復時會調用相應的函數進行解析。例如 insert 語句和 delete 語句寫入 redo log 的內容是不一樣的。

checkpoint 機制

在 InnoDB 中 checkpoint 分為兩種情況:

  • sharp checkpoint:在重用 redo log 文件(例如切換日志文件)的時候,將所有已記錄到 redo log 中對應的臟數據刷到磁盤。
  • fuzzy checkpoint:一次只刷一小部分的日志到磁盤,而非將所有臟日志刷盤。有以下幾種情況會觸發該檢查點:
    • master thread checkpoint:由 master 線程控制,每秒或每 10 秒 刷入一定比例的臟頁到磁盤。
    • flush_lru_list checkpoint:從 MySQL5.6 開始可通過 innodb_page_cleaners 變量指定專門負責臟頁刷盤的 page cleaner 線程的個數,該線程的目的是為了保證 LRU 列表有可用的空閑頁。
    • async/sync flush checkpoint:同步刷盤還是異步刷盤。例如還有非常多的臟頁沒刷到磁盤(非常多是多少,有比例控制),這時候會選擇同步刷到磁盤,但這很少出現;如果臟頁不是很多,可以選擇異步刷到磁盤,如果臟頁很少,可以暫時不刷臟頁到磁盤。
    • dirty page too much checkpoint:臟頁太多時強制觸發檢查點,目的是為了保證緩存有足夠的空閑空間。too much 的比例由變量 innodb_max_dirty_pages_pct 控制,MySQL 5.6 默認的值為 75,即當臟頁占緩沖池的 75% 后,就強制刷一部分臟頁到磁盤。

由於刷臟頁需要一定的時間來完成,所以記錄 checkpoint 的位置是在每次刷盤結束之后才在 redo log 中標記的。

redo log 總的寫入量叫 LSN(Log Secquence Numer)日志序列號,這個 redo log 變更實際寫入到實際數據文件中的數量叫 checkpoint LSN,表示的是有多少變更已經實際寫入到了相應的數據文件中。 一旦數據庫崩潰 InnoDB 開始恢復數據的時候,先讀取 checkpoint,然后從 checkpoint 所指示的 LSN 讀取其之后的 Redo log 進行數據恢復,從而減少 Crash Recovery 的時間。

LSN

根據 LSN,可以獲取到幾個有用的信息:

  1. 數據頁的版本信息
  2. 寫入的日志總量,通過 LSN 開始號碼和結束號碼可以計算出寫入的日志量
  3. 可知道檢查點的位置

LSN 不僅存在於 redo log 中,還存在於數據頁。在每個數據頁的頭部,有一個 fil_page_lsn 記錄了當前頁最終的 LSN 值是多少。通過數據頁中的 LSN 值和 redo log 中的 LSN 值比較,如果頁中的 LSN 值小於 redo log 中 LSN 值,則表示數據丟失了一部分。這時候可以通過 redo log 的記錄來恢復到 redo log 中記錄的 LSN 值時的狀態。

查看 redo log 中的 LSN 值:


mysql> show engine innodb status;

 =====================================
 2020-08-16 15:33:25 0x30f4 INNODB MONITOR OUTPUT
 =====================================
 Per second averages calculated from the last 20 seconds
 -----------------
 BACKGROUND THREAD
 -----------------
 srv_master_thread loops: 2 srv_active, 0 srv_shutdown, 229135 srv_idle
 srv_master_thread log flush and writes: 229137
 ----------
 SEMAPHORES
 ----------
 OS WAIT ARRAY INFO: reservation count 4
 OS WAIT ARRAY INFO: signal count 4
 RW-shared spins 0, rounds 4, OS waits 2
 RW-excl spins 0, rounds 0, OS waits 0
 RW-sx spins 0, rounds 0, OS waits 0
 Spin rounds per wait: 4.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
 ------------
 TRANSACTIONS
 ------------
 Trx id counter 25350
 Purge done for trx's n:o < 0 undo n:o < 0 state: running but idle
 ......
 ......
 ......
 ---
 LOG
 ---
 Log sequence number 2648197
 Log flushed up to   2648197
 Pages flushed up to 2648197
 Last checkpoint at  2648188
 0 pending log flushes, 0 pending chkp writes
 10 log i/o's done, 0.00 log i/o's/second
 ----------------------
 BUFFER POOL AND MEMORY
 ----------------------
 Total large memory allocated 8585216
 Dictionary memory allocated 124624
 Buffer pool size   512
 Free buffers       256
 Database pages     256
 Old database pages 0
 Modified db pages  0
 Pending reads      0
 Pending writes: LRU 0, flush list 0, single page 0
 Pages made young 0, not young 0
 0.00 youngs/s, 0.00 non-youngs/s
 Pages read 209, created 49, written 53
 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
 No buffer pool page gets since the last printout
 Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
 LRU len: 256, unzip_LRU len: 0
 I/O sum[17]:cur[0], unzip sum[0]:cur[0]
 --------------
 ROW OPERATIONS
 --------------
 0 queries inside InnoDB, 0 queries in queue
 0 read views open inside InnoDB
 Process ID=3952, Main thread ID=5596, state: sleeping
 Number of rows inserted 65, updated 0, deleted 0, read 73
 0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
 ----------------------------
 END OF INNODB MONITOR OUTPUT
 ============================

這里面有幾個字段:

log sequence number:就是當前的 redo log(in buffer) 中的 LSN

log flushed up to:是刷到 redo log file on disk 中的 LSN

pages flushed up to :是已經刷到磁盤數據頁上的 LSN

last checkpoint at :是上一次檢查點所在位置的 LSN

可以看到上面示例中因為是本地測試數據庫,所以落盤的 LSN 和 當前檢查的 LSN 已經齊平。

磁盤架構

磁盤空間從大類上划分比較簡單:表空間 和 日志空間。

  • 表空間:分為系統表空間(MySQL 目錄的 ibdata1 文件)、臨時表空間、常規表空間、Undo 表空間以及 獨立表空間(file-per-table 表空間,MySQL5.7 默認打開 file_per_table 配置)。

    系統表空間又包括 InnoDB 數據字典、雙寫緩沖區(Doublewrite Buffer)、修改緩存(Change Buffer)、Undo 日志等。

  • Redo 日志:存儲的就是 Log Buffer 刷到磁盤的數據。

表空間

表空間涉及的文件

相關文件默認在磁盤中的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 數據字典中的相應條目。

ibd 文件

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

在通用表空間中創建的表在現有的常規表空間 .ibd 文件中創建。常規表空間文件可以在MySQL 數據目錄內部或外部創建

ibdata 文件

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

表空間對應的存儲結構

在 InnoDB 存儲引擎中,每張表都有一個主鍵(Primary Key)。從 InnoDB 存儲引擎的邏輯存儲結構看,所有數據都根據主鍵順序被邏輯地存放在一個空間中,稱之為表空間(Tablespace)。從外部來看,一張表是由連續的固定大小的 Page 構成,其實表空間文件內部被組織為更復雜的邏輯結構,自頂向下可分為段(Segment)、區(Extent)、頁(Page)、Row(行),InnoDB 存儲引擎的文件邏輯存儲結構大致如下圖所示:

6

Segment 與數據庫中的索引相映射。InnoDB 引擎內,數據段即為 B+ Tree 的葉子節點,索引段即為 B+ Tree 的非葉子節點,創建索引中很關鍵的步驟便是分配 Segment。

Segment 的下一級是 Extent,Extent 代表一組連續的 Page,默認大小均為 1MB。Extent 的作用是提高 Page 分配效率,在數據連續性方面也更佳,Segment 擴容時也是以 Extent 為單位分配。

Page 則是表空間數據存儲的基本單位,InnoDB 將表文件按 Page 切分,依類型不同,Page 內容也有所區別,最為常見的是存儲行記錄的數據頁。

Row 行 對應着表里的一條數據記錄。

在默認情況下,InnoDB 存儲引擎 Page 的大小為 16KB,即一個 Extent 中一共有 64 個連續的 Page。在創建 MySQL 實例時,可以通過指定innodb_page_size選項對 Page 的大小進行更改,需要注意的是 Page 的大小可能會影響 Extent 的大小:

page size page nums extent size
4KB 256 1MB
8KB 128 1MB
16KB 64 1MB
32KB 64 2MB
64KB 64 4MB

從上表可以看出,一個 Extent 最小也有 1 MB,且最少擁有 64 個頁。

行記錄格式

InnoDB 存儲引擎和大多數數據庫一樣,記錄是以行的形式存儲的,每個 16KB 大小的頁中可以存放 2~200 條行記錄。InnoDB 早期的文件格式為Antelope,可以定義兩種行記錄格式,分別是CompactRedundant,InnoDB 1.0.x 版本開始引入了新的文件格式BarracudaBarracuda文件格式下擁有兩種新的行記錄格式:CompressedDynamic

Compact行記錄格式是在 MySQL 5.0 中引入的,其首部是一個非 NULL 變長列長度列表,並且是逆序放置的,其長度為:

  • 若列的長度小於等於 255 字節,用 1 個字節表示;
  • 若列的長度大於 255 字節,用 2 個字節表示。

變長字段的長度最大不可以超過 2 字節,這是因為 MySQL 數據庫中 VARCHAR 類型的最大長度限制為 65535。變長字段之后的第二個部分是 NULL 標志位,該標志位指示了該行數據中某列是否為 NULL 值,有則用 1 表示,NULL 標志位也是不定長的。接下來是記錄頭部信息,固定占用 5 字節。

Redundant是 MySQL 5.0 版本之前 InnoDB 的行記錄格式,Redundant行記錄格式的首部是每一列長度偏移列表,同樣是逆序存放的。從整體上看,Compact格式的存儲空間減少了約 20%,但代價是某些操作會增加 CPU 的使用。

行溢出數據

數據頁默認的大小是 16KB(6384 字節),而定義的 VARCHAR 行長度大小 65535 字節,這里會存在一個也放不下的情況,於是數據會被放到大對象頁中(Uncompressed BLOB Page),原數據中保留 768 字節 + 偏移量;

VARCHAR 最大長度問題: 定義 VARCHAR 所在行可以存放 6384 字節,然而實際有行頭數據開銷,最大值為 65532 字節。 需要注意的是這里不是單個列的長度。

數據是否溢出使用大對象頁存儲: 由於數據存儲使用的是 B+Tree 的結構,一個頁中至少要有兩個節點,並且頁大小為 16KB。 所以,這個閾值是 8098 字節,小於此值當行數據會存儲在本身頁中,大於這個值則會使用 BLOB 頁進行存儲,(這時原行數據只存儲前 768 字節數據 + BLOB 頁的偏移量)

關於 CHAR 的行存儲結構

在變長字符集的(例如:UTF8)的情況下,InnoDB 對 CHAR 的存儲也是看做變長字符類型的,與 VARCHAR 沒有區別。

關於 Redo Log 日志准備再開一篇說明,本文先到這里。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM