InnoDB存储引擎
MySQL数据库从5.5版本之后,默认的存储引擎从MyISAM引擎替换成InnoDB存储引擎,其特点是支持事务功能、支持外键、数据锁级别为行级锁、支持MVCC多版本并发控制、支持崩溃后的恢复等特性,越来越满足于对数据并发访问和数据安全性的要求。
一、InnoDB存储引擎的体系结构:
InnoDB后台有多个不同的线程,用来负责不同的任务,主要有:
1)Master Thread:这是最核心的一个线程,主要用来负责将缓冲池中的数据异步刷新到磁盘,以此来保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收等;
2)IO Thread:在InnoDB存储引擎中大量使用了异步IO来处理写IO请求,IO Thread的工作主要负责这些IO请求的回调处理;
3)Purge Thread:事务被提交更新了之后,undo log可能就不再需要了,因此我们需要Purge Thread来进行回收已经使用并分配的undo页,InnoDB支持多个Purge Thread,这样做可以加快undo页的快速回收;
4)Page Cleaner Thread:Page Cleaner Thread是在InnoDB 1.2.x版本中引入的,它的作用就是将之前版本中的脏页的刷新操作都放入到单独的线程中来完成,这样就可以减轻Master Thread的工作以及对用户查询线程的阻塞;
缓冲池:简单来说就是一块临时的内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响,在数据库中进行读取页的操作,首先将从磁盘读到的内存放在缓冲池中,这个过程称为将页"FIX"在缓冲池中,下一次再读取相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接在缓冲池中读取改页内容即可。否则,读取磁盘上的页。对于数据库中的页的修改操作,则首先修改缓冲池中的页,然后再以一定的频率刷新到磁盘上。这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是每一次页发生更新的时候就触发,而是通过一种称为Checkpoint的机制刷新回磁盘的。缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲、自适应哈希索引、InnoDB的锁信息、数据字典信息等。索引页和数据页占缓冲池的很大一部分,在InnoDB缓冲池中的页大小默认为16KB。
缓冲池中的页管理:通常来说,数据库中的缓冲是通过LRU(Latest Recent Used,最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取的页时,将首先会释放LRU列表中尾端的页。在InnoDB引擎找那个,缓冲页的大小默认为16KB,同样使用LRU算法对缓冲池进行管理。稍有不同的是InnoDB存储引擎对传统的LRU算法做了一些优化,在InnoDB的存储引擎中,LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到了LRU列表的midpoint位置。在默认配置下,该位置在LRU列表长度5/8处。midpoint位置可由参数innodb_old_blocks_pct控制。midpoint之前的列表称之为New列表,之后的列表称为Old列表。可以简单的将New列表中的页理解为最为活跃的热点数据;同时InnoDB存储引擎还引入了innodb_old_blocks_time来表示页读取到mid位置之后需要等待多久才会被加入到LRU列表的热端。可以通过设置该参数保证热点数据不轻易被刷出。
二、InnoDB的存储结构
数据库中的记录都是按照行来存储的,但是数据库的读取并不以行为单位,否则一次读取(也就是一次IO操作)只能处理一行数据,效率是非常低下的。因此在数据库中,无论是读一行还是读多行,都是将这些行所在的页进行加载,也就是说,数据库管理存储空间的基本单位是页(Page)。一个页中可以存储多个行记录(Row),同时在数据库中,还存在着区(Extent)、段(Segment)和表空间(Tablespace)。
从图中你可以看到一个表空间中包括了一个或者多个段、一个段中包括了一个或多个区、一个区中包括了多个页、而一个页中可以有多行记录;
区(Extent):是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页,因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB=1MB;
段(Segment):由一个或者多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页);不过在段中要求区与区之间是相邻的。段式数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时就会创建一个表段,创建一个索引时会创建一个索引段;
表空间(Table space):是一个逻辑容器,表空间存储的是对象是段,在一个表空间中可以有一个或者多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等;在InnoDB中存在两种表空间的类型:共享表空间和独立表空间,如果是共享表空间就意味着多张表共用一个表空间,如果是独立表空间就意味着每张表有一个独立的表空间,也就是数据和索引信息都会保存在自己的表空间中,独立的表空间可以在不同的数据库之间进行迁移;
页(Page):如果按照类型划分的话,常见的有数据页(保存B+树节点)、系统页、Undo页和事务数据页等,数据页是我们最常使用的页,表页的大小限定了表行的最大长度,不同DBMS的表页大小不同,在InnoDB存储引擎中,默认页的大小是16KB;数据库I/O操作的最小单位是页,与数据库相关的内容都会存储在页结构里。数据页包括七个部分,分别是文件头(File Header)、页头(Page Header)、最大最小记录(Infimum+supremum)、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer);
三、CheckPoint技术
当我们需要数据库做恢复操作时,为了能够缩短数据库的恢复时间,重做日志中记录了的checkponit的位置,这个点之前的页以及刷新回了磁盘,只需要再对checkpoint之后的重做日志进行恢复。这样就可以大大缩短恢复的时间;当缓冲池不够用时,根据LRU算法,溢出最近最少使用的页,如果页为脏页,强制执行checkpoint,将脏页刷新回磁盘。在InnoDB存储引擎内部,有两种checkpoint。
1、Sharp Checkpoint:只发生在数据库关闭时,将所有的脏页刷新回磁盘,这是默认的工作方式,即参数为innodb_fast_shutdown=1;
2、Fuzzy Checkpoint:在数据库运行时,InnoDB存储引擎内部采用Fuzzy Checkpoint,只刷新一部分脏页;
几种发生Fuzzy Checkpoint的情况:
1)Master Thread Checkpoint:异步刷新,每秒或每10秒从缓冲池脏页列表刷新一定比例的页回磁盘中。因为是
异步操作,所以此时InnoDB存储引擎可以进行其他的操作,用户查询线程不会受阻。
2)FLUSH_LRU_LIST Checkpoint:InnoDB存储引擎需要保证LRU列表中差不多有100个空闲页可供使用,在
InnoDB 1.1.x版本之前,用户查询线程会检查LRU列表是否有足够的空间操作。如果没有,根据LRU算法,溢出
LRU列表尾端的页,如果这些页有脏页,需要进行checkpoint。因此叫flush_lru_list_checkpoint,InnoDB 1.2.x开
始,这个检查放在了单独的进程(Page Cleaner)中进行,这样的好处是减少了Master Thread的压力,也减轻了用
户线程阻塞;
3)Async/Sync Flush Checkpoint:指重做日志不可用的情况下,需要强制刷新页回磁盘,此时的页是从脏页列表
选取的。这种情况是保证重做日志的可用性;InnoDB存储引擎通过LSN(Log Sequence Number)来标记版本,LSN
是8字节的数字,每个页有LSN,重做日志有LSN,checkpoint有LSN;若将已经写入到重做日志的LSN记为
redo_lsn,将已经刷新回磁盘最新页的LSN记为checkpoint_lsn;
4)Dirty Page too much Checkpoint:即脏页数量太多了,需要强制checkpoint来保证缓冲池有足够可用的页,参
数innodb_max_dirty_pages_pct=75表示当缓冲池中脏页的数量占75%时,强制checkpoint,1.0.x之后默认为
75%;
四、InnoDB存储引擎的关键特性
1、插入缓冲(Insert Buffer):一般的情况下,主键是行唯一的标识符,通常应用程序中中行记录的插入顺序是按照主键递增的顺序来进行插入的,因此插入聚集索引一般是顺序地,不需要磁盘的随机读取,因为对于此类情况下的插入,速度还是非常快的(如果主键类是UUID这样的类,那么插入和辅助索引一样,也是随机的)。如果索引是非聚集索引的且不是唯一的,在进行插入操作时,数据的存放对于非聚集索引叶子节点的插入不是顺序地,这时就需要离散地访问非聚集索引页,由于随机读取的存在而导致了插入操作性能下降。这时因为B+树的特性决定了非聚集索引插入的离散型;Insert Buffer的设计,对于非聚集索引的插入和更新操作,不是每一次直接插入到索引页中的,而是先判断插入非聚集索引页是否在缓冲池中,若存在,则直接插入,若不存在,则先放入一个Insert Buffer对象中。数据库这个非聚集的索引已经插到叶子节点,而实际上并没有,只是存放在一个位置,然后再以一定的频率和情况进行Insert Buffer和辅助索引叶子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能;插入缓冲需要满足两个不要条件:索引是辅助索引,索引不是唯一的。辅助索引不能是唯一的,因为在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性,如果去查找肯定又会有离散读取的情况发生,从而导致Insert Buffer失去了意义。
2、两次写(Double Write):在数据库的运行中,可能会出现这么一种场景:当数据库正在从内存向磁盘写一个数据页时,数据库宕机了,从而导致这个页只写了部分数据,这就是部分写失效,它会导致数据丢失,这时是没有办法通过重做日志来恢复的,因为重做日志记录的是对页的物理修改,如果页本身已经被损坏了,重做日志也是无能为力的;从上面的分析中可以知道,在部分写失效的情况下,我们在应用重做日志之前,需要原始页的一个副本数据,两次写就是为了解决这个问题。
两次写需要额外的添加两部分:
1)内存中的两次写缓冲(doublewrite buffer),大小为2MB;
2)磁盘上共享表空间中连续的128页,大小也为2MB;
两次写原理:当刷新缓冲池脏页时,并不直接写到数据文件中,而是先拷贝至内存中的两次写缓冲区,接着从两次写缓冲区分两次写入磁盘共享表空间中,每次写入1MB,完成之后再将两次写缓冲区数据写入到数据文件中。这样就可以解决部分写失效的问题,因为在磁盘共享表空间中已有数据页副本拷贝,如果数据在页写入数据文件的过程中宕机了,在实例恢复时,可以从共享表空间中找到该页副本,将其拷贝覆盖原有的数据页,再应用重做日志即可。
3、自适应哈希索引(Adaptive Hash Index):InnoDB存储引擎会自动监视对表上各索引页的查询,如果观察到建立哈希索引可以带来速度提升,则会自动建立哈希索引,称之为自适应哈希索引,自适应哈希索引是通过缓冲池的B+树的页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引,InnoDB存储引擎会自动根据访问的频率和模式来自动为某些热点页建立哈希索引;自适应哈希索引的前提条件是即对这个页的连续访问模式必须是一样的(联合索引中的查询条件),以该模式访问了100次,页通过该模式访问了N次,其中N=页中记录*1/16。
4、异步IO(Async IO):用户可以在发出一个IO请求后立即再发出另外一个IO请求,当全部IO请求发生完毕后,等待所有的IO操作的完成,这就是异步IO(AIO);AIO的另外一个优势是可以进行IO Merge操作,也就是将多个IO合并为1个IO,这样可以提高IOPS的性能。
5、刷新邻接页(Flush Neighbor Page):当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果其他页中也是脏页的话,那么就一起进行刷新操作,这样做的好处显而易见,通过AIO可以将多个IO写入操作合并为一个IO操作。