一张图让你看懂 InnoDB

未分类

熟悉 MySQL 的人,都知道 InnoDB 存储引擎,如大家所知,Redo Log 是 innodb 的核心事务日志之一,innodb 写入 Redo Log 后就会提交事务,而非写入到 Datafile。之后 innodb 再异步地将新事务的数据异步地写入 Datafile,真正存储起来。

那么 innodb 引擎有了 redo log 和 buffer pool 以后,为什么能够在提升性能的同时,还能保证不丢数据呢? Buffer Pool, Redo Log 以及 Datafile 之间的具体关系是什么呢。

另外 Innodb 还有一大堆概念,Dirty Page, LRU, LSN,Checkpoint 等等,这些概念在 Innodb 里是什么运作的呢?

下面通过一张图来告诉大家

Buffer Pool, Redo Log 以及 Datafile 的关系

未分类

图 1 Innodb 的原理

大家可以把 innodb 的事务写入过程看成写作一篇文章的过程。Innodb 的写入过程其实和我们写作的过程是非常类似的。

试想,领导让我们写一篇文章,发表在论坛上。然后我们想到了一个绝佳的点子,并决定要放到文章里,可是手上还有其他事情,一时半会儿写不完,又担心过后忘了,领导还等着我们答复,此时我们会怎么做呢?我们一定会先大概构思个提纲,并把提纲和一些关键细节记录到本子上,作为草稿,然后立刻告诉领导自己要写什么东西,让其确认。最后等晚上有时间了,再根据草稿去斟词酌句,编写正稿。

在这个过程中,我们用到的几个关键的东西:

我们的大脑,用来临时暂时记住我们的点子

草稿,我们需要草稿来保证不会把点子和关键的细节给忘了

正稿,这是我们最终要输出的东西

有了这几个东西,我们不仅能确保我们不会错过一篇漂亮的文章,还能快速告诉领导自己一定可以搞定这件事情。

Innodb 实际上也用到了这几个关键的东西:

Buffer Pool:就是我们的大脑

事务日志:就是我们的草稿

Datafile:就是我们的正稿

只要按照之前写文章的过程,来进行整个事务的写入操作,不仅能保证不丢失数据,而且能够快速响应。

一次写入操作是一次事务,innodb 首先把事务数据写入到 Buffer Pool 和事务日志中,也就是在大脑中记忆下来,并写下草稿。然后就可以提交事务,响应客户端了。之后 innodb 在 “有时间的时候”,异步地把这次写入的数据从 Buffer Pool,或者事务日志中正式地写入到 Datafile 中,形成 “正稿”。

其中,innodb 为了保证事务日志这个 “草稿” 一定能无损地还原成正稿,还不能占用太多空间,事务日志需要有以下特点:

事务日志中一定保存了要写入的所有数据内容

事务日志只会把新事务追加在日志最后,而不会去修改之前的内容

一旦事务数据被写到 datafile,事务日志中的 “草稿” 就可以删除了

通过上面 3 个特点我们可以看出,在形成 “正稿” 之前,“草稿” 是不会被删除的;同时,“草稿” 的空间是可以被循环利用的;最后,只要 “草稿” 在,我们一定能写出 “正稿”。

这里还需要说明的,是 Recovery 流程。也就是如果在形成 “正稿” 前,数据库 Crash 了,我们需要重启整个进程,服务器,甚至只能把数据复制到另外一台服务器来进行恢复。这个时候,事务日志这个 “草稿” 就发挥了它最大的作用——数据恢复。这也和我们在工作生活中常出现的问题——把事情忘了——非常类似。

Buffer Pool 本质就是存储于内存中的一个数据结构,内存和人的大脑一样,是 “健忘” 的。数据库 Crash 时,Buffer Pool中的数据极大可能 “灰飞烟灭” 了。因此,事务日志就如我们贴心的 “记事本”,它把我们的记忆,保存为 “草稿”,当我们忘了的时候,就可以翻开,把记忆重新回想起来。

未分类

图 2 恢复的逻辑

LSN 和 Checkpoint

上面介绍了一次写入事务的情况,而数据库在使用过程中,事务都是连续不断,根据上面所述 innodb 逻辑,写 “草稿” 和写 “正稿” 速度和进度绝大部分情况下是不一样的。

再继续上面 “写作文章” 例子,如果我们的文章很长,一天写不完,而白天都有其他工作,我们只能记录草稿,只有晚上回去才能继续写正稿。那么我们就面临一个问题:我们昨天写到哪了。

最常见的办法就是,每天晚上去对照一下草稿的内容和正稿的内容,以此来判断写到哪了,但这比较花时间,因为正稿中可能包含了很多华丽的语句,我们需要思考一下才能对比上内容。

另外一个更简单的办法,就是每天晚上写完正稿后,我们在草稿上做个标记,标记下最后一条被写为正稿的内容,这样第二天晚上,我们就可以从这个标记的后面一条开始,继续写我们的正稿,而不需要去对比内容。

显然第二个方法效率更高,而且没有什么额外的风险。因此 innodb 就使用了这个办法。LSN 是草稿上每一条记录的编号,我们每天晚上标记下最后一条写到正稿的记录编号,这个标记的编号,就是 Checkpoint。Innodb 根据这个 checkpoint,就可以很快知道上次回放到哪里,同时也可以把这个编号之前的草稿,全部删掉了。

MySQL的InnoDB引擎日志工作原理

当你使用UPDATE, INSERT, DELETE语句更新数据的时候,你就改变了两个地方的数据:log buffer和data buffers。Buffers是固定长度的内存块,通常是512字节。

LOG BUFFER           DATA BUFFER
=================    ===============
= Log Record #1 =    = Page Header =
= Log Record #2 =    = Data Row    =
= Log Record #3 =    = Data Row    =
= Log Record #4 =    = Data Row    =
=================    ===============

例如:INSERT INTO JOBS VALUES(1,2,3)语句执行之后,log buffer将增加一个新的log记录,称为Log Record #5,它包含一个rowid和新记录的内容。同时,data buffer也将增加一个新行,但是,它会同时在页头标识:该页最新的log记录是Log Record #5。在这个例子中#5是Log Sequence Number(LSN),它对于接下来操作的时序安排是至关重要的。

下面是data-change的一些细节:

1、一个INSERT log记录仅包含一个新数据,它对于在页上重做操作是足够的了,因此被称为一个redo条目。

2、LSN不是log记录的一个域,它是文件中的一个绝对地址的相对偏移值。
在InnoDB改变了log buffer和data buffer之后,接下来就是写盘了。这就是复杂的地方。有多个线程在监控buffer的活动情况,有三种情况――overflow, checkpoint和commit――可以导致写盘操作。

Overflows情况下发生了什么?

Overflow是很少发生的情况,因为InnoDB采用pro-active措施来防止buffers被填满。但是我们还是来看看下面两种情况:

1、如果log buffer满了,InnoDBInnoDB在buffer的末尾写log。那么情况向下面的图一样(log buffer只有四条记录的空间,现在插入第五条记录):

LOG FILE(S) BEFORE WRITING LOG RECORD #5
=================
= Log Record #1 =
= Log Record #2 =
= Log Record #3 =
= Log Record #4 =
=================

LOG FILE(S) AFTER WRITING LOG RECORD #5
=================
= Log Record #5 =
= Log Record #2 =
= Log Record #3 =
= Log Record #4 =
=================

logs不可能永远增长。即使InnoDB使用了某些压缩算法,log文件还是会由于太大而不能放到任何磁盘驱动器上。因此InnoDB采取循环写的办法,也就是说将会覆盖前面就的log记录。

2、如果data buffer满了,InnoDB将最近使用的buffer写入到数据库中,但是不可能足够的快。这种情况下,页头的LSN就起作用了。第一,InnoDB检查它的LSN是否比log文件中最近的log记录的LSN大,只有当log赶上了data的时候,才会将数据写到磁盘。换句话说,数据页不会写盘,直到相应的log记录需要写盘的时候。这就是先写日志策略。

CheckPoints的时候发生了什么?

前面说过InnoDB采取了一些pro-active措施来保证不发生overflows,其中最重要的措施就是checkpointing。有一个分离的线程,或者说从一组修改buffers的线程中分离出来的一个线程。在特定的时间间隔,checkpointer将醒来,检查buffer的改变,并保证写盘操作已经发生了。

大部分DBMS在这个时候,将会把所有的buffer写盘,这样可以保证所有改变了但是没写盘的buffer都写盘。就是说DBMS将通过”Sharp Checkpoint” flush所有”dirty”buffers。但是InnoDB只保证:(a)log和data buffers不会超过某个限制点;(b)log始终比data先写盘;(c)没有哪个data buffer的页头LSN等于被覆盖写的log记录。也就是说InnoDB是”Fuzzy Checkpoint”。

在COMMIT的时候,InnoDB不会将dirty data page写盘。之所以强调这个是因为,很容易让人想到,提交改变就是将所有东西写到一个持久媒介上。其实,只有log记录需要写。写dirty data page只可能发生在overflow或checkpoint时刻,因为它们的内容是多余的。

Recovery

在recovery里面可以看到log是非常必要的:当数据库发生异常的时候,数据是可以恢复的。
对于不是损坏磁盘驱动器的异常,恢复是自动进行的。InnoDB读取最新的checkpoint日志记录,检查dirty pages是否在异常发生前写到磁盘上了,如果没有,则读取影响该页的log记录并应用它们。这被称为”rolling forward”。因为有LSN,所以InnoDB只需要比较这个数字就可以进行同步。

MySQL · 引擎特性 · InnoDB崩溃恢复

前言

数据库系统与文件系统最大的区别在于数据库能保证操作的原子性,一个操作要么不做要么都做,即使在数据库宕机的情况下,也不会出现操作一半的情况,这个就需要数据库的日志和一套完善的崩溃恢复机制来保证。本文仔细剖析了InnoDB的崩溃恢复流程,代码基于5.6分支。

基础知识

lsn: 可以理解为数据库从创建以来产生的redo日志量,这个值越大,说明数据库的更新越多,也可以理解为更新的时刻。此外,每个数据页上也有一个lsn,表示最后被修改时的lsn,值越大表示越晚被修改。比如,数据页A的lsn为100,数据页B的lsn为200,checkpoint lsn为150,系统lsn为300,表示当前系统已经更新到300,小于150的数据页已经被刷到磁盘上,因此数据页A的最新数据一定在磁盘上,而数据页B则不一定,有可能还在内存中。

redo日志: 现代数据库都需要写redo日志,例如修改一条数据,首先写redo日志,然后再写数据。在写完redo日志后,就直接给客户端返回成功。这样虽然看过去多写了一次盘,但是由于把对磁盘的随机写入(写数据)转换成了顺序的写入(写redo日志),性能有很大幅度的提高。当数据库挂了之后,通过扫描redo日志,就能找出那些没有刷盘的数据页(在崩溃之前可能数据页仅仅在内存中修改了,但是还没来得及写盘),保证数据不丢。

undo日志: 数据库还提供类似撤销的功能,当你发现修改错一些数据时,可以使用rollback指令回滚之前的操作。这个功能需要undo日志来支持。此外,现代的关系型数据库为了提高并发(同一条记录,不同线程的读取不冲突,读写和写读不冲突,只有同时写才冲突),都实现了类似MVCC的机制,在InnoDB中,这个也依赖undo日志。为了实现统一的管理,与redo日志不同,undo日志在Buffer Pool中有对应的数据页,与普通的数据页一起管理,依据LRU规则也会被淘汰出内存,后续再从磁盘读取。与普通的数据页一样,对undo页的修改,也需要先写redo日志。

检查点: 英文名为checkpoint。数据库为了提高性能,数据页在内存修改后并不是每次都会刷到磁盘上。checkpoint之前的数据页保证一定落盘了,这样之前的日志就没有用了(由于InnoDB redolog日志循环使用,这时这部分日志就可以被覆盖),checkpoint之后的数据页有可能落盘,也有可能没有落盘,所以checkpoint之后的日志在崩溃恢复的时候还是需要被使用的。InnoDB会依据脏页的刷新情况,定期推进checkpoint,从而减少数据库崩溃恢复的时间。检查点的信息在第一个日志文件的头部。

崩溃恢复: 用户修改了数据,并且收到了成功的消息,然而对数据库来说,可能这个时候修改后的数据还没有落盘,如果这时候数据库挂了,重启后,数据库需要从日志中把这些修改后的数据给捞出来,重新写入磁盘,保证用户的数据不丢。这个从日志中捞数据的过程就是崩溃恢复的主要任务,也可以成为数据库前滚。当然,在崩溃恢复中还需要回滚没有提交的事务,提交没有提交成功的事务。由于回滚操作需要undo日志的支持,undo日志的完整性和可靠性需要redo日志来保证,所以崩溃恢复先做redo前滚,然后做undo回滚。

我们从源码角度仔细剖析一下数据库崩溃恢复过程。整个过程都在引擎初始化阶段完成(innobase_init),其中最主要的函数是innobase_start_or_create_for_mysql,innodb通过这个函数完成创建和初始化,包括崩溃恢复。首先来介绍一下数据库的前滚。

redo日志前滚数据库

前滚数据库,主要分为两阶段,首先是日志扫描阶段,扫描阶段按照数据页的space_id和page_no分发redo日志到hash_table中,保证同一个数据页的日志被分发到同一个哈希桶中,且按照lsn大小从小到大排序。扫描完后,再遍历整个哈希表,依次应用每个数据页的日志,应用完后,在数据页的状态上至少恢复到了崩溃之前的状态。我们来详细分析一下代码。

首先,打开所有的ibdata文件(open_or_create_data_files)(ibdata可以有多个),每个ibdata文件有个flush_lsn在头部,计算出这些文件中的max_flush_lsn和min_flush_lsn,因为ibdata也有可能有数据没写完整,需要恢复,后续(recv_recovery_from_checkpoint_start_func)通过比较checkpont_lsn和这两个值来确定是否需要对ibdata前滚。

接着,打开系统表空间和日志表空间的所有文件(fil_open_log_and_system_tablespace_files),防止出现文件句柄不足,清空buffer pool(buf_pool_invalidate)。接下来就进入最最核心的函数:recv_recovery_from_checkpoint_start_func,注意,即使数据库是正常关闭的,也会进入。

虽然recv_recovery_from_checkpoint_start_func看过去很冗长,但是很多代码都是为了LOG_ARCHIVE特性而编写的,真正数据崩溃恢复的代码其实不多。

首先,初始化一些变量,查看srv_force_recovery这个变量,如果用户设置跳过前滚阶段,函数直接返回。

接着,初始化recv_sys结构,分配hash_table的大小,同时初始化flush list rbtree。recv_sys结构主要在崩溃恢复前滚阶段使用。hash_table就是之前说的用来存不同数据页日志的哈希表,哈希表的大小被初始化为buffer_size_in_bytes/512, 这个是哈希表最大的长度,超过就存不下了,幸运的是,需要恢复的数据页的个数不会超过这个值,因为buffer poll最多(数据库崩溃之前脏页的上线)只能存放buffer_size_in_bytes/16KB个数据页,即使考虑压缩页,最多也只有buffer_size_in_bytes/1KB个,此外关于这个哈希表内存分配的大小,可以参考bug#53122。flush list rbtree这个主要是为了加入插入脏页列表,InnoDB的flush list必须按照数据页的最老修改lsn(oldest_modifcation)从小到大排序,在数据库正常运行时,可以通过log_sys->mutex和log_sys->log_flush_order_mutex保证顺序,在崩溃恢复则没有这种保证,应用数据的时候,是从第一个元素开始遍历哈希表,不能保证数据页按照最老修改lsn(oldest_modifcation)从小到大排序,这样就需要线性遍历flush_list来寻找插入位置,效率太低,因此引入红黑树,加快查找插入的位置。

接着,从ib_logfile0的头中读取checkpoint信息,主要包括checkpoint_lsn和checkpoint_no。由于InnoDB日志是循环使用的,且最少要有2个,所以ib_logfile0一定存在,把checkpoint信息存在里面很安全,不用担心被删除。checkpoint信息其实会写在文件头的两个地方,两个checkpoint域轮流写。为什么要两个地方轮流写呢?假设只有一个checkpoint域,一直更新这个域,而checkpoint域有512字节(OS_FILE_LOG_BLOCK_SIZE),如果刚好在写这个512字节的时候,数据库挂了,服务器也挂了(先不考虑硬件的原子写特性,早期的硬件没有这个特性),这个512字节可能只写了一半,导致整个checkpoint域不可用。这样数据库将无法做崩溃恢复,从而无法启动。如果有两个checkpoint域,那么即使一个写坏了,还可以用另外一个尝试恢复,虽然有可能这个时候日志已经被覆盖,但是至少提高了恢复成功的概率。两个checkpoint域轮流写,也能减少磁盘扇区故障带来的影响。checkpoint_lsn之前的数据页都已经落盘,不需要前滚,之后的数据页可能还没落盘,需要重新恢复出来,即使已经落盘也没关系,因为redo日志时幂等的,应用一次和应用两次都一样(底层实现: 如果数据页上的lsn大于等于当前redo日志的lsn,就不应用,否则应用。checkpoint_no可以理解为checkpoint域写盘的次数,每次刷盘递增1,同时这个值取模2可以用来实现checkpoint_no域的轮流写。正常逻辑下,选取checkpoint_no值大的作为最终的checkpoint信息,用来做后续崩溃恢复扫描的起始点。

接着,使用checkpoint域的信息初始化recv_sys结构体的一些信息后,就进入日志解析的核心函数recv_group_scan_log_recs,这个函数后续我们再分析,主要作用就是解析redo日志,如果内存不够了,就直接调用应用(recv_apply_hashed_log_recs)日志,然后再接着解析。如果需要应用的日志很少,就仅仅解析分发日志,到recv_recovery_from_checkpoint_finish函数中在应用日志。

接着,依据当前刷盘的数据页状态做一次checkpoint,因为在recv_group_scan_log_recs里可能已经应用部分日志了。至此recv_recovery_from_checkpoint_start_func函数结束。
在recv_recovery_from_checkpoint_finish函数中,如果srv_force_recovery设置正确,就开始调用函数recv_apply_hashed_log_recs应用日志,然后等待刷脏的线程退出(线程是崩溃恢复时临时启动的),最后释放recv_sys的相关资源以及hash_table占用的内存。

至此,数据库前滚结束。接下来,我们详细分析一下redo日志解析函数以及redo日志应用函数的实现细节。

redo日志解析函数

解析函数的最上层是recv_group_scan_log_recs,这个函数调用底层函数(log_group_read_log_seg),按照RECV_SCAN_SIZE(64KB)大小分批读取。读取出来后,首先通过block_no和lsn之间的关系以及日志checksum判断是否读到了日志最后(所以可以看出,并没一个标记在日志头标记日志的有效位置,完全是按照上述两个条件判断是否到达了日志尾部),如果读到最后则返回(之前说了,即使数据库是正常关闭的,也要走崩溃恢复逻辑,那么在这里就返回了,因为正常关闭的checkpoint值一定是指向日志最后),否则则把日志去头掐尾放到一个recv_sys->buf中,日志头里面存了一些控制信息和checksum值,只是用来校验和定位,在真正的应用中没有用。在放到recv_sys->buf之前,需要检验一下recv_sys->buf有没有满(RECV_PARSING_BUF_SIZE,2M),满了就报错(如果上一批解析有不完整的日志,日志解析函数不会分发,而是把这些不完整的日志留在recv_sys->buf中,直到解析到完整的日志)。接下的事情就是从recv_sys->buf中解析日志(recv_parse_log_recs)。日志分两种:single_rec和multi_rec,前者表示只对一个数据页进行一种操作,后者表示对一个或者多个数据页进行多种操作。日志中还包括对应数据页的space_id,page_no,操作的type以及操作的内容(recv_parse_log_rec)。解析出相应的日志后,按照space_id和page_no进行哈希(如果对应的表空间在内存中不存在,则表示表已经被删除了),放到hash_table里面(日志真正存放的位置依然在buffer pool)即可,等待后续应用。这里有几个点值得注意:

  • 如果是multi_rec类型,则只有遇到MLOG_MULTI_REC_END这个标记,日志才算完整,才会被分发到hash_table中。查看代码,我们可以发现multi_rec类型的日志被解析了两次,一次用来校验完整性(寻找MLOG_MULTI_REC_END),第二次才用来分发日志,感觉这是一个可以优化的点。

  • 目前日志的操作type有50多种,每种操作后面的内容都不一样,所以长度也不一样,目前日志的解析逻辑,需要依次解析出所有的内容,然后确定长度,从而定位下一条日志的开始位置。这种方法效率略低,其实可以在每种操作的头上加上一个字段,存储后面内容的长度,这样就不需要解析太多的内容,从而提高解析速度,进一步提高崩溃恢复速度,从结果看,可以提高一倍的速度(从38秒到14秒,详情可以参见bug#82937)。

  • 如果发现checkpoint之后还有日志,说明数据库之前没有正常关闭,需要做崩溃恢复,因此需要做一些额外的操作(recv_init_crash_recovery),比如在错误日志中打印我们常见的“Database was not shutdown normally!”和“Starting crash recovery.”,还要从double write buffer中检查是否发生了数据页半写,如果有需要恢复(buf_dblwr_process),还需要启动一个线程用来刷新应用日志产生的脏页(因为这个时候buf_flush_page_cleaner_thread还没有启动)。最后还需要打开所有的表空间。。注意是所有的表。。。我们在阿里云RDS MySQL的运维中,常常发现数据库hang在了崩溃恢复阶段,在错误日志中有类似“Reading tablespace information from the .ibd files…”字样,这就表示数据库正在打开所有的表,然后一看表的数量,发现有几十甚至上百万张表。。。数据库之所以要打开所有的表,是因为在分发日志的时候,需要确定space_id对应哪个ibd文件,通过打开所有的表,读取space_id信息来确定,另外一个原因是方便double write buffer检查半写数据页。针对这个表数量过多导致恢复过慢的问题,MySQL 5.7做了优化,WL#7142, 主要思想就是在每次checkpoint后,在第一次修改某个表时,先写一个新日志mlog_file_name(包括space_id和filename的映射),来表示对这个表进行了操作,后续对这个表的操作就不用写这个新日志了,当需要崩溃恢复时候,多一次扫描,通过搜集mlog_file_name来确定哪些表被修改过,这样就不需要打开所有的表来确定space_id了。

  • 最后一个值得注意的地方是内存。之前说过,如果有太多的日志已经被分发,占用了太多的内存,日志解析函数会在适当的时候应用日志,而不是等到最后才一起应用。那么问题来了,使用了多大的内存就会出发应用日志逻辑。答案是:buffer_pool_size_in_bytes – 512 * buffer_pool_instance_num * 16KB。由于buffer_pool_instance_num一般不会太大,所以可以任务,buffer pool的大部分内存都被用来存放日志。剩下的那些主要留给应用日志时读取的数据页,因为目前来说日志应用是单线程的,读取一个日志,把所有日志应用完,然后就可以刷回磁盘了,不需要太多的内存。

redo日志应用函数

应用日志的上层函数为recv_apply_hashed_log_recs(应用日志也可能在io_helper函数中进行),主要作用就是遍历hash_table,从磁盘读取对每个数据页,依次应用哈希桶中的日志。应用完所有的日志后,如果需要则把buffer_pool的页面都刷盘,毕竟空间有限。有以下几点值得注意:

  • 同一个数据页的日志必须按照lsn从小到大应用,否则数据会被覆盖。只应用redo日志lsn大于page_lsn的日志,只有这些日志需要重做,其余的忽略。应用完日志后,把脏页加入脏页列表,由于脏页列表是按照最老修改lsn(oldest_modification)来排序的,这里通过引入一颗红黑树来加速查找插入的位置,时间复杂度从之前的线性查找降为对数级别。

  • 当需要某个数据页的时候,如果发现其没有在Buffer Pool中,则会查看这个数据页周围32个数据页,是否也需要做恢复,如果需要则可以一起读取出来,相当于做了一次io合并,减少io操作(recv_read_in_area)。由于这个是异步读取,所以最终应用日志的活儿是由io_helper线程来做的(buf_page_io_complete),此外,为了防止短时间发起太多的io,在代码中加了流量控制的逻辑(buf_read_recv_pages)。如果发现某个数据页在内存中,则直接调用recv_recover_page应用日志。由此我们可以看出,InnoDB应用日志其实并不是单线程的来应用日志的,除了崩溃恢复的主线程外,io_helper线程也会参与恢复。并发线程数取决于io_helper中读取线程的个数。

执行完了redo前滚数据库,数据库的所有数据页已经处于一致的状态,undo回滚数据库就可以安全的执行了。数据库崩溃的时候可能有一些没有提交的事务或者已经提交的事务,这个时候就需要决定是否提交。主要分为三步,首先是扫描undo日志,重新建立起undo日志链表,接着是,依据上一步建立起的链表,重建崩溃前的事务,即恢复当时事务的状态。最后,就是依据事务的不同状态,进行回滚或者提交。

undo日志回滚数据库

在recv_recovery_from_checkpoint_start_func之后,recv_recovery_from_checkpoint_finish之前,调用了trx_sys_init_at_db_start,这个函数做了上述三步中的前两步。

第一步在函数trx_rseg_array_init中处理,遍历整个undo日志空间(最多TRX_SYS_N_RSEGS(128)个segment),如果发现某个undo segment非空,就进行初始化(trx_rseg_create_instance)。整个每个undo segment,如果发现undo slot非空(最多TRX_RSEG_N_SLOTS(1024)个slot),也就行初始化(trx_undo_lists_init)。在初始化undo slot后,就把不同类型的undo日志放到不同链表中(trx_undo_mem_create_at_db_start)。undo日志主要分为两种:TRX_UNDO_INSERT和TRX_UNDO_UPDATE。前者主要是提供给insert操作用的,后者是给update和delete操作使用。之前说过,undo日志有两种作用,事务回滚时候用和MVCC快照读取时候用。由于insert的数据不需要提供给其他线程用,所以只要事务提交,就可以删除TRX_UNDO_INSERT类型的undo日志。TRX_UNDO_UPDATE在事务提交后还不能删除,需要保证没有快照使用它的时候,才能通过后台的purge线程清理。

第二步在函数trx_lists_init_at_db_start中进行,由于第一步中,已经在内存中建立起了undo_insert_list和undo_update_list(链表每个undo segment独立),所以这一步只需要遍历所有链表,重建起事务的状态(trx_resurrect_insert和trx_resurrect_update)。简单的说,如果undo日志的状态是TRX_UNDO_ACTIVE,则事务的状态为TRX_ACTIVE,如果undo日志的状态是TRX_UNDO_PREPARED,则事务的状态为TRX_PREPARED。这里还要考虑变量srv_force_recovery的设置,如果这个变量值为非0,所有的事务都会回滚(即事务被设置为TRX_ACTIVE),即使事务的状态应该为TRX_STATE_PREPARED。重建起事务后,按照事务id加入到trx_sys->trx_list链表中。最后,在函数trx_sys_init_at_db_start中,会统计所有需要回滚的事务(事务状态为TRX_ACTIVE)一共需要回滚多少行数据,输出到错误日志中,类似:5 transaction(s) which must be rolled back or cleaned up。InnoDB: in total 342232 row operations to undo的字样。

第三步的操作在两个地方被调用。一个是在recv_recovery_from_checkpoint_finish的最后,另外一个是在recv_recovery_rollback_active中。前者主要是回滚对数据字典的操作,也就是回滚DDL语句的操作,后者是回滚DML语句。前者是在数据库可提供服务之前必须完成,后者则可以在数据库提供服务(也即是崩溃恢复结束)之后继续进行(通过新开一个后台线程trx_rollback_or_clean_all_recovered来处理)。因为InnoDB认为数据字典是最重要的,必须要回滚到一致的状态才行,而用户表的数据可以稍微慢一点,对外提供服务后,慢慢恢复即可。因此我们常常在会发现数据库已经启动起来了,然后错误日志中还在不断的打印回滚事务的信息。事务回滚的核心函数是trx_rollback_or_clean_recovered,逻辑很简单,只需要遍历trx_sys->trx_list,按照事务不同的状态回滚或者提交即可(trx_rollback_resurrected)。这里要注意的是,如果事务是TRX_STATE_PREPARED状态,那么在InnoDB层,不做处理,需要在Server层依据binlog的情况来决定是否回滚事务,如果binlog已经写了,事务就提交,因为binlog写了就可能被传到备库,如果主库回滚会导致主备数据不一致,如果binlog没有写,就回滚事务。

崩溃恢复相关参数解析

innodb_fast_shutdown:
innodb_fast_shutdown = 0。这个表示在MySQL关闭的时候,执行slow shutdown,不但包括日志的刷盘,数据页的刷盘,还包括数据的清理(purge),ibuf的合并,buffer pool dump以及lazy table drop操作(如果表上有未完成的操作,即使执行了drop table且返回成功了,表也不一定立刻被删除)。
innodb_fast_shutdown = 1。这个是默认值,表示在MySQL关闭的时候,仅仅把日志和数据刷盘。
innodb_fast_shutdown = 2。这个表示关闭的时候,仅仅日志刷盘,其他什么都不做,就好像MySQL crash了一样。
这个参数值越大,MySQL关闭的速度越快,但是启动速度越慢,相当于把关闭时候需要做的工作挪到了崩溃恢复上。另外,如果MySQL要升级,建议使用第一种方式进行一次干净的shutdown。

innodb_force_recovery:
这个参数主要用来控制InnoDB启动时候做哪些工作,数值越大,做的工作越少,启动也更加容易,但是数据不一致的风险也越大。当MySQL因为某些不可控的原因不能启动时,可以设置这个参数,从1开始逐步递增,知道MySQL启动,然后使用SELECT INTO OUTFILE把数据导出,尽最大的努力减少数据丢失。
innodb_force_recovery = 0。这个是默认的参数,启动的时候会做所有的事情,包括redo日志应用,undo日志回滚,启动后台master和purge线程,ibuf合并。检测到了数据页损坏了,如果是系统表空间的,则会crash,用户表空间的,则打错误日志。
innodb_force_recovery = 1。如果检测到数据页损坏了,不会crash也不会报错(buf_page_io_complete),启动的时候也不会校验表空间第一个数据页的正确性(fil_check_first_page),表空间无法访问也继续做崩溃恢复(fil_open_single_table_tablespace、fil_load_single_table_tablespace),ddl操作不能进行(check_if_supported_inplace_alter),同时数据库也被不能进行写入操作(row_insert_for_mysql、row_update_for_mysql等),所有的prepare事务也会被回滚(trx_resurrect_insert、trx_resurrect_update_in_prepared_state)。这个选项还是很常用的,数据页可能是因为磁盘坏了而损坏了,设置为1,能保证数据库正常启动。
innodb_force_recovery = 2。除了设置1之后的操作不会运行,后台的master和purge线程就不会启动了(srv_master_thread、srv_purge_coordinator_thread等),当你发现数据库因为这两个线程的原因而无法启动时,可以设置。
innodb_force_recovery = 3。除了设置2之后的操作不会运行,undo回滚数据库也不会进行,但是回滚段依然会被扫描,undo链表也依然会被创建(trx_sys_init_at_db_start)。srv_read_only_mode会被打开。
innodb_force_recovery = 4。除了设置3之后的操作不会运行,ibuf的操作也不会运行(ibuf_merge_or_delete_for_page),表信息统计的线程也不会运行(因为一个坏的索引页会导致数据库崩溃)(info_low、dict_stats_update等)。从这个选项开始,之后的所有选项,都会损坏数据,慎重使用。
innodb_force_recovery = 5。除了设置4之后的操作不会运行,回滚段也不会被扫描(recv_recovery_rollback_active),undo链表也不会被创建,这个主要用在undo日志被写坏的情况下。
innodb_force_recovery = 6。除了设置5之后的操作不会运行,数据库前滚操作也不会进行,包括解析和应用(recv_recovery_from_checkpoint_start_func)。

总结

InnoDB实现了一套完善的崩溃恢复机制,保证在任何状态下(包括在崩溃恢复状态下)数据库挂了,都能正常恢复,这个是与文件系统最大的差别。此外,崩溃恢复通过redo日志这种物理日志来应用数据页的方法,给MySQL Replication带来了新的思路,备库是否可以通过类似应用redo日志的方式来同步数据呢?阿里云RDS MySQL团队在后续的产品中,给大家带来了类似的特性,敬请期待。

MySQL的InnoDB的幻读问题

MySQL InnoDB事务的隔离级别有四级,默认是“可重复读”(REPEATABLE READ)。

  • 未提交读(READ UNCOMMITTED)。另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据(脏读)。

  • 提交读(READ COMMITTED)。本事务读取到的是最新的数据(其他事务提交后的)。问题是,在同一个事务里,前后两次相同的SELECT会读到不同的结果(不重复读)。

  • 可重复读(REPEATABLE READ)。在同一个事务里,SELECT的结果是事务开始时时间点的状态,因此,同样的SELECT操作读到的结果会是一致的。但是,会有幻读现象(稍后解释)。

  • 串行化(SERIALIZABLE)。读操作会隐式获取共享锁,可以保证不同事务间的互斥。

四个级别逐渐增强,每个级别解决一个问题。

  • 脏读,最容易理解。另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据。

  • 不重复读。解决了脏读后,会遇到,同一个事务执行过程中,另外一个事务提交了新数据,因此本事务先后两次读到的数据结果会不一致。

  • 幻读。解决了不重复读,保证了同一个事务里,查询的结果都是事务开始时的状态(一致性)。但是,如果另一个事务同时提交了新数据,本事务再更新时,就会“惊奇的”发现了这些新数据,貌似之前读到的数据是“鬼影”一样的幻觉。

借鉴并改造了一个搞笑的比喻:

  • 脏读。假如,中午去食堂打饭吃,看到一个座位被同学小Q占上了,就认为这个座位被占去了,就转身去找其他的座位。不料,这个同学小Q起身走了。事实:该同学小Q只是临时坐了一小下,并未“提交”。

  • 不重复读。假如,中午去食堂打饭吃,看到一个座位是空的,便屁颠屁颠的去打饭,回来后却发现这个座位却被同学小Q占去了。

  • 幻读。假如,中午去食堂打饭吃,看到一个座位是空的,便屁颠屁颠的去打饭,回来后,发现这些座位都还是空的(重复读),窃喜。走到跟前刚准备坐下时,却惊现一个恐龙妹,严重影响食欲。仿佛之前看到的空座位是“幻影”一样。

一些文章写到InnoDB的可重复读避免了“幻读”(phantom read),这个说法并不准确。

做个试验:(以下所有试验要注意存储引擎和隔离级别)

mysql> show create table t_bitflyG;
CREATE TABLE `t_bitfly` (
`id` bigint(20) NOT NULL default '0',
`value` varchar(32) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk

mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+

试验一:

t Session A                   Session B
|
| START TRANSACTION;          START TRANSACTION;
|
| SELECT * FROM t_bitfly;
| empty set
|                             INSERT INTO t_bitfly
|                             VALUES (1, 'a');
|
| SELECT * FROM t_bitfly;
| empty set
|                             COMMIT;
|
| SELECT * FROM t_bitfly;
| empty set
|
| INSERT INTO t_bitfly VALUES (1, 'a');
| ERROR 1062 (23000):
| Duplicate entry '1' for key 1
v (shit, 刚刚明明告诉我没有这条记录的)

如此就出现了幻读,以为表里没有数据,其实数据已经存在了,傻乎乎的提交后,才发现数据冲突了。

试验二:

t Session A                  Session B
|
| START TRANSACTION;         START TRANSACTION;
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|                            INSERT INTO t_bitfly
|                            VALUES (2, 'b');
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|                            COMMIT;
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|
| UPDATE t_bitfly SET value='z';
| Rows matched: 2  Changed: 2  Warnings: 0
| (怎么多出来一行)
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | z     |
| |    2 | z     |
| +------+-------+
|
v

本事务中第一次读取出一行,做了一次更新后,另一个事务里提交的数据就出现了。也可以看做是一种幻读。


那么,InnoDB指出的可以避免幻读是怎么回事呢?

http://dev.mysql.com/doc/refman/5.0/en/innodb-record-level-locks.html

By default, InnoDB operates in REPEATABLE READ transaction isolation level and with the innodb_locks_unsafe_for_binlog system variable disabled. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows (see Section 13.6.8.5, “Avoiding the Phantom Problem Using Next-Key Locking”).

准备的理解是,当隔离级别是可重复读,且禁用innodb_locks_unsafe_for_binlog的情况下,在搜索和扫描index的时候使用的next-key locks可以避免幻读。

关键点在于,是InnoDB默认对一个普通的查询也会加next-key locks,还是说需要应用自己来加锁呢?如果单看这一句,可能会以为InnoDB对普通的查询也加了锁,如果是,那和序列化(SERIALIZABLE)的区别又在哪里呢?

MySQL manual里还有一段:

13.2.8.5. Avoiding the Phantom Problem Using Next-Key Locking (http://dev.mysql.com/doc/refman/5.0/en/innodb-next-key-locking.html)

To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking.

You can use next-key locking to implement a uniqueness check in your application: If you read your data in share mode and do not see a duplicate for a row you are going to insert, then you can safely insert your row and know that the next-key lock set on the successor of your row during the read prevents anyone meanwhile inserting a duplicate for your row. Thus, the next-key locking enables you to “lock” the nonexistence of something in your table.

我的理解是说,InnoDB提供了next-key locks,但需要应用程序自己去加锁。manual里提供一个例子:

SELECT * FROM child WHERE id > 100 FOR UPDATE;

这样,InnoDB会给id大于100的行(假如child表里有一行id为102),以及100-102,102+的gap都加上锁。

可以使用show innodb status来查看是否给表加上了锁。

再看一个实验,要注意,表t_bitfly里的id为主键字段。实验三:

t Session A                 Session B
|
| START TRANSACTION;        START TRANSACTION;
|
| SELECT * FROM t_bitfly
| WHERE id<=1
| FOR UPDATE;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|                           INSERT INTO t_bitfly
|                           VALUES (2, 'b');
|                           Query OK, 1 row affected
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|                           INSERT INTO t_bitfly
|                           VALUES (0, '0');
|                           (waiting for lock ...
|                           then timeout)
|                           ERROR 1205 (HY000):
|    

Innodb索引以及查询优化的一些见解

聚集索引

索引如果没有特指,一般是指B+TREE,通常意味着所有值都是顺序存放,因此对于范围查询会非常快。InnoDB按照原数据格式进行存储。InnoDB存储引擎表是索引组织表,表中数据按照主键存放(InnoDB会隐式定义一个主键作为聚集索引,切记不能重复定义)。索引的叶节点中存放表的行记录,使叶节点成为数据页。而普通索引仅仅存放键值以及偏移量而已。(ps:MYISAM使用前缀压缩使索引更小,表中数据是通过索引所记录的数据物理的位置,直接引用的。)

Tips1:

范围查询非常快

eg:limit 优化

select * from yanxue8_visit limit 10,10

多次运行,时间保持在0.0004-0.0005之间

Select * From yanxue8_visit Where vid >=(
Select vid From yanxue8_visit Order By vid limit 10,1
) limit 10

Tips2:

在使用索引时需要独立的列

eg:

select * from tablename where id +1 = 5

这样的写法是不能用索引的,这也是为什么很多mysql优化语句中常提到的一点。

Tip3:

可以使用前缀索引

eg:

alter table tablename add key(a(7))

这样可以使索引变小并变快,节约空间,提高索引效率。但会减小索引的选择性。

前缀索引,一种优化索引大小的解决方案: https://yq.aliyun.com/articles/39841

事实上,聚集索引页有些缺点,提高了使用磁盘I/O的次数。并且要严格按照插入顺序插入,否则可能面临数据页分裂,耗用更多空间并降低性能。因此我们都需要一个自增长键作为主键,保证数据顺序写入。若使用UUID情况会变糟糕,所以现在分布式系统基本放弃了原生的UUID寻找替代品。

辅助索引(非聚集索引)

辅助索引只是存取了聚集索引的书签。若想查询需要对辅助索引遍历一遍再在聚集索引遍历一遍,遍历深度由索引树高度决定。

联合索引

联合索引顾名思义是多索引组成的联合索引.
如一张t表中有两列,a与b,其中a为主键,b为普通列
创建:alter table t add key idx_a_b(a,b)

未分类

如上图,数据先根据a排放,a值相同则根据b排放。既该索引包括了a与b的笛卡尔积。若where a = xxx and b = xxx 以及 where a = xxx则可以利用该索引,但where b = xxx 就不能利用该索引。(最左前缀)

好处:

可以联合读取多个列
可以做联合排序

Tip1:

利用多列条件做分页操作

创建:alter table tablename add key idx_a_b(a,b)

select * from tablename t1
left join(
select id from tablename
where a = 1 oder by b limit 1000,10) t2
on t1.id = t2.id

但事实上,也不能一棒子拍死。查询结果会非常依赖于选定的具体指,会对其他查询不公平,会使服务器运行不如预期。其顺序必须以选择性高低进行排序。事实上实际开发中,本人是因为某个查询量大的业务才建立联合索引,基本业务也是单个索引(但要注意重复索引,如我创建了(a,b)就得把(a)索引删除)。

哈希表

InnoDB中只用哈希对字典进行查找,事实上,哈希表只需理解有这么个东西就行。InnoDB会自动创建哈希表并维护,并只是对字典查询会变快(where a = ‘xxx’),不对范围查询起作用。

innodb_adaptive_hash_index 启用/禁用特性
innodb_buffer_pool_size/256=哈希的槽数

使用分析工具

explain

非常强大的工具,分析sql语句的性能以及所运用的索引。可以解决绝大多数sql性能问题。

慢查询日志

超过阈值的SQL语句记录的日志。

show index from tablename

可以分析表中索引,有些字段对于分析该表索引非常有用,如Cardinality。

analyze table

能优化索引存储,使索引更好工作.可每周或每天凌晨运行一次。

值得注意的地方

mysql在查询时,若数据大于整张表的20%,则会放弃索引

参考书籍

高性能MySQL: https://book.douban.com/subject/23008813/

MySQL技术内幕: https://book.douban.com/subject/5373022/

Mysql InnoDB的索引,锁如何协调作用以实现事务隔离级别?

未分类

MySQL的InnoDB存储引擎行锁是加在索引上的,所以只当增删改查操作是通过索引找到指定数据行的时候,才能对相应数据行的索引加锁,否则只能对整个表加表锁,表共享读锁或表独占写锁。

当一个事务不经过索引查询数据,即顺序读取(全表扫描)时,先获取表的意向共享锁,然后对表添加共享读锁,阻止其他事务对表的更新,新增和删除操作,但不影响查询操作,共享读锁之间是兼容的。

当一个事务不经过索引更新,删除数据,即全表扫描符合条件的数据行时,先获取表的意向独占锁,然后对表添加独占写锁,在执行更新,删除时阻止其他事务对表的读及写操作。

当一个事务使用索引去查询数据,即随机读取时,先获得表的意向共享锁,然后对符合条件的的索引区间加共享读锁(聚集索引肯定会加锁,若用到了非聚集索引一样加锁),共享读锁之间是兼容的,因此不影响其他事务对被锁数据行的访问。当其他事务想要修改,删除加锁的数据行时,若未使用索引则在获取表独占写锁时会失败。若使用索引检索这些数据行,则可能会在非聚集索引处被阻塞(查询事务和修改事务使用同一个索引检索数据行),也可能在聚集索引处被阻塞(通过非聚集索引查询到聚集索引的键,通过此键查询到相应的聚集索引,同时读到数据航,这是读和写,写和写之间的事务串行化保证),只能等待查询事务释放索引区间的共享读锁,然后执行更新,删除操作。当一个事务想要将新的数据行插入到被加锁的数据行中,也需要等待共享读锁的释放。因为InnoDB实现了间隙锁机制,即当一个事务按一个条件(id < 10,id列含有索引)加锁数据行时,其他事务不能在锁释放前将符合此条件的数据行(id = 5)插入到表中,此机制一定程度防止了幻读的出现。猜测实现机制:前一个事务对数据行的索引加了锁(聚集索引和非聚集索引),其他的事务插入数据时,需要新增聚集/非聚集索引,但是此时符合条件的聚集/非聚集索引区间已经被加锁,不能实时插入索引,需要等到索引区间的锁被释放,这个可能是间隙锁的实现原理。

当一个事务通过索引去更新,删除数据行时,先获取表的意向排他锁,然后对符合条件的数据行的索引加锁(聚集/非聚集),阻止其他事务对被锁数据行的读,写。同理实现间隙锁。

浅谈MySQL存储引擎选择 InnoDB与MyISAM的优缺点分析

下面先让我们回答一些问题:

  • 你的数据库有外键吗?

  • 你需要事务支持吗?

  • 你需要全文索引吗?

  • 你经常使用什么样的查询模式?

  • 你的数据有多大?

思考上面这些问题可以让你找到合适的方向,但那并不是绝对的。如果你需要事务处理或是外键,那么InnoDB 可能是比较好的方式。如果你需要全文索引,那么通常来说 MyISAM是好的选择,因为这是系统内建的,然而,我们其实并不会经常地去测试两百万行记录。所以,就算是慢一点,我们可以通过使用Sphinx从InnoDB中获得全文索引。

数据的大小,是一个影响你选择什么样存储引擎的重要因素,大尺寸的数据集趋向于选择InnoDB方式,因为其支持事务处理和故障恢复。数据库的在小决定了故障恢复的时间长短,InnoDB可以利用事务日志进行数据恢复,这会比较快。而MyISAM可能会需要几个小时甚至几天来干这些事,InnoDB只需要几分钟。

您操作数据库表的习惯可能也会是一个对性能影响很大的因素。比如: COUNT() 在 MyISAM 表中会非常快,而在InnoDB 表下可能会很痛苦。而主键查询则在InnoDB下会相当相当的快,但需要小心的是如果我们的主键太长了也会导致性能问题。大批的inserts 语句在MyISAM下会快一些,但是updates 在InnoDB 下会更快一些——尤其在并发量大的时候。

所以,到底你检使用哪一个呢?根据经验来看,如果是一些小型的应用或项目,那么MyISAM 也许会更适合。当然,在大型的环境下使用MyISAM 也会有很大成功的时候,但却不总是这样的。如果你正在计划使用一个超大数据量的项目,而且需要事务处理或外键支持,那么你真的应该直接使用InnoDB方式。但需要记住InnoDB 的表需要更多的内存和存储,转换100GB 的MyISAM 表到InnoDB 表可能会让你有非常坏的体验。

MyISAM存储引擎

MyISAM是 默认存储引擎。它基于更老的ISAM代码,但有很多有用的扩展。MyISAM存储引擎的一些特征:

  • 所有数据值先存储低字节。这使得数据机和操作系统分离。二进制轻便性的唯一要求是机器使用补码(如最近20年的机器有的一样)和IEEE浮点格式(在主流机器中也完全是主导的)。唯一不支持二进制兼容性的机器是嵌入式系统。这些系统有时使用特殊的处理器。

  • 先存储数据低字节并不严重地影响速度;数据行中的字节一般是未联合的,从一个方向读未联合的字节并不比从反向读更占用更多的资源。服务器上的获取列值的代码与其它代码相比并不显得时间紧。

  • 大文件(达63位文件长度)在支持大文件的文件系统和操作系统上被支持。

  • 当把删除和更新及插入混合的时候,动态尺寸的行更少碎片。这要通过合并相邻被删除的块,以及若下一个块被删除,就扩展到下一块来自动完成。

  • 每个MyISAM表最大索引数是64。 这可以通过重新编译来改变。每个索引最大的列数是16个。

  • 最大的键长度是1000字节。这也可以通过编译来改变。对于键长度超过250字节的情况,一个超过1024字节的的键块被用上。

  • BLOB和TEXT列可以被索引。

  • NULL值被允许在索引的列中。这个占每个键的0-1个字节。

  • 所有数字键值以高字节为先被存储以允许一个更高地索引压缩。

  • 当记录以排好序的顺序插入(就像你使用一个AUTO_INCREMENT列之时),索引树被劈开以便高节点仅包含一个键。这改善了索引树的空间利用率。

  • 每表一个AUTO_INCREMEN列的内部处理。MyISAM为INSERT和UPDATE操作自动更新这一 列。这使得AUTO_INCREMENT列更快(至少10%)。在序列顶的值被删除之后就不能再利用。(当AUTO_INCREMENT列被定义为多列索 引的最后一列,可以出现重使用从序列顶部删除的值的情况 )。AUTO_INCREMENT值可用ALTER TABLE或myisamch来重置。

  • 如果数据文件中间的表没有自由块了,在其它线程从表读的同时,你可以INSERT新行到表中。(这被认识为并发操作 )。自由块的出现是作为删除行的结果,或者是用比当前内容多的数据对动态长度行更新的结果。当所有自由块被用完(填满),未来的插入又变成并发。

  • 你可以把数据文件和索引文件放在不同目录,用DATA DIRECTORY和INDEX DIRECTORY选项CREATE TABLE以获得更高的速度,请参阅13.1.5节,“CREATE TABLE语法”。

  • 每个字符列可以又不同的字符集。

  • 在MyISAM索引文件里又一个标志,它表明表是否被正确关闭。如果用–myisam-recover选项启动mysqld,MyISAM表在打开得时候被自动检查,如果被表被不恰当地关闭,就修复表。

  • 如果你用–update-state选项运行myisamchk,它标注表为已检查。myisamchk –fast只检查那些没有这个标志的表。

  • myisamchk –analyze为部分键存储统计信息,也为整个键存储统计信息。

  • myisampack可以打包BLOB和VARCHAR列。

MyISAM也支持下列特征:

  • 支持true VARCHAR类型;VARCHAR列以存储在2个字节中的长度来开始。

  • 有VARCHAR的表可以有固定或动态记录长度。

  • VARCHAR和CHAR列可以多达64KB。

  • 一个被搞乱的已计算索引对可对UNIQUE来使用。这允许你在表内任何列的合并上有UNIQUE。(尽管如此,你不能在一个UNIQUE已计算索引上搜索)。

InnoDB存储引擎

InnoDB给MySQL提供 了具有提交,回滚和崩溃恢复能力的事务安全(ACID兼容)存储引擎。InnoDB锁定在行级并且也在SELECT语句提供一个Oracle风格一致的非 锁定读。这些特色增加 了多用户部署和性能。没有在InnoDB中扩大锁定的需要,因为在InnoDB中行级锁定适合非常小的空间。InnoDB也支持FOREIGN KEY强制。在SQL查询中,你可以自由地将InnoDB类型的表与其它MySQL的表的类型混合起来,甚至在同一个查询中也可以混合。

InnoDB是为处理巨大数据量时的最大性能设计。它的CPU效率可能是任何其它基于磁盘的关系数据库引擎所不能匹敌的。

InnoDB存储引擎被完全与MySQL服务器整合,InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。 InnoDB存储它的表&索引在一个表空间中,表空间可以包含数个文件(或原始磁盘分区)。这与MyISAM表不同,比如在MyISAM表中每个表被存在 分离的文件中。InnoDB 表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上。

InnoDB默认地被包含在MySQL二进制分发中。Windows Essentials installer使InnoDB成为Windows上MySQL的 默认表。

InnoDB被用来在众多需要高性能的大型数据库站点上产生。著名的Internet新闻站点Slashdot.org运行在 InnoDB上。Mytrix, Inc.在InnoDB上存储超过1TB的数据,还有一些其它站点在InnoDB上处理平均每秒800次插入/更新的负荷。

InnoDB和MyISAM的区别

区别概述:

MyISAM 是MySQL中默认的存储引擎,一般来说不是有太多人关心这个东西。决定使用什么样的存储引擎是一个很tricky的事情,但是还是值我们去研究一下,这里的文章只考虑 MyISAM 和InnoDB这两个,因为这两个是最常见的。

下面先让我们回答一些问题:

  • 你的数据库有外键吗?

  • 你需要事务支持吗?

  • 你需要全文索引吗?

  • 你经常使用什么样的查询模式?

  • 你的数据有多大?

思考上面这些问题可以让你找到合适的方向,但那并不是绝对的。如果你需要事务处理或是外键,那么InnoDB 可能是比较好的方式。如果你需要全文索引,那么通常来说 MyISAM是好的选择,因为这是系统内建的,然而,我们其实并不会经常地去测试两百万行记录。所以,就算是慢一点,我们可以通过使用Sphinx从 InnoDB中获得全文索引。

数据的大小,是一个影响你选择什么样存储引擎的重要因素,大尺寸的数据集趋向于选择InnoDB方式,因为其支持事务处理和故障恢复。数据库的在小 决定了故障恢复的时间长短,InnoDB可以利用事务日志进行数据恢复,这会比较快。而MyISAM可能会需要几个小时甚至几天来干这些事,InnoDB 只需要几分钟。

您操作数据库表的习惯可能也会是一个对性能影响很大的因素。比如: COUNT() 在 MyISAM 表中会非常快,而在InnoDB 表下可能会很痛苦。而主键查询则在InnoDB下会相当相当的快,但需要小心的是如果我们的主键太长了也会导致性能问题。大批的inserts 语句在MyISAM下会快一些,但是updates 在InnoDB 下会更快一些——尤其在并发量大的时候。

所以,到底你检使用哪一个呢?根据经验来看,如果是一些小型的应用或项目,那么MyISAM 也许会更适合。当然,在大型的环境下使用MyISAM 也会有很大成功的时候,但却不总是这样的。如果你正在计划使用一个超大数据量的项目,而且需要事务处理或外键支持,那么你真的应该直接使用InnoDB方 式。但需要记住InnoDB 的表需要更多的内存和存储,转换100GB 的MyISAM 表到InnoDB 表可能会让你有非常坏的体验。

区别总结:

  1. InnoDB不支持FULLTEXT类型的索引。

  2. InnoDB 中不保存表的具体行数,也就是说,执行select count() from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count()语句包含 where条件时,两种表的操作是一样的。

  3. 对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。

  4. DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。

  5. LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。

另外,InnoDB表的行锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%”

提升InnoDB性能的方法:

MyISAM和InnoDB存储引擎性能差别并不是很大,针对InnoDB来说,影响性能的主要是 innodb_flush_log_at_trx_commit 这个选项,如果设置为1的话,那么每次插入数据的时候都会自动提交,导致性能急剧下降,应该是跟刷新日志有关系,设置为0效率能够看到明显提升,当然,同 样你可以SQL中提交“SET AUTOCOMMIT = 0”来设置达到好的性能。另外,还听说通过设置innodb_buffer_pool_size能够提升InnoDB的性能,但是我测试发现没有特别明显 的提升。

基本上我们可以考虑使用InnoDB来替代我们的MyISAM引擎了,因为InnoDB自身很多良好的特点,比如事务支持、存储 过程、视图、行级锁定等等,在并发很多的情况下,相信InnoDB的表现肯定要比MyISAM强很多,当然,相应的在my.cnf中的配置也是比较关键 的,良好的配置,能够有效的加速你的应用。

任何一种表都不是万能的,只用恰当的针对业务类型来选择合适的表类型,才能最大的发挥MySQL的性能优势。

Mysql入门mysql innodb 异常修复经验分享

一套测试用的mysql库,之前用的centos6默认源里的mysql 5.1.71的版本 .后来想试用下Percona server 5.7,由于这套库里没有什么重要数据 .所以操作前也未进行备份,配置好源后,直接就进行了安装.数据文件也存放在默认位置,安装完成后,直接启动mysql,发现启动失败,发现无法启动正常启动.

一、回退重新装mysql

为避免再从其他地方导入这个数据的麻烦,先对当前库的数据库文件做了个备份(/var/lib/mysql/位置).接下来将Percona server 5.7包进行了卸载,重新安装原先老的5.1.71的包,启动mysql服务,提示Unknown/unsupported table type: innodb,无法正常启动.

110509 12:04:27 InnoDB: Initializing buffer pool, size = 384.0M
110509 12:04:27 InnoDB: Completed initialization of buffer pool
InnoDB: Error: log file ./ib_logfile0 is of different size 0 5242880 bytes
InnoDB: than specified in the .cnf file 0 157286400 bytes!
110509 12:04:27 [ERROR] Plugin 'InnoDB' init function returned error.
110509 12:04:27 [ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed.
110509 12:04:27 [ERROR] Unknown/unsupported table type: innodb
110509 12:04:27 [ERROR] Aborting
110509 12:04:27 [Note] /usr/sbin/mysqld: Shutdown complete

删除/var/lib/mysql/目录,重新启动数据库服务,并初始化,发现正常,show engines能发现有innodb引擎.再将数据库停掉,将之前备份的/var/lib/mysql/目录的内容覆盖当前位置的内容,重启.又发现不能进行启动,报错内容和刚刚一样.

/var/lib/mysql目录内容的结构如下:

-rw-rw---- 1 mysql mysql 10485760 2月  26 18:10 ibdata1
-rw-rw---- 1 mysql mysql 5242880 2月  26 18:10 ib_logfile0
-rw-rw---- 1 mysql mysql 5242880 2月  26 17:20 ib_logfile1
drwx------ 2 mysql mysql   4096 2月  26 17:20 mysql
drwx------ 2 mysql mysql   4096 2月 26 17:24 wiki

wiki目录是测试数据的库,ibdata1文件为数据文件,ib开头的两个文件为日志文件,mysql 目录下为系统库相关的东西 .再次使用初始化的数据,并将wiki目录和ibdata1文件覆盖到/var/lib/mysql 目录下,可以正常启动,也可以正常登录.

二、innodb模块重装

不过在通过mysqldump备份时,又提示unknow table engine “Innodb” .登录后,查看当前所有的引擎类型,发现其中果然不存在innodb类型:

未分类

通过alter命令修改其中一个表的类型为MyISAM ,发现仍然报错.

未分类

通过 find 查找发现/usr/lib64/mysql/plugin/目录下有ha_innodb_plugin.so文件.印象中mysql5以后的版本支持在线插件安装 .通过下面查看确认,果然支持:

未分类

使用如下命令加载时,发现不成功:

install plugin innodb soname 'ha_innodb.so';

三、备份

在/etc/my.cnf中增加如下配置:

plugin-load=innodb=ha_innodb_plugin.so
plugin_dir=/usr/lib64/mysql/plugin/
default-storage-engine=InnoDB 

发现仍启动失败.查看mysql-error.log发现有如下内容:

InnoDB: Database page corruption on disk or a failed
InnoDB: file read of page 7.
InnoDB: You may have to recover from a backup.
InnoDB: It is also possible that your operating
InnoDB: system has corrupted its own file cache
InnoDB: and rebooting your computer removes the
InnoDB: error.
InnoDB: If the corrupt page is an index page
InnoDB: you can also try to fix the corruption
InnoDB: by dumping, dropping, and reimporting
InnoDB: the corrupt table. You can use CHECK
InnoDB: TABLE to scan your table for corruption.
InnoDB: See also http://dev.mysql.com/doc/refman/5.1/en/forcing-innodb-recovery.html

打开forcing-innodb-recovery官方页面,发现可以通过指定innodb_force_recovery参数,进行强制启动和恢复.在/etc/my.cnf中增加如下内容:

innodb_force_recovery=6

重新启动成功了.通过mysqldump备份也没有问题,将备份数据导入其他主机发现也正常可以测试.

这下就好搞了,将mysql彻底删除,重新安装Percona server 5.7,安装完后,建库,还原数据,程序重新连接,一切OK.

总结:

由于mysql innodb数据文件的特性,可以在出现问题,无法正常启动时,先将./ib_logfile0 和 ./ib_logfile1 两个日志文件先移走,再启动,如果还不成功,可以用innodb_force_recovery参数进行强制恢复.除此之外,日志也很重启,有问题先看日志.

InnoDB Buffer Pool巧配置全解

一、简介

InnoDB维护了一个缓存数据和索引信息到内存的存储区叫做Buffer Pool,它会将最近访问的数据缓存到缓冲区。我们通过配置各个Buffer Pool的参数,可以显著提高MySQL的性能。

InnoDB的Buffer Pool是基于LRU算法来实现的,下面我们可以简单了解一下LRU算法:

least recently used (LRU),InnoDB管理Buffer Pool是将Buffer Pool作为一个list管理,基于LRU算法的管理。当有新的页信息要读入到Buffer Pool里面的时候,Buffer Pool就将最近最少使用的页信息从Buffer Pool当中驱逐出去,并且将新页加入到list的中间位置,这就是所谓的中点插入策略。

一般情况下list 头部存放的是热数据,就是所谓的young page(最近经常访问的数据),list尾部存放的就是old page(最近不被访问的数据)。这个算法就保证了最近经常使用的page信息会被保存在最近访问的sublist,相反的不被经常访问的就会保存在old sublist。而old sublist当中的page信息都是在新数据写入时被驱逐的。

LRU算法有以下的标准算法:

  • 3/8的list信息是作为old list,这些信息是被驱逐的对象。

  • list的中点就是我们所谓的old list头部和new list尾部的连接点,相当于一个界限

  • 新数据的读入首先会插入到old list的头部,

  • 如果是old list的数据被访问到了,这个页信息就会变成new list,变成young page,就会将数据页信息移动到new sublist的头部。

  • 在数据库的Buffer Pool里面,不管是new sublist还是old sublist的数据如果不会被访问到,最后都会被移动到list的尾部作为牺牲者。

一般情况下,页信息会被查询语句立马查询到而被移动到new sublist,这就意味着他们会在Buffer Pool里面保留很长一段时间。

表扫描(包括mysqldump或者没有where条件的select等操作)等操作将会刷入大量的数据进入Buffer Pool,同时也会将更多的Buffer Pool当中的信息刷出去,即使这个操作可能只会使用到一次而已。同样的如果 read-ahead后台进程读入大量数据的情况下也是会造成Buffer Pool大量高频的刷新数据页,但是这些操作是可控的,下面会讲到。read-ahead操作简单说一下就是MySQL的一个后台预读进程,能够保证MySQL预读入数据进入Buffer Pool当中。

二、参数一览表

下面先看下InnoDB Buffer Pool的一些相关参数信息,后面会详细解释一下:

  • innodb_buffer_pool_size:这个值是设置InnoDB Buffer Pool的总大小;

  • innodb_buffer_pool_chunk_size:InnoDB Buffer Pool的执行单元 chunk size的大小。这里面有个关系要确定一下,最好按照这个设置 innodb_buffer_pool_size=innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances*N(N>=1);

  • innodb_buffer_pool_instances:设置InnoDB Buffer Pool实例的个数,每一个实例都有自己独立的list管理Buffer Pool;

  • innodb_old_blocks_pct:默认InnoDB Buffer Pool中点的位置,默认值是37,最大100,也就是我们所谓的3/8的位置,可以自己设置;

  • innodb_old_blocks_time:设置保留在Buffer Pool里面的数据在插入时候没有被改变list位置的时候的保存时间;

  • innodb_read_ahead_threshold:参数控制MySQL何时进行预读,也可以控制MySQL预读数据时候对于数据的敏感度,如果Buffer Pool里面存储的数据页的频繁值大于innodb_read_ahead_threshold的值,InnoDB就会启动一个异步的预读操作;

  • innodb_random_read_ahead:默认是disabled,是控制预读方式的参数,开启的话将不使用线性预读而是使用随机预读;

  • innodb_adaptive_flushing:指定是否动态自适应刷新脏页到盘,这个是MySQL根据负载自己决定的。不过还是尽量不要设置,让MySQL自己来管理自己;

  • innodb_adaptive_flushing_lwm:关闭adaptive_flushing的话才会有用,用来标记redo log使用率的百分比的最低线,当达到这个值的时候就会刷新脏页,默认为10;

  • innodb_flush_neighbors:控制是否刷新Buffer Pool脏页的脏数据的时候将同一区的脏数据页一同刷新,默认值为1;

  • innodb_flushing_avg_loops:为InnoDB保存InnoDB Buffer Pool前几次的冲洗状态快照的迭代数,默认值为30,增大的话,冲洗就会变得缓慢。减小的话冲洗的频率就会变高;

  • innodb_lru_scan_depth:控制LRU算法的一个参数,用来控制Buffer Pool后台进程page_cleaner 刷新脏页的位置;

  • innodb_max_dirty_pages_pct:参数会让InnoDB Buffer Pool刷新数据而不让脏数据的百分比超过这个值;

  • innodb_max_dirty_pages_pct_lwm:InnoDB会自动维护后台作业自动从Buffer Pool当中清除脏数据,当Buffer Pool中的脏页占用比 达到innodb_max_dirty_pages_pct_lwm的设定值的时候,就会自动将脏页清出Buffer Pool;

  • innodb_buffer_pool_filename:指定文件名字;

  • innodb_buffer_pool_dump_at_shutdown:配置的InnoDB是否保留当前的缓冲池的状态,以避免在服务器重新启动后,还要经历一个漫长的暖机时间;

  • innodb_buffer_pool_load_at_startup:指定此参数启动,数据库重启以后会自动暖机,读入Buffer Pool重启前保存的信息;

  • innodb_buffer_pool_dump_now和innodb_buffer_pool_load_now当数据库已经提起来的时候,我们忘了以前指定,也可以指定马上恢复;

  • innodb_buffer_pool_dump_pct:设置一下恢复Buffer Pool中多少数据;

  • innodb_buffer_pool_load_abort:终止Buffer Pool恢复,可以指定负载运行。

三、innoDB Buffer Pool解读

1、Buffer Pool Size设置和生效过程

理想情况下,在给服务器的其他进程留下足够的内存空间的情况下,Buffer Pool Size应该设置的尽可能大。当Buffer Pool Size设置的足够大时,整个数据库就相当于存储在内存当中,当读取一次数据到Buffer Pool Size以后,后续的读操作就不用再访问磁盘。

下面我们看一下Buffer Pool Size的设置方式:

当数据库已经启动的情况下,我们可以通过在线调整的方式修改Buffer Pool Size的大小。通过以下语句:

SET GLOBAL innodb_buffer_pool_size=402653184;

当执行这个语句以后,并不会立即生效,而是要等所有的事务全部执行成功以后才会生效;新的连接和事务必须等其他事务完全执行成功以后,Buffer Pool Size设置生效以后才能够连接成功,不然会一直处于等待状态。

期间,Buffer Pool Size要完成碎片整理,去除缓存page等等操作。在执行增加或者减少Buffer Pool Size的操作时,操作会作为一个执行块执行,innodb_buffer_pool_chunk_size的大小会定义一个执行块的大小,默认的情况下,这个值是128M。

Buffer Pool Size的大小最好设置为innodb_buffer_pool_chunk_size innodb_buffer_pool_instances的整数倍,而且是大于等于1。

如果你的机器配置的大小不是整数倍的话,Buffer Pool Size的大小是会自适应修改为innodb_buffer_pool_chunk_sizeinnodb_buffer_pool_instances的整数倍,会略小于你配置的Buffer Pool Size的大小。

比如以8G为例:

mysqld –innodb_buffer_pool_size=8G –innodb_buffer_pool_instances=16,然后innodb_buffer_pool_instances=16的大小刚好设置为16,是一个整数倍的关系。而且innodb_buffer_pool_chunk_size的大小也是可以在my.cnf里面指定的。

还有一种情况是innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances大于buffer pool size的情况下,innodb_buffer_pool_chunk_size 也会自适应为Buffer Pool size/innodb_buffer_pool_instances,可见MySQL的管理还是非常的智能的。

如果我们要查看Buffer Pool的状态的话:

SHOW STATUS WHERE Variable_name='InnoDB_buffer_pool_resize_status'

可以帮我们查看到状态。我们可以看一下增加Buffer Pool的时候的一个过程,再看一下减少的时候的日志,其实还是很好理解的,我们可以看成每次增大或者减少Buffer Pool的时候就是进行innodb_buffer_pool_chunk的增加或者释放,按照innodb_buffer_pool_chunk_size 设定值的大小增加或者释放执行块。

增加的过程:增加执行块,指定新地址,将新加入的执行块加入到free list(控制执行块的一个列表,可以这么理解)。

减少的过程:重新整理Buffer Pool和空闲页,将数据从块中移除,指定新地址。

2、Buffer Pool Instances

在64位操作系统的情况下,可以拆分缓冲池成多个部分,这样可以在高并发的情况下最大可能的减少争用。下面我们看一下怎么配置Buffer Pool Instances?

配置多个Buffer Pool Instances能在很大程度上能够提高MySQL在高并发的情况下处理事物的性能,优化不同连接读取缓冲页的争用。

我们可以通过设置 innodb_buffer_pool_instances来设置Buffer Pool Instances。当InnoDB Buffer Pool 足够大的时候,你能够从内存中读取时候能有一个较好的性能,但是也有可能碰到多个线程同时请求缓冲池的瓶颈。这个时候设置多个Buffer Pool Instances能够尽量减少连接的争用。

这能够保证每次从内存读取的页都对应一个Buffer Pool Instances,而且这种对应关系是一个随机的关系。并不是热数据存放在一个Buffer Pool Instances下,内部也是通过hash算法来实现这个随机数的。每一个Buffer Pool Instances都有自己的free lists,LRU和其他的一些Buffer Poll的数据结构,各个Buffer Pool Instances是相对独立的。

innodb_buffer_pool_instances 的设置必须大于1才算得上是多配置,但是这个功能起作用的前提是innodb_buffer_pool_size的大小必须大于1G,理想情况下innodb_buffer_pool_instances的每一个instance都保证在1G以上。

3、innoDB Buffer Poll LRU原理

你可以频繁的往Buffer Pool里面读取数据,当backup或者report的时候,不用有太多的顾虑。InnoDB采用的是一种不是像LRU那么严格的方法来保证将最近访问的数据写入到Buffer Pool里面,并且最大可能的降低减少数据的带入量。这个语句是全表扫描或者以后这个数据将不会再被访问到,但是缓冲数据还是会写入到Buffer Pool里面。

新写入的数据会被插入到LRU list的中间位置,默认会插入到从list尾部算起来的3/8的位置,当这些写入的数据在Buffer Pool中被第一次访问的时候,在list中的位置就会向前移动,这样其实就会在list保留两个位置,老的位置并不会被立即清除,直到老的LRU list的位置被标记为OLD的时候,才会在下一次插入数据的时候被作为牺牲者清除掉。

我们本身是可以指定插入LRU list的位置,并且也可以设置当索引扫描或者是全表扫描的时候是不是采用这个相同的优化方法。 innodb_old_blocks_pct这个参数设置的是插入的位置,默认的值是37,我们可以设置的值是5-95之间,其余部分并不用来保存热数据。

但是还有一个严重的问题就是当一个全表扫描或者索引的扫描经常被访问的时候,就会存储很大的数据到Buffer Pool里面,我们都知道这是很危险的一件事。

所以MySQL给我们以下参数来设置保留在Buffer Pool里面的数据在插入时候没有被改变list位置的时候的保存时间innodb_old_blocks_time,单位是毫秒,这个值的默认值是1000。如果增大这个值的话,就会让Buffer Pool里面很多页信息变老的速度变快,这个很好理解吧,因为这些数据会不会很快被内存中擦除的话,就会变成热数据而挤掉原有缓存的数据。

以上的两个参数都是可以动态设置的,当然也可以在my.cnf里面设置。当然设置这些前一定要对机器配置,表信息,负载情况有充分的了解才能进行设置,生产库尽量不要随便修改。如果OLTP系统中有大量的大查询的话,设置innodb_old_blocks_time能够较大的提供系统的稳定性。

如果当一个大查询很大不足够存储到Buffer Pool当中的时候,我们可以指定innodb_old_blocks_pct的值小一点,以保证这些数据只会被读取一次,比如说设置为5的时候,就就限制了一次读取数据最多只能被读取到Buffer Pool当中5%。一些小表并且是经常访问到的数据的话就可以适当设置较大的值,比如50。当然设置这两个值的时候一定要建立在你充分了解你的数据负载的基础上,不然千万不要乱改。

4、InnoDB Buffer Pool预读

我们可以控制MySQL何时以何种方式预读数据进入Buffer Pool。 预读就是IO异步读取多个页数据读入Buffer Pool的一个过程,并且这些页被认为是很快就会被读取到的,当需要读取这些数据的时候就会将需要的页放在一个区当中,InnoDB就是通过两次预读的方式来提高IO读写的性能。

线性预读:能够预测将有那些数据很快能被读到的一种技术,因为Buffer Pool中的页数据是顺序访问的。我们可以通过设置innodb_read_ahead_threshold参数控制MySQL何时进行预读,也可以控制MySQL预读数据时候对于数据的敏感度,如果Buffer Pool里面存储的数据页的频繁值大于innodb_read_ahead_threshold的值,INNODB就会启动一个异步的预读操作,innodb_read_ahead_threshold的值可以设置为0-64的任何一个值。默认值是56,值设置的越高就会造成检索更加严格。当设置为8的时候,只有小于8个页数据被读进Buffer Pool中才会被检索。

随机预读:随机预读能够将在内存当中的将被读取到的页信息很快的组织到一个区中,而且这些页面的读取顺序不用分顺序,InnoDB能够很快调度Buffer Pool当中相似的很多页的信息,并且发出请求。这些页可能并不是连续的,要想使用这个功能就要设置innodb_random_read_ahead 这个参数为ON。

5、动态调整MySQL后台进程刷新

我们可以控制MySQL后台进程何时刷新,或者根据MySQL负载的情况动态调整。 InnoDB会自动维护后台作业自动从Buffer Pool当中清除脏数据,当Buffer Pool中的脏页占用比达到innodb_max_dirty_pages_pct_lwm的设定值的时候,就会自动将脏页清出Buffer Pool,这是为了保证Buffer Pool当中脏页的占有率,也是为了防止脏页占有率超过innodb_max_dirty_pages_pct的设定值,当脏页的占有率达到了innodb_max_dirty_pages_pct的设定值的时候,InnoDB就会强制刷新Buffer Pool Pages。

InnoDB采用一种基于redo log的最近生成量和最近刷新频率的算法来决定冲洗速度。这样的算法可以保证数据库的冲洗不会影响到数据库的性能,也能保证数据库Buffer Pool中的数据的脏数据的占用比。这种自动调节的方式还能够防止突然的并发redo变大,但是flush的时候将不能进行普通的IO读写操作。

我们知道InnoDB使用日志的方式是循环使用的,在重用前一个日志文件之前,InnoDB就会将这个日志这个日志记录相关的所有在Buffer Pool当中的数据刷新到磁盘,也就是所谓的sharp checkpoint,和sqlserver的checkpoint很像。当一个插入语句产生大量的redo信息,需要记录的日志当前redo log文件不能够完全存储,也会写入到当前的redo 文件当中。当redo log当中的所有使用空间都被用完了的,就会触发 sharp checkpoint,所以这个时候即使脏数据占有率没有达到innodb_max_dirty_pages_pct ,还是会进行刷新。这种算法是经得住考验的,所以说千万不要随便设置,最好是默认值。但是我们从中也就会知道为什么一个事物的redo信息只能记录在一个redo log文件当中了。

因为有这么多的好处,所以 innodb_adaptive_flushing的值默认就是true的。

6、buffer poll缓冲保留

我们可以配置的InnoDB如何保留当前的缓冲池的状态,以避免在服务器重新启动后,还要经历一个漫长的暖机时间。

通过innodb_buffer_pool_dump_at_shutdown来设置,当设置这些参数以后,MySQL就会在机器重启后快速恢复以前内存中的数据,当然这些数据是从磁盘重新读取到Buffer Pool当中的,个人认为这个值还是需要配置一下的,当然这会花费一些时间,在重新读取这些数据到内存当中的时候,新的DML操作是不能够进行操作的。

这些数据是怎么恢复呢?其实 InnoDB_BUFFER_PAGE_LRU 表(INFORMATION_SCHEMA )会记录缓存的table ID和page ID,通过这个来恢复。 在LOAD数据进入Buffer Pool之前,可以设置Buffer Pool恢复数据的百分比,当然默认值肯定是100,不设置默认就是全部恢复。

SET GLOBAL innodb_buffer_pool_dump_pct=40;

通过以下的语句,设置是否重启服务器的时候重新LOAD数据进入Buffer Pool,默认是关闭的,还可以在启动时候指定或者在my.cnf当中指定:

SET GLOBAL innodb_buffer_pool_dump_at_shutdown=ON;

如果我们保存了Buffer Pool的信息,也可以在启动MySQL时候指定重新LOAD数据:

SET GLOBAL innodb_buffer_pool_dump_now=ON;保存信息,在重启前要指定 SET GLOBAL innodb_buffer_pool_load_now=ON;LOAD信息

如果要终止Buffer Pool加载,可以指定负载运行:

SET GLOBAL innodb_buffer_pool_load_abort=ON;

也可以通过以下的命令查看状态:

SHOW STATUS LIKE 'Innodb_buffer_pool_dump_status'; SHOW STATUS LIKE 'Innodb_buffer_pool_load_status';

而且我们可以通过innodb 的performance schema监控Buffer Pool的 LOAD状态:

  • 打开或者关闭stage/innodb/buffer pool load
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES' WHERE NAME LIKE 'stage/innodb/buffer%';
  • 打开以下参数来获取最近的Buffer Pool的dump状态:
SET GLOBAL innodb_buffer_pool_dump_now=ON; SET GLOBAL innodb_buffer_pool_load_now=ON; SHOW STATUS LIKE 'Innodb_buffer_pool_dump_status'G SELECT EVENT_NAME, WORK_COMPLETED, WORK_ESTIMATED FROM performance_schema.events_stages_current; SELECT EVENT_NAME, WORK_COMPLETED, WORK_ESTIMATED FROM performance_schema.events_stages_history;

需要留意的是,如果是压缩表的话,在读取到Buffer Pool的时候还是会保持压缩的格式。直到被读取的时候才会调用解压程序进行解压。

四、结语

InnoDB Buffer Pool不可不说是MySQL的核心功能之一,合理的配置InnoDB Buffer Pool能够显著地提高我们数据库的性能。而且本身InnoDB Buffer Pool的配置也给予我们很高的可控性,我们可以根据自己的业务场景,负载等寻找较优的配置。

不过这些都要建立在你对自己的MySQL服务充分了解的基础上,不然可能会适得其反。最后,如果有不对的地方,欢迎拍砖。

Mysql InnoDB 共享表空间和独立表空间

前言:学习mysql的时候总是习惯性的和oracle数据库进行比较。在学习mysql InnoDB的存储结构的时候也免不了跟oracle进行比较。Oracle的数据存储有表空间、段、区、块、数据文件;mysql InnoDB的存储管理也类似,但是mysql增加了一个共享表空间和独立表空间的概念;

未分类

一、概念

共享表空间: Innodb的所有数据保存在一个单独的表空间里面,而这个表空间可以由很多个文件组成,一个表可以跨多个文件存在,所以其大小限制不再是文件大小的限制,而是其自身的限制。从Innodb的官方文档中可以看到,其表空间的最大限制为64TB,也就是说,Innodb的单表限制基本上也在64TB左右了,当然这个大小是包括这个表的所有索引等其他相关数据。

独立表空间:

二、查看数据库的表空间

mysql> show variables like 'innodb_data%';

未分类

l 表空间有四个文件组成:ibdata1、ibdata2、ibdata3、ibdata4,每个文件的大小为10M,当每个文件都满了的时候,ibdata4会自动扩展;

l 当前的存储空间满的时候,可以在其他的磁盘添加数据文件,语法如下:语法如下所示:

pathtodatafile:sizespecification;pathtodatafile:sizespec;.;pathtodatafile:sizespec[:autoextend[:max:sizespecification]]

如果用 autoextend 选项描述最后一个数据文件,当 InnoDB 用尽所有表自由空间后将会自动扩充最后一个数据文件,每次增量为 8 MB。示例:

不管是共享表空间和独立表空间,都会存在innodb_data_file文件,因为这些文件不仅仅要存放数据,而且还要充当着类似于ORACLE的UNDO表空间等一些角色。

三、共享表空间优缺点

既然Innodb有共享表空间和独立表空间两种类型,那么这两种表空间存在肯定都有时候自己的应用的场景,存在即合理。以下是摘自mysql官方的一些介绍:

3.1 共享表空间的优点

表空间可以分成多个文件存放到各个磁盘,所以表也就可以分成多个文件存放在磁盘上,表的大小不受磁盘大小的限制(很多文档描述有点问题)。

数据和文件放在一起方便管理。

3.2 共享表空间的缺点

所有的数据和索引存放到一个文件,虽然可以把一个大文件分成多个小文件,但是多个表及索引在表空间中混合存储,当数据量非常大的时候,表做了大量删除操作后表空间中将会有大量的空隙,特别是对于统计分析,对于经常删除操作的这类应用最不适合用共享表空间。

共享表空间分配后不能回缩:当出现临时建索引或是创建一个临时表的操作表空间扩大后,就是删除相关的表也没办法回缩那部分空间了(可以理解为oracle的表空间10G,但是才使用10M,但是操作系统显示mysql的表空间为10G),进行数据库的冷备很慢;

四、独立表空间的优缺点

4.1 独立表空间的优点

每个表都有自已独立的表空间,每个表的数据和索引都会存在自已的表空间中,可以实现单表在不同的数据库中移动。

空间可以回收(除drop table操作处,表空不能自已回收)

Drop table操作自动回收表空间,如果对于统计分析或是日值表,删除大量数据后可以通过:alter table TableName engine=innodb;回缩不用的空间。

对于使innodb-plugin的Innodb使用turncate table也会使空间收缩。
对于使用独立表空间的表,不管怎么删除,表空间的碎片不会太严重的影响性能,而且还有机会处理。

4.2 独立表空间的缺点

单表增加过大,当单表占用空间过大时,存储空间不足,只能从操作系统层面思考解决方法;

五、共享表空间和独立表空间之间的转换

5.1 查看当前数据库的表空间管理类型

脚本:show variables like “innodb_file_per_table”;

mysql> show variables like "innodb_file_per_table";

未分类

ON代表独立表空间管理,OFF代表共享表空间管理;(查看单表的表空间管理方式,需要查看每个表是否有单独的数据文件)

5.2 修改数据库的表空间管理方式

修改innodb_file_per_table的参数值即可,但是修改不能影响之前已经使用过的共享表空间和独立表空间;

  • innodb_file_per_table=1 为使用独占表空间
  • innodb_file_per_table=0 为使用共享表空间

5.3共享表空间转化为独立表空间的方法(参数innodb_file_per_table=1需要设置)

单个表的转换操作,脚本:alter table table_name engine=innodb;
当有大量的表需要操作的时候,先把数据库导出,然后删除数据再进行导入操作,该操作可以用mysqldump进行操作(http://blog.itpub.net/12679300/viewspace-1259451/)
总结:经过以上操作便完成数据库的存储空间的转换,了解技术是为了更好的利用技术,当数据量很小的时候建议使用共享表空间的管理方式。数据量很大的时候建议使用独立表空间的管理方式。