MySQL3️⃣InnoDB 引擎
1、逻辑存储结构
结构图
1.1、表空间
存储表结构、记录、索引等数据
- 参数:innodb_file_per_table,开启则每张表都对应一个表空间(8.0 默认开启)
- 文件:xxx.ibd
1.2、段
管理多个区
包括以下三部分
- 数据段(Leaf node segment):即 B+tree 的叶子结点
- 索引段(Non-leaf node segment):即 B+tree 树的非叶子节点
- 回滚段(Rollback segment):存放 undo log(回滚日志,后面会讲到)
1.3、区
表空间的单元结构
每个区的大小为 1M
1.4、页
InnoDB 存储引擎磁盘管理的最小单元
- 默认大小:16KB(即默认一个区有 64 个连续的页)
- 为了保证页的连续性,InnoDB 每次从磁盘申请 4-5 个区。
1.5、行
InnoDB 中按行存放数据
有三个隐藏字段,在 MVCC 中会具体讲解
- DB_TRX_ID:事务 ID,值为插入或最近一次修改此记录的事务 ID。
- DB_ROLL_PTR:回滚指针,指向记录的上一版本
- DB_ROW_ID:隐藏主键,当表结构没有指定主键时生成。
2、架构
MySQL 5.5 之后默认使用 InnoDB 存储引擎
(事务处理,崩溃恢复特性)
InnoDB 架构图
左侧为内存结构,右侧为磁盘结构。
2.1、内存结构
- 内存结构 = 后台线程 + 三个缓冲区。
- 在专用服务器上,通常将多达 80% 的物理内存分配给缓冲区。
2.1.1、缓冲池
Buffer Pool
- 功能:缓存磁盘上经常操作的真实数据,减少磁盘 I/O。
- 参数:
innodb_buffer_pool_size
- 缓存信息:索引页、数据页、undo页、插入缓存、自适应哈希索引、InnoDB 的锁信息等。
情景
- 若缓冲池中没有数据,通过后台线程从磁盘加载并缓存。
- 执行增删改查操作时,操作缓冲池中的数据(产生脏页),而不是直接操作磁盘。
- 缓冲池再以一定频率,通过后台线程将脏页刷新到磁盘。
缓冲池以 Page 为单位,以链表结构组织管理
包括以下三种 Page
- free page:未被使用
- clean page:被使用但未被修改
- dirty page:被使用且被修改(即数据与磁盘的数据不一致,需要刷新到磁盘)
2.1.2、更改缓冲区
Change Buffer
- MySQL 8 引入,针对于非唯一的二级索引页
- 功能:缓存尚未被读取到缓冲池的数据对应的 DML 操作,减少磁盘 I/O。
情景
- 在执行 DML 语句时,数据尚未从磁盘读到缓冲池。
- 此时在更改缓冲区中存储数据变更,而不是直接操作磁盘。
- 待数据被缓冲池读取后,更改缓冲区将数据合并恢复到缓冲池中。
- 缓冲池再将合并后的数据刷新到磁盘中。
2.1.3、自适应 hash 索引
Adaptive Hash Index
- 功能:优化 Buffer Pool 数据的查询。
- 参数:
adaptive_hash_index
回顾 hash 索引
- hash 索引
- 支持等值查询,不支持范围查询;不支持排序
- 查询效率高,通常只需一次检索(前提是不存在哈希冲突)
- Memory 引擎支持 hash 索引
- InnoDB 引擎支持 B+tree 索引,不支持 hash 索引,但是提供了自适应 hash 索引。
自适应 hash 索引
- InnoDB 对表中的各索引页查询进行监控。
- 若观察到在特定的条件下,使用 hash 索引可以提升速度,则建立 hash 索引。
- 无需人工操作,是系统根据情况自动完成。
2.1.4、日志缓冲区
Log Buffer
- 功能:保存要写入到磁盘中的日志数据(redo log 、undo log)
- 日志先保存到日志缓冲区,再定期刷新到磁盘中。
- 增加日志缓冲区的大小可以节省磁盘 I/O。
参数
innodb_log_buffer_size
:缓冲区大小,默认 16MBinnodb_flush_log_at_trx_commit
:日志刷新到磁盘时机- 1:日志在每次事务提交时写入,并刷新到磁盘(默认)。
- 0:每秒将日志写入并刷新到磁盘一次。
- 2:日志在每次事务提交后写入,并每秒刷新到磁盘一次。
2.2、磁盘结构
磁盘结构 = 重做日志 + 双写缓冲文件 + 5 个表空间
2.2.1、系统表空间
System Tablespace
存储内容
- MySQL 8:作为更改缓冲区的存储区域
- MySQL 5.x:还包含 InnoDB 数据字典、undolog 等信息
回顾:表空间参数
innodb_file_per_table
- 如果未开启参数,并且没有通用表空间。
- 则系统表空间可能也会包含表结构、索引等数据。
2.2.2、每个表的文件表空间
File-Per-Table Tablespaces
开启表空间参数 innodb_file_per_table
,每个表都对应一个 xxx.ibd
文件。
2.2.3、通用表空间
General Tablespaces
通用表空间需要手动创建,建表时可以指定该表空间。
# 创建通用表空间
CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name;
# 指定表空间
CREATE TABLE xxx ... TABLESPACE ts_name;
2.2.4、撤销表空间
Undo Tablespaces
- MySQL 实例在初始化时,自动创建 2 个默认 undo 表空间(初始大小16M)
- 用于存储 undo log 日志
2.2.5、临时表空间
Temporary Tablespaces
- 功能:存储用户创建的临时表等数据。
- 分类:会话临时表空间、全局临时表空间
2.2.6、双写缓冲区文件
Doublewrite Buffer Files
- 缓冲池将脏页刷新到磁盘前,先将脏页保存到双写缓冲区文件中。
- 若系统出现异常,用于恢复数据。
2.2.7、重做日志
Redo Log
功能:实现事务的持久性
- 事务提交后,会把所有修改信息都会存到该日志中。
- 若缓冲池将脏页刷新到磁盘的过程中出错,使用 Redo Log 恢复数据。
2.3、后台线程
作用:将 InnoDB 缓冲池中的数据刷新到磁盘文件中
2.3.1、主线程
Master Thread:核心后台线程,负责以下功能
- 调度其它线程。
- 将缓冲池的数据刷新到磁盘中。
- 脏页刷新、合并插入缓存、回收 undo 页等。
2.3.2、IO 线程
IO Thread
-
InnoDB 使用 AIO 来处理 IO 请求,IO Thread 负责处理 IO 请求回调
-
查看 InnoDB 状态:
show engine 1 innodb status \G;
线程类型 默认个数 职责 Read thread 4 读操作 Write thread 4 写操作 Log thread 1 将日志缓冲区刷新到磁盘 Insert buffer thread 1 将写缓冲区内容刷新到磁盘
2.3.3、净化线程
Purge Thread
回收已提交事务的 undo log 页。
2.3.4、页面清理线程
Page Cleaner Thread
协助 Master Thread 刷新脏页到磁盘,减轻 Master Thread 压力,减少阻塞。
3、事务原理(❗)
回顾 事务:
- 事务操作:控制、提交、回滚
- 四大特性:ACID(原子性、一致性、隔离性、持久性)
- 并发事务:脏读、不可重复读、幻读
- 隔离级别:RU、RC、RR、Serializable
事务原理围绕 ACID 展开。
-
原子性、一致性、持久性(ACD):通过日志实现
- 持久性:redo log
- 原子性:undo log
- 一致性:redo log + undo log
-
隔离性(I):通过 锁 + MVCC 实现
3.1、redo log
3.1.1、说明
重做日志
- 组成
- 重做日志缓冲(redo log buffer):存放在内存中的 Log Buffer。
- 重做日志文件(redo log file):存放在磁盘中的 Redo Log。
- 功能:记录事务提交时数据页的物理修改,实现持久性。
- 事务提交后,将所有修改信息保存到该日志中。
- 若缓冲池将脏页刷新到磁盘的过程中出错,使用 Redo Log 恢复数据。
3.1.2、流程
-
事务执行增删改操作,首先操作缓冲池中的数据。
(若缓冲池中没有对应的数据,通过后台线程加载磁盘数据)
-
缓冲池中的数据被修改,修改后的数据页即为脏页。
-
缓冲池会以一定频率,通过后台线程将脏页刷新到磁盘,从而保证数据一致性。
问题
- 缓冲区中的脏页数据是异步刷新的,而不是实时刷新。
- 事务 A 操作缓冲池的数据后即“认为”成功。
- 若刷新过程出错,数据实际上没有被持久化。
3.1.3、WAL
预写日志(Write-Ahead Logging,WAL)
-
缓冲区中的数据被修改后,将脏页的变化记录在 redo log buffer 中。
-
事务提交时,将 redo log buffer 中的数据刷新到 redo log 磁盘文件中。
-
若脏页刷新的过程出错,可使用 redo log 进行恢复。
-
当脏页成功刷新后,redo log 可删除(因此两个 redolog 文件是循环写的)
思考:每次提交事务,实时刷新 redo log,异步刷新脏页。
为什么不实时将脏页数据刷新到磁盘呢?
在业务操作中,一般是随机读写磁盘,而不是顺序读写磁盘。
- 直接刷新:直接将脏页数据刷新到磁盘,是随机写,并且浪费磁盘 I/O。
- 使用 redo log:日志文件往磁盘文件中写入数据时,是顺序写,效率高于随机写。
3.2、undo log
回滚日志
在事务执行时产生,记录数据被修改前的信息
- 作用:回滚(事务的原子性)、MVCC。
- 存储:
- 回顾逻辑存储结构:段中有一个回滚段(Rollback segment)。
- undo log 采用段的方式进行管理和记录,每个回滚段中包含 1024 个 undo log segment。
undo log 是逻辑日志(记录相反操作),redo log 是物理日志(记录实际操作)
示例:
- 执行 DELETE 时,undo log 中记录一条对应的 INSERT。
- 执行 UPDATE 时,undo log 中记录一条对应的 UPDATE。
当执行 rollback 时,从 undo log 中读取相应逻辑记录并回滚。
3.3、MVCC(❗)
多版本并发控制(Multi-Version Concurrency Control)
维护一个数据的多个版本,使读写操作不冲突。
3.3.1、当前读 & 快照读
当前读:读取记录的最新版本
- 对记录加锁,阻塞其它事务的修改。
- 常见当前读
- 共享锁:
select ... lock in share mode
- 排它锁:增删改、
select ... for update
- 共享锁:
快照读:读取记录的可见版本(可能是历史数据)
- 不加锁,非阻塞。
- 不同隔离级别的快照
- RC:每次 select 都生成一个快照。
- RR:开启事务后第一个select 生成快照。
- Serializable:快照读会退化为当前读。
3.3.2、MVCC
MVCC 的实现:快照读、隐藏字段、回滚日志、readView
- 隐藏字段
- DB_TRX_ID:事务ID,值为插入或最后一次修改此记录的事务 ID。
- DB_ROLL_PTR:回滚指针。指向这条记录的上一个版本。
- DB_ROW_ID:隐藏主键。当表结构没有指定主键时生成。
- 回滚日志(undolog):在事务执行时产生,记录数据被修改前的信息。
- 增:产生的 undo log 只在回滚时需要,事务提交后即删除。
- 删改:产生的undo log 在回滚和快照读时都需要,事务提交后不会立即删除。
3.3.3、版本链
隐藏字段中的回滚指针(DB_ROLL_PTR),指向了记录的上一版本。
-
记录的多个版本之间形成一条版本链。
-
链头是最新的历史版本,链尾是最早的历史版本。
示例
-
事务 2、3、4 先后执行三条 UPDATE 语句
-
版本链情况
3.3.4、读视图
ReadView
功能
快照读时 MVCC 读取数据的依据,记录并维护系统当前活跃的事务 ID。
- m_ids:当前活跃事务 ID 集合
- min_trx_id:最小活跃事务 ID
- max_trx_id:预分配事务 ID,当前最大事务 ID+1(因为事务ID是自增的)
- creator_trx_id:ReadView 创建者的事务 ID
生成时机
- RC:事务每次执行快照读时生成。
- RR:仅在事务中第一次执行快照读时生成
访问规则
-
遍历版本链,逐条判断 trx_id 是否满足条件
-
trx_id 代表当前版本对应的事务 ID
trx_id 含义 是否可访问 == creator_trx_id 数据是当前事务更改的 ✔ < min_trx_id 数据已提交 ✔ [min_trx_id, max_trx_id] 数据已提交 trx_id 不在 m_ids 中才 ✔ > max_trx_id 当前事务在 ReadView 生成后开启 ❌