本文将带您遍览 InnoDB 存储引擎的内部机制,深入其逻辑存储结构和内存架构,解析页、段、区至行的层级,揭秘索引与数据是如何存放的。深入缓冲池的秘密,发现 InnoDB 如何以 Buffer Pool 为纽带,缓解物理硬盘与内存间的速度差异,保持高效。掌握更改缓冲区的智慧,理解非唯一二级索引背后的故事。嗅探自适应哈希索引和日志缓冲区,探寻其提升查询与事务写入的神奇之处。
文章还展现了 InnoDB 独特的磁盘结构布局,包含一应俱全的系统表空间、表级空间和特有的回滚机制,为数据的持久化与安全性搭建了坚实的基础。揭示了后台线程如何无声地协助数据在内存和磁盘间舞动,以及事务原理的严密逻辑,事务的 ACID 特性在 InnoDB 中如何实现。
最后,本文还深入探讨了 MVCC 的实现原理和细节,通过生动的快照读和 ReadView 机制,让并发控制如丝般顺滑,值得每位数据库爱好者细细品味。
一、InnoDB引擎逻辑储存结构
段,分为数据段、索引段、回滚段,InnoDB是索引组织表,数据段是B+树的非叶子结点,段用来管理多个区
区,表空间的单元结构,每个区大小为1M,默认情况下InnoDB存储引擎页大小为16K,即一个区公共有64个连续的页
页,是InnoDB存储引擎磁盘管理的最小单元,每个页的大小默认为16KB。为了保证页的连续性,每次InnoDB都会向磁盘申请4-5个区
行,InnoDB存储引擎数据是按照行进行存放的
记录中的每一列col1,col2,col3
两个隐藏列
Trx id,最后一次操作该行的事务ID
Roll pointer,每次对改行记录进行改动时,会把旧版本写入undo日志中,该值是指针,通过它可以找到之前没有改动的旧版本
二、架构——内存结构
InnoDB存储引擎基于磁盘文件存储,在物理硬盘和在内存中的速度相差很大,为了尽可能弥补这两者之间的I/O效率的差值,就需要把经常使用的数据加载到缓冲池中,避免每次访问都进行非常慢的且大部分都是随机的磁盘I/O。
在InnoDB的缓冲池中不仅缓存了索引页和数据页,还包含了undo页、插入缓存、自适应哈希索引以及 InnoDB的锁信息等等。
## Buffer Pool缓冲池
缓冲池 Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增 删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。
缓冲池以Page页为单位,底层采用链表数据结构管理Page。根据状态,将Page分为三种类型:
free page:空闲page,未被使用
clean page:被使用page,数据没有被修改过
dirty page:脏页,被使用page,数据被修改过,页中数据与磁盘的数据产生了不一致
## Change Buffer 更改缓冲区(针对于非唯一的二级索引页)
在执行DML(数据 增删改)语句时,如果这些数据Page 没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer 中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据以一定频率刷新到磁盘中。
意义:与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新 可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了 ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO
## Adaptive Hash Index 自适应哈希索引
用于优化对Buffer Pool数据的查询。MySQL的innoDB引擎中虽然没有直接支持 hash索引,但是提供功能,即自适应hash索引。
hash索引对于等值匹配,一般性能高于B+树,因为hash索引一般只需要一次IO即可,而B+树可能需 要几次匹配,所以hash索引的效率要高,但hash索引又不适合做范围查询、模糊匹配等。
因此InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在某条件下hash索引效率更高, 则建立hash索引,称为自适应hash索引。 自适应哈希索引无需人工干预,是系统根据情况自动完成。
## Log Buffer 日志缓冲区
Log Buffer:日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log 、undo log), 默认大小为 16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘 I/O。
参数:innodb_log_buffer_size:缓冲区大小
参数:innodb_flush_log_at_trx_commit:日志刷新到磁盘时机,取值主要包含以下三个:
1日志在每次事务提交时写入并刷新到磁盘,默认值
0: 每秒将日志写入并刷新到磁盘一次
2: 日志在每次事务提交后写入,并每秒刷新到磁盘一次
三、架构——磁盘结构
磁盘结构
## System Tablespace 系统表空间
是更改缓冲区Change Buffer 的存储区域。如果表在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)
## File-Per-Table Tablespaces
每张表的独立表空间,则每个表的文件表空间包含单个InnoDB表的数据和索引 ,并存储在文件系统上的单个数据文件中。 一个.ibd 对应一个表
## General Tablespaces 通用表空间
需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间
#创建表空间 CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name; #创建表时指定表空间 CREATE TABLE xxx ... TABLESPACE ts_name; #-----------举例----------- create tablespace ts_01 add datafile 'mydb01.ibd' engine = innodb; 创建表空间成功后,后续创建表时 可以指定把表创建至该表空间里
## Undo Tablespaces 撤销表空间
撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储 undo log日志,undo_001, undo_002
## Temporary Tablespaces 临时会话表空间
InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。
## Doublewrite Buffer Files 双写缓冲区
双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据
## Redo Log 重做日志
用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲区(redo log buffer)(在内存结构的Log Buffer中)以及 重做日志文件(redo log),前者是在内存中,后者在磁盘中。
当事务提交之后会把所有修改信息存到该日志中,用于在刷新脏页到磁盘 发生错误时,进行数据恢复使用。以循环方式写入重做日志文件,涉及两个文件
四、架构——后台线程
内存中的数据和磁盘的数据 是怎么 写入和读取的呢?后台线程
## Master Thread(核心后台线程):
是MySQL的一个核心后台线程,负责管理和协调其他后台线程的工作,并将缓冲池中的数据异步刷新到磁盘中,保持数据的一致性;脏页的刷新、合并插入缓存、undo页的回收
## IO Thread(读写线程):
异步非阻塞IO 极大地提高数据库的性能,这些线程负责处理InnoDB存储引擎的IO请求,包括读取和写入磁盘上的数据。
## Purge Thread(清理线程):
Purge Thread负责回收已完成提交事务的undo log,将其释放以供后续事务使用。
## Page Cleaner Thread(页清理线程):
该线程负责在InnoDB存储引擎中执行脏页的刷新操作,将脏页写回磁盘,以确保数据的持久性和一致性。协助Master Thread。
五、事务原理
事务:一组操作的集合,要么全部成功,要么全部失败:
事务的四大特性:(ACID)原子性,一致性,隔离性,持久性
A:事务是不可分割的最小操作单元,要么全部成功,要么全部失败
C:事务完成时,必须使所有的数据都保持一致状态
I:数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
D:事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
持久性:redo log。重做日志
记录事务提交时,对数据页的物理修改,实现事务的持久性
包含两部分:重做日志缓冲(内存结构中的Log Buffer)与 磁盘的重做日志文件,提交后会把所有修改信息保存到该文件中,用于当刷新脏页到磁盘发生错误时 进行数据恢复使用
update/delete执行:
先看内存的BufferPool中有没有该页,如果没有该页则通过后台线程从磁盘中把页读到Buffer Pool
直接操作缓冲区中的数据,该页变成脏页
首先把数据页的物理变化记录在内存中的RodoLogBuffer,commit 事务提交的时候,redologBuffer会直接把数据页变化刷新到磁盘当中,即ib_logfile0,ib_logfile1中
在某个时机 该页以一定频率刷新到磁盘中进行持久化,若此时出错,则可以通过磁盘文件中的redo_log进行数据恢复
若脏页顺利写入磁盘,则redolog文件就不再需要,因此每过一段时间就清理一次redo log日志,是循环性的,不是永久的
日志文件都是追加的,是顺序磁盘I/O,效率比数据在磁盘的随机存取快速的多
WAL 先写日志 Write-Ahead Logging
原子性:undo log。回滚日志
用于记录被修改前的信息,作用包括两个:提供回滚 和 MVCC(多版本并发控制)
undo log 和 redo log 记录的物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的 update记录(执行update之前 数据长的样子)。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚
Undo log 销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC
Undo log 存储:undo log采用段的方式进行管理和记录,存放在段中的rollback segment 回滚段中,内部包含1024个undo log segment
六、MVCC基本概念
当前读:读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读。
客户端1使用select 语句,客户端2使用 update语句进行更新,因为当前隔离级别是可重复读,因此客户端1无法看到客户端2事务对数据的更改
当前读
快照读:简单的select(不加锁)就是快照读,快照读 读取的是记录数据的可见版本,有可能是历史数据, 不加锁,是非阻塞读
Read Committed 读已提交:每次select,都生成一个快照读
Repeatable Read 可重复高读:开启事务后第一个select语句才是快照读的地方。即第一次select查询产生快照读,后面的select查询直接使用前面的快照数据
Serializable 串行化:快照读会退化为当前读,每次读取都需要加锁
MVCC: Multi-Version Concurrency Control,多版本并发控制
维护一个数据的多个版本, 使得读写操作没有冲突,快照读 为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现 依赖于数据库记录中的三个隐式字段、undo log日志、readView
七、MVCC实现原理
八、undo log日志 回滚日志,版本链
undo log日志 回滚日志
在insert、update、delete的时候产生的便于数据回滚的日志。
当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除
update、delete时,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除
undo log 版本链
最终不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条 记录版本链表。
链表的头部是最新的旧记录,链表尾部是最早的旧记录。
那么每次查询的时候,返回哪一个版本的记录呢?ReadView的作用
九、readView 读视图 决定查询读取 的记录
ReadView(读视图)是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务 (未提交的)id
不同的隔离级别,生成ReadView的时机不同:
READ COMMITTED :在事务中每一次执行快照读时生成ReadView
REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView
在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该 ReadView,那么既然ReadView都一样, ReadView的版本链匹配规则也一样, 那么最终快照读返 回的结果也是一样的
因此,MVCC的实现原理就是通过 InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。 而MVCC + 锁,则实现了事务的隔离性。 而一致性则是由redolog 与 undolog保证