之前在写存储引擎的时候只是简单的介绍了下引擎是干什么的以及一些引擎的特点,这篇重点包括InnoDB引擎的逻辑存储结构,架构,事务原理和MVCC
1.逻辑存储结构
InnoDB引擎的逻辑存储结构的存储引擎之前有简单介绍过:表空间->段->区->页->行
- 表空间(.ibd文件):一个mysql实例可以对应多个表空间,用于存储记录,索引等数据
- 段:分为数据段,索引段和回滚段,InnoDB中所有的数据是基于索引组织的,数据段则是B+树的叶子节点,索引段是非叶子节点
- 区:表空间的单元结构,每个区的大小为1M,默认情况下InnoDB引擎的页大小为16KB,所以1个区有64个连续的页
- 页:InnoDB引擎管理磁盘最小的单元,为保证页的连续性,InnoDB引擎每次会申请4-5个区
- 行:InnoDB引擎存储的数据按照行进行存放
2.架构
MySQL从5.5版本开始就使用InnoDB引擎作为默认引擎,因为InnoDB擅长处理事务,且具有崩溃恢复特性
InnoDB的架构分为内存结构和磁盘结构,我们分开讲
2.1 内存结构
内存结构包括4个区域:缓冲池(Buffer Pool),更改缓冲区(Change Buffer),日志缓冲区(Log Buffer)和自适应哈希索引(Adaptive Hash Index)
2.1.1 缓冲池
缓冲池用于缓存磁盘上经常操作的真实数据,在执行增删改查的操作的时候会先操作缓冲池里面的数据(没有则去磁盘加载并缓存),然后以一定的频率刷新到磁盘,减少磁盘的读写操作
缓冲池以页为单位,底层采用链表进行管理,根据状态将页分为三种类型:
free page:空闲页
clean page:被使用的页,数据没有被修改
dirty page:脏页,被使用且数据被修改,页里面的数据和磁盘数据不一致
2.1.2 更改缓冲区
更改缓冲区针对的是非唯一二级索引页,因为二级索引通常是非唯一的,并且插入的顺序比较随机,但是如果删除或更新某个二级索引也是会影响到其它二级索引的,所以如果每次直接操作磁盘的话也会造成大量的磁盘IO操作
在执行DML语句的时候如果这些数据页不在缓冲池中,那么不会直接操作磁盘,而是将数据变更存储在更改缓冲区中,在后续数据被读取的时候再将其合并到缓冲池中
2.1.3 自适应哈希索引
用于优化对缓冲池的查询,InnoDB引擎会监控对索引页的查询,如果觉得hash索引可以提升速度的话就会建立hash索引,这就是自适应哈希索引
自适应哈希索引是系统根据情况自动完成的,不需要人工干预
2.1.4 日志缓冲区
保存要写入到磁盘的日志数据(redo log,undo log),默认大小为16MB
日志缓冲区的日志会定期刷新到磁盘中,日志缓冲区大小可调节
innodb_log_buffer_size 日志缓冲区大小的参数
innodb_flush_log_at_trx_commit 日志刷新到磁盘的时机,0是每秒将日志写入然后刷新,1是事务提交的时候写入并刷新,2是事务提交后写入,之后每秒刷新到磁盘
2.2 磁盘结构
磁盘结构区域比较多,包括系统表空间(System Tablespace),文件表空间(File-Per-Table Tablespaces),通用表空间(General Tablespaces),撤销表空间(Undo Tablespaces),临时表空间(Temporary Tablespaces),双写缓冲区(Doublewrite Buffer Files)和重做日志(Redo Log)
2.2.1 系统表空间
是更改缓冲区的存储区域,这是8.0之后重新规划的,之前的版本还会存放其它的东西
2.2.2 文件表空间
每个表都会有一个这样的空间,里面包含表自己的数据和索引
2.2.3 通用表空间
这个需要我们自己去创建
create tablespace 表空间名 add datafile 表空间文件 engine=存储引擎名;
之后在建表的时候可以指定使用这个空间
create table ...... tablespace 表空间名;
2.2.4 撤销表空间
用于存储redo log日志,MySQL实例在初始化的时候会自动创建两个默认的撤销表空间,初始大小为16M
2.2.5 临时表空间
存储用户创建的临时表等数据
2.2.6 双写缓冲区
InnoDB将数据页从缓冲池刷新到磁盘前会先将数据写在这个区域的文件里面,用于系统异常的时候进行恢复数据,双写缓冲区的文件后缀名为.dblwr
2.2.7 重做日志
用来实现事务的持久性,主要由两个部分组成 :重做日志缓冲(redo log buffer)和重做日志文件(redo log file)
重做日志缓冲在内存里面,重做日志文件在磁盘中
事务提交之后会把所有的信息都存储到重做日志当中,用于刷新脏页到磁盘发生错误的时候进行数据恢复
内存结构和磁盘结构的分区和主要功能基本如上所述,至于内存结构里面缓冲池的数据如何刷新到磁盘里面,这涉及到后台线程
2.3 后台线程
后台线程有4类: Master Thread,IO Thread,Purge Thread,Page Cleaner Thread
2.3.1 Master Thread
核心后台线程,负责调度其它线程以及将缓冲池的数刷新到磁盘中,还有就是脏页的刷新,undo页的回收等
2.3.2 IO Thread
InnoDB中大量使用AIO来处理IO请求,而IO Thread则是负责处理这些IO请求的回调,主要有4类
线程 | 默认个数 | 说明 |
---|---|---|
Read thread | 4 | 负责读操作 |
Write thread | 4 | 负责写操作 |
Log thread | 1 | 负责将日志缓冲区刷新到磁盘 |
Insert buffer thread | 1 | 负责将写缓冲区内容刷新到磁盘 |
IO Thread可以在InnoDB引擎的状态里面查看到
show engine innodb status;
2.3.3 Purge Thread
用于回收事务已经提交的undo log
2.3.4 Page Cleaner Thread
协助Master Thread刷新脏页,减少阻塞
3.事务原理
还是先回顾下事务的概念和特性:
事务是一组操作的集合,我们将一组操作作为一个整体,所以事务里面的操作的时候要么同时成功,要么同时失败
事务特性:
原子性 : 事务是不可分割的最小的操作单元,里面的操作要么全部成功,要么全部失败
一致性 : 事务完成的时候必须使所有的数据保持一致状态
隔离性 : 数据库系统提供的隔离机制,目的是保证事务在不受外部并发操作影响的环境下运行
持久性 : 一个事务一旦被提交,它对数据库中数据所做的改变是永久的
事务隔离级别(懒得打字了,截之前的图得了( ̄▽ ̄)):
事务的原理就是我们要如何保证事务的这4大特性
事务的原子性,一致性和持久性是由InnoDB引擎的两份日志保证的,分别是redo log和undo log
事务的隔离性则是由锁机制和MVCC来保证
3.1 redo log
redo log为重做日志,保证的是事务的持久性,上面介绍过redo log分为重做日志缓冲(redo log buffer)和重做日志文件(redo log file)两个部分,前者在内存中,后者在磁盘里面
来看看没有redo log提交事务的执行流程(事务里面有update语句):
- 首先要查看缓冲池里面有没有对应的数据,没有的话就要通过后台线程去磁盘进行读取,然后缓存在缓冲池
- 执行事务里面的语句,直接在缓冲池进行操作,此时数据进行更新,但磁盘的数据没有更新,于是有了脏页
- 事务提交,后台找时间刷新脏页到磁盘
问题出现在第三步,由于脏页刷新不是实时的,所以就会出现显示事务提交成功但其实后台线程刷新脏页失败的情况
有redo log之后的流程:
前面的流程是一样的,不同的是在对缓冲池的数据进行修改之后,redo log buffer 会记录变更的数据页,当事务提交后这些数据页会刷新到磁盘的redo log file中,后面如果脏页刷新失败的话就可以通过redo log file进行恢复
缓冲池其实可以直接把变更的数据页直接刷新到磁盘中,之所以不这样做是因为事务里面操作的数据页是随机的,直接操作是随机IO,效率太低,而日志文件是可以追加的,为顺序IO,效率会大大提升
3.2 undo log
undo log为回滚日志,里面记录的是数据被修改之前的信息,主要作用是提供回滚和MVCC,事务的原子性就是由undo log来保证的
undo log为逻辑日志,记录的不是修改前数据的物理信息,而是SQL操作,比如当执行delect语句的时候,undo log里面就会记录一条相反的insert语句,当执行rollback的时候就可以读取到insert进行回滚
undo log销毁:在事务执行时产生,事务提交的时候并不会立即删除,因为可能会用于MVCC
undo log存储:采用段的方式进行管理和记录,开头介绍InnoDB的逻辑存储结构的时候有提到一个回滚段,undo log就存放在这里
事务的一致性是由两个日志共同来保证的
4.MVCC
MVCC指的是多版本并发控制,是InnoDB引擎里重要的机制,作用是在快照读的时候来确定查找的数据历史版本
MVCC里面涉及的基本概念比较多,主要有
- MVCC: 多版本并发控制,指维护一个数据的多个版本,使读写操作不会产生冲突,具体的实现依靠数据库中的三个隐式字段,undo log日志和readView
- 当前读: 读取记录最新版本,读取时要保证其它并发事务不能修改当前记录,所以会对读取的记录进行加锁
- 快照读: 读取的是记录数据的可见版本,可能是历史数据,不加锁,非阻塞读,简单的select语句就是快照读;MySQL默认的隔离级别在开启事务后的第一个select语句为快照读的地方
4.1 实现原理
MVCC的实现原理依靠三个部分:三个隐式字段,undo log日志和ReadView
4.1.1 隐式字段
我们在创建表的时候,InnoDB会额外增加三个字段: DB_TRX_ID,DB_ROLL_PTR和DB_ROW_ID
其中DB_ROW_ID不是每一张表都会有的,当表没有主键的时候才会有
字段 | 说明 |
---|---|
DB_TRX_ID | 最近修改事务ID,记录最后修改该记录的事务ID |
DB_ROLL_PTR | 回滚指针,主要是和undo log配合指向上一个版本 |
DB_ROW_ID | 隐藏主键,数据表中没有主键的时候生成这个字段 |
4.1.2 undo log日志
回滚日志
是在insert,update,delete操作的时候产生的日志,便于后面进行数据回滚
insert操作产生的undo log日志在事务提交后便会删除,但update和delete操作产生的undo log日志因为在快照读的时候也会需要,所以便不会立即删除
这里有一个概念:undo log版本链
假设有一张表,其原始数据如下:
现在同时有多个事务对上面的数据进行修改,执行顺序如下:
我们来看看undo log里面的情况,为演示方便undo log就直接写数据的物理信息了,但实际上undo log为逻辑日志,记录的不是修改前数据的物理信息,这个需要注意
最先开始进行修改的是事务2,在修改之前undo log要先记录修改前的信息
然后事务2进行修改,最新版本的字段DB_ROLL_PTR字段要指向旧版本
后面的事务同理,当所有事务都修改完之后undo log如图:
最后我们会发现undo log生成的是一条记录版本的链表,头部是最新的记录,尾部是最旧的记录
至于查询的时候是查哪一个版本则是由ReadView决定
4.1.3 ReadView
读视图,是快照读执行时MVCC提供数据的依据,读视图里面记录了系统当前还未提交的事务id
ReadView包含四个核心字段:
字段 | 说明 |
---|---|
m_ids | 当前活跃的事务id集合 |
min_trx_id | 最小活跃事务id |
max_trx_id | 预分配事务id,为当前最大事务id+1 |
creator_trx_id | ReadView创建者的事务id |
版本链数据访问规则如下:
访问规则中会有一个trx_id,代表的是当前事务id(简单来说就是现在是哪个事务要查看数据)
- trx_id=creator_trx_id,可以访问该版本(说明数据就是当前这个事务改的)
- trx_id<min_trx_id,可以访问该版本(说明trx_id代表的事务在之前已经提交)
- trx_id>max_trx_id,不可以访问该版本(说明trx_id代表的事务还未开启)
- min_trx_id<=trx_id<=max_trx_id,若trx_id不在m_ids内,可以访问该版本(说明trx_id代表的事务已经提交)
不同的隔离级别生成ReadView的时机是不同的:读已提交(read commited)是在事务中每执行一次快照读的时候生成,可重复读(repeatable read)是在第一次执行快照读的时候生成,后面是复用这个生成的ReadView
以上面的事务为例,来看看版本链数据访问规则,以可重复读隔离级别为例
假设现在是事务5在进行查询
第一个select语句的ReadView字段内容如下:
m_ids=[3,4,5]
min_trx_id=3
max_trx_id=6
creator_trx_id=5
第二个select语句会复用上面的ReadView
将之前的版本链拿出来
先看第一条记录: trx_id=4,
- 4!=5,不符合
- 4>3,不符合
- 4<6,符合
- 4在m_ids内,不符合
所以第一条记录不是我们要找的版本
再看第二条: trx_id=3 - 4!=5,不符合
- 3=3,不符合
- 3<6,符合
- 3在m_ids内,不符合
所以第二条记录也不是
继续往下看第三条:trx_id=2 - 2!=5,不符合
- 2<3,符合
那么这就是当前事务要找的版本
读已提交隔离级别思路类似,不同的是ReadView有多个
InnoDB引擎内容到这结束,最后整理下事务的四大特性的实现
原子性 : undo log
持久性 : redo log
一致性 : undo log+redo log
隔离性 : 锁+MVCC
完