InnoDB存储引擎之事务和可靠性

  最近读了姜承尧的《MySQL技术内幕 InnoDB存储引擎》,感觉此书乃是为数不多的国产数据库经典教材。虽然说到数据库大家首推《高性能MySQL》,不过感觉这本书的知识体系被梳理的更加系统条理一些,而且作者的数据库知识渊博、功力深厚,读完确实让人有一种欲罢不能的快感。接下来的几篇文章,将是结合这本书对InnoDB存储引擎之事务、锁、索引等知识进行相关的梳理,虽然不会去做存储引擎,但是InnoDB是事实上被用的最多的事务级存储引擎,了解一下知其所以然才能对网上那些天花乱醉的教程指南做到心中有数,才能更好的去使用MySQL,而不是把MySQL当做一个黑盒一样把玩。
  总体来说,感觉MySQL从5.5开始,在可靠性、性能、维护性这些方面做了相当大的改进,虽然人家说新版本的数据库资源消耗比之前的版本要增加很多,但是在硬件技术突飞猛进的今天这些消耗都基本可以被忽略的。书中的内容在后面翻阅资料发现,一些部分和MySQL手册雷同,But who cares … ^_^

1. 事务概述

  数据库事务的意义在于保证把数据库从一种一致状态迁移到另外一种一致状态,InnoDB中的事务是完全支持A(Atomicity)、C(Consistency)、I(Isolation)、D(Durability)特性的:事务隔离性采用锁机制来实现,原子性和持久性通过数据库的Redo log来实现,Undo log则用来保证事务的一致性。
  Redo log的内容是页物理级别的日志,比如在某个数据页的偏移某处的值为多少,所以该日志是所以这种日志格式是满足操作幂等性要求的。Redo log基本都是顺序写的,在数据库运行的时候不需要对Redo log文件进行读取操作,只有在数据库启动或者特殊的情况下才需要用到该记录来执行重做恢复操作。
  Undo log可以使行记录回滚到某个特定的版本,一般是按照每一行记录的逻辑日志,除了用来执行事务回滚外,还可以用来支持MVCC实现的非锁定一致性读功能。Undo log在数据库运行时候是会进行大量随机性读写操作的。
  MySQL5.6提供了只读事务优化的机制,这种情况下对于使用START TRANSACTION READ ONLY或者在AutoCommit激活时候只进行SELECT查询的事务,InnoDB存储引擎会将其识别为只读事务,不会派发事务ID,也不会分配变更数据所需的内存结构体,整体优化后可以得到更快的执行性能。

2. 页缓冲和刷新

  和大多数系统一样,MySQL也使用内存作为服务和低速磁盘之间的桥梁,在数据库读取页的时候,会将从磁盘中读到的页存放在缓冲池当中,以便下次再次访问相同页的时候直接从缓冲返回;同时对页的修改,也是先修改在缓冲池中的页,这个时候的页就被称为脏页,然后需要以一定的频率来刷新到磁盘上,这是通过一种称为CheckPoint的机制来实现将数据刷回到磁盘上的。虽然我们希望数据库所有的页都能缓存在内存中,但是磁盘的空间总是有限的,当新的数据页读取进来需要缓冲但是内存空间不足的时候,就需要使用LRU算法淘汰末尾不常用的缓冲页了。通过SHOW ENGINE INNODB STATUS可以查看存储引擎的工作状态,这里的Buffer pool hit rate通常不应该低于95%。
  InnoDB有一个开关innodb_flush_neighbors,表示存储引擎在刷新一个脏页的时候,会检测该页所在的区(Extend)的所有页,如果还有其他脏页也一并进行刷新,对于传统硬盘建议打开,对于固态硬盘可以将该特性关闭。  新版本数据库对于数据刷新的任务,也从MasterThread独立出PageCleanerThread来处理了。

3. DoubleWrite

  InnoDB通过DoubleWrite技术实现数据页可靠性的保证。
  比如在突然宕机的时候,可能InnoDB正在写入的某个页只写了一部分,因为InnoDB的页大小通常是16K,而磁盘操作一般是512字节的块单位操作,这种不完全更新的情况称为部分写失效。在数据页被部分写入破坏了,此时通过Redo log在不完整的页上做恢复也是没有意义的,所以这种情况就需要使用原始完整的页副本先对写入失效的页面进行恢复,然后再使用Redo log进行重做以恢复数据的一致性。在数据库启动的时候,InnoDB存储引擎总是将DoubleWrite缓冲区中的内容和数据文件的页面全部进行比较,如果存在不同的页面,就将DoubleWrite中的内容复制到数据文件的页面。
  DoubleWrite有两部分组成:包括内存中2M大小的DoubleWrite buffer,以及磁盘上共享表空间中连续128页(2个区),其大小同样是2M。每当InnoDB工作线程在对缓冲池中的脏页进行刷新的时候,该操作不是直接写磁盘,而是通过memcpy将脏页先复制到DoubleWrite buffer,然后再分两次、每次1M进行物理磁盘的顺序写入,并且写完后立马调用fsync同步持久化到磁盘。因为这里的两次磁盘写是连续顺序写,所以这个磁盘IO开销不大,一旦DoubleWrite固化到物理磁盘上之后,就可以将DoubleWrite buffer中的页慢慢地更新到表文件中去,这里的写则是随机离散写了。
  这里的DoubleWrite算是对HDD硬盘特性实现的一种保证数据完整性的机制,但是对于SDD这类的介质,随机I/O和顺序I/O的成本是相同的,所以FusionIO这类的SSD通过特殊的API,能保证InnoDB存储引擎向磁盘写入16K数据的时候,是以原子的方式执行整个操作的——要么16K全部成功写入磁盘,要么一点也不写入磁盘,支持这种特性下,就可以关闭DoubleWrite机制了,能够大大降低SSD的写入量。

4. Redo log

  Redo log用来实现事务的持久性,其由内存中的Redo log buffer和磁盘上的Redo log file共同组成,其目的就是在于数据库异常时如果还有数据页没有刷到磁盘中去,数据库在启动的时候就可以根据Redo log进行重做以实现数据恢复。InnoDB的事务机制通过Write Ahead Log机制来实现事务的持久性,即在默认情况下每当事务执行提交的时候,必须先将该事务所有日志写入到Redo log file中保证持久化,而且每次执行写入后InnoDB都会调用一次fsync刷盘操作以保证日志可靠落盘。Redo log file的写入在任何时候都是可能的,除了上面说的在事务提交的时候会将Redo log buffer中的重做日志写入到Redo log file,触发这个操作的还有:MasterThread每1s会主动将执行这个操作;当重做日志缓冲区的剩余空间小于1/2的时候,所以Redo log不仅仅是在事务提交的时候才刷盘写入。
  Redo log记录的是物理操作日志,所以每个事务会具有多个Redo log条目,而且多个事务可以并发地操作数据页,因此Redo log的写入是并发的,他们会显示出各个事务交错得对数据修改的操作,而且即使事务被回滚了,其Redo log也不会被删除,仍然和其他事务一起保存在一个连续的日志序列中。为了保证写磁盘的性能,Redo log都是先组织在Redo log buffer中,然后在需要写入的时候执行大批量的写入操作,而且Redo log file是预先分配在一个连续空间中并被循环使用的,而且写的过程都是执行顺序追加写入。Redo log buffer和Redo log file都是以512字节的块组织管理的,每个块包含12字节头和8字节尾,所以有效负载为492字节,由于块大小和磁盘扇区一致,所以Redo log的写入可以保证原子性,因而不需要使用像数据页刷盘时候用到的DoubleWrite技术的支持,当数据页被刷到磁盘上持久化之后,对应的Redo log占用的资源就可以被释放后被重新使用了。通常Redo log file的大小设置为8~16M就可以了,如果TPS很高的话可以设置为32M,不需要将该文件尺寸设置的很大。
  对于数据库需要使用Redo log进行恢复的时候,他会取CheckPoint之后的日志进行重做。前面说到那些没有提交的日志也存在于Redo log之中了,对此InnoDB的做法是,将Undo log也作为数据更改保留在Redo log中,并一同固化在Redo log file中,在恢复的时候对于没提交的事务(回滚)使用Undo log再进行恢复,这样就保证了数据的一致性了。

5. CheckPoint机制

  数据库CheckPoint机制的设定,可以解决如下问题:a. 当数据库异常需要恢复的时候,就只需要将CheckPoint之后的日志进行重做就可以了,因为能够保证之前的数据页都刷新到磁盘了,既CheckPoint之前的数据都是完整的;b. 当缓存池不够用的时候,数据库需要根据LRU算法淘汰部分缓存,那么被淘汰的缓存的脏数据页就会被强制刷新到磁盘;c. 当前系统对Redo log file的设计都是循环使用的,当需要释放Redo log file空间的时候,也会强制执行CheckPoint。
  InnoDB数据库对CheckPoint的执行具有以下情况:
  a. MasterThread
  新版本中主线程会根据当前数据库是否活跃来确定每1秒或者每10秒执行一些周期性的任务,在周期性的任务中,会选择从缓冲区的脏页列表中刷新一定比例的页回磁盘上去。
  b. Flush_LRU_list and Async/sync Flush
  InnoDB需要确保LRU列表中需要有差不多100个空闲页面可供使用,当然这个数目也不能过大,因为我们总是想要充分地使用缓存来提高系统的性能。现在PageCleanerThread会检查可用缓存数目是否够,否则会将LRU中尾端的页面移除淘汰,而如果这些页面是脏的话,就会执行CheckPoint。此外,Async/Sync Flush是指Redo log file不可用的情况,此时也会强制将一些页刷新回磁盘中,此时脏页直接从脏页列表中选择的,执行CheckPoint以保证Redo log file的循环可用性,该操作也是在PageCleanerThread中执行的。
  c. Dirty Page too much
  当缓存中脏页的比率达到75%之上,也是会触发CheckPoint执行的。

6. Undo log

  事务需要进行回滚操作时候就需要Undo log,当数据库事务由于某种原因执行失败了,或者用户手动使用ROLLBACK请求事务回滚,则可以使用Undo信息将数据回滚到之前的样子,所以对数据做出修改之前都需要先保存记录一份到Undo log中。
  Undo log记录的内容是逻辑日志,通常是针对数据行来记录的,因为数据库是一个高度并发访问的系统,所以在回滚和恢复的时候不能将一个页简单变为事务开始的样子,这样会影响到其他事务正在进行的工作,因此在实现上Undo的回滚操作实际上是执行与先前事务相反的操作,比如INSERT->DELETE、UPDATE->~UPDATE。除了支持回滚功能之外,Undo另外一个重要功能是实现MVCC,这种特性下当用户需要读取一行记录的时候,若该记录已经被其他事务占用,则当前事务可以通过Undo读取之前的行版本,实现非锁定一致性读取。
  Undo存放在共享表空间中数据库内部的一个特殊段中,InnoDB通过Rollback segment进行Undo的管理,每个Rollback segment可以记录1024个Undo log segment,而真正的Undo log页就是在这个Undo log segment中申请得到的,这也就是说MySQL支持最大1024个在线事务数。此外Undo log的产生也会伴随着Redo log的产生,因为Undo log也需要持久性的保护。
  当事务执行完毕提交后,数据库并不能马上删除回收Undo log和Undo log页,因为此时还可能有其他的事务需要通过Undo log来得到行记录之前的版本(一致性的要求),所以当事务提交的时候是先将Undo log放到一个链表当中,是否可以删除Undo log及Undo log页需要由其他的线程来异步的判断和执行。在老版本中该任务是MasterThread来完成的,而MySQL5.5的时候引入了可以设置创建单独的PurgeThread,而MySQL5.6允许设置创建多个Purge线程。

7. Binlog

  和上面的日志不同的是,Binlog是由MySQL数据库上层(而非存储引擎)产生的,MySQL数据库中任何存储引擎对于数据库的更改都会产生Binlog,Binlog记录的也是逻辑性的数据库的更改操作,在每次执行事务提交的时候一次性的写入到文件里面去。

本文完!

参考