目录
- 存储引擎概述
- 各个存储引擎介绍
- InnoDB
- MySIAM
- Memeory
- 其他引擎
- 引擎有关的SQL语句
- InnoDB引擎
- 逻辑存储结构
- 架构
- 内存部分
- 磁盘部分
- 后台线程
- InnoDB三大特性
存储引擎概述
数据引擎是与数据真正存储的磁盘文件打交道的,它的上层(服务层)将处理好的SQL指令(SQL处理分析结果)交给数据引擎,数据引擎按照指令或将数据存储在磁盘文件,或将磁盘文件数据进行读取。所以说数据引擎是整个DBMS的核心,它有着最本质的功能:读取数据,存储数据。
存储引擎也可以看做是表的类型(存储引擎原名表处理器
),因为不同的表可以选择不同的存储引擎,不同的存储引擎数据处理方式、表的结构,文件形式等也不同
MySQL的数据存储引擎采用的是可插拔式的,即可以任意切换。数据存储引擎作用单位是表,每张表可以选择一个合适的存储引擎。
MySQL5.5版本后默认的存储引擎是InnoDB引擎(之前是MyISAM),它相较与其他数据引擎有非常多的优势(具体优势后面会说),所以除极特殊的场景外,都常建议每张表都使用默认的存储引擎InnoDB
SQL语句的大致执行流程
当我们想要操作数据库时,首先要对数据库服务进行一个连接,使用图形化客户端也好,还是命令行也好,总之要想能够连接都需要连接层提供连接,认证,授权等服务。
连接好后,我们能够输入SQL进行操作,这时服务层会对我们输入的SQL语句进行语义、语法的检查,创建语法解析树,并进行自动的优化(5.7版本之前还要查询缓存,如果缓存中有,直接返回缓存结果。因为命中率过低,8版本后已经废弃)。然后将处理数据交引擎层
引擎层就负责按照对上一层的结果对数据进行存储或抽取等
存储层是真正对数据,日志进行存储的,与引擎层来管理它们
各个存储引擎介绍
可以通过命令行 show engines
查看MySQL支持的所有命令行
如图,可以看出MySQL共支持8种类型的数据存储引擎,默认使用的是InnoDB
引擎,它的最大特点是支持事务以及分布式事务,支持行级锁,支持外键
每个引擎都有各自的特点,因为是开源的,所以大的公司可根据具体业务对引擎进行定制
在数据库中最重要的就是 InnoDB引擎和MyISAM引擎,他俩各有各自的优劣势,不是替代关系,一个现版本使用的引擎,一个是老版本使用的引擎
InnoDB
MySQL3.2版本后出现此引擎,MySQL5.5版本后作为默认存储引擎
InnoDB的优点
- InnoDB引擎支持外键,但外键会带来性能问题
- InnoDB引擎支持事务(十分重要),引入了提交和回滚两个操作。对于重要的数据,有了事务能够确保数据的ACID(原子性、一致性、隔离性、持久性)。在崩溃后,能够自行恢复,对没有提交的数据进行回滚。
- InnoDB支持行级锁。在多线程并发时,为了解决线程安全问题,要进行上锁。其他引擎通常是表级锁,二InnoDB可以在具体操作的行进行上锁,这样就不影响整张表其他数据的并发操作,提高并发效率。因此,在数据量大,并发量高时特点尤为突出
InnoDB的特点
- 在MySQL8之前,表的存储文件分为两类:表结构
.frm
,表数据和索引.idb
;在MySQL8版本之后,表结构,表数据,表索引都放入一个文件.idb
进行存储。 - InnoDB的主键索引在叶子节点中存储的是整条数据
InnoDB相较于MyISAM的缺点
- 批量查询不如MyISAM
- 对内存占用高:因为InnoDB是将数据与索引放在一起,在加载叶子页时也要加载此页的数据。而MyISAM加载的是此页数据的地址地址就相对于数据节省很多空间,这就使得InnoDB对内存的要求更高。
InnoDB不支持hash索引,但会引擎内部根据查询自动优化创建自适应hash索引,不能够人为干预
在新增大量数据时,可先将表中索引全部删除,进行批量添加后再重新创建,这样可以使得在添加时不用去维护索引,提升效率
MySIAM
MySIAM的优点
- 访问速度更快
- count(*)效率更高O(1)级别,因为有变量进行统计计算,而InnoDB是用到去逐个查O(n)级别
MySIAM相较于InnoDB的缺点:
- 不支持外键,事务,行级锁(MyISAM是表级锁)
MySIAM处理适用于小型,并发量不高(因为表锁影响并发),对数据没有完整性要求的(因为不支持事物,但性能高),查询和增加远远高于修改和删除的表,能够节省内存资源,访问效率更高 (绝大多数场景建议使用InnoDB数据引擎)
Memeory
- 表结构在磁盘存储, 表数据在内存存储, 因为是在内存,所有速度更快,但也受到内存大小的限制,使得不能存放大量数据
- 默认索引结构是hash索引,该索引的特点是在定位某条数据时十分的快速,但范围查找时,就不如B+树索引。
- 受到内存大小、断电等物理因素影响,只能用于存储临时的数据或用作缓存。但这些特性又有其他的服务器软件代替它,如Redis。所以较少使用。
三个引擎的对比
特点 | InnoDB | MyISAM | Memeory |
---|---|---|---|
事务 | 支持 | 不支持 | 不支持 |
外键 | 支持 | 不支持 | 不支持 |
锁级别 | 行级锁 | 表级别 | 表级别 |
B+树索引 | 支持 | 支持 | 支持 |
hash索引 | 不支持,但内部优化会创建且无法干预 | 不支持 | 支持(默认) |
插入速度 | 慢 | 中 | 快(内存级别) |
内存使用 | 中(索引结构) | 低 | 高 (数据放在内存存储) |
使用场景 | 大量数据,并发写,有事务的要求 | 小数据,节省资源消耗,业务简单,数据没有事务要求 | 数据量少,不在意数据安全性,对性能要求高 |
使用关系型数据库主要就是因为其具有的事务的特性能够使得数据更加安全,如果数据不需要事务的特性,那么也会先考虑使用NoSQL来代替。
其他引擎
- Archive:为大量很少引用的历史、归档、或安全审计信息的存储和检索提供了完美的解决方案。不支持索引,不支持更新,支持行级锁。适合插入一次再也不进行修改且查询也较少的数据。
- CSV引擎: 可以将csv文件作为mysql的表进行处理。存储格式就是普通的csv文件
等其他极少用到的引擎
引擎有关的SQL语句
- 查看当前系统默认的存储引擎
show variables like '%storage_engine%';
- 修改系统默认存储引擎
SET DEFAULT_STORAGE_ENGINE=引擎名称;
这样设置在服务器重启后又会恢复为原来设置,可以在SQL的配置配置文件my.cnf
进行修改
- 创建表时指定表的存储引擎,不指定采用默认的存储引擎
CREATE TABLE 表名( 建表语句; ) ENGINE = 存储引擎名称;
- 修改表的存储引擎
ALTER TABLE 表名 ENGINE = 存储引擎名称;
InnoDB引擎
推荐阅读
逻辑存储结构
InnoDB引擎共分为5级结构:按照从大到小依次是 表空间、段、区、页、行
表空间
表空间是InnoDB逻辑存储的最高结构,又分为系统表空间、独立表空间、通用表空间、临时表空间、Undo表空间
MySQL8后默认每张表的数据
以及索引
都会存在一个独立的表空间中(结构仍放在系统表空间),在磁盘使用单独的xx.ibd
文件存放。可以通过innodb_file_per_table
进行设置,如果将其设置为0,则每张表的数据都会放入系统表空间ibdata1
段
段又分为数据段,索引段,回滚段等。一个表的数据以及索引放入一个表空间中,在一个表空间中,索引与数据也是分为不同的段进行存储的。根据索引B+树的结构,数据段存放的是索引树叶子节点
,索引段存放是索引树中的非叶子节点
。
一个段的空间是随着表的大小扩展的,表有多大,段就有多大。一个段至少包含一个区,段的最小扩展单位就是区。
区
一个区大小默认为1M,即64个连续页。在进行区扩展时,为了确保页的连续性,尽量避免随机IO,每次会从磁盘申请4~5个区。
页
每个页的大小默认为16K
,正好是Linux页大小的4倍
,InnoDB中页是磁盘与内存交互的最小单位
,操作系统会分4次将进行读取或写入
索引树上每一个节点都是一个页,一个页上至少存储2个数据项。如果向一个已满的数据页中顺序插入数据,就会从区中重新分配一个新页。如果是从索引树中间插入一个数据,这个页满了就会发生页分裂。
-
页分裂
叶子中行数据的存储一定是按照顺序进行存储的,如果是乱序插入,如果插入到一个已满的页,就会发生页分裂。思考?为什么不插入新的页中?因为数据的存储是有顺序的。如何避免页分裂呢?不向中间已满的数据页插入数据,即有序插入。
直接从满的页中间分开,分为两个页,自己按照顺序插入一个页中
分裂后,页就留有空余,再将要插入的数据插入
最后再将新开辟的页按顺序连接,注意是InnoDB引擎对bB+树做了优化,页和页是双向连接
页分裂导致会有大量的页剩余,浪费空间。页分裂耗费性能,所以在插入数据时尽量要按照顺序进行插入 -
页合并
当数据删除时,InnoDB引擎只是做了一个标记,当一个页中删除数达到页的50%,就会寻找前后的页判断是否可以进行合并使用。
将两个不满页进行合并
行
行中存储的就是表中的数据行,每一行的存储大小取决于表的字段设计和行中数据大小
InnoDB是以行
为单位存储的,一个页中包含多个行。
总结
架构
InnoDB引擎由内存池,后台线程,磁盘文件三部分组成。
内存部分
由图中来看,内存主要由4部分构成 分别是 Buffer Pool
、Change buffer
、Log Buffer
、Adaptive Hash Index
Buffer Pool
:
首先注意:Buffer pool与查询缓存
不是一个东西(因为我开始时,就对这个混淆了)
查询缓存:当服务层收到要执行查询类的SQL时,先去内存的查询缓存中去寻找是否有该SQL的缓存的结果,如果没有再经过语义语法解析等,最终交给引擎层去处理。 而Buffer Pool就是在引擎层面的。且这种查询缓存十分的不智能,也就是虽然是同一个语义的SQL,有略微改动(如加个空格)就查找不到上一次SQL的缓存,命中率十分低下,已经在MySQL8版本去除。
为什么要引入Buffer Pool:
各种数据以及索引都是以页的形式放在表空间中的,表空间也是磁盘上文件的抽象。也就是说数据是放在磁盘中的。
而磁盘速度与内存有鸿沟,在访问某个数据时,是将数据所在的整个页加载到磁盘中,这样效率会有巨大的提升。但访问后,并不立即把页刷回磁盘,一是这样频繁刷盘随机IO大大的影响性能,二是,内存中数据可能还会用上,免得再次去读取。
Buffer Pool介绍:
Buffer Pool默认的大小是128M
,越大内存数据越多,去查询磁盘的概率越小,性能就越高,所以在设备允许的情况下,是越大越好的。是可以存在多个Buffer Pool的,默认是一个,也可以修改其个数。
缓冲池是以页为单位的,每页大小与磁盘大小一致是16K
,按照页的功能分为:索引页
,数据页
,undo 页,插入缓存,自适应哈希,锁信息等
按照页的状态又分为:
- free page:空闲页,内存中未被使用的页
- clean page:从磁盘加载到内存数据后,页中未被修改与与磁盘一致的页
- dirty:页中数据被修改,还未刷新到磁盘的页,也就是内存数据与磁盘不一致的页
MySQL使用的内存中,通常将80%多的空间给缓冲池,其大小将直接影响整体的性能。当内存不够时,InnoDB引擎采用LRU页面置换算法
即最近时间最少使用,将最近没有用到的页从磁盘中移出,刷新到磁盘。但InnoDB引擎是对LUR算法进行了优化,例如:select * ,会进行全表扫描,这就使得不常用的数据被扫描到,成为最新使用数据。而InnoDB引擎就是对这种情况进行了优化。
Changer Pool
:又叫插入缓冲池,是针对非主键索引插入的优化
,因为主键通常都要按顺序插入,在主键索引树的重新建立索引时较为方便。(只需叶子节点按需添加即可)。而在插入时,如果建立了非主键索引,不仅需要向主键索引树插入,还要向非主键索引树中插入。对于非主键索的索引key往往插入时是无序的,离散的访问非主键索引,随机IO会导致性能下降。使用change pool,在插入非主键索引时,先判断在插入时用到的索引树的页是否在内存中,存在则直接插入,不存在则在磁盘读取后,将对应索引页放入内存中,再插入。在读取数据时,将Change Buffer与 Buffer Pool进行合并,Buffer Pool再以一定频率刷新到磁盘
例如:要插入一条 id=2,name=‘zs’的数据,id为主键索引,name为普通索引。在插入数据时,id是有序的直接插入主键索引页中即可,而插入name时,名字通常是无序的,这就需要将部分索引放入插入缓冲池中,先在changer Pool
中进行插入,最终再以一起刷新到磁盘。提高非主键索引的插入性能
自适应哈希索引 AHI
:引擎自动为热点页建立一个哈希索引,是以页
为单位的。因为hash索引在没有hash碰撞的情况下,只需要一次就可以访问到数据,而B+树主键索引通常需要1~3次才可以。但是hash索引不支持范围查询,这就需要引擎判断情况为适合的数据建立hash索引,无需人为干预
logo buffer
:日志缓冲区,像redo日志的内存部分就在这里,默认大小为16M,内存中的数据会以指定的频率刷新到磁盘,共有三种选择。第一种是共有的,每隔1s刷新到磁盘;第二种是在事务提交后刷新到磁盘,同时兼用第一种。第三种是事务提交后,交给系统缓存页,由系统指定时机刷新到磁盘,同时兼用第一种。
磁盘部分
磁盘结构重要有以下几部分:
-
System tablespace:系统表空间
系统表空间可以有一个或多个数据文件。默认情况下,在数据目录中创建一个名为ibdata1
的系统表空间数据文件。
系统表空间包含 数据字典(包含元数据),双写缓冲区(8版本后没有存储区),变更缓冲区、undo日志,以及在系统表空间创建的表的数据和索引(不在系统表空间创建则保存在独立表空间)。 -
File-Per-Table Tablespaces :独立表空间
每张表默认使用独立表空间,存储数据和索引,对应磁盘上一个单个.idb
文件 -
General Tablespaces :通用表空间
是共享的表空间,可以存储多个表 -
redo log
redo 的两个循环写文件 ib_logfile0和ib_logfile1
后台线程
是线程往来于内存与磁盘之间,进行相应调度,提供数据服务
主要分为 Master Thread
,IO Thread
,Purge Thread
,Page Cleaner Thread
Master Thread
核心后台线程,负责其他线程的调度,还负责合并 buffer pool
与change buffer
的合并、脏页落盘,回收undo页等
IO Thread
Purge Thread
主要是 回收事务提交后的undo log日志,减少Master 线程压力
page Cleaner Thread
将内存中的脏页数据刷新到磁盘,减少Master 线程压力
InnoDB三大特性
推荐文章
InnoDB的三大特性分别是double write
,Buffer pool
,自适应哈希索引
后两个上面已经介绍,这里只介绍双写机制。
为什么称之为双写呢? 因为在内存中一份数据先是顺序
写入磁盘中双写缓冲区,确定写入成功后,在将内存中同样的数据离散
写入对应表空间中。
为什么要有双写机制呢?
先来说 双写机制带来的坏处:将同样的数据写入磁盘两次,多进行了一次IO,虽然是顺序写入,但多了一次IO,使得整体性能下降了5~10%。
再来说为什么引入了双写机制?根本原因就是解决内存数据在刷新磁盘过程中的页损坏问题,也就是部分写问题
页损坏问题:InnoDB引擎是以页为单位的,每页大小为16K,操作系统(Linux)也是以页为单位的,但它的大小是4K,也就是说当内存中的数据要想刷新到磁盘,要先交给操作系统页,由操作系统页去刷新到磁盘。这就代表内存数据每刷新一页,就要分4次写入操作系统。但如果在这4次中发生宕机等意外,导致内存中有一个不完整的页写入了磁盘,导致页损坏(部分写问题
)。这时,也不能违背持久性原则,那因为宕机等没有正确写入磁盘的数据怎样进行恢复呢?
首先,我想到的就是确保内存数据写入磁盘的还有一个redo日志。但是redo log记载的是什么?是物理记录,即xxx页偏移量为xxx的地址数据为xxxx,但没有写完整导致宕机记载的页已经损坏!
,页损坏了,相当于对应页地址的数据记录也无效了。因此redo无法使用
binlog呢?binlog的功能时数据备份和主从复制,它根本不关注内存与磁盘数据之间即它无法区分哪些是已写入磁盘,哪些是在脏页。
要是进行全量恢复,貌似还可以。但代价也太大了,是不现实的事。
这时就引入了双写机制,即不先操作redo log记载的页,这样就能防止在写入过程中发生意外后,导致redo 记载的页发生损坏使得无效。
那么先把数据写入哪里呢?写入的地方一定要是磁盘中,否则没有意义。因为做的的就是内存与磁盘间交互备份。
InnoDB引擎给出的是顺序先写入系统表空间中,成功后离散写入对应独立表空间。InnoDB还为double write的写入速度做了优化,因为这样写入是顺序写入,多了一个IO操作,也尽量减少其带来的性能损耗。
以上就是为什么要引入双写机制
Double write文件
有了这个机制肯定有对应的存储文件。
双写缓存去共有部分,一部分在内存,一部分在磁盘。每部分都固定为2M
双写缓冲区是磁盘表空间上的128个页,即两个区每个区1M,共2M。当Buffer Pool数据进行复制时,要先将数据复制到双写缓存的内存部分,再由内存部分分两次,每次以1M大小先顺序写入磁盘部分的双写文件,写入成功后,再将同样的数据离散写入对应的独立表空间中。
当内存触发了checkponit机制后,开始将部分页写磁盘。具体流程如下:
- 将要写入的脏页进行在内存中进行复制,复制到
double write
内存部分 - double write 内存部分每次写入1个区即1M,分两次顺序写入系统表空间部分
- 确定写入成功后, double write内存部分再将同样的数据离散写入对应的独立表空间
如果在双写过程发生意外呢
第一写发生意外:第一次是将内存数据(double write 内存缓存区)写入系统表文件中进行备份
如果此过程发生意外,但redo记载的页没有发生损坏
,可以使用redo直接进行恢复
第二次写发生意外:第二次写入是将内存数据(double write 内存缓存区)写入对应的独立表空间中
写入磁盘发生意外会导致页损坏,这时redo记载的页也无效。但有了系统表空间的文件,系统在重新启动,会检查过程页是否完好,发现页损坏会从系统表空间中进行恢复
redo页是不可能发生意外的, 因为只有redo页写入成功后,事务才算完成。
例: 有一张user表,id为主键,name为普通索引,age为普通字段。 当我连接MySQL并开启事务向中user表将 name为’zs’的用户age修改
为10并提交时,MySQL内部的大致流程:
MySQL的连接层创建与客户端建立连接,将SQL语句进行语法语义检查后,交给引擎层。发现此张表的引擎是InnoDB引擎,InnoDB 引擎将从Buffer Pool中寻找name为‘zs’的用户,没有找到再去通过非聚簇索引将name作为索引值查找到此行数据的id,在通过id去聚簇索引中查找到此行数据所在的页(在xxx.ibd文件中
) 将其加载到Buffer Pool
中,并将此行上写锁、MDL锁等(这个过程有来读取此行数据的就MVCC机制)。同时将数据中的undo log指针指向此事务开启前的undo log记录,同时开启新的undo log。然后在Buffer Pool中将age修改为10,此时redo 记载着磁盘地址中数据的修改,undo记载着如何恢复原记录操作,binlog记载着用户的命令操作,当事务提交后,redo、binlog立即将缓冲区内容刷新到磁盘,undo交给Purge Thread线程回收。但内存中修改的数据可能并未刷新到磁盘,当有其他数据来读取时,直接返回Buffer Pool中的数据。当此数据的脏页触发了cheakPoint
机制,此时会将此脏页从Buffer Pool中复制到double writer
内存缓冲区,内存中先将数据分两次写入到系统表中,当确定写入成功后,在将内存中的数据写入对应的页。对应的redo log记录无用,将被新的数据覆盖。