MySQL 探秘 (三):InnoDB 的内存结构和特性
备注:公众号原名张狗蛋的技术之路,现已改名为程序员历小冰
常言说得好,每个成功男人背后都有一个为他默默付出的女人,而对于 MySQL 来说,这个“人”就是 InnoDB 存储引擎。
MySQL 区别于其他数据库的最为重要的特点就是其插件式的表存储引擎。而在众多存储引擎中,InnoDB 是最为常用的存储引擎。从 MySQL5.5.8 版本开始,InnoDB 存储引擎是默认的存储引擎。
InnoDB 存储引擎支持事务,其设计目标主要面向在线事务处理(OLTP)的应用。其特点是行锁设计、支持外键,并支持非锁定读,即默认读操作不会产生锁。
InnoDB 通过使用多版本并发控制(MVCC)来获取高并发性,并且实现了 SQL 标准的 4 中隔离级别,默认为 REPEATABLE 级别。同时,使用一种被称为 next-key-locking 的策略来避免幻读现象的产生。除此之外,InnoDB 存储引擎还提供了插入缓冲(insert buffer)、二次写(double write)、自适应哈希索引(adaptive hash index)、预读(read ahead)等高性能和高可用的功能。
上图详细显示了 InnoDB 存储引擎的体系架构,从图中可见,InnoDB 存储引擎由内存池,后台线程和磁盘文件三大部分组成。接下来我们就来简单了解一下内存相关的概念和原理。
缓冲池
InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。但是由于 CPU 速度和磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池记录来提高数据库的的整体性能。
在数据库中进行读取操作,首先将从磁盘中读到的页放在缓冲池中,下次再读相同的页中时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。
对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为 CheckPoint 的机制刷新回磁盘。
所以,缓冲池的大小直接影响着数据库的整体性能,可以通过配置参数 innodb_buffer_pool_size 来设置。
具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo 页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB 存储的锁信息(lock info)和数据字典信息(data dictionary)。
在架构图上可以看到,InnoDB 存储引擎的内存区域除了有缓冲池之外,还有重做日志缓冲和额外内存池。InnoDB 存储引擎首先将重做日志信息先放到这个缓冲区中,然后按照一定频率将其刷新到重做日志文件中。重做日志缓冲一般不需要设置的很大,该值可由配置参数 innodb_log_buffer_size 控制。
数据页和索引页
Page 是 Innodb 存储的最基本结构,也是 Innodb 磁盘管理的最小单位,与数据库相关的所有内容都存储在 Page 结构里。Page 分为几种类型,数据页和索引页就是其中最为重要的两种类型。
插入缓冲(Insert Buffer)
我们都知道,在 InnoDB 引擎上进行插入操作时,一般需要按照主键顺序进行插入,这样才能获得较高的插入性能。当一张表中存在非聚簇的且不唯一的索引时,在插入时,数据页的存放还是按照主键进行顺序存放,但是对于非聚簇索引叶节点的插入不再是顺序的了,这时就需要离散的访问非聚簇索引页,由于随机读取的存在导致插入操作性能下降。
InnoDB 为此设计了 Insert Buffer 来进行插入优化。对于非聚簇索引的插入或者更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非聚集索引是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个 Insert Buffer 中。看似数据库这个非聚集的索引已经查到叶节点,而实际没有,这时存放在另外一个位置。然后再以一定的频率和情况进行 Insert Buffer 和非聚簇索引页子节点的合并操作。这时通常能够将多个插入合并到一个操作中,这样就大大提高了对于非聚簇索引的插入性能。
两次写(Double Write)
如果说 Insert Buffer 给 InnoDB 存储引擎带来了性能上的提升,那么 Double Write 带给 InnoDB 存储引擎的是数据页的可靠性。
如上图所示,Double Write 由两部分组成,一部分是内存中的 double write buffer,大小为 2MB,另一部分是物理磁盘上共享表空间连续的 128 个页,大小也为 2MB。在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是通过 memcpy 函数将脏页先复制到内存中的该区域,之后通过 doublewrite buffer 再分两次,每次 1MB 顺序地写入共享表空间的物理磁盘上,然后马上调用 fsync 函数,同步磁盘,避免操作系统缓冲写带来的问题。在完成 doublewrite 页的写入后,再讲 doublewirite buffer 中的页写入各个表空间文件中。
如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB 存储引擎可以从共享表空间中的 doublewrite 中找到该页的一个副本,将其复制到表空间文件中,再应用重做日志。
重做日志(Redo Log Buffer)
当缓冲池中的页的版本比磁盘要新时,数据库需要将新版本的页从缓冲池刷新到磁盘。但是如果每次一个页发送变化,就进行刷新,那么性能开发是非常大的,于是 InnoDB 采用了 Write Ahead Log 策略,即当事务提交时,先写重做日志,然后再择时将脏页写入磁盘。如果发生宕机导致数据丢失,就通过重做日志进行数据恢复。
InnoDB 存储引擎会首先将重做日志信息先放入重做日志缓冲中,然后再按照一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置得很大,因为一般情况每一秒钟都会讲重做日志缓冲刷新到日志文件中。可通过配置参数 innodb_log_buffer_size 控制,默认为 8MB。
除了每秒刷新机制之外,每次事务提交时重做日志缓冲也会刷新到日志中。InnoDB 是事务的存储引擎,其通过 Force Log at Commit 机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,然后事务的提交操作完成才算完成。InnoDB 的写入机制大致入下图所示。
为了确保每次日志都写入到重做日志文件,在每次讲重做日志缓冲写入重做日志后,必须调用一次 fsync 操作,将缓冲文件从文件系统缓存中真正写入磁盘。
可以通过 innodb_flush_log_at_trx_commit 来控制重做日志刷新到磁盘的策略。该参数默认值为 1,表示事务提交必须进行一次 fsync 操作,还可以设置为 0 和 2。0 表示事务提交时不进行写入重做日志操作,该操作只在主线程中完成,2 表示提交时写入重做日志,但是只写入文件系统缓存,不进行 fsync 操作。由此可见,设置为 0 时,性能最高,但是丧失了事务的一致性。
自适应哈希索引(Adaptive Hash Index)
InnoDB 会根据访问的频率和模式,为热点页建立哈希索引,来提高查询效率。InnoDB 存储引擎会监控对表上各个索引页的查询,如果观察到建立哈希索引可以带来速度上的提升,则建立哈希索引,所以叫做自适应哈希索引。
自适应哈希索引是通过缓冲池的 B+树页构建而来,因此建立速度很快,而且不需要对整张数据表建立哈希索引。其有一个要求,即对这个页的连续访问模式必须是一样的,也就是说其查询的条件(WHERE)必须完全一样,而且必须是连续的。
锁信息(lock info)
我们都知道,InnoDB 存储引擎会在行级别上对表数据进行上锁。不过 InnoDB 也会在数据库内部其他很多地方使用锁,从而允许对多种不同资源提供并发访问。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。关于锁的具体知识我们之后再进行详细学习。
数据字典信息(Data Dictionary)
InnoDB 有自己的表缓存,可以称为表定义缓存或者数据字典。当 InnoDB 打开一张表,就增加一个对应的对象到数据字典。
数据字典是对数据库中的数据、库对象、表对象等的元信息的集合。在 MySQL 中,数据字典信息内容就包括表结构、数据库名或表名、字段的数据类型、视图、索引、表字段信息、存储过程、触发器等内容。MySQL INFORMATION_SCHEMA 库提供了对数据局元数据、统计信息、以及有关 MySQL server 的访问信息(例如:数据库名或表名,字段的数据类型和访问权限等)。该库中保存的信息也可以称为 MySQL 的数据字典。
版权声明: 本文为 InfoQ 作者【程序员历小冰】的原创文章。
原文链接:【http://xie.infoq.cn/article/724aa99813dc0480dfeeda1d8】。文章转载请联系作者。
评论