掘金 后端 ( ) • 2024-04-26 16:44

3. MySQL的存储引擎之InnoDB

3.1 存储引擎种类

存储引擎 说明 MyISAM 高速引擎,拥有较高的插入、查询速度,但不支持事务 InnoDB 5.5版本之后Mysql默认数据库引擎,支持事务和行级锁,比MyISAM处理速度稍慢 ISAM MyISAM的前身,Mysql5.0之后不再默认安装 MRG_MyISAM 将多个表联合成一个表使用,在超大规模数据存储时很有用 Memory 内存存储引擎,拥有极高的插入,更新和查询效率。 但是会比较占用和数据量成正比的内存空间。只能在内存上保存数据,意味着数据可能会丢失 Archive 将数据压缩后进行存储,非常适合存储大量的独立的,作为历史记录的数据,但是只能进行插入和查询操作 CSV CSV 存储引擎是基于 CSV 格式文件存储数据(应用于跨平台的数据交换)

引擎怎么选择? 归纳一句话:除非需要用到某些InnoDB不具备的特性,并且没有其他办法可以替代,否则都应该选择InnoDB。

InnoDB和MyISAM存储引擎比较

比较项 InnoDb MyISAM 存储文件 .frm表定义文件 .ibd数据文件和索引文件 .frm表定义文件 .myd数据文件 .myi索引文件 锁 表锁、行锁 表锁 | 事务 支持 不支持 | CRUD 读、写 读多 | 索引结构 B+树 B+树 |

存储引擎查看与设置

在mysql中可以使用不同的存储引擎(表为维度)

# 查看支持的存储引擎
show engines;

image.png

只有InnoDB支持事务、行锁、外键,在选择引擎时尽可能使用InnoDb

  • MyISAM: 早期版本默认的引擎
  • Memory:所有的数据都存储在内存中
# 使用其他引擎,在mysql中默认使用InnoDb,一个数据库中不同的表可以使用不同的引擎
create table t_myisam(a int primary key, b int) engine = myisam;

3.2 InnoDb架构图

image.png

InnoDB由内存结构和磁盘结构两部分构成;

3.2.1 内存结构

InnoDb内存结构主要分为四个区域: 1、Buffer Pool 缓冲池 2、Change Pool 修改缓冲 3、Adaptive Hash Index 自适应索引 4、Log Buffer 日志缓冲

3.2.1.1 缓冲池(Buffer Pool)

缓冲池Buffer Pool用于加速数据的访问和修改,通过将热点数据存在内存的方式,最大限量地减少磁盘IO,加速热点数据读写。

  • 默认大小为128M, Buffer Pool中数据以页为存储单位,其实现的数据结构是以页为单位的单链表。
  • 由于内存的限制,Buffer Pool仅能容纳最热点的数据
  • Buffer Pool使用LRU(Last Recently Used 最近最少使用)淘汰非热点数据页
    • LRU:根据页数据的历史访问来淘汰数据,如果数据最近被访问过,那么将来被访问的几率也更高,优先淘汰最近没有被访问到的数据。
  • 对于Buffer Pool中数据的查询, InnoDb直接读取返回。对于Buffer Pool中的数据修改,InnoDb直接在Buffer Pool中修改,并将修改写入redo log。

image.png

image.png

# 查看innoDb存储引擎状态,包含缓冲池、修改缓冲、自适应哈希状态信息、日志缓冲等
mysql> show engine innodb status;
# 查看InnoDb的Buffer Pool大小
mysql> show variables like 'innodb_buffer_pool_size';
3.2.1.2 修改缓冲(change buffer)

Change Buffer是Mysql5.5引入的一种优化策略。Change Buffer用于加速非热点数据中二级索引的写入操作。 由于二级索引数据的不连续性,导致修改二级索引时需要进行频繁的磁盘IO消耗大量性能,change buffer缓冲对二级索引的修改操作,同时将写操作录入redo log中,在缓冲到一定量或系统空闲的时候进行merge操作将修改写入磁盘。change buffer在系统表空间有相应的持久化区域。

Change Buffer大小默认占Buffer pool的25%,最大占50%,在引擎启动时便初始化完成。其物理结构为一颗名为ibuf的B Tree

二级索引就是辅助索引,除了聚簇索引之外的所有索引都是二级索引。 聚簇索引也叫聚集索引,索引组织表,指的是一种数据存储方式,指数据与索引的数据结构存储在一起。如InnoDb的主键索引中所有叶子结点都存储了对应行的数据。因为数据肯定只是存储在一个地方,所以一个表只有一个聚簇索引。

image.png

3.2.1.3 自适应哈希索引(AHI)

自适应哈希索引,用于实现与对热点数据页的一次查询。是建立在索引之上的索引。使用聚簇索引进行数据页定位的时候需要根据索引树的高度从根节点走到叶子结点,通常需要3-4次查询才能定位到数据。InnoDb根据对索引使用情况的分析和索引字段的分析,通过自调优Self-tuning的方式为索引页建立或删除哈希索引。

AHI的大小是Buffer Pool的1/64,在Mysql5.7之后支持分区,以减少对于全局AHI锁的竞争,默认分区数位8.

AHI所作用的目标是 频繁查询的数据页和索引页, 而由于数据页是聚簇索引的一部分,因此AHI是建立在索引之上的索引,对于二级索引,若命中AHI,则将直接从AHI获取二级索引页的记录指针,再根据主键沿着聚簇索引查找数据;如聚簇索引同样命中AHI,则直接返回目标数据页的记录指针,此时就可以根据记录指针直接定位数据页。

image.png

# 查看innoDB存储引擎状态,包含自适应哈希状态信息
mysql> show engine innodb status;
# 查看是否开启自适应哈希配置,默认是开启的
mysql> show variables like 'innodb_adaptive_hash_index';
3.2.1.4 日志缓冲(Log Buffer)

InnoDb使用Log Buffer来缓冲日志文件的写入操作。 内存写入加上日志文件顺序写的特点,使得InnoDb日志写入性能极高。

对于任何修改操作,都能录入诸如redo log与 undo log这样的日志文件中,因此日志文件的写入操作十分频繁,却又十分零散。这些文件都存储在磁盘中,因此日志记录将引发大量的磁盘IO。Log buffer将分散的写入操作放在内存中,通过定期批量写入磁盘的方式提高日志的写入效率和减少磁盘IO。

image.png

注意: 这种将分散操作改为批量操作的优化方式将增加数据丢失的风险!

3.2.2 磁盘结构

3.2.2.1 磁盘文件之表空间

在磁盘中,InnoDB将所有数据都逻辑的存放在一个空间中,称为表空间(Tablespace)。表空间由段(Segment)、区(extent)、页(Page)构成。

  • 开始独立表空间innodb_file_per_table = 1, 每张表的数据都会存储到一个独立表空间,即表名.ibd文件
  • 关闭独立表空间innodb_file_per_table = 0, 则所有基于InnoDb存储引擎的表数据都会记录到系统表空间,即ibdata1文件

表空间是InnoDb物理存储中的最高层,目前的表空间类别包含:

  • 系统表空间(System Tablespace)
  • 独立表空间(File-per-table Tablespace)
  • 通用表空间(General Tablespace)
  • 回滚表空间(Undo Tablespace)
  • 临时表空间(The Temporary Tablespace)

image.png

3.2.2.1.1 系统表空间

系统表空间是InnoDb数据字典、双写缓冲、修改缓冲、回滚日志的存储位置。 如果关闭独立表空间,他将存储所有表的数据和索引。

他默认下是一个初始大小12M,名为ibdata1的文件,系统表空间所对应的文件由innodb_data_file_path定义

指定系统表空间文件自动增长后,其增长大小由innobd_autoextend_increment设置(默认为64M)且不可缩减,即使删除系统表空间中存储的表和索引,此过程释放的空间仅仅是表空间文件中标志为已释放而已,并不会缩减其在磁盘中的大小。

  • 数据字典(Data Dictionary):数据字典是由各种表对象的元数据信息(表结构、索引、列信息等组成的内部表)
  • 双写缓冲(Doublewrite Buffer):用于保证写入磁盘时页数据的完整性,防止发生部分写失效问题。非常重要!!!
  • 修改缓冲(change buffer):内存中change buffer对应的持久化区域
  • 回滚日志(Undo log):实现事务进行回滚操作时对数据的恢复。是实现多版本并发控制(MVCC)重要组成
3.2.2.1.2 独立表空间

独立表空间用于存放每个表的数据和索引。 其他类型的信息:如:回滚日志、双写缓冲区、系统事务信息、修改缓冲等仍存放在系统表空间。因此即使使用了独立表空间,系统表空间也会不断地增长。在5.7版本中默认开启。

开启独立表空间(File-per-table Tablespace)(innodb_file_per_table = ON)之后,innoDB会为每个数据库单独创建子文件夹,数据库文件夹内为每个数据表单独创建一个表空间文件table.ibd。同时创建一个table.frm文件用于保存表结构信息。

每个独立表空间大小为96KB。

3.2.2.1.3 其他

1) 通用表空间 通用表空间是一个由create tbalespace命令创建的共享表空间,创建时必须指定该表空间名称和ibd文件位置,ibd文件可以放置于任何Mysql有权限的地方。该表空间可以容纳多张数据表,同时在创建时可以指定该表空间默认的存储引擎。

通用表空间存在的目的是为了在系统表空间与独立表空间之间走出平衡。 系统表空间与独立表空间可以向通用表空间移动,反之亦可,但系统表空间中的表无法直接与独立表空间的表相互转化。

image.png

2)Undo表空间 用来存放一个或多个undo log文件。默认undo log存储在系统表空间中,mysql5.7中支持自定义undo log表空间并存储所有undo log。一旦用户定义了undo tablespace,则系统表空间中的undo log区域将失效。对于Undo Tablespace的启用必须在Mysql初始化前设置,默认大小为10M。Undo Tablespace中的Undo log表可以进行truncate操作。

3)临时表空间 5.7版本之后 InnoDB 引擎从系统表空间中抽离出临时表空间(Temporary Tablespace),用于独立保存临时表数据及其回滚信息。

该表空间文件路径由innodb_temp_data_file_path指定,但必须继承innodb_data-home_dir

3.3 磁盘文件之存储结构

image.png

3.3.1 段(Segment)

表空间由各个段(Segment)构成,创建的段类型分为数据段、索引段、回滚段等。 由于InnoDB采用聚簇索引与B+树的结构存储数据,所以事实上数据页和二级索引页仅仅只是B+树的叶子结点,因此数据段称为Leaf node segment,索引段其实指的是B+树的非叶子结点,称为Non-Leaf node segment。 一个段会包含多个区,至少会有一个区,段拓展的最小单位是区

  • 数据段称为 Leaf node segment
  • 索引段称为 Non-Leaf node segment

3.3.2 区(extend)

区是由连续的页组成的空间,大小固定为1MB,由于默认页大小为16K,因此一个区默认存储64个连续的页。如果页大小调整为4K,则256个连续页组成一个区。为了保证页的连续性,InnoDb存储引擎会一次从磁盘申请4-5个区。

3.3.3 页(Page)

页(Page)是InnoDB的基本存储单位,每个页大小默认为16K,从 InnoDB1.2.x 版本开始,可通过 设置 innodb_page_size 修改为 4K、8K、16K。InnoDB 首次加载后便无法更改。

# 查看Mysql页大小
show variables like 'innodb_page_size';

Mysql规定一个页上最少存储2个数据项。如果向一个页插入数据时,这个页已经满了,就会从区中分配一个新页。如果向索引树叶子结点中间的一个页中插入数据,如果这个页是满的,就会发生页分裂。

操作系统读写磁盘最小单位也是页,当然,此页非彼页。Linux的页一般是4K,通过命令查看:

# 默认 4096 4K
getconf PAGE_SIZE

由此可知,InnoDB从磁盘中读取一个数据页,操作系统会分为4次从磁盘文件中读取数据到内存。写入也是一样的,需要分4次从内存写入磁盘。

image.png

3.3.4 行(row)

InnoDB的数据是以行为单位存储的,一个页中包含多个行。在Mysql5.7中,InnoDB提供了4中行格式:Compact、Redundant、Dynamic、Compressed格式。其中Dynamic为Mysql5.7默认的行格式。

创建表时可以指定行格式

CREATE TABLE t1(c1 INT) ROW_FORMAT=DYNAMIC;
# 修改行格式
ALTER TABLE tablename ROW_FORMAT=行格式名称;
# 修改默认行格式
SET GLOBAL innodb_default_row_format = DYNAMIC;
# 查看表行格式
show table status like 't1';

3.4 内存数据落盘

3.4.1 整体思路分析

image.png

在数据库进行读操作:将从磁盘中读到的页放在缓冲区中,下次再读相同的页时,首先判断该页是否在缓冲区中。若在缓冲区中,称该页在缓冲区中被命中,直接去读该页。否则读取磁盘上的页。

对于数据库中页的修改操作:首先修改在缓冲区中的页,然后再以一定的频率刷新到磁盘上。页从缓冲区刷新回磁盘的操作并不是在每次页发生更新时都触发,而是通过一种称为checkPoint机制刷新回磁盘。

内存数据落盘要考虑的核心问题:高性能写入数据,同时保证数据的绝对安全性! 1、 写入性能怎么保证?

  • 分散写入操作放在内存中,通过定期批量写入磁盘的方式提高写入效率,减少磁盘IO。

2、 如何持久化? 也就是修改后的数据如何到磁盘中去。内存里缓冲池中的数据页要完成持久化通过两个流程来完成。

  • 通过checkPoint机制进行脏页落盘
  • 日志先行,所有操作前先写redo日志

3、 数据安全性怎么保证?

  • 记录操作日志:Force Log at Commit机制 与 Write Ahead Log(WAL)策略
  • checkPoint机制
  • Double Write机制

3.4.2 脏页落盘

image.png

什么是脏页? 对于数据库中页的修改操作,首先修改在缓冲区的页,缓冲区的页与磁盘内的页数据不一致,所以缓冲区中的页称为脏页。然后再以一定的频率将脏页的数据刷新到磁盘上。页从缓冲区刷新回磁盘的操作并不是每次页发生更新的时候触发,而是通过一种称为checkPoint的机制刷新回磁盘。

为什么不是每次更新直接写入磁盘?

  • 如果每次一个页发生变化就进行落盘,每次落盘一个页,必然伴随着4次IO操作,那么性能开销会非常大。而且这个开销是随着写入操作的增加指数型增长的!
  • 如果数据长期在内存中保存,那么数据就存在安全性风险!
  • InnoDB采用了Write Ahead Log(WAL)策略和Force Log At Commit机制实现事务级别下数据的持久性。
    • Force Log At Commit机制:当事务提交时,所有事务产生的日志都必须刷新到磁盘。 如果日志刷新成功后,缓冲池的数据刷新到磁盘前数据库发生了宕机,那么重启时,数据库可以从日志中恢复数据。这样可以保证数据的安全性。
    • Write Ahead Log(WAL)策略:要求数据的变更写入到磁盘前,首先必须将内存中的日志写入到磁盘。InnoDB的WAL技术的产物就是redo log,对于写操作,永远是日志先行,先写入redo log保证一致性后,在对修改数据进行落盘。
    • 说白了就是保证数据的持久性和安全性,我们采用记录日志的方式,那么也就是说,日志安全了,数据就安全了!

image.png

怎么确保日志就能安全的写入系统?

  • 为了确保每次日志都写入到redo日志文件,在每次将redo日志缓冲写入到redo日志后,调用一次fsync操作,将缓冲文件从文件系统缓存中真正写入磁盘。

这样做不就是等同于数据直接写入磁盘吗?

  • redo日志不会记录完整的一页数据,因为这样日志太大,他只会记录那次(sequence)如何操作了(insert,update)哪页(page)哪行(row)
  • 日志是顺序写入的,而数据是随机写入。顺序写入效率更高,省去了寻址的消耗
  • 日志也不是改一条写一条,而是采用redo日志落盘策略来兼顾安全性与性能
  • 可以通过innodb_flush_log_at_trx_commit来控制redo日志刷新到磁盘的策略

3.4.3 Redo日志落盘

image.png

Log Buffer写入磁盘的时机由参数innodb_flush_log_at_trx_commit控制,此属性控制每次事务提交时innoDb的行为。是InnoDb性能调优的一个基础参数,涉及InnoDb的写入性能和数据安全性。默认1,表示事务提交后立即落盘。

# 查看写入时机参数配置
show variables like 'innodb_flush_log_at_trx_commit';

innodb_flush_log_at_trx_commit配置详解

  • 为0时:事务提交时,不会立即把log buffer里的数据写入到redo log日志文件的。而是等待主线程每秒写入一次。
    • 如果Mysql崩溃或者服务器宕机,此时内存中的数据会全部丢失,最多丢失1秒的事务。
    • 写入效率最高,但是数据安全最低
  • 为1时:每次事务提交时,会将数据从log buffer写入redo日志文件与文件系统缓存,并同时fsync刷新到磁盘中。
    • 系统配置默认为1,Mysql崩溃已经提交的事务不会丢失,要完全符合ACID必须使用默认设置1.
    • 写入效率最低,但是数据安全最高
  • 为2时:事务提交时,也会将数据写入redo日志文件与文件系统缓存,但是不会调用fsync,而是让操作系统自己去判断何时将缓存写入磁盘。
    • 事务提交都会将数据刷新到操作系统缓冲区,可以认为是已经持久化到磁盘,但是没有真正意义上持久化到磁盘
    • 如果Mysql崩溃已经提交的事务不会丢失。但是如果服务器宕机或者意外断电,操作系统缓存内的数据会丢失,所以最多丢失1秒的事务

设置为1时,安全性最好但是写入效率比较低。如果没有设置为1,是无法满足ACID的D(持久性),由于Mysql执行刷新操作fsync()是阻塞的,所以磁盘刷新速度比较慢,如果开启了1,Mysql性能会下降。

0是性能最好的模式,但是会存在丢失数据的风险。

2是介于0和1之间的一个选择,数据的安全性会依赖于操作系统是否稳定。在配置这一项的时候需要慎重考虑。

3.4.4 CheckPoint检查点机制

1. 什么是checkPoint? CheckPoint要做的事情是将缓冲池中的脏页数据刷到磁盘上。 CheckPoint决定了脏页落盘的时机、条件及脏页的选择,不同的CheckPoint做法并不相同。

2. CheckPoint要解决什么问题?

  1. 脏页落盘:避免数据更改直接操作磁盘
  2. 缩短数据库的恢复时间:当数据库发生宕机时,数据库不需要重做所有日志,因为CheckPoint之前的页都已经刷新回磁盘。数据库只需对checkPoint之后的redo日志进行恢复,这样就大大缩短了恢复的时间。
  3. 缓冲池不够用时,将脏页刷新到磁盘:当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行CheckPoint,将脏页也就是页的新版本刷回磁盘。
  4. redo日志不可用时,刷新脏页:当redo日志出现不可用时,CheckPoint将缓冲池中的页刷新到当前redo日志的位置

可以通过命令 SHOW ENGINE INNODB STATUS 来观察: show engine innodb status;

image.png

3. CheckPoint分类? 在InnoDB存储引擎内部,有2种CheckPoint, 分别为Sharp CheckPoint, Fuzzy CheckPoint;

  • sharp CheckPoint: 在关闭数据库时,将buffer pool中的脏页全部刷新到磁盘中
  • fuzzy CheckPoint:数据库正常运行时,在不同的时机,将部门脏页写入磁盘。仅刷新部分脏页到磁盘,也是为了避免一次刷新全部的脏页造成的性能问题

Fuzzy CheckPoint:默认方式,只刷新一部分脏页,不是刷新全部脏页 主要有以下几种情况:

  • Master Thread CheckPoint:在Master Thread中,以每秒或者每10秒一次的频率,将部分脏页从内存中刷新到磁盘,这个过程是异步的。正常用户线程对数据的操作不会阻塞。
  • FLUSH_LRU_LIST CheckPoint: 缓冲池不够用时,根据LRU算法会淘汰最近最少使用的页,如果这个页是脏页的话,会强制执行CheckPoint,将该脏页刷回磁盘(由Page Cleaner Thread完成)
  • Async/Sync Flush CheckPoint:重做日志不可用的情况,需要强制从脏页列表中选取一些脏页刷盘(由Page Cleaner Thread完成)。由于磁盘是一种比较慢的存储设备,内存与磁盘的交互是一个相对慢的过程。innodb_log_file_size定义的是一个相对较大的值,正常情况下,由于前面2种CheckPoint刷新脏页到磁盘,在前面2种CheckPoint刷新脏页到磁盘之后,脏页对应的redo log空间随即释放,一般不会发生Async/Sync Flush CheckPoint。
  • Dirty Page too Mush:脏页数据量太多,导致强制进行CheckPoint。由参数innodb_max_dirry_pages_pct控制,默认75(75%)。当脏页数量占据75%缓冲池时,刷新一部分脏页到磁盘(由Page cleaner Thread完成)

show variables like 'innodb_max_dirty_pages_pct';

image.png

3.4.5 Double Write双写

1. 脏页落盘出现的问题:写失效 我们知道脏页会在某些场景下进行刷盘,将缓冲池内的脏页数据落地到磁盘。因为存储引擎缓冲池内的数据页大小默认为16K,而文件系统一页大小为4KB,所以在进行刷盘操作的时候,就可能发生如下的场景:

image.png

如图所示,数据库准备刷新脏页时,将16K的数据刷入磁盘,但写入8K时,就宕机了,这种情况被称为写失效(partial page write)

2. 怎么解决? 上备胎

Doublewrite其实就是写2次,解决写失效的问题。简单说就是在redo日志前,需要对写入的页做个副本,当写失效发生时,通过页的副本还原本页再重做,这就是所谓的double weite。写失效后redo日志也是无法进行恢复的,因为redo日志记录的是对页的物理修改。

image.png

Double Write分两个部分:

  • 内存中的Doublewrite buffer,大小为2M
  • 磁盘上的Doublewrite buffer,大小为2M,连续的128个页,相当于两个extent

Double write脏页刷新流程

  1. 首先复制:脏页刷新时不直接写磁盘,而是先将脏页复制到内存的doublewrite buffer
  2. 再顺序写:内存的doublewrite buffer分2次,每次1MB顺序地写入共享表空间的物理磁盘上,会立即调用fsync函数同步OS缓存到磁盘中,顺序写性能好
  3. 最后离散写:内存的doublewrite buffer最后将页写入各自表空间文件中,离散写较顺序写差一点

3. Double Write崩溃恢复

image.png

如果脏页数据未来得及落盘,系统就崩溃了,直接应用redo日志重新执行脏页落盘。

如果操作系统在将页写入磁盘的过程中发生了崩溃,其恢复过程如下:

  1. 首先innoDb存储引擎从系统表空间中的double write中找到该页的一个副本
  2. 然后将其复制到独立表空间
  3. 最后清除redo日志,完成数据恢复