大家好,本文基于《 MySQL技术内幕:InnoDB存储引擎 第二版 基于mysql 5.6》。文章是对本书中第一章和第二章内容的总结提炼,总结了读者认为比较重要的知识点,方便学习、阅读和回顾。
欢迎大家点赞收藏,后续会对其他章节也进行一些总结和提炼。
一、MySQL 体系结构和存储引擎
MySQL 被设计为一个单进程多线程架构的数据库。
MySQL区别于其他数据库的最重要的一个特点就是其插件式的表存储引擎。
存储引擎是基于表的,而不是数据库。MySQL数据库的核心在于存储引擎。
1.1 InnoDB 存储引擎
- 支持事务
- 行锁设计
- 支持外键
- 通过多版本并发控制(MVCC)实现高并发性
- 四种隔离级别
- next-key locking 避免幻读
- 提供插入缓冲(insert Buffer)、二次写(double write)、自适应哈希索引(adaptive hash index)、预读(read ahead)等高性能和高可用功能
表中数据的存储,采用了聚集的方式,因此每张表的存储都是按照主键的顺序进行存放。如果没有显式指定主键,会为每一行生成一个6字节的ROWID,并依次作为主键
1.2 MyISAM 存储引擎
不支持事务、表锁设计、支持全文索引,它的缓冲池只缓存索引文件,而不缓存数据文件。
二、InnoDB 存储引擎
第一个完整支持ACID事务的MySQL存储引擎。
2.1 特点
- 行锁设计
- 支持MVCC
- 支持外键
- 提供一致性非锁定读
- 支持事务
2.2 后台线程
- Master Thread:主要负责将缓冲池中的数据异步刷新到磁盘。保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO 页的回收等
- IO Thread:InnoDB 中大量使用了 AIO(Async IO)来处理 IO 请求。IO Thread 主要是负责这些IO请求的回调(call back)处理
- Purge Thread:Purge Thread 来回收已经使用并分配的 undo 页。在 InnoDB1.1 之前,purge 操作在 Master Thread 中完成,之后到独立的线程中进行,提高性能。 InnoDB1.2 之后,支持多个 Purge Thread,进一步加快 undo 页的回收
- Page Cleaner Thread:InnoDB1.2.x版本中引入,将之前版本中脏页的刷新操作都放入单独的线程中来完成。目的是减轻 Master Thread 的工作及对于用户查询线程的阻塞。
2.3 内存
InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。
由于 CPU 和硬盘速度之间的鸿沟,使用缓冲池技术来提高数据库的性能。读写都优先在缓冲之中进行。
- 读:将从硬盘中读取到的页存放到缓冲中,下次读相同的页先判断缓冲中是否有,有则读,没有则读取硬盘
- 改:先修改缓冲中的页,然后再以一定频率刷新到磁盘上(Checkpoint机制)
缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲、自适应哈希索引、InnoDB存储的所信息、数据字典信息等。InnoDB 1.0.x 版本开始, 允许有多个缓冲池实例,好处是减少数据库内部的资源竞争,增加并发处理能力。
预读失败: 由 IO 操作的特性将未实际使用的数据加载到了 Buffer Pool 中缓存了,但后续却未使用。
缓冲池污染: 可能 SQL 的执行需要扫描大量页数据,为了缓存这些页数据,导致缓冲池中大量热点数据替换出去
缓冲池中页的大小默认为 16KB。
数据库中的缓冲池是通过 LRU(最近最少使用)算法来进行管理的。最频繁使用的在列表前端,最少使用的在列表尾端。 InnoDB 对 LRU 算法进行了一些改进,加入了 midpoint 位置。新读取的页放到 midpoint 位置上,默认配置下该值为 5/8。 midpoint 之前的列表叫做 new 列表,之后的列表叫做 old 列表。new 列表即热点数据。
这样做的目的是为了防止热点数据被刷出。
LRU 列表用来管理已经读取的页。
InnoDB 从 1.0.x 开始支持压缩页功能。将原本16K的页压缩为1K、2K、4K、8K,对于非 16K 的页,通过 unzip LRU 列表管理,LRU 列表的页包含 unzip LRU 列表中的页。对于 unzip LRU 的管理详见 P29。
脏页: LRU 列表中的页被修改了,但是磁盘还没有。
Flush 列表中的页为脏页列表。 脏页既存在于 LRU 列表中,页存在于 Flush 列表中。LRU 列表用来管理缓冲池中页的可用性(用于查询、更新?),Flush 列表用来管理 将页刷新回磁盘,二者互不影响。
InnoDB首先将重做日志信息先放入到重做日志缓冲中,然后以一定频率刷新到重做日志文件。
重做日志缓冲刷新到重做日志文件的时机:
- Master Thread 每秒刷新
- 每个事务提交时
- 重做日志缓冲池剩余空间小于1/2时
2.4 Checkpoint 技术
Checkpoint 所做的事情就是将缓冲池中的脏页刷回磁盘。
为了避免数据丢失,当前事务型数据库系统都普遍采用了 Write Ahead Log 策略。事务提交时,先写重做日志,再写修改页。
主要是为了解决下述问题:
- 缩短数据库恢复时间
- 缓冲池不够用时,将脏页刷新到磁盘(缓冲池大小有限,数据刷不到磁盘,查询命中率低)
- 重做日志不可用时,刷新脏页(Redo 日志大小有限,通过刷新策略可以有效的重复使用文件,不用开辟新空间)
InnoDB 通过 LSN(Log Sequence Number)来标记版。LSN 是 8 字节的数字,其单位是字节,每个页有 LSN,重做日志中也有 LSN, Checkpoint 也有 LSN。
分为两种 Checkpoint
- Sharp Checkpoint:发生在数据库关闭时将所有的脏页刷新回磁盘。
- Fuzzy Checkpoint:只刷新一部分脏页,而不是刷新所有的脏页回磁盘。
InnoDB 中可讷讷个发生如下几种情况的Fuzzy Checkpoint:
- Master Thread Checkpoint:每秒或每十秒的速度刷新一定比例,异步,用户查询线程不会阻塞
- FLUSH_LRU_LIST Checkpoint:需要保证 LRU 有空闲页可以使用,没有则清除尾端的页,如果尾端的页是脏页则触发。MySQL5.6 版本也就是 InnoDB1.2.x 之后这个检查被放在了一个单独的 Page Cleaner 线程中进行,并且用户可以控制 LRU 列表中可用页的数量
- Async/Sync Flush Checkpoint:指重做日志文件不可用的情况(重做日志文件空余空间太少)。此时脏页是从脏页列表中选取的。详细情况见P35。
- 这是为了保证重做日志的循环使用的可用性。要保证空闲空间在 0.3 。
- 在 InnoDB 1.2.x 版本之前,Async 会阻塞发现问题的用户查询线程;Sync 会阻塞所有用户查询线程。之后,也就是 MySQL 5.6 版本之后,这部分的刷新操作也放入单独的 Page Cleaner Thread 中,故不会阻塞用户查询线程。
- Dirty Page too much Checkpoint:脏页太多时进行,InnoDB 1.0.x 版本之前,该参数默认为 90 ,之后版本为 75。
- 目的是为了保证缓冲池中有足够可用的页
2.5 Master Thread 工作方式
2.5.1 InnoDB 1.0.x 版本之前
Master Thread 具有最高的线程优先级别。内部由多个循环组成,主循环(loop)、后台循环(backgroup loop)、刷新循环(flush loop)、暂停循环(suspend loop)。 Master Thread 会根据数据库运行状态在以上循环中切换。
2.5.1.1 Master Thread
大多数的操作在主循环(loop)中。其中由两大部分,每秒钟的操作和每十秒中的操作。
每秒一次的操作如下:
- 日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)(这就是为什么再大的事务提交的时间也是有限的)
- 合并插入缓冲(可能,前一秒内发生的IO次数小于5次,即IO压力小,则执行)
- 至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能,如果脏页太多,超过配置的阈值,执行)
- 如果当前没有用户活动,则切换到 background loop(可能)
每十秒一次的操作:
- 刷新 100个脏页到磁盘(可能,过去 10秒之内磁盘的 IO 小于200次,则执行)
- 合并至多 5个插入缓冲(总是)
- 将日志缓冲刷新到磁盘(总是)
- 删除无用的 Undo 页(总是)(full purge 操作,每次最多尝试收回 20 个 undo 页)
- 刷新 100个或者 10个脏页到磁盘(总是,脏页比例小于 70,刷新 10脏页,大于 70,刷新 100脏页)
2.5.1.2 background loop
若当前没有用户活动,或者数据库关闭,就会切换到这个循环。
会执行如下操作:
- 删除无用的 Undo 页(总是)
- 合并 20 个插入缓冲(总是)
- 跳回主循环(总是)
- 不断刷新 100 个页知道符合条件(可讷讷个。跳转到 flush loop 中完成)
若 flush loop 也没有什么可以做了,就切换到 suspend loop,将 Master Thread 挂起。
2.5.2 InnoDB 1.2.x 版本之前
之前的版本,InnoDB 对 IO 是有限制的,在缓冲池向磁盘刷新时做了一定的硬编码(hard coding),对于今天的硬件来说,对磁盘 IO 性能有一定的限制。
改进如下:
- InnoDB Plugin(从 InnoDB 1.1.x 版本开始)提供了参数 innodb_io_capacity ,用来表示磁盘 IO 的吞吐量,默认值为 200。
- 在合并插入缓冲时,合并插入缓冲的数量为 innodb_io_capacity 的 5%
- 在从缓冲区刷新脏页时,刷新脏页的数量为 innodb_io_capacity。
- 从 InnDB 1.0.x 版本开始 innodb_max_dirty_pages_pct 默认值从 90 调到 75。脏页占据总体的 90 太大了。75 既加快了刷新脏页的频率,又能保证磁盘 IO 的负载。
- 引入 innodb_adaptive_flushing(自适应地刷新),原来只有脏页的比例大于阈值之后才会刷新,加入这个参数之后,会根据产生重做日志的速度来决定最合适的刷新脏页数量,在小于阈值时,也会刷新一定的脏页。
- 之前每次进行 full Purge 时,最多回收 20 个 Undo 页,InnoDB 1.0.x 引入了一个参数,可以动态修改,默认 20。
2.5.2 InnoDB 1.2.x 版本
该版本将刷新脏页的操作,从 Master Thread 分离到一个单独的 Page Cleaner Thread,减轻了 Master Thread 的工作,进一步提高了系统的并发性。
2.6 InnoDB 关键特性
- 插入缓冲(insert Buffer)
- 两次写(Double Write)
- 自适应哈希索引(Adaptive Hash Index)
- 异步IO(Async IO)
- 刷新邻接页(Flush Neighbor Page)
2.6.1 插入缓冲
2.6.1.1 Insert Buffer
B+ 树的特性决定了非聚集索引插入的离散性,需要离散的访问非聚集索引页,由于随机读取的存在导致插入操作性能下降。
Insert Buffer 的设计中,对于非聚集索引的插入和更新操作,不是每次都直接插入到索引页中,而是先判断是否在缓存池中,若在,则直接插入; 不在则先放到一个 Insert Buffer 对象中,再以一定的频率和情况进行 Insert Buffer 和辅助索引页子节点的 merge(合并)操作。 这将多个插入合并为一个操作中,大大提高了对于非聚集索引插入的性能。
Insert Buffer 的使用需要同时满足下面两个条件:
- 索引是辅助索引
- 索引不是唯一(如果是,则插入需判断唯一性,会造成随机读,则失去意义)
存在一个问题:当写密集时,插入缓冲会占用过多的缓冲池内存,默认最大为 1/2,可以修改配置解决。
2.6.1.2 Change Buffer
在 InnoDB 1.0.x 版本开始引入 Change Buffer,可将其视为 Insert Buffer 的升级版。在这个版本中,对 DML 操作(插入、删除、更新) 都进行缓冲,分别是:Insert Buffer、Delete Buffer、Purge buffer。
跟 Insert Buffer 一样,Change Buffer的适用对象依然是非唯一辅助索引。
从 InnoDB 1.2.x 版本开始,可以通过参数 innodb_change_buffer_max_size 来控制 Change Buffer 最大使用内存的数量,默认值为 25 ,即 1/4 的缓冲池内存空间, 该参数最大有效值是 50。
2.6.1.3 Insert Buffer 的内部实现
Insert Buffer 是一棵 B+ 树。B+ 树就分叶子结点和非叶子结点。
非叶子结点存放的是查询的 search key(键值),构造如图:
一共占用 9 字节,space 表示待插入记录所在表的表空间id,占 4 字节,marker 占 1 字节,用来兼容老版本的 Insert Buffer。 offset 表示页所在的偏移量,占 4 字节。
叶子结点结构:
其中 Space、marker、offset 与非叶子结点含义相同,一共占用 9 字节,第四个字段 metadata 占据 4 字节,第五个字段是实际记录。 所以需要额外的 13 字节的开销。
还有一个页类型为 Insert Buffer Bitmap 的页用来标记每个辅助索引页的可用空间。
2.6.1.4 Merge Insert Buffer
何时将 Insert Buffer 合并到真正的辅助索引中呢?
- 辅助索引页被读取到缓冲池时,此时若Insert Buffer中对应的记录则合并到该辅助索引中。
- Insert Buffer Bitmap 页追踪到该辅助索引页已无可用空间时(可用空间少于 1/32 时,进行强制合并)
- Master Thread
2.6.2 两次写
double write 可以提高数据页的可靠性。
部分写失效: 当 InnoDB 正在进行数据写入时,某个页只写了前 4KB,之后就发生了宕机,导致页损坏。
double write 由两部分组成,一部分是内存中的 double write buffer,大小为 2MB,另一部分是屋里磁盘上共享表空间中连续的 128 个页, 大小同样为 2MB。
在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是先通过memcpy函数将脏页先复制到内存中的doublewrite buffer, 之后通过 doublewrite buffer 再分两次,每次 1MB 顺序地写入共享表空间的物理磁盘上,然后马上 调用 fsync 函数,同步磁盘。 doublewrite 是连续的。是顺序写,开销小。完成doublewrite的写入之后,再将doublewrite buffer 中的页也写入各个表空间文件中。 此时的写入是离散的。
注:doublewrite 中存储的是完整的页。
为什么不怕在数据写到共享表空间的 doublewrite 时,发生宕机导致页副本缺失?
doublewrite 是为了解决数据文件本身页损坏导致的数据丢失问题,这种情况单纯靠 redo log 是无法完成修复的,因为本身数据文件在中的页就有问题。 而如果在数据写到共享表空间 doublewrite 时发生宕机,导致 doublewrite 的页损伤,其实是可以通过 redo log 来进行修复的,因为我们 本身的数据文件中的页是完好的,那么直接执行 redo log 进行重写就好了。
2.6.3 自适应哈希索引
哈希的查找时间复杂度是 O(1),B+ 树的查找次数,取决于 B+ 树的高度,在生产环境中,一般是 3-4 层,故需要查询 3-4 次。
InnoDB 会监控对表上各索引页的查询,如果建立哈希索引会带来性能提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)。
AHI 通过缓冲池的 B+ 树页结构而来,建立速度很快,而且不需要对整张表结构建哈希索引。
InnoDB 会自动根据访问频率和模式来自动为某些热点页建立哈希索引。
- 对这个页的连续访问模式必须一样,访问模式一样,也就是查询条件一样。
- 以该模式访问了 100 次
- 页通过该模式访问了 N 次,其中 N= 页中记录 * 1/16
哈希索引只能用来搜索等值的查询,其他查找类型,如范围查询是不能使用哈希索引的。
2.6.4 异步 IO
与 AIO 对应的是 Sync IO,每进行一次 IO 操作,都需要等待此次操作结束才能继续接下来的操作。
当全部 IO 请求发送完毕后,等待所有 IO 操作的完成,这就是 AIO。
AIO 的另一个优势是可以进行 IO Merge 操作,也就是将多个 IO 合并为一个 IO,这样可以提高 IOPS 性能。
假如用户需要访问三个连续的页,同步 IO 需要运行 3 次 IO 操作,而 AIO 会判断这三个页是连续的,而只读取一次,全部读取。
InnoDB 1.1.x 之前,AIO 通过 InnoDB 中的代码模拟实现,之后提供了内核级别 AIO 的支持,称为 Native AIO。
2.6.5 刷新邻接页(Flush Neighbor Page)
工作原理:当刷新一个脏页时,如果该页所在区的页是脏页,则会一起进行刷新,好处就是可以通过 AIO 将多个 IO 操作合并为一个,提高性能。
该工作机制在传统机械硬盘下有着显著的优势,但是对于固态硬盘有着超高 IOPS 性能的磁盘,则建议将该功能关闭。
持续更新go语言及K8S等go相关生态的文章、后端组件、前沿技术趋势及概念,欢迎大家点赞收藏,关注后续文章。