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 日志准备再开一篇说明,本文先到这里。

posted @ 2020-08-17 23:28  rickiyang  阅读(1159)  评论(1编辑  收藏  举报