mysql-innodb初步了解(一)
mysql结构
数据库与实例
数据库跟数据库实例是有区分的,平时我们所说的数据库其实是指数据库实例。
数据库:物理操作系统文件或者其他文件集合。对于mysql-innodb来说,就是每个data目录下的数据库目录下的frm,MYD,MYI,idb等格式文件
数据库实例:MYSQL数据库的后台线程以及一个共享内存区域,数据库实例才是真正操作数据库文件的,业务是与数据库实例交互。
数据库架构
数据库的架构如下:
主要包含以下几个部分组成:
- 连接池组件:管理数据库的连接
- 管理服务与工具组件:数据库的管理工具
- SQL接口组件:提供SQL的接口
- 查询分析器组件:对SQL语句进行分析
- 优化器组件:对SQL语句进行优化,得到执行计划
- 缓存组件:数据库的缓存
- 插件式存储引擎:存储引擎
- 物理文件:数据库文件
插件式存储引擎是mysq的特点,可以根据不同的场景选择不同的引擎
内存管理
mysql中是内存+文件的结构来提供数据服务的,最终数据是持久化到文件。innodb引擎通过若干线程维护mysql在内存中的缓存页(大的缓存池),并且保证缓存数据的正确性与可靠性。
一般内存作为数据缓存是用来加速数据读写的,所以缓存+文件这种架构所面临的问题mysql也会有。针对mysql的思考,提出以下几个问题:
- 数据在缓存中是怎么组织管理的,数据结构是怎么样的?
- 查询场景下,缓存数据没有是如何从磁盘文件中加载的,怎么保证缓存的命中率?
- 对于insert,update语句,一般都是只更新缓存,异步方式更新数据库,mysql是如何实现异步更新的?
- 因为缓存跟数据库都保存数据,那么必然会出现缓存跟文件数据不一致的场景,那么mysql是如何保证数据的正确性?尤其是在异常场景下是怎么做到数据的恢复的?
- 因为缓存与磁盘的IO性能差异,innodb是如何保证高性能IO?
对于上面的问题,首先了解下内存数据的组织结构。内存中主要是数据缓存池与重做日志缓存池(redo buffer)还有一些额外内存池。缓存池中的数据页主要包括索引页,数据页,change buffer,自适应hash索引,锁信息等等。数据是以页为单位的,页的大小为16K,在内存中的缓存页大小是16K。结构如下:
引擎是通过LRU算法管理这些内存数据页的,下面介绍下LRU算法
LRU算法
引擎是通过LRU算法管理这些内存数据页。首先数据内存页有三种状态:
- free,该内存页没有被使用,空闲页
- clean,该内存页被使用,但是没有被修改
- dirty,该内存页被使用,也被修改
引擎维护着几个列表对应的上面的不同的数据页。LRU列表,Free列表,Flush列表分别管理不同的状态的内存页。Free列表用来管理没有被使用的内存页,所有磁盘数据加载到内存中必须要先从Free内存页中申请资源,申请到的内存页从Free列表中移除,挂载到LRU列表中。
当我们有数据修改时,如果对应的数据页在LRU中,则直接修改,此时该数据页就变成脏页,Flush列表中就会把这个页加进去,Flush列表中是按脏页的时间排序的。Flush列表是用来维护脏页的。作用是为了后面把脏页更新到磁盘提供列表,有多重时机会让脏页从LRU刷新到Flush列表中。
注意FLush列表是维护脏页的,LRU是管理缓存中的数据页,LRU中有脏页跟干净页,这两个列表数据有重叠,因为两者是不同作用的列表。
LRU数据类别长度是有限的,因为内存是有线的,当Free列表为空后,再加载数据到缓存,只能把LRU中的数据置换出来,至于置换哪些数据,LRU采用冷热数据分离方式来置换。LRU列表中的数据按照一定比例(3:5)分成冷数据跟热数据,新数据加入到LRU列表时,并不是直接加到热端,而是加入到中间位置,这么做是为了避免那种全表扫描数据的场景,把真正比较热的数据从LRU列表中挤出去,提高了缓存的命中率,冷数据放在链表后端,当需要发生置换数据的时候,就是把尾部的冷数据页置换(这些数据必须已经刷新到磁盘的clean的)。
redo log(重做日志)
对于DML操作内存数据页的时候,并不是直接操作数据页,而是先写redolog(重做日志),然后再写数据,这就是WAL(write-ahead-logging)。引擎内存中有一个重做内存缓存池。重做日志是为了保证DB写入缓存的可靠性。如果写数据页的时候发生故障导致没有写入成功,那么就可以从redo log文件中恢复该页的记录。引擎中对于redolog也是有缓存的,对于redolog刷新到磁盘也是异步的。那么如果redolog写入发生故障怎么办?这里引擎提供了一个配置项innodb_flush_log_at_trx_commit,指定何时将redolog刷到磁盘,有三个值0,1,2。默认是1,0 表示每秒将‘log buffer’同步到‘os buffer’,然后同步到重做日志文件;1表示当提交事务前,同时刷新重做日志文件;2表示每次事务操作提交后,将‘log buffer’同步到‘osbuffer’,每秒从osbuffer中刷到重做日志文件,0跟2都是一部的,速度快,但是会存在数据丢失的可能,1是同步的,在commit之前刷入会进行写入,虽然速度慢,但是可靠,如果在刷入前宕机,此时没有提交事务,数据可以丢,在刷入后宕机,就可以用redolog恢复。
重做日志缓存不需要很大,是指定大小的两个文件,而且是循环使用的。当脏页刷到磁盘后,对应的redolog会被删掉,因为redolog是顺序写的,所以性能很高。
重做日志会在三种场景下由buffer写入到file:
- master thread每秒将redolog buffer部分刷入file
- 事务commit前会将redolog buffer刷入file
- 当redolog buffer 不足一半时,会强制刷新buffer到file
后面介绍整个流程时,会介绍redolog的使用原理。
LSN(Log sequence number)
LSN,是日志序列号,引擎是通过LSN来标记版本号的,LSN是8字节数字,每个页有LSN,重做日志也有LSN,有下面的四个值
Log sequence number 19417196
Log flushed up to 19417196
Pages flushed up to 19417196
Last checkpoint at 19417187
- Log sequence number(LSN1) 当前内存中最大的LSN,每次操作一个数据页,对应的LSN都会更新,也可以认为是redolog的最新的LSN
- Log flushed up to (LSN2) 当前写入到redolog文件的LSN
- Pages flushed up to(LSN3) 下次要做checkoutpoint的数据页的LSN
- Last checkpoint at(LSN4) 最后一次刷新数据到磁盘的LSN
以上四个SLN的关系是依次变小的LSN1>=SLN2~LSN3>=LSN4
- LSN1~LSN2:这两个数字之间的的操作是指那些在事务中产生的redo log buffer,注意这部分是在缓存中的,如果此时系统故障或者重启,这部分的redolog没法恢复,但是不影响,因为一般系统会配置innodb_flush_log_at_trx_commit=1,这样在commit之前,对数据的修改不会被保存,丢失的是没有被commit的事务操作。另外补充说下,redolog buffer中的LSN对应的transID肯定是没有commit,但是transID对应的事务没有提交,不代表对应的redolog全部在buffer中,还有可能是在redolog file中,因为redolog buffer写入file不仅仅commit会强制刷,master thread等都会执行。
- LSN2~LSN4:这两个之间是redolog文件中还没有执行刷盘的数据页对应的LSN,这部分的LSN就是可以用来恢复没有保存到磁盘的数据修改记录部分。
- LSN3~LSN4:这两个之间是数据是在缓存中已经修改,但是还没有刷新到磁盘的数据记录,如果出现宕机,这部分数据是会丢失的,这部分丢失的数据就是依靠redolog file来恢复的
- LSN2–LSN3:这两个之间没有必然关系,LSN2是redolog的,LSN3是数据页的
checkpoint机制–innodb数据保存原理
checkpoint机制是基于这样的考虑:我们mysql内存跟磁盘数据的差异是可以通过redolog来恢复的,如果redolog无限大,那么我们内存数据不需要保存到磁盘,即使宕机,也可以从redolog恢复;但是redolog是有限的,所以我们要定期把内存数据同步到磁盘上,这个定期同步的机制就是checkpoint(当然不仅是redolog有限,还有一个原因是当数据量很大,对应redolog就会很大,恢复要很久,所以不可取)
在mysql中,对于insert、update、delete等操作,是直接操作缓存的,此时缓存数据跟磁盘文件数据不一致,变成了脏页,而因为上面分析,我们需要定期刷新到磁盘。引擎中以下几种场景会将脏页刷新到磁盘
- 数据库停止前,会强制刷新脏页到磁盘
- matser主线程定时任务刷新部分脏页到磁盘(百度innodb master线程,了解mysql 的几个IO线程工作原理)
- 空闲页不足时刷新(引擎规定要有100左右的空闲页,如果不足,则把LRU尾部移除刷新,如果是脏页,则执行刷新)
- 脏页太多。当内存中脏页太多(动态过程,说明master线程刷赶不上脏页增加速度)达到一个比例阈值后,需要强制刷新,这个刷新会根据脏页比例来决定是用同步还是异步刷新(新版引擎是用一个新的线程执行)
- 重做日志不可用(下面介绍重做日志)
当一个增删改请求到引擎时,引擎会根据WAL原理先写入redolog buffer,此时对应LSN是最大的为1(LSN1=1),然后将这个修改动作执行到对应的数据页,那么数据页上页就是1,如果一个事务有多个操作,LSN经过一系列操作LSN1=5,对应的数据脏页也紧接着LSN=5,此时由异步线程可能将LSN2=3都是可能的,最终commit的时候,假设LSN=10,那么LSN1=10,脏页中最大LSN=10,LSN2=10,可能此时checkpoint任务计算得到LSN3=10,对于本次的操作到此完成,数据修改并没有到磁盘,而是缓存页修改变成脏页(如果缓存没有,则从磁盘文件加载,插入LRU列表)
当更多这样的增删改查请求达到引擎后,redolog buffer增加,脏页会增加,根据上面说的checkpoint机制,如果系统的脏页太多,或者redolog文件满了不可用,或者master轮训时间到了,那么就要把这些脏页刷新到磁盘。checkpoint会根据自己的算法选择LSN的刷新区间(从LSN4往后的一段LSN),然后选择这些LSN对应的脏页(Flush List的脏页),确定要刷盘的脏页后,然后执行数据刷盘(下面讲解),在数据刷新完成后,修改redolog的checkpoint,等到下次checkpoint到来时,可以在这一次基础上继续更新,并且在这个checkpoint之前的redolog就可以被释放了(master thread处理)。
下面就介绍下数据刷盘的过程,这里先介绍下double write机制
doublewrite机制
所谓doublewrite,就是把磁盘缓存页刷入对应的磁盘页有一个备份页, 确保磁盘页的刷新是可靠的。整个doublewrite过程:把待刷盘的数据缓存页复制到doublewrite缓存,然后把doublewrite缓存顺序写入到磁盘文件中(这个磁盘文件是在共享表空间中的,有2M,页是连续的,而且写入是顺序写入,效率高),然后从doublewrite文件复制到对应的数据页中。
数据刷盘的过程:将脏页复制在doublewrite缓存,然后doublewrite缓存刷入对应的磁盘文件,再将磁盘文件复制到对应的数据页中,这样就完成数据刷盘。那么这个doublewrite机制存在的意义是什么呢?我们直观想到的就是将数据缓存脏页直接刷入数据磁盘页中即可,为啥还要刷入doublewirte?我们知道,因为redolog机制,所有缓存中的脏页就是缓存与磁盘不一致的数据部分,都在redolog中可以恢复,但是如果当缓存页数据正在刷盘到文件时,出现宕机,有可能会让磁盘页损坏,而redolog是sql语句导致的数据变化记录,如果磁盘页损坏的话,redolog是恢复不了的,所以这时候需要doublewrite,把缓存页->磁盘页的刷盘动作变成缓存页->doublewrite->磁盘页。如果缓存页->doublewrite过程出现宕机,因为数据磁盘页没有变化,所以可以通过redolog就可以恢复;如果doublewrite->磁盘页过程出现宕机,导致磁盘页出现异常时,因为doublewrite磁盘页没有损坏,可以再次从doubelwrite磁盘页复制到数据磁盘页;所以这样可以保证数据刷盘是可靠的。
insert buffer
上面介绍的都是数据页的修改刷盘过程,我们修改数据库数据的时候,同时对应的索引页数据也会修改,而我们知道一般数据写入是根据主键的顺序写入的,写入到磁盘可以认为是顺序的,但是对于非聚集索引,对应的修改就不一定是顺序的,随机性增大,这样会增加磁盘随机IO(对于SSD这个问题不大),所以对于mysql来说,为了避免写入索引的随机性,引入insertbuffer,将修改动作记录到insert buffer中,待系统有对应的索引页的时候,就可以进行合并,而且当insert buffer中的记录多了,还可以将属于同一个索引页的操作合并一起,减少IO。