1. 一些概念
- 表空间:一个mysql实例可以对应多个表空间,用于存储记录,索引等数据
- 段:分为数据段,索引段,回滚段,Innodb是索引组织表,数据段存放的是b+树的叶子节点,索引段存储的是b+树的非叶子节点。段用来管理多个Extent(区)
- 区:表空间的单元结构,每个区大小为1M。默认情况下,Innodb存储引擎页大小为16k,即一个区中一共有64个连续的页
- 页:是Innodb存储引擎磁盘管理的最小单元,每个页的大小默认为16kb。为了保证页的连续性,Innodb存储引擎每次从磁盘申请4-5个区
- 行:Innodb存储引擎数据是按照行进行存放的
- 内存结构
- 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中,再将合并后的数据刷新的磁盘中(之前叫做Insert Buffer)
- 自适应哈希索引:用于优化对Buffer Pool中的数据的查询。Innodb存储引擎会监控对表上各索引页的查询,如果观察到hash索引可以提升速度,则建立hash索引,称之为自适应hash索引,无序人工干预,是系统根据情况自动完成
- Log Buffer:日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log,undo log),默认大小为16m,日志缓冲区的日志会定期刷新到磁盘中,如果需要更新,插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘IO
- 磁盘结构
- System Tablespace:系统表空间是更改缓冲区的磁盘存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据(在mysql5.x版本中还包括Innodb数据字典,undolog等)
- File-Per-Table Tablespaces:每个表的文件表空间包含单个Innodb表的数据和索引,并存储在文件系统上的单个数据文件中。
- General Tablespaces:通用表空间,需要通过create tablespace 语法创建通用表空间,在创建表时,可以指定使用该表空间。
- Undo Tablespaces:撤销表空间,mysql实例在初始化时会自动创建两个默认的undo表空间(初始大小为16M,undo_001,undo_002),用于存储undo log日志
- Temporary Tablespaces:临时表空间,Innodb使用的会话临时表和全局临时表空间,存储用户创建的临时表等数据
- DoubleWrite Buffer File:双写缓冲区,Innodb引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据(#ib_16384_0.dblwr, #ib_16384_1.dblwr)
- Redo Log:重做日志,用来实现事务的持久性,该日志文件由两个部分组成:重做日志缓冲区(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中,用于刷新脏页到磁盘时,发生错误时,进行数据恢复使用(ib_logfile0, ib_logfile1)
- 后台线程
- Master Thread:核心后台线程,负责调用其他线程,还负责将缓冲池中的数据异步刷新到磁盘中,保持数据的一致性,还包括脏页的刷新,合并插入缓存,undo页的回收
- IO Thread:在Innodb存储引擎中大量使用了AIO来处理IO请求,这样可以极大地提高数据库的性能,而IO Thread主要负责这些IO请求的回调
- Read thread:默认4个,负责读操作
- Write thread:默认4个,负责写操作
- Log thread:默认1个,负责将日志缓冲区刷新到磁盘
- Insert buffer thread:默认1个,负责将写缓冲区刷新到磁盘
- Purge Thread:主要用于回收事务已经提交了的undo log,在事务提交之后,undo log可能不用了,就用它来回收
- Page Cleaner Thread:协助Master Thread刷新脏页到磁盘的线程,它可以减轻Master Thread的工作压力,减少阻塞
1.1 表空间(数据表存储的空间)
- 表空间由多个段对象组成(段对用户来说是透明,逻辑的概念)
- 每个段由区组成。区是申请磁盘空间的最小单位,区在物理磁盘上是连续的。区的大小固定为1M
- 每个区由页组成。页是调入内存的最小单位。页的默认大小是16k,可以通过参数调整。
- 每个页里保存着数据,保存的是记录(里面还包括了一个row offset array。用于给每个页里面的记录行排序的)。
1.2 磁盘数据页的组成方式
- 文件头 file header
- 记录了该页在文件中的偏移量、上一页和下一页,以及LSN等信息
- 页头 page header
- 下届Infimum和上界Supremum
- 用户记录User Record
- 记录实际的row记录
- 记录按照索引顺序以单向链表存储的,所以顺序访问速度快,随机访问比较慢。
- 为了加快页内记录访问速度,每个数据页还会有一个页目录,你可以理解是一个数组,支持随机访问,可以通过二分查找快速定位到要查找的行记录位置。
- 空闲空间 Free Space。
- 页目录 page directory
- 用于按顺序记录该页的行记录所在位置。(主键索引的话就是id)
- B+树索引只能索引到具体某个页,至于页内的具体哪个行需要通过页目录在进行二分查找。不过当数据页加载入内存,在内存中进行二分查找速度可以忽略不记。
- 文件尾信息 File Trailer
- 用于检测页的完整性。保存了LSN值。通过与文件头中的LSN值比较,看是否相等,以此来保证页的完整性。
1.3 innodb的记录行的隐藏列
- rowid:
- 如果我们自己设置了主键,则rowid就是我们的主键
- 如果我们没有设置主键,则mysql自动帮我们添加一个rowid,大小为6个字节
- txid:
- rollptr
1.4 innoDB的缓冲池
- innodb_buffer_pool_size
- 用于将数据页读到内存缓存起来,这样下次使用的时候就可以不需要进行IO操作了。
- 主要用来保存热点数据的。
- 官方推荐缓冲区大小设置为内存的60~80%
- 在缓冲区中如何标志一个数据页
- space+page_no
- space:标识表空间
- page_no:代表这个页是表空间的第几个页
1.5 innodb_buffer_pool管理方式
- Free List:组织空闲的内存页
- LRU List:组织被占用的内存页
- Flush List:组织被占用的内存页中脏页的指针(脏页需要刷新到磁盘中)。
1.6 检查点checkpoint
- 检查点标志出了上一次刷新脏页的位置。
- 检查点与检查点之间如果宕机了,则需要通过redo日志来恢复。
2. LSN(日志序列号)
2.1 什么是LSN
- 在InnoDB存储引擎的redo日志系统中,定义了LSN,代表了日志序号的意思。
- 占8个字节
- 其含义标识存储引擎向redo日志系统写入的日志量(字节数)
- LSN是不会减小的,它是日志位置的唯一标记。
- 磁盘上每个数据页上都有LSN,重做日志也有LSN,检查点也有LSN
2.2 通过客户端命令查看LSN相关信息
-- \G 表示格式化输出
show engine innodb status\G
Log sequence number 6927323612
Log flushed up to 6927323612
Pages flushed up to 6927323612
Last checkpoint at 6927323603
- Log sequence number
- Log flushed up to
- 表示redo log 从内存缓存区刷新到磁盘文件上时LSN
- Pages flushed up to
- 表示数据内存缓冲区脏页写入磁盘上的数据文件的物理页时的LSN
- Last checkpoint at
2.3 页、redo日志、检查点上的LSN的表示含义
- 检查点上的LSN
- 它表示了在检查点时刻该日志序列号之前的操作已经被持久化了。
- 而且这个日志序列号之前的更新操作都刷新到磁盘上的数据页上了。
- 由master thread定期去更新。
- redo日志上的LSN
- 它表示数据库实际上已经完成(提交)的操作的最新日志序列号。
- 但是这个日志序列号之前的更新操作不一定刷新到了磁盘的数据页上。
- redo的LSN必然大于等于检查点上的LSN,因为检查点上的LSN只是记录到检查点时刻被持久化的日志序列号。而redo日志的LSN是记录实时的被持久化的日志序列号。
- 磁盘上每个数据页上的LSN
- 内存缓冲池中每个数据页的LSN
- 它含有两个LSN
- 第一次修改时的LSN:主要用于Flush List排序用的
- 最新修改时的LSN:记录了这个数据页更新到了何处。
2.3 数据脏页的刷盘流程
- 对于数据更新操作,存储引擎会将数据页先加载到内存缓冲池,然后修改内存中该数据页的内容。这样就会产生脏页,脏页需要刷新到磁盘才能保证对数据表的更新被持久化。
- 但是如果更新一条记录就需要将一个页刷盘一次,则这个开销就有点太大。太多次IO操作非常影响性能。
- 所以存储引擎对于内存数据页的修改,不是一有修改就会刷盘,而是达到一定的阈值才会去刷盘。那么就会产生一个问题,如果此时数据库宕机了,则内存的的脏数据页会没有完成刷盘就丢失了。这样就会导致有些更新被丢失了。
- 因此InnoDB存储引擎引入了redo日志,在数据库对内存数据页的更新后会先写入redo日志的内存缓冲区中,当redo日志的内存缓冲区中的日志写入redo日志的磁盘文件上,就表示这个操作完成了持久化。然后存储引擎在合适的时间再将数据脏页刷回磁盘。
- 可能你会问,redo日志的内存缓冲区中的日志写入redo日志的磁盘文件上,也是需要IO啊,为什么不直接将数据页写入磁盘呢?
- 因为redo日志写入磁盘文件这个过程时顺序存储的。而IO刷盘是随机存储的。所以redo日志写入磁盘的速度远远快于数据的刷盘。
2.4 InnoDB恢复机制
- 当数据库突然宕机,重启数据库会采取恢复机制
- InnoDB会获取检查点的LSN,从此处开始进行数据恢复
- 在redo日志的LSN与检查点的LSN之间的更新操作是可能没有刷新到磁盘但已经提交的操作。
- 它会去找相应的磁盘数据页,
- 如果page页的LSN小于redo日志的LSN,则需要重做该数据页的记录。
- 如果page页的LSN大于redo日志的LSN,证明数据页已经修改了,但是还未提交,则需要借助undo log日志进行回滚。
3. InnoDB的双写缓冲(doublewrite buffer)
3.1 为什么要引入doublewrite
- 由于InnoDB和操作系统的页大小是不一致的。InnoDB页大小是16k,而操作系统的页通常为4k
- 这样会导致InnoDB回写dirty数据到磁盘时,一个页需要写入4次,而写入的过程无法保证原子性,所以可能导致数据的部分写回到操作系统文件系统中。如果写的过程突然断电,则会造成数据页异常。(可以通过检查数据页的头尾部的LSN是否一致来说明数据页是否损坏)
- 可能你想问,即使数据页损坏了,难道不能通过redo日志重做吗?
- 原因是redo log日志不会记录完整的一页数据,这样的话日志的开销就太大了。它只会记录哪次(sequence)如何操作(update,insert)哪页(page)的哪行(row)。所以无法实现对数据页异常的恢复。
- 所以如何保证写入的数据页在磁盘中没有损坏?因此引入了doublewrite。
3.2 doublewrite是什么
- 在Innodb的共享表空间里存在一个段对象(doublewrite),它大小是128个页,占用存储空间2M。
- 当mysql将脏数据批量刷新到数据文件中的时候,先使用memcopy将脏数据复制到内存的 double write buffer中。
- 之后通过double write buffer再分两次操作。每次写入1M到共享表空间。然后马上调入fsync函数,将数据同步到磁盘上,避免缓冲带来的问题。
- 在这个过程中,doublewrite是顺序写,开销不大。
- 完成doublewrite写入后,将double write buffer写入各个表空间文件中,这是离散写入。
- 如果发生了极端情况,系统断电了。InoDB再次启动后,发现一个Page数据已经损坏了,那么可以通过共享表空间中的doublewrite段进行数据恢复。
3.3 如何通过(共享表空间中的)doublewrite进行恢复
- 如果写入doublewrite失败,那么这些数据不会被写入磁盘数据文件,则磁盘数据文件中的页是正常页,只不过不是最新页。InnoDB此时会从磁盘载入原始数据,然后根据redo日志来计算出正确的数据,重新写入内存doublewrite buffer。
- 如果写入doublewrite成功,但是写磁盘数据文件失败,InnoDB直接使用doublewrite中的数据恢复磁盘文件中的数据页。
3.4 双写的缺点
- 因为双写,则有一定的性能消耗。但是消耗也不是很大。
- 可以通过设置参数关闭双写。
4. Insert Buffer
4.1 为什么要有Insert Buffer
- 在InnoDB中,如果主键是自增的,则在聚集索引中插入数据一般是顺序的。
- 但是插入数据的同时我们还需要对其他的辅助索引上进行索引插入,而这个索引插入就是很随机的,可能需要多次IO操作。
- 所以InnoDB存储引擎引入了Insert Buffer,来加速对辅助索引上的插入操作。
4.2 什么是Insert Buffer
- 它是共享表空间中的一个Insert Buffer对象。即它是占用磁盘空间的。也是以一颗B+树的形式组织起来的。
- 内存缓冲池中也有对于的Insert buffer缓冲区的。即先在内存更新,然后同步到磁盘上的insert buffer中。
4.3 Insert Buffer如何工作
- 当对聚簇索引插入完毕后,要进行对辅助索引的插入,此时会先判断要插入的辅助索引页是否在缓存池中。
- 若在,则直接插入
- 若不在,则先插入Insert Buffer中去,等积累了一定的数量后,再对辅助索引进行合并。
4.4 什么时候会用到Insert Buffer
- 插入的索引必须是辅助索引
- 聚集索引是要插入数据的,每次插入还是要立即插入为好。
- 索引必须不是唯一索引
- 用到Insert Buffer,则说明没去找具体的辅助索引,而是暂时将待插入的索引记录插入到Insert Buffer中,如果是唯一索引,则需要去判断索引中是否存在这个记录,那么这样不还是要遍历整个索引。
4.5 什么时候会将Insert Buffer中的所有和辅助索引合并
4.6 总结
- Insert Buffer在InnoDB中已经开启,不需要我们手动配置任何东西。对我们来说是透明的。
5. 自适应哈希索引
- 通过给内存中热点页的记录(自适应就表现在它自己去判断哪些页是热点页)建立hash索引,从而加速的等值查询速度。时间复杂度为O(1)
- 但是这个索引不是持久化的,仅存在内存中。
5.1 自适应哈希索引的缺点
- 创建代价大
- 更新代价大
- 只适合等值查询,无法做到范围查询。
- 不是很推荐打开。
5.2 查看是否打开了自适应hash索引
show variables like 'innodb_adaptive_hash%'