大家好,我是此林。
今天来介绍下InnoDB底层架构。
1. 磁盘架构
我们所有的数据库文件都保存在 /var/lib/mysql目录下。
由于我这边是docker部署的mysql,用如下命令查看mysql数据挂载。
docker inspect mysql-master
如下图,目前只有一个数据库 db_user,数据库本质上是文件夹的形式。
在db_user文件夹下,目前只有一张表,表空间就是idb文件形式。
表空间idb文件以B+树的形式组织存储,其中B+树的非叶子结点存储索引段,叶子结点存储的是数据段。此外还有回滚段。段用来管理多个区。
区是表空间的单元结构,每个区大小为1M,一个区有64个连续的页。
页,是InnoDB存储引擎磁盘管理的最小单元,每个页默认大小为16K。为了保证页的连续性,Inn哦DB存储引擎每次从磁盘申请4-5个区。
行,InnoDB存储引擎数据是按行存放的。
所有文件介绍:
1. db_user:自定义的数据库
2. mysql:mysql自带数据库,包含用户、权限和管理信息表
3. performance_schema、sys:mysql自带数据库,用于mysql性能监控
4. ib_logfile0、ib_logfile1:redo_log(重做日志)
5. binlog.000001、binlog000002:binlog(二进制日志)
6. undo_001、undo_002:undo_log(回滚日志)
2. 内存架构
1. BufferPool
数据表都保存在磁盘文件中,而用户是无法直接操作磁盘文件的,必须先把数据文件加载到内存中,用户对内存中的数据进行增删改操作,最后再以一定的频率把内存中的数据刷新到磁盘中。
InnoDB内存架构中,BufferPool(缓冲区)就是类似的作用。
BufferPool 中,以页(Page)为单位,每次它会去磁盘中加载整页(16K)数据。这样做的好处是减少磁盘IO,相当于缓存的作用。
BufferPool 中的 Page有三种类型:
1. 空页(free page),空闲页,未被使用
2. clean page,被使用的页,但是数据没有被修改过
3. 脏页(dirty page),被使用的page,数据被修改过,也就是和磁盘中数据不一致的页。
2. ChangeBuffer
更改缓冲区(针对非唯一的二级索引),在执行DML语句(增删改语句)时,如果这些这些数据Page不在BufferPool中,不会区操作磁盘,而是先把数据变更存在更改缓冲区中,未来数据被读取时,再将数据合并恢复到BufferPool中,最后再刷新到磁盘。
存在的意义?
和聚簇索引不同,二级索引通常是非唯一的,并且每次以相对随机的顺序插入二级索引。同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作一次磁盘IO,会造成大量的磁盘IO,有了ChangeBuffer后,我们可以在缓冲池中进行合并操作,减少磁盘IO。
3. Adaptive Hash Index
InnoDB不支持Hash索引,它只支持B+树索引,我们目前所有的索引,底层结构都是B+树,主要原因是Hash索引虽然快,但是只支持等值匹配,不支持范围查询。
自适应Hash索引,用于优化对BufferPool的查询。InnoDB 存储引擎会监控对表上各个索引页的查询,如果观察到Hash索引可以提升速度,则建立Hash索引。系统自动优化,无需人工干预。
查询参数:
show variables like '%hash%';
默认是开启的。
4. LogBuffer
日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log,undo log)。默认大小为16MB,日志缓冲区的日志会定期刷到磁盘中。
参数:
show variables like 'innodb_log_buffer_size'; # 缓冲区大小
show variables like 'innodb_flush_log_at_trx_commit'; # 日志刷新到磁盘的时机
innodb_flush_log_at_trx_commit:
1: 日志在每次事务提交时写入并刷新磁盘
0:每秒将日志写入并刷新磁盘一次
2:日志在每次事务提交后写入,并且每秒刷新一次到磁盘中。
3. 后台线程
1. Master Thread
核心后台线程,负责调度其他线程,还负责将缓冲区中的数据异步刷新到磁盘中,保持数据的一致性,还包括脏页的刷新、合并插入缓存、undo页的回收。
2. IO Thread
在InnoDB中大量使用AIO(异步非阻塞)来处理IO请求,极大提高数据库性能。
IO Thread主要负责这些IO请求的回调。
命令:
show engine innodb status;
3. Purge Thread
主要用于回收事务已经提交的undo_log,在事务提交之后,undo log已经无用,需要回收。
4. Page Cleaner Thread
协助Master Thread刷新脏页到磁盘的线程,减轻Master Thread的工作压力,减少阻塞。
4. InnoDB 事务原理
事务特性(ACID):
1. 原子性:事务操作不可分割,要么同时成功,要么同时失败。
2. 一致性:事务完成时,所有数据必须保证一致性。
3. 隔离性:数据库系统提供的隔离机制,保证事务在不受外部并发的影响的独立环境下运行。
4. 持久性:事务一旦 提交,对数据库的改变就是永久的。
原子性、一致性由undo_log来实现,
持久性由redo_log来实现,
隔离性由锁和MVCC机制来实现。
1. redo_log重做日志
我们知道,在执行update、delete、insert操作的时候,实际操作的是BufferPool内存中的数据,
但是BufferPool中的数据是由后台线程以一定的频率或由操作系统来决定何时同步到磁盘中的。
若BufferPool数据没来得及刷到磁盘中,服务器宕机,那么就会导致数据丢失。
redo_log出现后,流程变化如下:
1. 变更BufferPool中的数据
2. 数据页变化写入redo_log_buffer(内存)
3. 事务提交后,redo_log_buffer刷新到磁盘(redo_log)中。
4. 之后即使BufferPool没来得及刷新到磁盘,也可以通过redo_log来恢复。
问:redo_log不是多此一举吗?为什么不操作完BufferPool后,直接把脏页刷新到磁盘里?
答:
1. 我们一般在事务操作很多条记录,这些记录一般都是随机操作数据页的,此时将涉及大量的磁盘IO,性能极低。
2. 而redo_log日志文件写入都是追加顺序写入,性能高于随机磁盘IO。这种机制叫WAL(Write-Ahead-logging)——先写日志。
3. 当BufferPool中脏页的数据成功刷新到磁盘中,redo_log日志文件会被定期删除。
2. undo_log 回滚日志
回滚日志,用于记录数据被修改前的信息,作用有两个:
1. 提供回滚
2. MVCC(多版本并发控制)
可以认为当delete一条语句,undo_log中会记录一条对应的insert语句;当update一条语句,undo_log中会记录一条相反的update语句。
事务提交后,undo_log不会被立即删除,因为可能用于多版本并发控制(MVCC)。
3. MVCC 机制
1. 概要
简单来说,要保证并发安全,MYSQL觉得每次读的时候都要加锁,太损耗性能了。所以提出了MVCC机制,是不加锁的快照读(非阻塞读),提升了性能。
1. 当前读:
select ... lock in share mode (读锁)
select ... for update (写锁)
update、insert、delete
以上操作都是当前读,会对读取的记录加锁。
2. 快照读:
简单的select(不加锁)就是快照读。
Read Committed: 每次select,都生成一个快照读。
Repeatable Comitted: 开启事务后第一个select语句就是快照读的地方。
Serializable: 快照读退化成当前读。
2. 实现原理
MVCC的实现依赖数据库的行记录中的三个隐藏字段、undo_log、readView。
- DB_TRX_ID:最近修改的事务ID,记录插入这条数据或最后一次修改该记录的事务ID。
- DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo_log,指向上一个版本。
- DB_ROW_ID:当表中没有定义主键,MYSQL自动生成此字段作为主键。
查看命令:
idb2sdi table_name.idb
当insert的时候,产生的undo_log日志只在回滚时需要,事务提交后可以立即删除。
当update、delete时候,产生的undo_log日志不仅在回滚时需要,在快照读的时候也需要,不能立即删除。
3. 案例分析
来看下面一个案例:
undo_log版本链如下:
那我select的时候如何知道要访问哪个版本呢?
简单来说,它会拿到当前行的DB_TRX_ID(事务id),逐个和ReadView各个字段做条件判断,满足条件即返回该版本数据;若不满足会通过DB_ROLL_PTR回滚指针继续寻找版本,进行比对。
先看事务5:
假设当前事务隔离级别为RC,即每次select都会生成一个ReadView。
1. 执行第一个查询id为30的记录命令时,生成如下ReadView。读取到的事务2修改的age=10,name=A3。
m_ids(当前活动事务id) | [3,4,5] |
min_trx_id(最小活动事务id,事务id时自增的) | 3 |
max_trx_id(最大活动事务id + 1,预分配的) | 6 |
creator_trx_id(当前ReadView创建的事务id) | 5 |
2. 执行第二个查询id为30的记录命令时,生成如下ReadView。读取到的是age=3,name=A30。
m_ids(当前活动事务id) | [4,5] |
min_trx_id(最小活动事务id,事务id时自增的) | 4 |
max_trx_id(最大活动事务id + 1,预分配的) | 6 |
creator_trx_id(当前ReadView创建的事务id) | 5 |
但是当RR隔离级别下,第二个查询id为30的记录 会和 第一个查询id为30的记录 的结果一样,因为用的是同一个ReadView。如果这个时候不想快照读,用select for update可以查到最新的记录,因为这条加锁命令不走MVCC。
4. 总结
综上,MYSQL使用了MVCC和锁机制来保证了事务的四大特性(ACID)。