MySQL技术内幕-InnoDB引擎

写在前面

本文是根据《MySQL技术内幕-InnoDB存储引擎》阅读而来的总结。该书内容比较充足,但我个人觉得不足的地方在于目录安排得不是很合理,同一个知识点的内容分散在各个章节中,比如重做日志redo log相关的知识,就分散在三个章节中。因此本篇文章将按照我自己的总结来写,一个一个知识点来组织内容。

redo log

重做日志是以事务为单位记录日志的,顾名思义,就是当事务所做的操作的结果丢失之后,重做日志可以重新还原事务的操作结果。

  1. 为什么需要重做日志。简言之就是为什么事务的结果会丢失。我们知道数据库是保存在硬盘中的,而对数据库的操作是需要将硬盘上的数据读取到内存中,然后事务对内存中的数据进行读取修改等操作,而内存中的这些脏数据最终会刷新到磁盘中持久化。那么如果当脏数据还没刷新到磁盘中时,数据库崩了或者是操作系统崩了,那么内存中的脏数据便丢失,事务的结果也就无法持久化到磁盘中。而如果我们记录了redo log,那么就可以使用redo log来将事务的结果恢复。
  2. redo log需要在磁盘上有对应的文件,称为redo log file。这点是必然的,写redo log最终要写到磁盘上的redo log file文件中去才行,不然当数据库或操作系统崩了时redo log也会丢失。
  3. redo log写到redo log file的策略。首先要知道,内存中有redo log buffer,事务生成的redo log会先保存在buffer中,buffer再刷新到磁盘中,而由于操作系统的文件读写策略,内存中的内容也不是每次都直接写进磁盘,而是写进文件系统的缓存中,再有操作系统按时将文件系统缓存写进实际的文件中。因此牢记redo log的写顺序如下:redo log buffer—>file system buffer—>redo log file。redo log的写策略包括以下三种:第一,数据库的主线程会每秒将redo log buffer写进redo log file;第二,当redo log buffer占用超过一半时,redo log buffer也会被写进redo log file;第三,默认情况下事务提交时,先将redo log buffer写进redo log file再提交事务。三个策略同时在执行(而不是三选一)。而这第三点是可以通过配置修改的,除了默认情况外还有两种选择:第一,事务提交时不理会redo log buffer,此时redo log buffer的写就交给了前两种策略了;第二,事务提交时仅仅将redo log buffer写到file system buffer,将写进redo log file的任务交给操作系统(至于操作系统何时做这件事,数据库就管不着而且不知道了)。
  4. 通过以上分析,我们可以得知,redo log的写是随着事务进行的过程中不断完成的,而不是仅在事务提交时一次性把所有的redo log写入磁盘。另外,事务提交时redo log buffer到三种写配置的时间效率也不一样,显然不理会redo log buffer的提交效率最高,但是当数据库崩溃时就可能会丢失一部分redo log;仅写到file system buffer的效率第二,数据库崩溃不会导致redo log丢失,但操作系统崩溃就会;默认情况的效率第三,但是redo log能够及时写进磁盘不会丢失。
  5. redo log是物理日志,它记录的是内存中的页的实际变化,而不是记录操作。因此使用redo log来恢复事务结果是比较快的,比重新执行事务快。

两次写

  1. redo log适用于当脏页还没刷新到磁盘时数据库就崩溃了,当恢复的时候就能从磁盘中读取旧的数据页然后应用重做日志进行恢复。但是,如果脏页刷新到中途的时候数据库崩了呢?MYSQL中默认一个页大小为16K,但是文件系统IO的最小单位只有4K,因此一个脏页刷新到磁盘中需要4次IO,那么就有可能在刷新中途发生失败,那么此时磁盘中的数据页就是损坏的,新旧数据掺杂在一起,redo log失去意义。为了解决这种部分写失效问题,MYSQL实现了两次写策略。(我认为如果是全部写失效,那么redo log就能恢复数据)
  2. 两次写就是脏页刷新时先保存页的副本,然后再更新到磁盘中的数据库。我们知道脏页的更新是离散的,也就是一个页一个页地更新到磁盘中,速度是比较慢的,那么保存页的副本必然不能采用这种方式,不然也很容易破坏持久性,于是需要采用一种连续型的磁盘写入策略,方法就是先将脏页复制到内存中一块连续的doublewrite buffer(2MB),再将这块连续的区域分两次一次1MB地写入磁盘共享表空间的doublewrite,然后马上调用fsync函数,将文件系统缓冲同步到磁盘中,这样的连续写策略开销是比较小的,写完之后再更新一个一个的脏页。这就是两次写的含义。
  3. 有了两次写,那些部分写失效的页便能从doublewrite中恢复。而如果脏页能够全部写完成的话,这个doublewrite便能重用了。至于在写之前就崩溃的脏页来说,则使用redo log完成数据恢复。

undo log

  1. undo log是逻辑日志,可以理解为记录的是DML语句,即insert和update,由于MySQL中记录的delete语句只是将delete_mark标记为真,因此delete语句其实和update没有区别。(被标记delete_mark的记录会由purge线程完成,如果该记录不再被任何事务引用,那么就可以真正删除该记录)。
  2. undo log在事务执行的过程中产生,本来是一个事务申请一个undo page然后将undo log写进这个undo page中,但是如果该undo page的使用空间小于3/4,那该undo page就可以被重用,也就是说其它事务也可以把自己的undo log写在这个undo page中而不用重新申请(这样可以减少内存占用),这样就表示一个undo page可以记录多个事务的undo log。多个undo page又组成一个undo log segment,1024个undo log segment组成一个rollback segment,MySQL支持128个rollback segment。
  3. undo log是实现MVCC的关键,事务进行一致性非锁定读时,通过读取与自己的版本号对应的快照数据使得事务执行过程中读取结果一致,而实际上快照数据是通过读取undo log然后进行计算得到的。每一行记录都有个隐藏字段,回滚指针,指向最新提交的事务的undo log,而undo log按照事务提交的顺序进行连接,因此当前事务通过读取undo log列表就能读到与自己的版本号对应的undo log然后计算得到快照数据。该列表称为history list(书中没有明说,不过我觉得应该每行记录都有一个history list)。
  4. MYSQL中专门的purge线程负责清除无用的undo log,以便重用undo page。history list中的undo log是逻辑连续的而不是物理连续的,因此如果一个一个undo log进行清理的话那效率是很低的,因此purge线程采用如下策略:先从history list的尾端找到可以被清理的undo log,然后在该undo log所在的undo page中寻找可以被清理的其它undo logs,由于同一个undo page中的undo logs自然是物理连续的,因此连续清理同一undo page内的多个undo logs效率较高,避免了大量的随机读取。如果该undo page清理之后的使用空间小于3/4,那么该page就可以被重用。每次purge操作需要清理的undo page数目可由用户指定。
  5. 非常重要的一点,事务在undo log segment中分配页并写入undo log这个过程同样需要写入重做日志redo log file,因为undo log也需要持久性的保护。
  6. insert undo log在事务提交后可以直接删除,不需要purge。

插入缓冲Insert Buffer

看了前面三个知识点,有没有发现MYSQL的很多优化,解决的问题其实都跟磁盘有关,要么是磁盘读写速率与内存读写速率之间存在差异,要么是机械磁盘顺序读取和离散读取的性能差异。插入缓冲同样也是针对磁盘,具体来说,是解决插入辅助索引时的磁盘离散读取问题。

  1. 对于一张表,MYSQL是以主键为索引进行组织的,也就是我们说的聚集索引,索引即数据,而表除了主键之外还可以为其它的键也建立索引,称为辅助索引,当我们以这些辅助索引的键作为查询条件时,就可以使用辅助索引查找数据。我们知道,辅助索引的叶子节点并不存放行记录,而是存放一个书签,用来指向该行记录的主键,然后拿着这个主键再去聚集索引中查找行记录。
  2. 在插入数据时,一般是按照主键大小顺序插入的,因此对于聚集索引来说,插入叶子节点也是顺序插入的,很少涉及其它非连续的数据页,那么也就不需要离散读取磁盘,就算要读取(内存中无缓存)也是顺序读取的,速度快。但是对于辅助索引来说,值并不是连续的,那么如果待插入的辅助索引页不在内存中,就需要从磁盘中加载,而由于值不是连续的,就可能需要离散读取很多个页,速度慢,因此Insert Buffer就派上用场了。
  3. 总的来说,如果待插入的辅助索引页不在内存中,那么先把辅助索引项插入到内存中的Insert Buffer,这样当将Insert Buffer与磁盘上的辅助索引合并时就能将多个连续的索引项一次性插入,减少了离散读取次数,提高了性能。
  4. 除了要求是辅助索引之外,还要求该索引不能是唯一的,因为如果是唯一的,那么还需要保证其唯一性,就需要额外的操作去查找索引页判断其唯一性,可能涉及离散读取,那就违背了插入缓冲的初衷。
  5. Insert Buffer的数据结构也是一棵B+树。MYSQL中所有表共享同一棵Insert BufferB+树,因此将辅助索引项插入时就需要记录下待插入的辅助索引页,即(space, page_no),因此Insert Buffer的非叶子节点记录的就是这个信息,然后叶子节点除了这信息之外,还需要记下序号和记录本身,序号指的是在同一个(space, page_no)中,该记录是第几个插入的。这样看来,Insert Buffer的叶子节点就是按照(space, page_no)进行排序,再按照序号排序的。
  6. 在介绍何时合并Insert Buffer之前,先看Insert Buffer Bitmap数据结构,它可记录16384个磁盘上的辅助索引页的可用空间,如果少于1/32就标记为无可用空间,此外还记录了该页是否有记录缓存在插入缓冲中。合并Insert Buffer的三种策略之一,就是当Insert Buffer Bitmap监测到如果合并之后的辅助索引页的可用空间会小于1/32,那就强制从磁盘中读取辅助索引页然后和Insert Buffer合并,保证合并的成功。另外两种策略分别是:当执行了SQL语句将一个新的辅助索引页读进内存中后,根据Insert Buffer Bitmap的标记,如果该页有记录缓存在Insert Buffer中,那么就进行合并,理所当然;主线程每秒和每10秒都会进行一次合并,先随机挑选Insert Buffer中的一个页,根据space以及要合并的页的数目顺序读取一定数量的页进行合并,保证公平性。如果合并时发现目标表已经被删除,那么就丢弃该表的所有Insert Buffer项。
    以上第五点关于Insert Buffer叶子节点的排序状况存在疑问,以及第6点中合并Insert Buffer的第一个策略也存在疑问,该策略是能保证合并成功,那合并之后Insert Buffer Bitmap就会将该标记为无可用空间,那然后呢?后面如果还有需要插入到该页的记录怎么办?该页会分裂成两个吗?小伙伴们如果有答案的话欢迎写在评论里。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值