读书笔记 — —《MySQL技术L幕:InnoDB存储引擎(第2版)》

第1章 MySQL体系结构和存储引擎

1.1 定义数据库和实例

        数据库:物理操作系统文件或其他形式文件类型的集合。在MySQL数据库中,数据库文件可以是frm、MYD、MYI、ibd结尾的文件。

         实例:MySQL数据库由后台线程以及一个共享内存区组成。共享内存可以被运行的后台线程所共享。数据库实例才是真正用于操作数据库文件的。

        MySQL被设计为一个单进程多线程架构的数据库,即MySQL数据库实例在系统上的表现就是一个进程。

        在Linux操作系统中通过以下命令启动MySQL数据库实例,并通过命令ps观察MySQL数据库启动后的进程情况:

[root@xen-server bin]#./mysqld_safe&

[root@xen-server bin]#ps-ef|grep mysqld

root 3441 3258 0 10:23 pts/3 00:00:00/bin/sh./mysqld_safe

mysql 3578 3441 0 10:23 pts/3 00:00:00

/usr/local/mysql/libexec/mysqld--basedir=/usr/local/mysql

--datadir=/usr/local/mysql/var--user=mysql

--log-error=/usr/local/mysql/var/xen-server.err

--pid-file=/usr/local/mysql/var/xen-server.pid

--socket=/tmp/mysql.sock--port=3306

root 3616 3258 0 10:27 pts/3 00:00:00 grep mysqld

        当启动实例时,MySQL数据库会去读取配置文件,根据配置文件的参数来启动数据库实例。

        用以下命令可以查看当MySQL数据库实例启动时,会在哪些位置查找配置文件。

        可以看到,MySQL数据库是按/etc/my.cnf→/etc/mysql/my.cnf→/usr/local/mysql/etc/my.cnf→~/.my.cnf的顺序读取配置文件的。如果几个配置文件中都有同一个参数,MySQL数据库会以读取到的最后一个配置文件中的参数为准。

1.2 MySQL体系结构

        从概念上来说,数据库是文件的集合,是依照某种数据模型组织起来并存放于二级存储器中的数据集合;数据库实例是程序,是位于用户与操作系统之间的一层数据管理软件,用户对数据库数据的任何操作,包括数据库定义、数据查询、数据维护、数据库运行控制等都是在数据库实例下进行的,应用程序只有通过数据库实例才能和数据库打交道。

        MySQL由以下几部分组成:

        □ 连接池组件 □ 管理服务和工具组件 □ SQL接口组件 □ 查询分析器组件 □ 优化器组件

        □ 缓冲(Cache)组件 □ 插件式存储引擎 □ 物理文件

        需要特别注意的是,存储引擎是基于表的,而不是数据库。

1.3 MySQL存储引擎

        存储引擎的好处是,每个存储引擎都有各自的特点,能够根据具体的应用建立不同存储引擎表。

1.3.1 InnoDB存储引擎

        InnoDB存储引擎支持事务,其设计目标主要面向在线事务处理(OLTP)的应用。其特点是行锁设计、支持外键,并支持类似于Oracle的非锁定读,即默认读取操作不会产生锁。从MySQL数据库5.5.8版本开始,InnoDB存储引擎是默认的存储引擎。

        InnoDB存储引擎将数据放在一个逻辑的表空间中,可以将每个InnoDB存储引擎的表单独存放到一个独立的ibd文件中。此外,InnoDB存储引擎支持用裸设备(row disk)用来建立其表空间。

        InnoDB通过使用多版本并发控制(MVCC)来获得高并发性,并实现了SQL标准的4种隔离级别,默认为REPEATABLE级别。同时,使用一种被称为next-key locking的策略来避免幻读(phantom)现象的产生。除此之外,InnoDB储存引擎还提供了插入缓冲(insert buffer)、二次写(double write)、自适应哈希索引(adaptive hash index)、预读(read ahead)等高性能和高可用的功能。对于表中数据的存储,InnoDB存储引擎采用了聚集(clustered)的方式,因此每张表的存储都是按主键的顺序进行存放。如果没有显式地在表定义时指定主键,InnoDB存储引擎会为每一行生成一个6字节的ROWID,并以此作为主键。

1.3.2 MyISAM存储引擎

        MyISAM存储引擎不支持事务、表锁设计,支持全文索引,主要面向一些OLAP数据库应用。在MySQL 5.5.8版本之前MyISAM存储引擎是默认的存储引擎(除Windows版本外)。

        MyISAM存储引擎的另一个与众不同的地方是它的缓冲池只缓存(cache)索引文件,而不缓冲数据文件,这点和大多数的数据库都非常不同。

        MyISAM存储引擎表由MYD和MYI组成,MYD用来存放数据文件,MYI用来存放索引文件。可以通过使用myisampack工具来进一步压缩数据文件,因为myisampack工具使用赫夫曼编码静态算法来压缩数据,因此使用myisampack工具压缩后的表是只读的,当然用户也可以通过myisampack来解压数据文件。

        在MySQL 5.0版本之前,MyISAM默认支持的表大小为4GB,如果需要支持大于4GB的MyISAM表时,则需要制定MAX_ROWS和AVG_ROW_LENGTH属性。从MySQL 5.0版本开始,MyISAM默认支持256TB的单表数据

1.3.3 NDB存储引擎

        NDB存储引擎是一个集群存储引擎,其结构是share nothing的集群架构,能提供更高的可用性。NDB的特点是数据全部放在内存中(从MySQL 5.1版本开始,可以将非索引数据放在磁盘上),主键查找(primary key lookups)的速度极快,并且通过添加NDB数据存储节点(Data Node)可以线性地提高数据库性能,是高可用、高性能的集群系统。

        NDB存储引擎的连接操作(JOIN)是在MySQL数据库层完成的,而不是在存储引擎层完成的。这意味着,复杂的连接操作需要巨大的网络开销,因此查询速度很慢。

1.3.4 Memory存储引擎

        Memory存储引擎(之前称HEAP存储引擎)将表中的数据存放在内存中,如果数据库重启或发生崩溃,表中的数据都将消失。它非常适合用于存储临时数据的临时表,以及数据仓库中的纬度表。Memory存储引擎默认使用哈希索引,而不是我们熟悉的B+树索引。

        虽然Memory存储引擎速度非常快,但在使用上还是有一定的限制。比如,只支持表锁,并发性能较差,并且不支持TEXT和BLOB列类型。最重要的是,存储变长字段(varchar)时是按照定常字段(char)的方式进行的,因此会浪费内存。

        MySQL数据库使用Memory存储引擎作为临时表来存放查询的中间结果集。如果中间结果集大于Memory存储引擎表的容量设置,又或者中间结果含有TEXT或BLOB列类型字段,则MySQL数据库会把其转换到MyISAM存储引擎表而存放到磁盘中。之前提到MyISAM不缓存数据文件,因此这时产生的临时表的性能对于查询会有损失。

1.3.5 Archive存储引擎

        Archive存储引擎只支持INSERT和SELECT操作,从MySQL 5.1开始支持索引。Archive存储引擎使用zlib算法将数据行(row)进行压缩后存储,压缩比一般可达1∶10。Archive存储引擎非常适合存储归档数据,如日志信息。Archive存储引擎使用行锁来实现高并发的插入操作,但是其本身并不是事务安全的存储引擎,其设计目标主要是提供高速的插入和压缩功能。

1.3.6 Federated存储引擎

        Federated存储引擎表并不存放数据,它只是指向一台远程MySQL数据库服务器上的表。Federated存储引擎只支持MySQL数据库表,不支持异构数据库表。

1.3.7 Maria存储引擎

        Maria存储引擎是新开发的引擎,设计目标主要是用来取代原有的MyISAM存储引擎,从而成为MySQL的默认存储引擎。Maria存储引擎的特点是:支持缓存数据和索引文件,应用了行锁设计,提供了MVCC功能,支持事务和非事务安全的选项,以及更好的BLOB字符类型的处理性能。

1.3.8 其他存储引擎

        除了上面提到的7种存储引擎外,MySQL数据库还有很多其他的存储引擎,包括Merge、CSV、Sphinx和Infobright,它们都有各自使用的场合,这里不再一一介绍。在了解MySQL数据库拥有这么多存储引擎后,现在我可以回答1.2节中提到的问题了。

        ❑为什么MySQL数据库不支持全文索引?不!MySQL支持,MyISAM、InnoDB(1.2版本)和Sphinx存储引擎都支持全文索引。

        ❑MySQL数据库速度快是因为不支持事务?错!虽然MySQL的MyISAM存储引擎不支持事务,但是InnoDB支持。“快”是相对于不同应用来说的,对于ETL这种操作,MyISAM会有其优势,但在OLTP环境中,InnoDB存储引擎的效率更好。

        ❑当表的数据量大于1000万时MySQL的性能会急剧下降吗?不!MySQL是数据库,不是文件,随着数据行数的增加,性能当然会有所下降,但是这些下降不是线性的,如果用户选择了正确的存储引擎,以及正确的配置,再多的数据量MySQL也能承受。

1.4 各存储引擎之间的比较

        可以通过SHOW ENGINES语句查看当前使用的MySQL数据库所支持的存储引擎,也可以通过查找information_schema架构下的ENGINES表。

1.5 连接MySQL

        连接MySQL操作是一个连接进程和MySQL数据库实例进行通信,本质上是进程通信。常用的进程通信方式有管道、命名管道、命名字、TCP/IP套接字、UNIX域套接字。

1.5.1 TCP/IP

        TCP/IP套接字方式是MySQL数据库在任何平台下都提供的连接方式。

        在通过TCP/IP连接到MySQL实例时,MySQL数据库会先检查一张权限视图,用来判断发起请求的客户端IP是否允许连接到MySQL实例。

1.5.2 命名管道和共享内存

        在Windows 2000、Windows XP、Windows 2003和Windows Vista以及在此之上的平台上,如果两个需要进程通信的进程在同一台服务器上,那么可以使用命名管道,Microsoft SQL Server数据库默认安装后的本地连接也是使用命名管道。在MySQL数据库中须在配置文件中启用--enable-named-pipe选项。在MySQL 4.1之后的版本中,MySQL还提供了共享内存的连接方式,这是通过在配置文件中添加--shared-memory实现的。

1.5.3 UNIX域套接字

        在Linux和UNIX环境下,还可以使用UNIX域套接字。UNIX域套接字其实不是一个网络协议,所以只能在MySQL客户端和数据库实例在一台服务器上的情况下使用。用户可以在配置文件中指定套接字文件的路径,如--socket=/tmp/mysql.sock。当数据库实例启动后,用户可以通过下列命令来进行UNIX域套接字文件的查找:

        SHOW VARIABLES LIKE'socket';

第2章 InnoDB存储引擎

        InnoDB是事务安全的MySQL存储引擎,是OLTP应用中核心表的首选存储引擎。

2.1 InnoDB存储引擎概述

         InnoDB存储引擎最从MySQL 5.5版本开始是默认的表存储引擎,是第一个完整支持ACID事务的MySQL存储引擎,其特点是行锁设计支持MVCC支持外键提供一致性非锁定读,同时被设计用来最有效地利用以及使用内存和CPU。InnoDB是一个高性能、高可用、高可扩展的存储引擎。

2.2 InnoDB存储引擎的版本

        InnoDB存储引擎被包含于所有MySQL数据库的二进制发行版本中。

2.3 InnoDB体系架构

        图2-1简单显示了InnoDB的存储引擎的体系架构。

        从图可见,InnoDB存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,负责如下工作:

        □ 维护所有进程/线程需要访问的多个内部数据结构。

        □ 缓存磁盘上的数据,方便快速地读取,同时在对磁盘文件的数据修改之前在这里缓存。

        □ 重做日志(redo log)缓冲。……

        后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下InnoDB能恢复到正常运行状态。

2.3.1 后台线程

        InnoDB存储引擎是多线程的模型,其后台有多个不同的后台线程,负责处理不同的任务。

1. Master Thread

        Master Thread是一个非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收等。

2. IO Thread

        在InnoDB存储引擎中大量使用了AIO(Async IO)来处理写IO请求,这样可以极大提高数据库的性能。IOThread的工作主要是负责这些IO请求的回调(call back)处理。InnoDB 1.0版本之前共有4个IO Thread,分别是write、read、insert buffer和log IO thread。从InnoDB 1.0.x版本开始,read thread和write thread分别增大到了4个。

3. Purge Thread

        事务被提交后,其所使用的undolog可能不再需要,因此PurgeThread回收已经使用并分配的undo页。从InnoDB 1.2版本开始,InnoDB支持多个Purge Thread,这样做的目的是为了进一步加快undo页的回收。

4. Page Cleaner

        ThreadPage Cleaner Thread是在InnoDB 1.2.x版本中引入的。其作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。而其目的是为了减轻原Master Thread的工作及对于用户查询线程的阻塞,进一步提高InnoDB存储引擎的性能。

2.3.2 内存

1. 缓冲池

        InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理

        缓冲池就是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中,这个过程称为将页“FIX”在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。

        对于数据库页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为Checkpoint的机制刷新回磁盘。同样,这也是为了提高数据库的整体性能。

        缓冲池的大小直接影响着数据库的整体性能。InnoDB存储引擎缓冲池的配置通过参数innodb_buffer_pool_size来设置。缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lock info)、数据字典信息(data dictionary)等

        从InnoDB 1.0.x版本开始,允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。这样做的好处是减少数据库内部的资源竞争,增加数据库的并发处理能力。可以通过参数innodb_buffer_pool_instances来进行配置,该值默认为1。

2. LRU List、Free List和Flush List

        缓冲池是一个很大的内存区域,其中存放各种类型的页,InnoDB存储引擎是怎么对这么大的内存区域进行管理的呢?

        数据库中的缓冲池是通过LRU(Latest Recent Used,最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。

        在InnoDB存储引擎中,缓冲池中页的大小默认为16KB,InnoDB存储引擎对传统的LRU算法做了一些优化。在InnoDB的存储引擎中,LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。这个算法在InnoDB存储引擎下称为midpoint insertion strategy。在默认配置下,该位置在LRU列表长度的5/8处。midpoint位置可由参数innodb_old_blocks_pct控制。

        为什么不采用朴素的LRU算法,直接将读取的页放入到LRU列表的首部呢?因为若直接将读取到的页放入到LRU的首部,那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引数据的扫描操作。这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说又仅在这次查询操作中需要,并不是活跃的热点数据。如果页被放入LRU列表的首部,那么非常可能将所需要的热点数据页从LRU列表中移除,而在下一次需要读取该页时,InnoDB存储引擎需要再次访问磁盘。

        为了解决这个问题,InnoDB存储引擎引入了另一个参数来进一步管理LRU列表,这个参数是innodb_old_blocks_time,用于表示页读取到mid位置后需要等待多久才会被加入到LRU列表的热端。

        LRU列表用来管理已经读取的页,但当数据库刚启动时,LRU列表是空的,即没有任何的页。这时页都存放在Free列表中。当需要从缓冲池中分页时,首先从Free列表中查找是否有可用的空闲页,若有则将该页从Free列表中删除,放入到LRU列表中。否则,根据LRU算法,淘汰LRU列表末尾的页,将该内存空间分配给新的页。当页从LRU列表的old部分加入到new部分时,称此时发生的操作为page made young,而因为innodb_old_blocks_time的设置而导致页没有从old部分移动到new部分的操作称为page not made young。可以通过命令SHOW ENGINE INNODB STATUS来观察LRU列表及Free列表的使用情况和运行状态

        通过命令SHOW ENGINE INNODB STATUS可以看到:当前Buffer pool size共有327 679个页,即327679*16K,总共5GB的缓冲池。Free buffers表示当前Free列表中页的数量,Database pages表示LRU列表中页的数量。可能的情况是Free buffers与Database pages的数量之和不等于Buffer pool size,因为缓冲池中的页还可能会被分配给自适应哈希索引、Lock信息、Insert Buffer等页,而这部分页不需要LRU算法进行维护,因此不存在于LRU列表中。

        pages made young显示了LRU列表中页移动到前端的次数,因为该服务器在运行阶段没有改变innodb_old_blocks_time的值,因此not young为0。youngs/s、non-youngs/s表示每秒这两类操作的次数。Buffer pool hit rate,表示缓冲池的命中率,这个例子中为100%,说明缓冲池运行状态非常良好。通常该值不应该小于95%。若发生Buffer pool hit rate的值小于95%这种情况,用户需要观察是否是由于全表扫描引起的LRU列表被污染的问题。

        从InnoDB 1.2版本开始,还可以通过表INNODB_BUFFER_POOL_STATS来观察缓冲池的运行状态。还可以通过表INNODB_BUFFER_PAGE_LRU来观察每个LRU列表中每个页的具体信息。

        LRU列表中的页被修改后,称该页为脏页(dirty page),即缓冲池中的页和磁盘上的页的数据产生了不一致。数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。注意:脏页既存在于LRU列表中,也存在于Flush列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响。

        同LRU列表一样,Flush列表也可以通过命令SHOW ENGINE INNODB STATUS来查看,前面例子中Modified db pages 24673就显示了脏页的数量。脏页同样存在于LRU列表中,故可以通过元数据表INNODB_BUFFER_PAGE_LRU来查看,唯一不同的是需要加入OLDEST_MODIFICATION大于0的SQL查询条件,如:

         可以看到当前共有5个脏页及它们对应的表和页的类型。TABLE_NAME为NULL表示该页属于系统表空间。

3.重做日志缓冲

         InnoDB存储引擎首先将重做日志信息先放入到这个缓冲区,然后按一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置得很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可由配置参数innodb_log_buffer_size控制,默认为8MB。

        在通常情况下,8MB的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中。

        ❑Master Thread每一秒将重做日志缓冲刷新到重做日志文件;

        ❑每个事务提交时会将重做日志缓冲刷新到重做日志文件;

        ❑当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件。

4.额外的内存池

        在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。

2.4 Checkpoint技术

        缓冲池的设计目的为了协调CPU速度与磁盘速度的鸿沟。因此页的操作首先都是在缓冲池中完成的。如果一条DML语句,如Update或Delete改变了页中的记录,那么此时页是脏的,即缓冲池中的页的版本要比磁盘的新。数据库需要将新版本的页从缓冲池刷新到磁盘。

        倘若每次一个页发生变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的。若热点数据集中在某几个页中,那么数据库的性能将变得非常差。同时,如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么数据就不能恢复了。为了避免发生数据丢失的问题,当前事务数据库系统普遍都采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。这也是事务ACID中D(Durability持久性)的要求。

        Checkpoint(检查点)技术的目的是解决以下几个问题:

        ❑缩短数据库的恢复时间;

        ❑缓冲池不够用时,将脏页刷新到磁盘;

        ❑重做日志不可用时,刷新脏页。

        当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。故数据库只需对Checkpoint后的重做日志进行恢复。这样就大大缩短了恢复的时间。

        此外,当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。

        重做日志出现不可用的情况是因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的。重做日志可以被重用的部分是指这些重做日志已经不再需要,即当数据库发生宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。若此时重做日志还需要使用,那么必须强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。

        对于InnoDB存储引擎而言,其是通过LSN(Log Sequence Number)来标记版本的。而LSN是8字节的数字,其单位是字节。每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN。

        在InnoDB存储引擎内部有两种Checkpoint:

        ❑Sharp Checkpoint

        Sharp Checkpoint发生在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1。

        ❑Fuzzy Checkpoint

        若数据库在运行时也使用Sharp Checkpoint,那么数据库的可用性就会受到很大的影响。故在InnoDB存储引擎内部使用Fuzzy Checkpoint进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘。

        在InnoDB存储引擎中可能发生如下几种情况的Fuzzy Checkpoint:

        ❑Master Thread Checkpoint

        ❑FLUSH_LRU_LIST Checkpoint

        ❑Async/Sync Flush Checkpoint

        ❑Dirty Page too much Checkpoint

        Master Thread中发生的Checkpoint,差不多以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘。这个过程是异步的,即此时InnoDB存储引擎可以进行其他的操作,用户查询线程不会阻塞。

        FLUSH_LRU_LIST Checkpoint是因为InnoDB存储引擎需要保证LRU列表中需要有差不多100个空闲页可供使用。在InnoDB1.1.x版本之前,需要检查LRU列表中是否有足够的可用空间操作发生在用户查询线程中,显然这会阻塞用户的查询操作。倘若没有100个可用空闲页,那么InnoDB存储引擎会将LRU列表尾端的页移除。如果这些页中有脏页,那么需要进行Checkpoint,而这些页是来自LRU列表的,因此称为FLUSH_LRU_LIST Checkpoint。而从MySQL 5.6版本,也就是InnoDB1.2.x版本开始,这个检查被放在了一个单独的Page Cleaner线程中进行,并且用户可以通过参数innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认为1024。

        Async/Sync Flush Checkpoint指的是重做日志文件不可用的情况,这时需要强制将一些页刷新回磁盘,而此时脏页是从脏页列表中选取的。Async/Sync Flush Checkpoint是为了保证重做日志的循环使用的可用性。在InnoDB 1.2.x版本之前,Async Flush Checkpoint会阻塞发现问题的用户查询线程,而Sync Flush Checkpoint会阻塞所有的用户查询线程,并且等待脏页刷新完成。从InnoDB 1.2.x版本开始——也就是MySQL 5.6版本,这部分的刷新操作同样放入到了单独的Page Cleaner Thread中,故不会阻塞用户查询线程。

        Dirty Page too much,即脏页的数量太多,导致InnoDB存储引擎强制进行Checkpoint。其目的总的来说还是为了保证缓冲池中有足够可用的页。其可由参数innodb_max_dirty_pages_pct控制。

2.5 Master Thread工作方式

       InnoDB存储引擎的主要工作都是在一个单独的后台线程Master Thread中完成的,这一节将具体解释该线程的具体实现及该线程可能存在的问题。

2.5.1 InnoDB 1.0.x版本之前的Master Thread

        Master Thread具有最高的线程优先级别。其内部由多个循环(loop)组成:主循环(loop)、后台循环(backgroup loop)、刷新循环(flush loop)、暂停循环(suspend loop)。Master Thread会根据数据库运行的状态在loop、background loop、flush loop和suspendloop中进行切换。

        Loop被称为主循环,因为大多数的操作是在这个循环中,其中有两大部分的操作——每秒钟的操作每10秒的操作。伪代码如下:

void master_thread(){

loop:

for(int i=0;i<10;i++){

        do thing once per second

        sleep 1 second if necessary

}

        do things once per ten seconds

        goto loop;

}

        loop循环通过thread sleep来实现,所谓的每秒一次或每10秒一次的操作是不精确的,在负载很大的情况下可能会有延迟(delay)。InnoDB源代码中通过了其他的方法来尽量保证这个频率。

        每秒一次的操作包括

        ❑日志缓冲刷新到磁盘,即使这个事务还没有提交(总是);

        ❑合并插入缓冲(可能);

        ❑至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能);

        ❑如果当前没有用户活动,则切换到background loop(可能)。

        即使某个事务还没有提交,InnoDB存储引擎仍然每秒会将重做日志缓冲中的内容刷新到重做日志文件。

        合并插入缓冲(Insert Buffer)并不是每秒都会发生的。InnoDB存储引擎会判断当前一秒内发生的IO次数是否小于5次,如果小于5次,InnoDB认为当前的IO压力很小,可以执行合并插入缓冲的操作。

        刷新100个脏页也不是每秒都会发生的。InnoDB存储引擎通过判断当前缓冲池中脏页的比例(buf_get_modified_ratio_pct)是否超过了配置文件中innodb_max_dirty_pages_pct这个参数(默认为90,代表90%),如果超过了这个阈值,InnoDB存储引擎认为需要做磁盘同步的操作,将100个脏页写入磁盘中。

        每10秒的操作,包括如下内容:

        ❑刷新100个脏页到磁盘(可能的情况下);

        ❑合并至多5个插入缓冲(总是);

        ❑将日志缓冲刷新到磁盘(总是);

        ❑删除无用的Undo页(总是);

        ❑刷新100个或者10个脏页到磁盘(总是)。

        InnoDB存储引擎会先判断过去10秒之内磁盘的IO操作是否小于200次,如果是,InnoDB存储引擎认为当前有足够的磁盘IO操作能力,因此将100个脏页刷新到磁盘。接着,InnoDB存储引擎会合并插入缓冲。之后,InnoDB存储引擎会再进行一次将日志缓冲刷新到磁盘的操作。接着InnoDB存储引擎会进行一步执行full purge操作,即删除无用的Undo页。对表进行update、delete这类操作时,原先的行被标记为删除,但是因为一致性读(consistent read)的关系,需要保留这些行版本的信息。但是在full purge过程中,InnoDB存储引擎会判断当前事务系统中已被删除的行是否可以删除,比如有时候可能还有查询操作需要读取之前版本的undo信息,如果可以删除,InnoDB会立即将其删除。从源代码中可以发现,InnoDB存储引擎在执行full purge操作时,每次最多尝试回收20个undo页。然后,InnoDB存储引擎会判断缓冲池中脏页的比例(buf_get_modified_ratio_pct),如果有超过70%的脏页,则刷新100个脏页到磁盘,如果脏页的比例小于70%,则只需刷新10%的脏页到磁盘。

        background loop,若当前没有用户活动(数据库空闲时)或者数据库关闭(shutdown),就会切换到这个循环。background loop会执行以下操作:

        ❑删除无用的Undo页(总是);

        ❑合并20个插入缓冲(总是);

        ❑跳回到主循环(总是);

        ❑不断刷新100个页直到符合条件(可能,跳转到flush loop中完成)。

        若flush loop中也没有什么事情可以做了,InnoDB存储引擎会切换到suspend__loop,将Master Thread挂起,等待事件的发生。若用户启用(enable)了InnoDB存储引擎,却没有使用任何InnoDB存储引擎的表,那么Master Thread总是处于挂起的状态。

2.5.2 InnoDB1.2.x版本之前的Master Thread

        1.0.x版本之前的Master Thread,InnoDB存储引擎对于IO其实是有限制的,在缓冲池向磁盘刷新时其实都做了一定的硬编码(hard coding)。InnoDB存储引擎最大只会刷新100个脏页到磁盘,合并20个插入缓冲。因此InnoDB Plugin(从InnoDB1.0.x版本开始)提供了参数innodb_io_capacity,用来表示磁盘IO的吞吐量,默认值为200。对于刷新到磁盘页的数量,会按照innodb_io_capacity的百分比来进行控制。规则如下:

        ❑在合并插入缓冲时,合并插入缓冲的数量为innodb_io_capacity值的5%;

        ❑在从缓冲区刷新脏页时,刷新脏页的数量为innodb_io_capacity。

        若用户使用了SSD类的磁盘,或者将几块磁盘做了RAID,当存储设备拥有更高的IO速度时,完全可以将innodb_io_capacity的值调得再高点,直到符合磁盘IO的吞吐量为止。

        另一个问题是,参数innodb_max_dirty_pages_pct默认值的问题,在InnoDB 1.0.x版本之前,该值的默认为90,意味着脏页占缓冲池的90%。但是该值“太大”了,因为InnoDB存储引擎在每秒刷新缓冲池和flush loop时会判断这个值,如果该值大于innodb_max_dirty_pages_pct,才刷新100个脏页,如果有很大的内存,或者数据库服务器的压力很大,这时刷新脏页的速度反而会降低。同样,在数据库的恢复阶段可能需要更多的时间。

        InnoDB 1.0.x版本带来的另一个参数是innodb_adaptive_flushing自适应地刷新),该值影响每秒刷新脏页的数量。原来的刷新规则是:脏页在缓冲池所占的比例小于innodb_max_dirty_pages_pct时,不刷新脏页;大于innodb_max_dirty_pages_pct时,刷新100个脏页。随着innodb_adaptive_flushing参数的引入,InnoDB存储引擎会通过一个名为buf_flush_get_desired_flush_rate的函数来判断需要刷新脏页最合适的数量。粗略地翻阅源代码后发现buf_flush_get_desired_flush_rate通过判断产生重做日志(redo log)的速度来决定最合适的刷新脏页数量。因此,当脏页的比例小于innodb_max_dirty_pages_pct时,也会刷新一定量的脏页。

        还有一个改变是:之前每次进行full purge操作时,最多回收20个Undo页,从InnoDB 1.0.x版本开始引入了参数innodb_purge_batch_size,该参数可以控制每次full purge回收的Undo页的数量。该参数的默认值为20,并可以动态地对其进行修改。

2.5.3 InnoDB 1.2.x版本的Master Thread

        在InnoDB 1.2.x版本中再次对Master Thread进行了优化,由此也可以看出Master Thread对性能所起到的关键作用。在InnoDB 1.2.x版本中,Master Thread的伪代码如下:

if InnoDB is idle

        srv_master_do_idle_tasks();

else

        srv_master_do_active_tasks();

        其中srv_master_do_idle_tasks()就是之前版本中每10秒的操作,srv_master_do_active_tasks()处理的是之前每秒中的操作。同时对于刷新脏页的操作,从Master Thread线程分离到一个单独的Page Cleaner Thread,从而减轻了Master Thread的工作,同时进一步提高了系统的并发性。

2.6 InnoDB关键特性

        InnoDB存储引擎的关键特性包括:

        ❑插入缓冲(Insert Buffer)        ❑两次写(Double Write)

        ❑自适应哈希索引(Adaptive Hash Index)

        ❑异步IO(Async IO)                 ❑刷新邻接页(Flush Neighbor Page)

2.6.1 插入缓冲

1.Insert Buffer

        InnoDB缓冲池中虽然有Insert Buffer信息,但是Insert Buffer和数据页一样,也是物理页的一个组成部分。

        在InnoDB存储引擎中,主键是行唯一的标识符。通常应用程序中行记录的插入顺序是按照主键递增的顺序进行插入的。因插入聚集索引(Primary Key)一般是顺序的,不需要磁盘的随机读取。

        但是不可能每张表上只有一个聚集索引,更多情况下,一张表上有多个非聚集的辅助索引(secondary index)。

        数据页的存放还是按主键进行顺序存放的,但是对于非聚集索引叶子节点的插入不再是顺序的,这时就需要离散地访问非聚集索引页,由于随机读取的存在而导致了插入操作性能下降。当然这并不是这个b字段上索引的错误,而是因为B+树的特性决定了非聚集索引插入的离散性。

        InnoDB存储引擎设计了Insert Buffer,对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer对象中。数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。

        Insert Buffer的使用需要同时满足以下两个条件:

        ❑ 索引是辅助索引(secondary index);

        ❑ 索引不是唯一(unique)的。

        不过考虑这样一种情况:应用程序进行大量的插入操作,这些都涉及了不唯一的非聚集索引,也就是使用了Insert Buffer。若此时MySQL数据库发生了宕机,这时势必有大量的Insert Buffer并没有合并到实际的非聚集索引中去。因此这时恢复可能需要很长的时间,在极端情况下甚至需要几个小时。

        辅助索引不能是唯一的,因为在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性。如果去查找肯定又会有离散读取的情况发生,从而导致Insert Buffer失去了意义。

        Insert Buffer存在一个问题是:在写密集的情况下,插入缓冲会占用过多的缓冲池内存(innodb_buffer_pool),默认最大可以占用到1/2的缓冲池内存。

        修改IBUF_POOL_SIZE_PER_MAX_SIZE就可以对插入缓冲的大小进行控制。比如将IBUF_POOL_SIZE_PER_MAX_SIZE改为3,则最大只能使用1/3的缓冲池内存。

2.Change Buffer

        InnoDB从1.0.x版本开始引入了Change Buffer,可将其视为Insert Buffer的升级。从这个版本开始,InnoDB存储引擎可以对DML操作——INSERT、DELETE、UPDATE都进行缓冲,他们分别是:Insert Buffer、Delete Buffer、Purge buffer。

        和之前Insert Buffer一样,Change Buffer适用的对象依然是非唯一的辅助索引。

        对一条记录进行UPDATE操作分为两个过程:1) 将记录标记为已删除;2) 真正将记录删除。

        Delete Buffer对应UPDATE操作的第一个过程,即将记录标记为删除。Purge Buffer对应UPDATE操作的第二个过程,即将记录真正的删除。InnoDB存储引擎提供了参数innodb_change_buffering,用来开启各种Buffer的选项。该参数可选的值为:inserts、deletes、purges、changes、all、none。inserts、deletes、purges就是前面讨论过的三种情况。changes表示启用inserts和deletes,all表示启用所有,none表示都不启用。该参数默认值为all。

        从InnoDB 1.2.x版本开始,可以通过参数innodb_change_buffer_max_size来控制Change Buffer最大使用内存的数量,值默认为25,表示最多使用1/4的缓冲池内存空间。而需要注意的是,该参数的最大有效值为50。

3.Insert Buffer的内部实现

        Insert Buffer的使用场景,即非唯一辅助索引的插入操作。

        Insert Buffer的数据结构是一棵B+树。在MySQL 4.1之前的版本中每张表有一棵Insert Buffer B+树。而在现在的版本中,全局只有一棵Insert Buffer B+树,负责对所有的表的辅助索引进行Insert Buffer。这棵B+树存放在共享表空间中,默认也就是ibdata1中。因此,试图通过独立表空间ibd文件恢复表中数据时,往往会导致CHECK TABLE失败。这是因为表的辅助索引中的数据可能还在Insert Buffer中,也就是共享表空间中,所以通过ibd文件进行恢复后,还需要进行REPAIR TABLE操作来重建表上所有的辅助索引。

        Insert Buffer是一棵B+树,因此其也由叶节点和非叶节点组成。非叶节点存放的是查询的search key(键值),其构造如图2-3所示。

图2-3 Insert Buffer非叶节点中的search key

        search key一共占用9个字节,其中, space表示待插入记录所在表的表空间id,在InnoDB存储引擎中,每个表有一个唯一的space id,通过space id查询得知是哪张表。space占用4字节。marker占用1字节,它是用来兼容老版本的Insert Buffer。offset表示页所在的偏移量,占用4字节。

        当一个辅助索引要插入到页(space,offset)时,如果这个页不在缓冲池中,那么InnoDB存储引擎首先根据上述规则构造一个search key,接下来查询Insert Buffer这棵B+树,然后再将这条记录插入到Insert Buffer B+树的叶子节点中。

        对于插入到Insert Buffer B+树叶子节点的记录(如图2-4所示),并不是直接将待插入的记录插入,而是需要根据如下的规则进行构造:

图 2-4 Insert Buffer叶子节点中的记录

        space、marker、page_no字段和之前非叶节点中的含义相同,一共占用9字节。第4个字段metadata占用4字节,其存储的内容如表2-2所示。

        IBUF_REC_OFFSET_COUNT是保存两个字节的整数,用来排序每个记录进入Insert Buffer的顺序。通过这个顺序回放(replay)才能得到记录的正确值。

        从Insert Buffer叶子节点的第5列开始,就是实际插入记录的各个字段了。因此较之原插入记录,Insert Buffer B+树的叶子节点记录需要额外13字节的开销。

        因为启用Insert Buffer索引后,辅助索引页(space,page_no)中的记录可能被插入到Insert Buffer B+树中,所以为了保证每次Merge Insert Buffer页必须成功,还需要有一个特殊的页用来标记每个辅助索引页(space,page_no)的可用空间。这个页的类型为Insert Buffer Bitmap。

        每个Insert Buffer Bitmap页用来追踪16384个辅助索引页,也就是256个区(Extent)。每个Insert Buffer Bitmap页都在16384个页的第二个页中。关于Insert Buffer Bitmap页的作用会在下一小节中详细介绍。

        每个辅助索引页在Insert Buffer Bitmap页中占用4位(bit),由表2-3中的三个部分组成。

4.Merge Insert Buffer

        通过前面介绍知道了Insert/Change Buffer是一棵B+树。若需要实现插入记录的辅助索引页不在缓冲池中,那么需要将辅助索引记录首先插入到这棵B+树中。Merge Insert Buffer的操作可能发生在以下几种情况下:

        ❑辅助索引页被读取到缓冲池时;

        ❑Insert Buffer Bitmap页追踪到该辅助索引页已无可用空间时;

        ❑Master Thread。

        第一种情况为当辅助索引页被读取到缓冲池中时,例如这在执行正常的SELECT查询操作,这时需要检查Insert Buffer Bitmap页,然后确认该辅助索引页是否有记录存放于Insert Buffer B+树中。若有,则将Insert Buffer B+树中该页的记录插入到该辅助索引页中。可以看到对该页多次的记录操作通过一次操作合并到了原有的辅助索引页中,因此性能会有大幅提高。

        第二种情况是 Insert Buffer Bitmap页用来追踪每个辅助索引页的可用空间,并至少有1/32页的空间。若插入辅助索引记录时检测到插入记录后可用空间会小于1/32页,则会强制进行一个合并操作,即强制读取辅助索引页,将Insert Buffer B+树中该页的记录及待插入的记录插入到辅助索引页中。

        第三种情况是Master Thread线程中每秒或每10秒会进行一次Merge Insert Buffer的操作,不同之处在于每次进行merge操作的页的数量不同。在Master Thread中,执行merge操作的不止是一个页,而是根据srv_innodb_io_capactiy的百分比来决定真正要合并多少个辅助索引页。但InnoDB存储引擎又是根据怎样的算法来得知需要合并的辅助索引页呢?

        在Insert Buffer B+树中,辅助索引页根据(space,offset)都已排序好,故可以根据(space,offset)的排序顺序进行页的选择。然而,对于Insert Buffer页的选择,InnoDB存储引擎并非采用这个方式,它随机地选择Insert Buffer B+树的一个页,读取该页中的space及之后所需要数量的页。该算法在复杂情况下应有更好的公平性。同时,若进行merge时,要进行merge的表已经被删除,此时可以直接丢弃已经被Insert/Change Buffer的数据记录。

2.6.2 两次写

        如果说Insert Buffer带给InnoDB存储引擎的是性能上的提升,那么doublewrite(两次写)带给InnoDB存储引擎的是数据页的可靠性。

        应用(apply)重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再进行重做,这就是doublewrite。在InnoDB存储引擎中doublewrite的体系架构如图2-5所示。

图 2-5 InnoDB存储引擎doublewrite架构

        doublewrite由两部分组成,一部分是内存中的doublewrite buffer,大小为2MB,另一部分是物理磁盘上共享表空间中连续的128个页,即2个区(extent),大小同样为2MB。在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。在这个过程中,因为doublewrite页是连续的,因此这个过程是顺序写的,开销并不是很大。在完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中,此时的写入则是离散的。可以通过以下命令观察到doublewrite运行的情况: SHOW GLOBAL STATUS LIKE'innodb_dblwr%'\G;

        如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到该页的一个副本,将其复制到表空间文件,再应用重做日志。

2.6.3 自适应哈希索引

        哈希(hash)是一种非常快的查找方法,在一般情况下这种查找的时间复杂度为O(1),即一般仅需要一次查找就能定位数据。而B+树的查找次数,取决于B+树的高度,在生产环境中,B+树的高度一般为3~4层,故需要3~4次的查询。

        InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)。AHI是通过缓冲池的B+树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。

2.6.4 异步IO

        为了提高磁盘操作性能,当前的数据库系统都采用异步IO(Asynchronous IO,AIO)的方式来处理磁盘操作。InnoDB存储引擎亦是如此。

        Sync IO,即每进行一次IO操作,需要等待此次操作结束才能继续接下来的操作。但是如果用户发出的是一条索引扫描的查询,那么这条SQL查询语句可能需要扫描多个索引页,也就是需要进行多次的IO操作。用户可以在发出一个IO请求后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成,这就是AIO

        AIO的另一个优势是可以进行IO Merge操作,也就是将多个IO合并为1个IO,这样可以提高IOPS的性能。例如用户需要访问页的(space,page_no)为:

        (8,6)、(8,7),(8,8)

        每个页的大小为16KB,那么同步IO需要进行3次IO操作。而AIO会判断到这三个页是连续的(显然可以通过(space,page_no)得知)。因此AIO底层会发送一个IO请求,从(8,6)开始,读取48KB的页。

        用户可以通过开启和关闭Native AIO功能来比较InnoDB性能的提升。官方的测试显示,启用Native AIO,恢复速度可以提高75%。

        在InnoDB存储引擎中,read ahead方式的读取都是通过AIO完成,脏页的刷新,即磁盘的写入操作则全部由AIO完成。

2.6.5 刷新邻接页

        InnoDB存储引擎还提供了Flush Neighbor Page(刷新邻接页)的特性。其工作原理为:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。这样做的好处显而易见,通过AIO可以将多个IO写入操作合并为一个IO操作,故该工作机制在传统机械磁盘下有着显著的优势。但是需要考虑到下面两个问题:

        ❑是不是可能将不怎么脏的页进行了写入,而该页之后又会很快变成脏页?

        ❑固态硬盘有着较高的IOPS,是否还需要这个特性?

        为此,InnoDB存储引擎从1.2.x版本开始提供了参数innodb_flush_neighbors,用来控制是否启用该特性。对于传统机械硬盘建议启用该特性,而对于固态硬盘有着超高IOPS性能的磁盘,则建议将该参数设置为0,即关闭此特性。

第3章 文件

        构成MySQL数据库和InnoDB存储引擎表的各种类型文件有以下这些:

        ❑ 参数文件:告诉MySQL实例启动时在哪里可以找到数据库文件,并指定某些初始化参数。

        ❑ 日志文件:用来记录MySQL实例对某种条件做出响应时写入的文件,如错误日志文件、二进制日志文件、慢查询日志文件、查询日志文件等。

        ❑ socket文件:当用UNIX域套接字方式进行连接时需要的文件。

        ❑ pid文件:MySQL实例的进程ID文件。

        ❑ MySQL表结构文件:用来存放MySQL表结构定义文件。

        ❑ 存储引擎文件:因为MySQL表存储引擎的关系,每个存储引擎都会有自己的文件来保存各种数据。这些存储引擎真正存储了记录和索引等数据

3.1 参数文件

      当MySQL实例启动时,数据库会先去读一个配置参数文件,寻找数据库的各种文件所在位置以及指定某些初始化参数。在默认情况下,MySQL实例会按照一定的顺序在指定的位置进行读取,用户只需通过命令mysql--help|grep my.cnf来寻找即可。

        MySQL数据库的参数文件是以文本方式进行存储的。用户可以直接通过一些常用的文本编辑软件(如vi和emacs)进行参数的修改。

3.1.1 什么是参数

        简单地说,可以把数据库参数看成一个键/值(key/value)对。例如:innodb_buffer_pool_size=1G。可以通过命令SHOW VARIABLES查看数据库中的所有参数,也可以通过LIKE来过滤参数名。从MySQL 5.1版本开始,还可以通过information_schema架构下的GLOBAL_VARIABLES视图来进行查找。

3.1.2 参数类型

        MySQL数据库中的参数可以分为两类:

        ❑ 动态(dynamic)参数        ❑ 静态(static)参数

        动态参数意味着可以在MySQL实例运行中进行更改,静态参数说明在整个实例生命周期内都不得进行更改,就好像是只读(read only)的。可以通过SET命令对动态的参数值进行修改,SET的语法如下:

SET

|[global|session]system_var_name=expr

|[@@global.|@@session.|@@]system_var_name=expr

        global和session关键字,它们表明该参数的修改是基于当前会话还是整个实例的生命周期。有些动态参数只能在会话中进行修改,如autocommit;而有些参数修改完后,在整个实例生命周期中都会生效,如binlog_cache_size;而有些参数既可以在会话中又可以在整个实例的生命周期内生效,如read_buffer_size。

3.2 日志文件

        日志文件记录了影响MySQL数据库的各种类型活动,常见的日志文件有:

        ❑ 错误日志(error log)                ❑ 二进制日志(binlog)        

        ❑ 慢查询日志(slow query log)   ❑ 查询日志(log)

3.2.1 错误日志

        错误日志文件对MySQL的启动、运行、关闭过程进行了记录。可以通过命令SHOW VARIABLES LIKE'log_error'来定位该文件。

3.2.2 慢查询日志

        慢查询日志(slow log)可帮助DBA定位可能存在问题的SQL语句,从而进行SQL语句层面的优化。例如,可以在MySQL启动时设一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询日志文件中。DBA每天或每过一段时间对其进行检查,确认是否有SQL语句需要进行优化。该阈值可以通过参数long_query_time来设置,默认值为10,代表10秒。

        在默认情况下,MySQL数据库并不启动慢查询日志,用户需要手工将这个参数设为ON。设置long_query_time这个阈值后,MySQL数据库会记录运行时间超过该值的所有SQL语句,但运行时间正好等于long_query_time的情况并不会被记录下。从MySQL 5.1开始,long_query_time开始以微秒记录SQL语句运行的时间,之前仅用秒为单位记录。

        另一个和慢查询日志有关的参数是log_queries_not_using_indexes,如果运行的SQL语句没有使用索引,则MySQL数据库同样会将这条SQL语句记录到慢查询日志文件,首先确认打开了log_queries_not_using_indexes。

        MySQL 5.6.5版本开始新增了一个参数log_throttle_queries_not_using_indexes,用来表示每分钟允许记录到slow log的且未使用索引的SQL语句次数。该值默认为0,表示没有限制。在生产环境下,若没有使用索引,此类SQL语句会频繁地被记录到slow log,从而导致slow log文件的大小不断增加,故DBA可通过此参数进行配置。

        MySQL数据库提供的mysqldumpslow命令,分析查询慢查询sql。MySQL 5.1开始可以将慢查询的日志记录放入一张表中,这使得用户的查询更加方便和直观。慢查询表在mysql架构下,名为slow_log。

        可以通过额外的参数long_query_io将超过指定逻辑IO次数的SQL语句记录到slow log中。该值默认为100,即表示对于逻辑读取次数大于100的SQL语句,记录到slow log中。而为了兼容原MySQL数据库的运行方式,还添加了参数slow_query_type,用来表示启用slow log的方式,可选值为:

        ❑0表示不将SQL语句记录到slow log

        ❑1表示根据运行时间将SQL语句记录到slow log

        ❑2表示根据逻辑IO次数将SQL语句记录到slow log

        ❑3表示根据运行时间及逻辑IO次数将SQL语句记录到slow log

3.2.3 查询日志

        查询日志记录了所有对MySQL数据库请求的信息,无论这些请求是否得到了正确的执行。默认文件名为:主机名.log。从MySQL 5.1开始,可以将查询日志的记录放入mysql架构下的general_log表中。

3.2.4 二进制日志

        二进制日志(binary log)记录了对MySQL数据库执行更改的所有操作,但是不包括SELECT和SHOW这类操作,因为这类操作对数据本身并没有修改。然而,若操作本身并没有导致数据库发生变化,那么该操作可能也会写入二进制日志。

        总的来说,二进制日志主要有以下几种作用。

        ❑ 恢复(recovery):某些数据的恢复需要二进制日志,例如,在一个数据库全备文件恢复后,用户可以通过二进制日志进行point-in-time的恢复。

        ❑ 复制(replication):其原理与恢复类似,通过复制和执行二进制日志使一台远程的MySQL数据库(一般称为slave或standby)与一台MySQL数据库(一般称为master或primary)进行实时同步。

        ❑ 审计(audit):用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入的攻击。

        通过配置参数log-bin[=name]可以启动二进制日志。如果不指定name,则默认二进制日志文件名为主机名,后缀名为二进制日志的序列号,所在路径为数据库所在目录(datadir)。

        二进制日志文件在默认情况下并没有启动,需要手动指定参数来启动。根据MySQL官方手册中的测试表明,开启二进制日志会使性能下降1%。

        二进制日志文件的文件格式为二进制,不能像错误日志文件、慢查询日志文件那样用cat、head、tail等命令来查看。要查看二进制日志文件的内容,必须通过MySQL提供的工具mysqlbinlog

3.3 套接字文件

        在UNIX系统下本地连接MySQL可以采用UNIX域套接字方式,这种方式需要一个套接字(socket)文件。套接字文件可由参数socket控制。一般在/tmp目录下,名为mysql.sock。

3.4 pid文件

        当MySQL实例启动时,会将自己的进程ID写入一个文件中——该文件即为pid文件。该文件可由参数pid_file控制,默认位于数据库目录下,文件名为主机名.pid。

3.5 表结构定义文件

        因为MySQL插件式存储引擎的体系结构的关系,MySQL数据的存储是根据表进行的,每个表都会有与之对应的文件。但不论表采用何种存储引擎,MySQL都有一个以frm为后缀名的文件,这个文件记录了该表的表结构定义。

        frm还用来存放视图的定义,如用户创建了一个v_a视图,那么对应地会产生一个v_a.frm文件,用来记录视图的定义,该文件是文本文件,可以直接使用cat命令进行查看。

3.6 InnoDB存储引擎文件

        之前介绍的文件都是MySQL数据库本身的文件,和存储引擎无关。除了这些文件外,每个表存储引擎还有其自己独有的文件。本节将具体介绍与InnoDB存储引擎密切相关的文件,这些文件包括重做日志文件、表空间文件。

3.6.1 表空间文件

        InnoDB采用将存储的数据按表空间(tablespace)进行存放的设计。在默认配置下会有一个初始大小为10MB,名为ibdata1的文件。该文件就是默认的表空间文件(tablespace file),用户可以通过参数innodb_data_file_path对其进行设置,格式如下:

innodb_data_file_path=datafle_spec1[;datafle_spec2]...

        用户可以通过多个文件组成一个表空间,同时制定文件的属性,如:

[mysqld]

innodb_data_file_path=/db/ibdata1:2000M;/dr2/db/ibdata2:2000M:autoextend

        这里将/db/ibdata1和/dr2/db/ibdata2两个文件用来组成表空间。若这两个文件位于不同的磁盘上,磁盘的负载可能被平均,因此可以提高数据库的整体性能。同时,两个文件的文件名后都跟了属性,表示文件idbdata1的大小为2000MB,文件ibdata2的大小为2000MB,如果用完了这2000MB,该文件可以自动增长(autoextend)。

        设置innodb_data_file_path参数后,所有基于InnoDB存储引擎的表的数据都会记录到该共享表空间中。若设置了参数innodb_file_per_table,则用户可以将每个基于InnoDB存储引擎的表产生一个独立表空间。独立表空间的命名规则为:表名.ibd。通过这样的方式,用户不用将所有数据都存放于默认的表空间中。单独的表空间文件仅存储该表的数据、索引和插入缓冲BITMAP等信息,其余信息还是存放在默认的表空间中。

        图3-1显示了InnoDB存储引擎对于文件的存储方式:

图3-1 InnoDB表存储引擎文件

3.6.2 重做日志文件

        在默认情况下,在InnoDB存储引擎的数据目录下会有两个名为ib_logfile0和ib_logfile1的文件,即 重做日志文件(redo log file)

        每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组下至少有2个重做日志文件,如默认的ib_logfile0和ib_logfile1。为了得到更高的可靠性,用户可以设置多个的镜像日志组(mirrored log groups),将不同的文件组放在不同的磁盘上,以此提高重做日志的高可用性。在日志组中每个重做日志文件的大小一致,并以循环写入的方式运行。InnoDB存储引擎先写重做日志文件1,当达到文件的最后时,会切换至重做日志文件2,再当重做日志文件2也被写满时,会再切换到重做日志文件1中

图 3-2 日志文件组

        下列参数影响着重做日志文件的属性:

        ❑innodb_log_file_size

        ❑innodb_log_files_in_group

        ❑innodb_mirrored_log_groups

        ❑innodb_log_group_home_dir

        innodb_log_file_size指定每个重做日志文件的大小。在InnoDB1.2.x版本之前,重做日志文件总的大小不得大于等于4GB,而1.2.x版本将该限制扩大为了512GB。

        innodb_log_files_in_group指定了日志文件组中重做日志文件的数量,默认为2。

        innodb_mirrored_log_groups指定了日志镜像文件组的数量,默认为1,表示只有一个日志文件组,没有镜像。若磁盘本身已经做了高可用的方案,如磁盘阵列,那么可以不开启重做日志镜像的功能。

        innodb_log_group_home_dir指定了日志文件组所在路径,默认为./,表示在MySQL数据库的数据目录下。

        重做日志文件的大小设置对于InnoDB存储引擎的性能有着非常大的影响。一方面重做日志文件不能设置得太大,如果设置得很大,在恢复时可能需要很长的时间;另一方面又不能设置得太小了,否则可能导致一个事务的日志需要多次切换重做日志文件。此外,重做日志文件太小会导致频繁地发生async checkpoint,导致性能的抖动。

        既然同样是记录事务日志,和之前介绍的二进制日志有什么区别?

        首先,二进制日志会记录所有与MySQL数据库有关的日志记录,包括InnoDB、MyISAM、Heap等其他存储引擎的日志。而InnoDB存储引擎的重做日志只记录有关该存储引擎本身的事务日志。

        其次,记录的内容不同,无论用户将二进制日志文件记录的格式设为STATEMENT还是ROW,又或者是MIXED,其记录的都是关于一个事务的具体操作内容,即该日志是逻辑日志。而InnoDB存储引擎的重做日志文件记录的是关于每个页(Page)的更改的物理情况。

        此外,写入的时间也不同,二进制日志文件仅在事务提交前进行提交,即只写磁盘一次,不论这时该事务多大。而在事务进行的过程中,却不断有重做日志条目(redo entry)被写入到重做日志文件中。

        从表3-2可以看到重做日志条目是由4个部分组成:

        ❑redo_log_type占用1字节,表示重做日志的类型

        ❑space表示表空间的ID,但采用压缩的方式,因此占用的空间可能小于4字节

        ❑page_no表示页的偏移量,同样采用压缩的方式

        ❑redo_log_body表示每个重做日志的数据部分,恢复时需要调用相应的函数进行解析

        在第2章中已经提到,写入重做日志文件的操作不是直接写,而是先写入一个重做日志缓冲(redo log buffer)中,然后按照一定的条件顺序地写入日志文件。图3-3很好地诠释了重做日志的写入过程。

图 3-3 重做日志写入过程

        从重做日志缓冲往磁盘写入时,是按512个字节,也就是一个扇区的大小进行写入。因为扇区是写入的最小单位,因此可以保证写入必定是成功的。因此在重做日志的写入过程中不需要有doublewrite。

        从日志缓冲写入磁盘上的重做日志文件是按一定条件进行的,那这些条件有哪些呢?第一个是在主线程中每秒会将重做日志缓冲写入磁盘的重做日志文件中,不论事务是否已经提交。另一个触发写磁盘的过程是由参数innodb_flush_log_at_trx_commit控制,表示在提交(commit)操作时,处理重做日志的方式。

        参数innodb_flush_log_at_trx_commit的有效值有0、1、2。0代表当提交事务时,并不将事务的重做日志写入磁盘上的日志文件,而是等待主线程每秒的刷新。1和2不同的地方在于:1表示在执行commit时将重做日志缓冲同步写到磁盘,即伴有fsync的调用。2表示将重做日志异步写到磁盘,即写到文件系统的缓存中。因此不能完全保证在执行commit时肯定会写入重做日志文件,只是有这个动作发生。

        为了保证事务的ACID中的持久性,必须将innodb_flush_log_at_trx_commit设置为1,也就是每当有事务提交时,就必须确保事务都已经写入重做日志文件。那么当数据库因为意外发生宕机时,可以通过重做日志文件恢复,并保证可以恢复已经提交的事务。而将重做日志文件设置为0或2,都有可能发生恢复时部分事务的丢失。不同之处在于,设置为2时,当MySQL数据库发生宕机而操作系统及服务器并没有发生宕机时,由于此时未写入磁盘的事务日志保存在文件系统缓存中,当恢复时同样能保证数据不丢失。

第4章 表

        本章将从InnoDB存储引擎表的逻辑存储及实现开始进行介绍,然后将重点分析表的物理存储特征,即数据在表中是如何组织和存放的。简单来说,表就是关于特定实体的数据集合,这也是关系型数据库模型的核心。

4.1 索引组织表

        在InnoDB存储引擎中,表都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table)。在InnoDB存储引擎表中,每张表都有个主键(Primary Key),如果在创建表时没有显式地定义主键,则InnoDB存储引擎会按如下方式选择或创建主键:

        ❑首先判断表中是否有非空的唯一索引(Unique NOT NULL),如果有,则该列即为主键。

        ❑如果不符合上述条件,InnoDB存储引擎自动创建一个6字节大小的指针。

        当表中有多个非空唯一索引时,InnoDB存储引擎将选择建表时第一个定义的非空唯一索引为主键。这里需要非常注意的是,主键的选择根据的是定义索引的顺序,而不是建表时列的顺序。

4.2 InnoDB逻辑存储结构

        从InnoDB存储引擎的逻辑存储结构看,所有数据都被逻辑地存放在一个空间中,称之为表空间(tablespace)。表空间由: 段(segment)、区(extent)、页(page)组成。页在一些文档中有时也称为块(block),InnoDB存储引擎的逻辑存储结构大致如图4-1所示。

图 4-1 InnoDB逻辑存储结构

4.2.1 表空间

        表空间可以看做是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。第3章中已经介绍了在默认情况下InnoDB存储引擎有一个共享表空间ibdata1,即所有数据都存放在这个表空间内。如果用户启用了参数innodb_file_per_table,则每张表内的数据可以单独放到一个表空间内。

        如果启用了innodb_file_per_table的参数,需要注意的是每张表的表空间内存放的只是数据、索引和插入缓冲Bitmap页,其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。

4.2.2 段

        表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。InnoDB存储引擎表是索引组织的(index organized),因此数据即索引,索引即数据。数据段即为B+树的叶子节点(图4-1的Leaf node segment),索引段即为B+树的非索引节点(图4-1的Non-leaf node segment)。回滚段较为特殊,将会在后面进行单独的介绍。

4.2.3 区

        区是由连续页组成的空间,在任何情况下每个区的大小都为1MB。为了保证区中页的连续性,InnoDB存储引擎一次从磁盘申请4~5个区。在默认情况下,InnoDB存储引擎页的大小为16KB,即一个区中一共有64个连续的页。

        InnoDB 1.0.x版本开始引入压缩页,即每个页的大小可以通过参数KEY_BLOCK_SIZE设置为2K、4K、8K,因此每个区对应页的数量就应该为512、256、128。

        InnoDB 1.2.x版本新增了参数innodb_page_size,通过该参数可以将默认页的大小设置为4K、8K,但是页中的数据库不是压缩。这时区中页的数量同样也为256、128。总之,不论页的大小怎么变化,区的大小总是为1M。

        用户启用了参数innodb_file_per_talbe后,创建的表默认大小是96KB

4.2.4 页

        页是InnoDB磁盘管理的最小单位。在InnoDB存储引擎中,默认每个页的大小为16KB。而从InnoDB 1.2.x版本开始,可以通过参数innodb_page_size将页的大小设置为4K、8K、16K。若设置完成,则所有表中页的大小都为innodb_page_size,不可以对其再次进行修改。除非通过mysqldump导入和导出操作来产生新的库。

        在InnoDB存储引擎中,常见的页类型有:

        ❑数据页(B-tree Node)

        ❑undo页(undo Log Page)

        ❑系统页(System Page)

        ❑事务数据页(Transaction system Page)

        ❑插入缓冲位图页(Insert Buffer Bitmap)

        ❑插入缓冲空闲列表页(Insert Buffer Free List)

        ❑未压缩的二进制大对象页(Uncompressed BLOB Page)

        ❑压缩的二进制大对象页(compressed BLOB Page)

4.2.5 行

        InnoDB存储引擎是面向列的(row-oriented),也就说数据是按行进行存放的。每个页存放的行记录也是有硬性定义的,最多允许存放16KB/2-200行的记录,即7992行记录。

4.3 InnoDB行记录格式

        在InnoDB 1.0.x版本之前,InnoDB存储引擎提供了CompactRedundant两种格式来存放行记录数据。Redundant格式是为兼容之前版本而保留的,如果阅读过InnoDB的源代码,用户会发现源代码中是用PHYSICAL RECORD(NEW STYLE)和PHYSICAL RECORD(OLD STYLE)来区分两种格式的。在MySQL 5.1版本中,默认设置为Compact行格式。用户可以通过命令SHOW TABLE STATUS LIKE'table_name'来查看当前表使用的行格式,其中row_format属性表示当前所使用的行记录结构类型。

4.3.1 Compact行记录格式

        Compact行记录是在MySQL 5.0中引入的,其设计目标是高效地存储数据。简单来说,一个页中存放的行数据越多,其性能就越高。图4-2显示了Compact行记录的存储方式:

图4-2 Compact行记录的格式

        从图4-2可以观察到,Compact行记录格式的首部是一个非NULL变长字段长度列表,并且其是按照列的顺序逆序放置的,其长度为:

        ❑若列的长度小于255字节,用1字节表示;

        ❑若大于255个字节,用2字节表示。

        变长字段的长度最大不可以超过2字节,这是因在MySQL数据库中VARCHAR类型的最大长度限制为65535。NULL标志位指示了该行数据中是否有NULL值,有则用1表示。该部分所占的字节应该为1字节。记录头信息(record header),固定占用5字节(40位),每位的含义见表4-1。

        最后的部分就是实际存储每个列的数据。需要特别注意的是,NULL不占该部分任何空间,即NULL除了占有NULL标志位,实际存储不占有任何空间。需要注意的是,每行数据除了用户定义的列外,还有两个隐藏列,事务ID列回滚指针列,分别为6字节和7字节的大小。若InnoDB表没有定义主键,每行还会增加一个6字节的rowid列。

4.3.2 Redundant行记录格式

        Redundant是MySQL 5.0版本之前InnoDB的行记录存储方式,MySQL 5.0支持Redundant是为了兼容之前版本的页格式。Redundant行记录采用如图4-3所示的方式存储。

图4-3 Redundant行记录格式

        字段长度偏移列表,同样是按照列的顺序逆序放置的。若列的长度小于255字节,用1字节表示;若大于255字节,用2字节表示。记录头信息(record header)占用6字节(48位),每位的含义见表4-2。从表4-2中可以发现,n_fields值代表一行中列的数量,占用10位。同时这也很好地解释了为什么MySQL数据库一行支持最多的列为1023。另一个需要注意的值为1byte_offs_flags,该值定义了偏移列表占用1字节还是2字节。而最后的部分就是实际存储的每个列的数据了。

4.3.3 行溢出数据

        InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外。一般认为BLOB、LOB这类的大对象列类型的存储会把数据存放在数据页面之外。但是,这个理解有点偏差,BLOB可以不将数据放在溢出页面,而且即便是VARCHAR列数据类型,依然有可能被存放为行溢出数据。

        对于行溢出数据,其存放采用图4-4的方式。

图4-4 行溢出数据的存储

        那多长的VARCHAR是保存在单个数据页中的,从多长开始又会保存在BLOB页呢?可以这样进行思考:InnoDB存储引擎表是索引组织的,即B+Tree的结构,这样每个页中至少应该有两条行记录(否则失去了B+Tree的意义,变成链表了)。因此,如果页中只能存放下一条记录,那么InnoDB存储引擎会自动将行数据存放到溢出页中。

        但是,如果可以在一个页中至少放入两行数据,那VARCHAR类型的行数据就不会存放到BLOB页中去。经过多次试验测试,发现这个阈值的长度为8098

4.3.4 Compressed和Dynamic行记录格式

        InnoDB 1.0.x版本开始引入了新的文件格式(file format,用户可以理解为新的页格式),以前支持的Compact和Redundant格式称为Antelope文件格式,新的文件格式称为Barracuda文件格式。Barracuda文件格式下拥有两种新的行记录格式:Compressed和Dynamic

        新的两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式,如图4-5所示,在数据页中只存放20个字节的指针,实际的数据都存放在Off Page中,而之前的Compact和Redundant两种格式会存放768个前缀字节。

图 4-5 Barracuda文件格式的溢出行

        Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。

4.3.5 CHAR的行结构存储

        通常理解VARCHAR是存储变长长度的字符类型,CHAR是存储固定长度的字符类型。从MySQL 4.1版本开始,CHR(N)中的N指的是字符的长度,而不是之前版本的字节长度。        

        InnoDB存储引擎内部对CHAR类型在多字节字符集类型的存储。CHAR类型被明确视为了变长字符类型,对于未能占满长度的字符还是填充0x20。InnoDB存储引擎内部对字符的存储和我们用HEX函数看到的也是一致的。因此可以认为在多字节字符集的情况下,CHAR和VARCHAR的实际行存储基本是没有区别的。

4.4 InnoDB数据页结构

        页是InnoDB存储引擎管理数据库的最小磁盘单位。页类型为B-tree Node的页存放的即是表中行的实际数据。

        InnoDB数据页由以下7个部分组成,如图4-6所示。

        ❑File Header(文件头)

        ❑Page Header(页头)

        ❑Infimun和Supremum Records

        ❑User Records(用户记录,即行记录)

        ❑Free Space(空闲空间)

        ❑Page Directory(页目录)

        ❑File Trailer(文件结尾信息)

图4-6 InnoDB存储引擎数据页结构

        File Header、Page Header、File Trailer的大小是固定的,分别为38、56、8字节,这些空间用来标记该页的一些信息,如Checksum,数据页所在B+树索引的层数等。User Records、Free Space、Page Directory这些部分为实际的行记录存储空间,因此大小是动态的。

4.4.1 File Header

        File Header用来记录页的一些头信息,由表4-3中8个部分组成,共占用38字节。

4.4.2 Page Header

        Page Header,该部分用来记录数据页的状态信息,由14个部分组成,共占用56字节,如表4-5所示。

4.4.3 Infimum和Supremum Record

        在InnoDB存储引擎中,每个数据页中有两个虚拟的行记录,用来限定记录的边界。Infimum记录是比该页中任何主键值都要小的值,Supremum指比任何可能大的值还要大的值。这两个值在页创建时被建立,并且在任何情况下不会被删除。在Compact行格式和Redundant行格式下,两者占用的字节数各不相同。图4-7显示了Infimum和Supremum记录。

图4-7 Infinum和Supremum Record

4.4.4 User Record和Free Space

        User Record即实际存储行记录的内容。

        Free Space很明显指的就是空闲空间,同样也是个链表数据结构。在一条记录被删除后,该空间会被加入到空闲链表中。

4.4.5 Page Directory

        Page Directory(页目录)中存放了记录的相对位置(注意,这里存放的是页相对位置,而不是偏移量),有些时候这些记录指针称为Slots(槽)或目录槽(Directory Slots)。在InnoDB中并不是每个记录拥有一个槽,InnoDB存储引擎的槽是一个稀疏目录(sparse directory),即一个槽中可能包含多个记录。伪记录Infimum的n_owned值总是为1,记录Supremum的n_owned的取值范围为[1,8],其他用户记录n_owned的取值范围为[4,8]。当记录被插入或删除时需要对槽进行分裂或平衡的维护操作。

        在Slots中记录按照索引键值顺序存放,这样可以利用二叉查找迅速找到记录的指针。假设有('i','d','c','b','e','g','l','h','f','j','k','a'),同时假设一个槽中包含4条记录,则Slots中的记录可能是('a','e','i')。

        由于在InnoDB存储引擎中Page Direcotry是稀疏目录,二叉查找的结果只是一个粗略的结果,因此InnoDB存储引擎必须通过recorder header中的next_record来继续查找相关记录。同时,Page Directory很好地解释了recorder header中的n_owned值的含义,因为这些记录并不包括在Page Directory中。

        需要牢记的是,B+树索引本身并不能找到具体的一条记录,能找到只是该记录所在的页。数据库把页载入到内存,然后通过Page Directory再进行二叉查找。只不过二叉查找的时间复杂度很低,同时在内存中的查找很快,因此通常忽略这部分查找所用的时间。

4.4.6 File Trailer

        为了检测页是否已经完整地写入磁盘(如可能发生的写入过程中磁盘损坏、机器关机等),InnoDB存储引擎的页中设置了File Trailer部分。

        File Trailer只有一个FIL_PAGE_END_LSN部分,占用8字节。前4字节代表该页的checksum值,最后4字节和File Header中的FIL_PAGE_LSN相同。将这两个值与File Header中的FIL_PAGE_SPACE_OR_CHKSUM和FIL_PAGE_LSN值进行比较,看是否一致(checksum的比较需要通过InnoDB的checksum函数来进行比较,不是简单的等值比较),以此来保证页的完整性(not corrupted)。

        在默认配置下,InnoDB存储引擎每次从磁盘读取一个页就会检测该页的完整性,即页是否发生Corrupt,这就是通过File Trailer部分进行检测,而该部分的检测会有一定的开销。用户可以通过参数innodb_checksums来开启或关闭对这个页完整性的检查。

        MySQL 5.6.6版本开始新增了参数innodb_checksum_algorithm,该参数用来控制检测checksum函数的算法,默认值为crc32,可设置的值有:innodb、crc32、none、strict_innodb、strict_crc32、strict_none。

        innodb为兼容之前版本InnoDB页的checksum检测方式,crc32为MySQL 5.6.6版本引进的新的checksum算法,该算法较之前的innodb有着较高的性能。但是若表中所有页的checksum值都以strict算法保存,那么低版本的MySQL数据库将不能读取这些页。none表示不对页启用checksum检查。

4.4.7 InnoDB数据页结构示例分析

4.5 Named File Formats机制

         InnoDB存储引通过Named File Formats机制来解决不同版本下页结构兼容性的问题。

        InnoDB存储引擎将1.0.x版本之前的文件格式(file format)定义为Antelope,将这个版本支持的文件格式定义为Barracuda。新的文件格式总是包含于之前的版本的页格式。图4-8显示了Barracuda文件格式和Antelope文件格式之间的关系,Antelope文件格式有Compact和Redudant的行格式,Barracuda文件格式既包括了Antelope所有的文件格式,以及新加入的Compressed和Dynamic行格式。

图4-8 文件格式

        innodb_file_format用来指定文件格式,可以通过下面的方式来查看当前所使用的InnoDB存储引擎的文件格式:SELECT@@version\G;

        innodb_file_format_check用来检测当前InnoDB存储引擎文件格式的支持度,该值默认为ON。

4.6 约束

4.6.1 数据完整性

        关系数据库本身能保证存储数据的完整性,不需要应用程序的控制,当前几乎所有的关系型数据库都提供了约束(constraint)机制,该机制提供了一条强大而简易的途径来保证数据库中数据的完整性。一般来说,数据完整性有以下三种形式

        实体完整性:保证表中有一个主键。在InnoDB存储引擎表中,用户可以通过定义Primary Key或Unique Key约束来保证实体的完整性。用户还可以通过编写一个触发器来保证数据完整性。

        域完整性:保证数据每列的值满足特定的条件。在InnoDB存储引擎表中,域完整性可以通过以下几种途径来保证:

        ❑选择合适的数据类型确保一个数据值满足特定条件。

        ❑外键(Foreign Key)约束。

        ❑编写触发器。

        ❑还可以考虑用DEFAULT约束作为强制域完整性的一个方面。

        参照完整性:保证两张表之间的关系。InnoDB存储引擎支持外键,因此允许用户定义外键以强制参照完整性,也可以通过编写触发器以强制执行。

        对于InnoDB存储引擎本身而言,提供了以下几种约束:

        ❑ Primary Key        ❑ Unique Key        ❑ Foreign Key        ❑ Default        ❑ NOT NULL

4.6.2 约束的创建和查找

        约束的创建可以采用以下两种方式:

        ❑ 表建立时就进行约束定义

        ❑ 利用ALTER TABLE命令来进行创建约束

        对Unique Key(唯一索引)的约束,用户还可以通过命令CREATE UNIQUE INDEX来建立。对于主键约束而言,其默认约束名为PRIMARY。而对于Unique Key约束而言,默认约束名和列名一样,当然也可以人为指定Unique Key约束的名字。Foreign Key约束似乎会有一个比较神秘的默认名称。

4.6.3 约束和索引的区别

        用户创建了一个唯一索引就创建了一个唯一的约束。但是约束和索引的概念还是有所不同的,约束更是一个逻辑的概念,用来保证数据的完整性,而索引是一个数据结构,既有逻辑上的概念,在数据库中还代表着物理存储的方式。

4.6.4 对错误数据的约束

        在某些默认设置下,MySQL数据库允许非法的或不正确的数据的插入或更新,又或者可以在数据库内部将其转化为一个合法的值,如向NOT NULL的字段插入一个NULL值,MySQL数据库会将其更改为0再进行插入,因此数据库本身没有对数据的正确性进行约束。

        如果用户想通过约束对于数据库非法数据的插入或更新,即MySQL数据库提示报错而不是警告,那么用户必须设置参数sql_mode,用来严格审核输入的参数,如:

        SET sql_mode='STRICT_TRANS_TABLES';

4.6.5 ENUM和SET约束

        MySQL数据库不支持传统的CHECK约束,但是通过ENUM和SET类型可以解决部分这样的约束需求。例如表上有一个性别类型,规定域的范围只能是male或female,在这种情况下用户可以通过ENUM类型来进行约束。

        对于传统CHECK约束支持的连续值的范围约束或更复杂的约束,ENUM和SET类型还是无能为力,这时用户需要通过触发器来实现对于值域的约束。

4.6.6 触发器与约束

        触发器的作用是在执行INSERT、DELETE和UPDATE命令之前或之后自动调用SQL命令或存储过程。

        创建触发器的命令是CREATE TRIGGER,只有具备Super权限的MySQL数据库用户才可以执行这条命令:

CREATE

[DEFINER={user|CURRENT_USER}]

TRIGGER trigger_name BEFORE|AFTER INSERT|UPDATE|DELETE

ON tbl_name FOR EACH ROW trigger_stmt

        最多可以为一个表建立6个触发器,即分别为INSERT、UPDATE、DELETE的BEFORE和AFTER各定义一个。BEFORE和AFTER代表触发器发生的时间,表示是在每行操作的之前发生还是之后发生。当前MySQL数据库只支持FOR EACH ROW的触发方式,即按每行记录进行触发。

        通过触发器,用户可以实现MySQL数据库本身并不支持的一些特性,如对于传统CHECK约束的支持,物化视图、高级复制、审计等特性。

4.6.7 外键约束

        外键用来保证参照完整性,MySQL数据库的MyISAM存储引擎本身并不支持外键,对于外键的定义只是起到一个注释的作用。而InnoDB存储引擎则完整支持外键约束。外键的定义如下:

[CONSTRAINT[symbol]]FOREIGN KEY

[index_name](index_col_name,...)

REFERENCES tbl_name(index_col_name,...)

[ON DELETE reference_option]

[ON UPDATE reference_option]

reference_option:

RESTRICT|CASCADE|SET NULL|NO ACTION

        用户可以在执行CREATE TABLE时就添加外键,也可以在表创建后通过ALTER TABLE命令来添加。一个简单的外键的创建示例如下:

mysql>CREATE TABLE parent(

->id INT NOT NULL,

->PRIMARY KEY(id)

->)ENGINE=INNODB;

mysql>CREATE TABLE child(

->id INT,parent_id INT,

->FOREIGN KEY(parent_id)REFERENCES parent(id)

->)ENGINE=INNODB;

        一般来说,称被引用的表为父表,引用的表称为子表。外键定义时的ON DELETE和ON UPDATE表示在对父表进行DELETE和UPDATE操作时,对子表所做的操作,可定义的子表操作有:

        ❑CASCADE

        ❑SET NULL

        ❑NO ACTION

        ❑RESTRICT

        CASCADE表示当父表发生DELETE或UPDATE操作时,对相应的子表中的数据也进行DELETE或UPDATE操作。

        SET NULL表示当父表发生DELETE或UPDATE操作时,相应的子表中的数据被更新为NULL值,但是子表中相对应的列必须允许为NULL值。

        NO ACTION表示当父表发生DELETE或UPDATE操作时,抛出错误,不允许这类操作发生。

        RESTRICT表示当父表发生DELETE或UPDATE操作时,抛出错误,不允许这类操作发生。如果定义外键时没有指定ON DELETE或ON UPDATE,RESTRICT就是默认的外键设置。

      对于参照完整性约束,外键能起到一个非常好的作用。但是对于数据的导入操作时,外键往往导致在外键约束的检查上花费大量时间。因为MySQL数据库的外键是即时检查的,所以对导入的每一行都会进行外键检查。但是用户可以在导入过程中忽视外键的检查,如:

mysql>SET foreign_key_checks=0;

mysql>LOAD DATA……

mysql>SET foreign_key_checks=1;

4.7 视图

        在MySQL数据库中,视图(View)是一个命名的虚表,它由一个SQL查询来定义,可以当做表使用。与持久表(permanent table)不同的是,视图中的数据没有实际的物理存储。

4.7.1 视图的作用

        视图的主要用途之一是被用做一个抽象装置,特别是对于一些应用程序,程序本身不需要关心基表(base table)的结构,只需要按照视图定义来取数据或更新数据,因此,视图同时在一定程度上起到一个安全层的作用。

        MySQL数据库从5.0版本开始支持视图,创建视图的语法如下:

CREATE

[OR REPLACE]

[ALGORITHM={UNDEFINED|MERGE|TEMPTABLE}]

[DEFINER={user|CURRENT_USER}]

[SQL SECURITY{DEFINER|INVOKER}]

VIEW view_name[(column_list)]

AS select_statement

[WITH[CASCADED|LOCAL]CHECK OPTION]

        虽然视图是基于基表的一个虚拟表,但是用户可以对某些视图进行更新操作,其本质就是通过视图的定义来更新基本表。一般称可以进行更新操作的视图为可更新视图(updatable view)。视图定义中的WITH CHECK OPTION就是针对于可更新的视图的,即更新的值是否需要检查。

        MySQL数据库DBA的一个常用的命令是SHOW TABLES,该命令会显示出当前数据库下所有的表。但因为视图是虚表,同样被作为表显示出来。

        要想查看视图的一些元数据(meta data),可以访问information_schema架构下的VIEWS表,该表给出了视图的详细信息,包括视图定义者(definer)、定义内容、是否是可更新视图、字符集等。

4.7.2 物化视图

        Oracle数据库支持物化视图,该视图不是基于基表的虚表,而是根据基表实际存在的实表,即物化视图的数据存储在非易失的存储设备上。

        物化视图可以用于预先计算并保存多表的链接(JOIN)或聚集(GROUP BY)等耗时较多的SQL操作结果。这样,在执行复杂查询时,就可以避免进行这些耗时的操作,从而快速得到结果。物化视图的好处是对于一些复杂的统计类查询能直接查出结果。

在Oracle数据库中,物化视图的创建方式包括以下两种:

        ❑BUILD IMMEDIATE

        ❑BUILD DEFERRED

        BUILD IMMEDIATE是默认的创建方式,在创建物化视图的时候就生成数据,而BUILD DEFERRED则在创建物化视图时不生成数据,以后根据需要再生成数据。

        查询重写是指当对物化视图的基表进行查询时,数据库会自动判断能否通过查询物化视图来直接得到最终的结果,如果可以,则避免了聚集或连接等这类较为复杂的SQL操作,直接从已经计算好的物化视图中得到所需的数据。

        物化视图的刷新是指当基表发生了DML操作后,物化视图何时采用哪种方式和基表进行同步。刷新的模式有两种:

        ❑ON DEMAND    ❑ON COMMIT

        ON DEMAND意味着物化视图在用户需要的时候进行刷新,ON COMMIT意味着物化视图在对基表的DML操作提交的同时进行刷新。

        而刷新的方法有四种:

        ❑FAST        ❑COMPLETE        ❑FORCE        ❑NEVER

        FAST刷新采用增量刷新,只刷新自上次刷新以后进行的修改。COMPLETE刷新是对整个物化视图进行完全的刷新。如果选择FORCE方式,则数据库在刷新时会去判断是否可以进行快速刷新,如果可以,则采用FAST方式,否则采用COMPLETE的方式。NEVER是指物化视图不进行任何刷新。

        MySQL数据库本身并不支持物化视图,换句话说,MySQL数据库中的视图总是虚拟的。

4.8 分区表

4.8.1 分区概述

        分区功能并不是在存储引擎层完成的,因此不是只有InnoDB存储引擎支持分区,常见的存储引擎MyISAM、NDB等都支持。但也并不是所有的存储引擎都支持,如CSV、FEDORATED、MERGE等就不支持。

        MySQL数据库在5.1版本时添加了对分区的支持。分区的过程是将一个表或索引分解为多个更小、更可管理的部分。就访问数据库的应用而言,从逻辑上讲,只有一个表或一个索引,但是在物理上这个表或索引可能由数十个物理分区组成。每个分区都是独立的对象,可以独自处理,也可以作为一个更大对象的一部分进行处理。

        MySQL数据库支持的分区类型为水平分区,并不支持垂直分区。MySQL数据库的分区是局部分区索引,一个分区中既存放了数据又存放了索引。全局分区是指,数据存放在各个分区中,但是所有数据的索引放在一个对象中。目前,MySQL数据库还不支持全局分区。

        可以通过以下命令来查看当前数据库是否启用了分区功能:SHOW VARIABLES LIKE'%partition%'\G;

        MySQL数据库支持以下几种类型的分区。

        ❑ RANGE分区:行数据基于属于一个给定连续区间的列值被放入分区。MySQL 5.5开始支持RANGE COLUMNS的分区。

        ❑ LIST分区:和RANGE分区类型,只是LIST分区面向的是离散的值。MySQL 5.5开始支持LIST COLUMNS的分区。

        ❑ HASH分区:根据用户自定义的表达式的返回值来进行分区,返回值不能为负数。

        ❑ KEY分区:根据MySQL数据库提供的哈希函数来进行分区。

        不论创建何种类型的分区,如果表中存在主键或唯一索引时,分区列必须是唯一索引的一个组成部分。


[1]水平分区,指将同一表中不同行的记录分配到不同的物理文件中。

[2]垂直分区,指将同一表中不同列的记录分配到不同的物理文件中。

4.8.2 分区类型

4.8.3 子分区

        子分区(subpartitioning)是在分区的基础上再进行分区,也称这种分区为复合分区(composite partitioning)。MySQL数据库允许在RANGE和LIST的分区上再进行HASH或KEY的子分区。

        子分区的建立需要注意以下几个问题:

        ❑ 每个子分区的数量必须相同。

        ❑ 要在一个分区表的任何分区上使用SUBPARTITION来明确定义任何子分区,就必须定义所有的子分区。

        ❑每个SUBPARTITION子句必须包括子分区的一个名字。

        ❑子分区的名字必须是唯一的。

4.8.4 分区中的NULL值

        MySQL数据库允许对NULL值做分区,MYSQL数据库的分区总是视NULL值视小于任何的一个非NULL值。

        对于RANGE分区,若向分区列插入了NULL值,则MySQL会将该值放入最左边的分区。

        在LIST分区下要使用NULL值,则必须显式地指出哪个分区中放入NULL值,否则会报错。

        HASH和KEY分区对于NULL的处理方式和RANGE分区、LIST分区不一样。任何分区函数都会将含有NULL值的记录返回为0。

4.8.5 分区和性能

        数据库的应用分为两类:一类是OLTP(在线事务处理),如Blog、电子商务、网络游戏等;另一类是OLAP(在线分析处理),如数据仓库、数据集市。在一个实际的应用环境中,可能既有OLTP的应用,也有OLAP的应用。

        对于OLAP的应用,分区的确是可以很好地提高查询的性能,因为OLAP应用大多数查询需要频繁地扫描一张很大的表。假设有一张1亿行的表,其中有一个时间戳属性列。用户的查询需要从这张表中获取一年的数据。如果按时间戳进行分区,则只需要扫描相应的分区即可。

        对于OLTP的应用,分区应该非常小心。在这种应用下,通常不可能会获取一张大表中10%的数据,大部分都是通过索引返回几条记录即可。而根据B+树索引的原理可知,对于一张大表,一般的B+树需要2~3次的磁盘IO。因此B+树可以很好地完成操作,不需要分区的帮助,并且设计不好的分区会带来严重的性能问题。

4.8.6 在表和分区间交换数据

        MySQL 5.6开始支持ALTER TABLE…EXCHANGE PARTITION语法。该语句允许分区或子分区中的数据与另一个非分区的表中的数据进行交换。如果非分区表中的数据为空,那么相当于将分区中的数据移动到非分区表中。若分区表中的数据为空,则相当于将外部表中的数据导入到分区中。

        要使用ALTER TABLE…EXCHANGE PARTITION语句,必须满足下面的条件:

        ❑要交换的表需和分区表有着相同的表结构,但是表不能含有分区

        ❑在非分区表中的数据必须在交换的分区定义内

        ❑被交换的表中不能含有外键,或者其他的表含有对该表的外键引用

        ❑用户除了需要ALTER、INSERT和CREATE权限外,还需要DROP的权限

        此外,有两个小的细节需要注意:

        ❑使用该语句时,不会触发交换表和被交换表上的触发器

        ❑AUTO_INCREMENT列将被重置

第5章 索引与算法

5.1 InnoDB存储引擎索引概述

        InnoDB存储引擎支持以下几种常见的索引:

        ❑B+树索引        ❑全文索引        ❑哈希索引

        InnoDB存储引擎支持的哈希索引是自适应的,InnoDB存储引擎会根据表的使用情况自动为表生成哈希索引,不能人为干预是否在一张表中生成哈希索引。

        B+树索引就是传统意义上的索引,这是目前关系型数据库系统中查找最为常用和最为有效的索引。B+树索引的构造类似于二叉树,根据键值(Key Value)快速找到数据。

        注意 B+树中的B不是代表二叉(binary),而是代表平衡(balance),因为B+树是从最早的平衡二叉树演化而来,但是B+树不是一个二叉树。

        B+树索引并不能找到一个给定键值的具体行。B+树索引能找到的只是被查找数据行所在的页。然后数据库通过把页读入到内存,再在内存中进行查找,最后得到要查找的数据。

5.2 数据结构与算法

5.2.1 二分查找法

        二分查找法(binary search)也称为折半查找法,用来查找一组有序的记录数组中的某一记录,其基本思想是:将记录按有序化(递增或递减)排列,在查找过程中采用跳跃式方式查找,即先以有序数列的中点位置为比较对象,如果要找的元素值小于该中点元素,则将待查序列缩小为左半部分,否则为右半部分。通过一次比较,将查找区间缩小一半。

5.2.2 二叉查找树和平衡二叉树

        在介绍B+树前,需要先了解一下二叉查找树。B+树是通过二叉查找树,再由平衡二叉树,B树演化而来。

        在二叉查找树中,左子树的键值总是小于根的键值,右子树的键值总是大于根的键值。二叉查找树退化会变成一个线性链表。因此若想最大性能地构造一棵二叉查找树,需要这棵二叉查找树是平衡的,从而引出了新的定义——平衡二叉树AVL树

        平衡二叉树:首先符合二叉查找树的定义,其次必须满足任何节点的两个子树的高度最大差为1。平衡二叉树的查找性能是比较高的,但不是最高的,只是接近最高性能。最好的性能需要建立一棵最优二叉树,但是最优二叉树的建立和维护需要大量的操作,因此,用户一般只需建立一棵平衡二叉树即可。

        平衡二叉树的查询速度的确很快,但是维护一棵平衡二叉树的代价是非常大的。通常来说,需要1次或多次左旋和右旋来得到插入或更新后树的平衡性。

5.3 B+树

        B+树由B树和索引顺序访问方法演化而来,但是在现实使用过程中几乎已经没有使用B树的情况了。

        B树与B+树介绍:B树和B+树的区别心里有B树https://mp.weixin.qq.com/s?src=11×tamp=1641566668&ver=3544&signature=wWFumW90OgaMpUduSNF44f8*plYelPxxQnvpiECYDGaAMTvUoOVKTl2qfGweVHYvqgnVi4DFXG86cnR4zGGfkXmTuyFxwIMjfspDCvZcpprItH6m1odhy3hUn09amUmg&new=1        B+树是为磁盘或其他直接存取辅助设备设计的一种平衡查找树。在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点上,由各叶子节点指针进行连接。先来看一个B+树,其高度为2,每页可存放4条记录,扇出(fan out)为5,如图5-6所示。

图 5-6 一棵高度为2的B+树

        从图5-6可以看出,所有记录都在叶子节点上,并且是顺序存放的,如果用户从最左边的叶子节点开始顺序遍历,可以得到所有键值的顺序排序:5、10、15、20、25、30、50、55、60、65、75、80、85、90。

5.3.1 B+树的插入操作

        B+树的插入必须保证插入后叶子节点中的记录依然排序,同时需要考虑插入到B+树的三种情况,每种情况都可能会导致不同的插入算法。如表5-1所示。

        这里用一个例子来分析B+树的插入。例如,对于图5-6中的这棵B+树,若用户插入28这个键值,发现当前Leaf Page和Index Page都没有满,直接进行插入即可,之后得图5-7。

图 5-7 插入键值28

        接着再插入70这个键值,这时原先的Leaf Page已经满了,但是Index Page还没有满,符合表5-1的第二种情况,这时插入Leaf Page后的情况为55、55、60、65、70,并根据中间的值60来拆分叶子节点,可得图5-8。

图 5-8 插入键值70

        因为图片显示的关系,这次没有能在各叶子节点加上双向链表指针。不过和图5-6、图5-7一样,它还是存在的。

        最后插入键值95,这时符合表5-1中讨论的第三种情况,即Leaf Page和Index Page都满了,这时需要做两次拆分,如图5-9所示。

图 5-9 插入键值95

        可以看到,不管怎么变化,B+树总是会保持平衡。但是为了保持平衡对于新插入的键值可能需要做大量的拆分页(split)操作。因为B+树结构主要用于磁盘,页的拆分意味着磁盘的操作,所以应该在可能的情况下尽量减少页的拆分操作。因此,B+树同样提供了类似于平衡二叉树的旋转(Rotation)功能。

        旋转发生在Leaf Page已经满,但是其的左右兄弟节点没有满的情况下。这时B+树并不会急于去做拆分页的操作,而是将记录移到所在页的兄弟节点上。在通常情况下,左兄弟会被首先检查用来做旋转操作,因此再来看图5-7的情况,若插入键值70,其实B+树并不会急于去拆分叶子节点,而是去做旋转操作,得到如图5-10所示的操作。

图 5-10 B+树的旋转操作

从图5-10可以看到,采用旋转操作使B+树减少了一次页的拆分操作,同时这棵B+树的高度依然还是2。

5.3.2 B+树的删除操作

        B+树使用填充因子(fill factor)来控制树的删除变化,50%是填充因子可设的最小值。B+树的删除操作同样必须保证删除后叶子节点中的记录依然排序,同插入一样,B+树的删除操作同样需要考虑以下表5-2中的三种情况,与插入不同的是,删除根据填充因子的变化来衡量。

        根据图5-9的B+树来进行删除操作。首先删除键值为70的这条记录,该记录符合表5-2讨论的第一种情况,删除后可得到图5-11。

图 5-11 删除键值70

        接着删除键值为25的记录,这也是表5-2讨论的第一种情况,但是该值还是IndexPage中的值,因此在删除Leaf Page中的25后,还应将25的右兄弟节点的28更新到Page Index中,最后可得图5-12。

图 5-12 删除键值25

        最后看删除键值为60的情况。删除Leaf Page中键值为60的记录后,Fill Factor小于50%,这时需要做合并操作,同样,在删除Index Page中相关记录后需要做Index Page的合并操作,最后得到图5-13。

图 5-13 删除键值60

5.4 B+树索引

        B+树索引的本质就是B+树在数据库中的实现。但是B+索引在数据库中有一个特点是高扇出性,因此在数据库中,B+树的高度一般都在2~4层,也就是说查找某一键值的行记录时最多只需要2到4次IO。

        数据库中的B+树索引可以分为聚集索引(clustered inex)和辅助索引(secondary index),但是不管是聚集还是辅助的索引,其内部都是B+树的,即高度平衡的,叶子节点存放着所有的数据。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的信息。

5.4.1 聚集索引

        InnoDB存储引擎表是索引组织表,即表中数据按照主键顺序存放。聚集索引就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子节点称为数据页。同B+树数据结构一样,每个数据页都通过一个双向链表来进行链接。

        由于实际的数据页只能按照一棵B+树进行排序,因此每张表只能拥有一个聚集索引。在多数情况下,查询优化器倾向于采用聚集索引。因为聚集索引能够在B+树索引的叶子节点上直接找到数据。此外,由于定义了数据的逻辑顺序,聚集索引能够特别快地访问针对范围值的查询。查询优化器能够快速发现某一段范围的数据页需要扫描。

        数据页上存放的是完整的每行的记录,而在非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录。因此这棵聚集索引树的构造大致如图5-14所示。

图 5-14 B+树索引

        聚集索引的另一个好处是,它对于主键的排序查找和范围查找速度非常快。叶子节点的数据就是用户所要查询的数据。

5.4.2 辅助索引

        辅助索引(Secondary Index,也称非聚集索引),叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签(bookmark)。该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB存储引擎表是索引组织表,因此InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。图5-15显示了InnoDB存储引擎中辅助索引与聚集索引的关系。

图 5-15 辅助索引与聚集索引的关系

        辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引。当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录。

5.4.3 B+树索引的分裂

        B+树索引页的分裂并不总是从页的中间记录开始,这样可能会导致页空间的浪费。例如下面的记录:


1、2、3、4、5、6、7、8、9


        插入是根据自增顺序进行的,若这时插入10这条记录后需要进行页的分裂操作,那么根据5.3.1节介绍的分裂方法,会将记录5作为分裂点记录(split record),分裂后得到下面两个页:


P1:1、2、3、4

P2:5、6、7、8、9、10


        然而由于插入是顺序的,P1这个页中将不会再有记录被插入,从而导致空间的浪费。而P2又会再次进行分裂。

        InnoDB存储引擎的Page Header中有以下几个部分用来保存插入的顺序信息:

        ❑PAGE_LAST_INSERT

        ❑PAGE_DIRECTION

        ❑PAGE_N_DIRECTION

        通过这些信息,InnoDB存储引擎可以决定是向左还是向右进行分裂,同时决定将分裂点记录为哪一个。

5.4.4 B+树索引的管理

1.索引管理

        索引的创建和删除可以通过两种方法,一种是ALTER TABLE,另一种是CREATE/DROP INDEX。通过ALTER TABLE创建索引的语法为:


ALTER TABLE tbl_name

|ADD{INDEX|KEY}[index_name]

[index_type](index_col_name,...)[index_option]...

ALTER TABLE tbl_name

DROP PRIMARY KEY

|DROP{INDEX|KEY}index_name


CREATE/DROP INDEX的语法同样很简单:


CREATE[UNIQUE]INDEX index_name

[index_type]

ON tbl_name(index_col_name,...)

DROP INDEX index_name ON tbl_name


        接着具体阐述命令SHOW INDEX展现结果中每列的含义。

❑Table:索引所在的表名。

❑Non_unique:非唯一的索引,可以看到primary key是0,因为必须是唯一的。

❑Key_name:索引的名字,用户可以通过这个名字来执行DROP INDEX。

❑Seq_in_index:索引中该列的位置,如果看联合索引idx_a_c就比较直观了。

❑Column_name:索引列的名称。

❑Collation:列以什么方式存储在索引中。可以是A或NULL。B+树索引总是A,即排序的。如果使用了Heap存储引擎,并且建立了Hash索引,这里就会显示NULL了。因为Hash根据Hash桶存放索引数据,而不是对数据进行排序。

❑Cardinality:非常关键的值,表示索引中唯一值的数目的估计值。Cardinality表的行数应尽可能接近1,如果非常小,那么用户需要考虑是否可以删除此索引。

❑Sub_part:是否是列的部分被索引。如果看idx_b这个索引,这里显示100,表示只对b列的前100字符进行索引。如果索引整个列,则该字段为NULL。

❑Packed:关键字如何被压缩。如果没有被压缩,则为NULL。

❑Null:是否索引的列含有NULL值。可以看到idx_b这里为Yes,因为定义的列b允许NULL值。

❑Index_type:索引的类型。InnoDB存储引擎只支持B+树索引,所以这里显示的都是BTREE。

❑Comment:注释。

        Cardinality值非常关键,优化器会根据这个值来判断是否使用这个索引,这个值并不是实时更新的,即并非每次索引的更新都会更新该值。因此这个值是不太准确的,只是一个大概的值。

2.Fast Index Creation

        MySQL 5.5版本之前(不包括5.5)存在的一个普遍被人诟病的问题是MySQL数据库对于索引的添加或者删除的这类DDL操作,MySQL数据库的操作过程为:

❑首先创建一张新的临时表,表结构为通过命令ALTER TABLE新定义的结构。

❑然后把原表中数据导入到临时表。

❑接着删除原表。

❑最后把临时表重名为原来的表名。

        可以发现,若用户对于一张大表进行索引的添加和删除操作,那么这会需要很长的时间。更关键的是,若有大量事务需要访问正在被修改的表,这意味着数据库服务不可用。

        InnoDB存储引擎从InnoDB 1.0.x版本开始支持一种称为Fast Index Creation(快速索引创建)的索引创建方式——简称FIC。

        对于辅助索引的创建,InnoDB存储引擎会对创建索引的表加上一个S锁。在创建的过程中,不需要重建表,因此速度较之前提高很多,并且数据库的可用性也得到了提高。删除辅助索引操作就更简单了,InnoDB存储引擎只需更新内部视图,并将辅助索引的空间标记为可用,同时删除MySQL数据库内部视图上对该表的索引定义即可。

        由于FIC在索引的创建的过程中对表加上了S锁,因此在创建的过程中只能对该表进行读操作,若有大量的事务需要对目标表进行写操作,那么数据库的服务同样不可用。此外,FIC方式只限定于辅助索引,对于主键的创建和删除同样需要重建一张表。

3.Online Schema Change

        Online Schema Change(在线架构改变,简称OSC)最早是由Facebook实现的一种在线执行DDL的方式,并广泛地应用于Facebook的MySQL数据库。所谓“在线”是指在事务的创建过程中,可以有读写事务对表进行操作,这提高了原有MySQL数据库在DDL操作时的并发性。

        实现OSC步骤如下:

❑init,即初始化阶段,会对创建的表做一些验证工作,如检查表是否有主键,是否存在触发器或者外键等。

❑createCopyTable,创建和原始表结构一样的新表。

❑alterCopyTable:对创建的新表进行ALTER TABLE操作,如添加索引或列等。

❑createDeltasTable,创建deltas表,该表的作用是为下一步创建的触发器所使用。之后对原表的所有DML操作会被记录到createDeltasTable中。

❑createTriggers,对原表创建INSERT、UPDATE、DELETE操作的触发器。触发操作产生的记录被写入到deltas表。

❑startSnpshotXact,开始OSC操作的事务。

❑selectTableIntoOutfile,将原表中的数据写入到新表。为了减少对原表的锁定时间,这里通过分片(chunked)将数据输出到多个外部文件,然后将外部文件的数据导入到copy表中。分片的大小可以指定,默认值是500 000。

❑dropNCIndexs,在导入到新表前,删除新表中所有的辅助索引。

❑loadCopyTable,将导出的分片文件导入到新表。

❑replayChanges,将OSC过程中原表DML操作的记录应用到新表中,这些记录被保存在deltas表中。

❑recreateNCIndexes,重新创建辅助索引。

❑replayChanges,再次进行DML日志的回放操作,这些日志是在上述创建辅助索引中过程中新产生的日志。

❑swapTables,将原表和新表交换名字,整个操作需要锁定2张表,不允许新的数据产生。由于改名是一个很快的操作,因此阻塞的时间非常短。

4.Online DDL

        MySQL 5.6版本开始支持Online DDL(在线数据定义)操作,其允许辅助索引创建的同时,还允许其他诸如INSERT、UPDATE、DELETE这类DML操作,这极大地提高了MySQL数据库在生产环境中的可用性。

        此外,不仅是辅助索引,以下这几类DDL操作都可以通过“在线”的方式进行操作:

❑辅助索引的创建与删除

❑改变自增长值

❑添加或删除外键约束

❑列的重命名

通过新的ALTER TABLE语法,用户可以选择索引的创建方式:


ALTER TABLE tbl_name

|ADD{INDEX|KEY}[index_name]

[index_type](index_col_name,...)[index_option]...

ALGORITHM[=]{DEFAULT|INPLACE|COPY}

LOCK[=]{DEFAULT|NONE|SHARED|EXCLUSIVE}


ALGORITHM指定了创建或删除索引的算法,COPY表示按照MySQL 5.1版本之前的工作模式,即创建临时表的方式。INPLACE表示索引创建或删除操作不需要创建临时表。DEFAULT表示根据参数old_alter_table来判断是通过INPLACE还是COPY的算法,该参数的默认值为OFF,表示采用INPLACE的方式。

        InnoDB存储引擎实现Online DDL的原理是在执行创建或者删除操作的同时,将INSERT、UPDATE、DELETE这类DML操作日志写入到一个缓存中。待完成索引创建后再将重做应用到表上,以此达到数据的一致性。这个缓存的大小由参数innodb_online_alter_log_max_size控制,默认的大小为128MB。若用户更新的表比较大,并且在创建过程中伴有大量的写事务,如遇到innodb_online_alter_log_max_size的空间不能存放日志时,会抛出类似如下的错误:


Error:1799SQLSTATE:HY000(ER_INNODB_ONLINE_LOG_TOO_BIG)

Message:Creating index'idx_aaa'required more than'innodb_online_alter_log_max_size'bytes of modification log.Please try again.


        对于这个错误,用户可以调大参数innodb_online_alter_log_max_size,以此获得更大的日志缓存空间。此外,还可以设置ALTER TABLE的模式为SHARE,这样在执行过程中不会有写事务发生,因此不需要进行DML日志的记录。

        需要特别注意的是,由于Online DDL在创建索引完成后再通过重做日志达到数据库的最终一致性,这意味着在索引创建过程中,SQL优化器不会选择正在创建中的索引。

5.5 Cardinality值

5.5.1 什么是Cardinality

        并不是在所有的查询条件中出现的列都需要添加索引。对于什么时候添加B+树索引,一般的经验是,在访问表中很少一部分时使用B+树索引才有意义。对于性别字段、地区字段、类型字段,它们可取值的范围很小,称为低选择性。相反,如果某个字段的取值范围很广,几乎没有重复,即属于高选择性,则此时使用B+树索引是最适合的。例如,对于姓名字段,基本上在一个应用中不允许重名的出现。

        怎样查看索引是否是高选择性的呢?可以通过SHOW INDEX结果中的列Cardinality来观察。Cardinality值非常关键,表示索引中不重复记录数量的预估值。在实际应用中,Cardinality/n_rows_in_table应尽可能地接近1。如果非常小,那么用户需要考虑是否还有必要创建这个索引。故在访问高选择性属性的字段并从表中取出很少一部分数据时,对这个字段添加B+树索引是非常有必要的。

5.5.2 InnoDB存储引擎的Cardinality统计

        数据库是怎样来统计Cardinality信息的呢?对Cardinality的统计是放在存储引擎层进行的。数据库对于Cardinality的统计都是通过采样(Sample)的方法来完成的。

        在InnoDB存储引擎中,Cardinality统计信息的更新发生在两个操作中:INSERT和UPDATE。InnoDB存储引擎内部对更新Cardinality信息的策略为:

        ❑表中1/16的数据已发生过变化。

        ❑stat_modified_counter>2 000 000 000。

        第一种策略为自从上次统计Cardinality信息后,表中1/16的数据已经发生过变化,这时需要更新Cardinality信息。第二种情况考虑的是,如果对表中某一行数据频繁地进行更新操作,这时表中的数据实际并没有增加,实际发生变化的还是这一行数据,则第一种更新策略就无法适用这这种情况。故在InnoDB存储引擎内部有一个计数器stat_modified_counter,用来表示发生变化的次数,当stat_modified_counter大于2 000 000 000时,则同样需要更新Cardinality信息。

        InnoDB进行Cardinality信息的统计和更新操作是通过采样的方法,默认InnoDB存储引擎对8个叶子节点(Leaf Page)进行采用。采样的过程如下:

        ❑取得B+树索引中叶子节点的数量,记为A。

        ❑随机取得B+树索引中的8个叶子节点。统计每个页不同记录的个数,即为P1,P2,…,P8。

        ❑根据采样信息给出Cardinality的预估值:Cardinality=(P1+P2+…+P8)*A/8。

        当然,有一种情况可能使得用户每次观察到的索引Cardinality值都是一样的,那就是表足够小,表的叶子节点数小于或者等于8个。这时即使随机采样,也总是会采取到这些页,因此每次得到的Cardinality值是相同的。

        在InnoDB 1.2版本之前,可以通过参数innodb_stats_sample_pages用来设置统计Cardinality时每次采样页的数量,默认值为8。同时,参数innodb_stats_method用来判断如何对待索引中出现的NULL值记录。该参数默认值为nulls_equal,表示将NULL值记录视为相等的记录。其有效值还有nulls_unequal,nulls_ignored,分别表示将NULL值记录视为不同的记录和忽略NULL值记录。

        当执行SQL语句ANALYZE TABLE、SHOW TABLE STATUS、SHOW INDEX以及访问INFORMATION_SCHEMA架构下的表TABLES和STATISTICS时会导致InnoDB存储引擎去重新计算索引的Cardinality值。若表中的数据量非常大,并且表中存在多个辅助索引时,执行上述这些操作可能会非常慢。虽然用户可能并不希望去更新Cardinality值。

        InnoDB1.2版本提供了更多的参数对Cardinality统计进行设置,这些参数如表5-3所示。

5.6 B+树索引的使用

5.6.1 不同应用中B+树索引的使用

        在OLTP应用中,查询操作只从数据库中取得一小部分数据,一般可能都在10条记录以下,甚至在很多时候只取1条记录,在这种情况下,B+树索引建立后,对该索引的使用应该只是通过该索引取得表中少部分的数据。这时建立B+树索引才是有意义的,否则即使建立了,优化器也可能选择不使用索引。

        在OLAP应用中,都需要访问表中大量的数据,根据这些数据来产生查询的结果,这些查询多是面向分析的查询,目的是为决策者提供支持。因此在OLAP中索引的添加根据的应该是宏观的信息,而不是微观,因为最终要得到的结果是提供给决策者的。例如不需要在OLAP中对姓名字段进行索引,因为很少需要对单个用户进行查询。但是对于OLAP中的复杂查询,要涉及多张表之间的联接操作,因此索引的添加依然是有意义的。但是,如果联接操作使用的是Hash Join,那么索引可能又变得不是非常重要了。不过在OLAP应用中,通常会需要对时间字段进行索引,这是因为大多数统计需要根据时间维度来进行数据的筛选。

5.6.2 联合索引

        联合索引是指对表上的多个列进行索引。联合索引也是一棵B+树,不同的是联合索引的键值的数量不是1,而是大于等于2。接着来讨论两个整型列组成的联合索引,假定两个键值的名称分别为a、b,如图5-22所示。

图 5-22 多个键值的B+树

        从图5-22可以观察到多个键值的B+树情况。其实和之前讨论的单个键值的B+树并没有什么不同,键值都是排序的,通过叶子节点可以逻辑上顺序地读出所有数据,就上面的例子来说,即(1,1)、(1,2)、(2,1)、(2,4)、(3,1)、(3,2)。数据按(a,b)的顺序进行了存放。

        因此,对于查询SELECT*FROM TABLE WHERE a=xxx and b=xxx,显然是可以使用(a,b)这个联合索引的。对于单个的a列查询SELECT*FROM TABLE WHERE a=xxx,也可以使用这个(a,b)索引。但对于b列的查询SELECT*FROM TABLE WHERE b=xxx,则不可以使用这棵B+树索引。可以发现叶子节点上的b值为1、2、1、4、1、2,显然不是排序的,因此对于b列的查询使用不到(a,b)的索引。

        联合索引的第二个好处是已经对第二个键值进行了排序处理。例如,在很多情况下应用程序都需要查询某个用户的购物情况,并按照时间进行排序,最后取出最近三次的购买记录,这时使用联合索引可以避免多一次的排序操作,因为索引本身在叶子节点已经排序了。

        联合索引(a,b)其实是根据列a、b进行排序,因此下列语句可以直接使用联合索引得到结果:


SELECT...FROM TABLE WHERE a=xxx ORDER BY b


        然而对于联合索引(a,b,c)来说,下列语句同样可以直接通过联合索引得到结果:


SELECT...FROM TABLE WHERE a=xxx ORDER BY b

SELECT...FROM TABLE WHERE a=xxx AND b=xxx ORDER BY c


        但是对于下面的语句,联合索引不能直接得到结果,其还需要执行一次filesort排序操作,因为索引(a,c)并未排序:


SELECT...FROM TABLE WHERE a=xxx ORDER BY c


5.6.3 覆盖索引

        InnoDB存储引擎支持覆盖索引(covering index,或称索引覆盖),即从辅助索引中就可以得到查询的记录,而不需要查询聚集索引中的记录。使用覆盖索引的一个好处是辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作。

        对于InnoDB存储引擎的辅助索引而言,由于其包含了主键信息,因此其叶子节点存放的数据为(primary key1,primary key2,…,key1,key2,…)。例如,下列语句都可仅使用一次辅助联合索引来完成查询:


SELECT key2 FROM table WHERE key1=xxx;

SELECT primary key2,key2 FROM table WHERE key1=xxx;

SELECT primary key1,key2 FROM table WHERE key1=xxx;

SELECT primary key1,primary key2,key2 FROM table WHERE key1=xxx;


        覆盖索引的另一个好处是对某些统计问题而言的。InnoDB存储引擎并不会选择通过查询聚集索引来进行统计。

         在通常情况下,诸如(a,b)的联合索引,一般是不可以选择列b中所谓的查询条件。但是如果是统计操作,并且是覆盖索引的,则优化器会进行选择。

5.6.4 优化器选择不使用索引的情况

        在某些情况下,当执行EXPLAIN命令进行SQL语句的分析时,会发现优化器并没有选择索引去查找数据,而是通过扫描聚集索引,也就是直接进行全表的扫描来得到数据。这种情况多发生于范围查找、JOIN链接操作等情况下。例如:


SELECT*FROM orderdetails

WHERE orderid>10000 and orderid<102000;


        上述这句SQL语句查找订单号大于10000的订单详情,通过命令SHOW INDEX FROM orderdetails,可观察到的索引如图5-28所示。

图 5-28 表orderdetails的索引详情

        可以看到表orderdetails有(OrderID,ProductID)的联合主键,此外还有对于列OrderID的单个索引。上述这句SQL显然是可以通过扫描OrderID上的索引进行数据的查找。然而通过EXPLAIN命令,用户会发现优化器并没有按照OrderID上的索引来查找数据,如图5-29所示。

图 5-29 上述范围查询的SQL执行计划

        在possible_keys一列可以看到查询可以使用PRIMARY、OrderID、OrdersOrder_Details三个索引,但是在最后的索引使用中,优化器选择了PRIMARY聚集索引,也就是表扫描(table scan),而非OrderID辅助索引扫描(index scan)。

        这是为什么呢?原因在于用户要选取的数据是整行信息,而OrderID索引不能覆盖到我们要查询的信息,因此在对OrderID索引查询到指定数据后,还需要一次书签访问来查找整行数据的信息。虽然OrderID索引中数据是顺序存放的,但是再一次进行书签查找的数据则是无序的,因此变为了磁盘上的离散读操作。如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据。因为之前已经提到过,顺序读要远远快于离散读。

        因此对于不能进行索引覆盖的情况,优化器选择辅助索引的情况是,通过辅助索引查找的数据是少量的。这是由当前传统机械硬盘的特性所决定的,即利用顺序读来替换随机读的查找。若用户使用的磁盘是固态硬盘,随机读操作非常快,同时有足够的自信来确认使用辅助索引可以带来更好的性能,那么可以使用关键字FORCE INDEX来强制使用某个索引,如:


SELECT*FROM orderdetails FORCE INDEX(OrderID)

WHERE orderid>10000 and orderid<102000;


        这时的执行计划如图5-30所示。

图 5-30 强制使用辅助索引

5.6.5 索引提示

        MySQL数据库支持索引提示(INDEX HINT),显式地告诉优化器使用哪个索引。个人总结以下两种情况可能需要用到INDEX HINT:

        ❑MySQL数据库的优化器错误地选择了某个索引,导致SQL语句运行的很慢。

        ❑某SQL语句可以选择的索引非常多,这时优化器选择执行计划时间的开销可能会大于SQL语句本身。

        在MySQL数据库中Index Hint的语法如下:

tbl_name[[AS]alias][index_hint_list]

index_hint_list:

index_hint[,index_hint]...

index_hint:

USE{INDEX|KEY}

[{FOR{JOIN|ORDER BY|GROUP BY}]([index_list])

|IGNORE{INDEX|KEY}

[{FOR{JOIN|ORDER BY|GROUP BY}](index_list)

|FORCE{INDEX|KEY}

[{FOR{JOIN|ORDER BY|GROUP BY}](index_list)

index_list:

index_name[,index_name]...

5.6.6 Multi-Range Read优化

        MySQL5.6版本开始支持Multi-Range Read(MRR)优化。Multi-Range Read优化的目的就是为了减少磁盘的随机访问,并且将随机访问转化为较为顺序的数据访问,这对于IO-bound类型的SQL查询语句可带来性能极大的提升。Multi-Range Read优化可适用于range,ref,eq_ref类型的查询。

        MRR优化有以下几个好处:

        ❑MRR使数据访问变得较为顺序。在查询辅助索引时,首先根据得到的查询结果,按照主键进行排序,并按照主键排序的顺序进行书签查找。

        ❑减少缓冲池中页被替换的次数。

        ❑批量处理对键值的查询操作。

        对于InnoDB和MyISAM存储引擎的范围查询和JOIN查询操作,MRR的工作方式如下:

        ❑将查询得到的辅助索引键值存放于一个缓存中,这时缓存中的数据是根据辅助索引键值排序的。

        ❑将缓存中的键值根据RowID进行排序。

        ❑根据RowID的排序顺序来访问实际的数据文件。

        此外,若InnoDB存储引擎或者MyISAM存储引擎的缓冲池不是足够大,即不能存放下一张表中的所有数据,此时频繁的离散读操作还会导致缓存中的页被替换出缓冲池,然后又不断地被读入缓冲池。若是按照主键顺序进行访问,则可以将此重复行为降为最低。

        此外,Multi-Range Read还可以将某些范围查询,拆分为键值对,以此来进行批量的数据查询。这样做的好处是可以在拆分过程中,直接过滤一些不符合查询条件的数据。

        倘若启用了Multi-Range Read优化,优化器会先将查询条件进行拆分,然后再进行数据查询。就上述查询语句而言,优化器会将查询条件拆分为(1000,1000),(1001,1000),(1002,1000),…,(1999,1000),最后再根据这些拆分出的条件进行数据的查询。

        是否启用Multi-Range Read优化可以通过参数optimizer_switch中的标记(flag)来控制。当mrr为on时,表示启用Multi-Range Read优化。mrr_cost_based标记表示是否通过cost based的方式来选择是否启用mrr。若将mrr设为on,mrr_cost_based设为off,则总是启用Multi-Range Read优化。

        参数read_rnd_buffer_size用来控制键值的缓冲区大小,当大于该值时,则执行器对已经缓存的数据根据RowID进行排序,并通过RowID来取得行数据。该值默认为256K。

5.6.7 Index Condition Pushdown(ICP)优化

        和Multi-Range Read一样,Index Condition Pushdown同样是MySQL 5.6开始支持的一种根据索引进行查询的优化方式。之前的MySQL数据库版本不支持Index Condition Pushdown,当进行索引查询时,首先根据索引来查找记录,然后再根据WHERE条件来过滤记录。在支持Index Condition Pushdown后,MySQL数据库会在取出索引的同时,判断是否可以进行WHERE条件的过滤,也就是将WHERE的部分过滤操作放在了存储引擎层。在某些查询下,可以大大减少上层SQL层对记录的索取(fetch),从而提高数据库的整体性能。

        Index Condition Pushdown优化支持range、ref、eq_ref、ref_or_null类型的查询,当前支持MyISAM和InnoDB存储引擎。当优化器选择Index Condition Pushdown优化时,可在执行计划的列Extra看到Using index condition提示。

        若支持Index Condition Pushdown优化,则在索引取出时,就会进行WHERE条件的过滤,然后再去获取记录。这将极大地提高查询的效率。当然,WHERE可以过滤的条件是要该索引可以覆盖到的范围。

5.7 哈希算法

        哈希算法是一种常见算法,时间复杂度为O(1),且不只存在于索引中,每个数据库应用中都存在该数据库结构。

5.7.1 哈希表

        哈希表(Hash Table)也称散列表,由直接寻址表改进而来。当关键字的全域U比较小时,直接寻址是一种简单而有效的技术。假设某应用要用到一个动态集合,其中每个元素都有一个取自全域U={0,1,…,m-1}[1]的关键字。同时假设没有两个元素具有相同的关键字。

        用一个数组(即直接寻址表)T[0..m-1]表示动态集合,其中每个位置(或称槽或桶)对应全域U中的一个关键字。

        直接寻址技术存在一个很明显的问题,如果域U很大,在一台典型计算机的可用容量的限制下,要在机器中存储大小为U的一张表T就有点不实际,甚至是不可能的。如果实际要存储的关键字集合K相对于U来说很小,那么分配给T的大部分空间都要浪费掉。

        因此,哈希表出现了。在哈希方式下,该元素处于h(k)中,即利用哈希函数h,根据关键字k计算出槽的位置。函数h将关键字域U映射到哈希表T[0..m-1]的槽位上,如图5-39所示。

图 5-39 哈希表

        哈希表技术很好地解决了直接寻址遇到的问题,但是这样做有一个小问题,如图5-39所示的两个关键字可能映射到同一个槽上。一般将这种情况称之为发生了碰撞(collision)。在数据库中一般采用最简单的碰撞解决技术,这种技术被称为链接法(chaining)。

        在链接法中,把散列到同一槽中的所有元素都放在一个链表中,如图5-40所示。槽j中有一个指针,它指向由所有散列到j的元素构成的链表的头;如果不存在这样的元素,则j中为NULL。

图 5-40 通过链表法解决碰撞的哈希表

        最后要考虑的是哈希函数。哈希函数h必须可以很好地进行散列。最好的情况是能避免碰撞的发生。一般来说,都将关键字转换成自然数,然后通过除法散列、乘法散列或全域散列来实现。数据库中一般采用除法散列的方法。

        在哈希函数的除法散列法中,通过取k除以m的余数,将关键字k映射到m个槽的某一个去,即哈希函数为:   h(k)=k mod m

5.7.2 InnoDB存储引擎中的哈希算法

        InnoDB存储引擎使用哈希算法来对字典进行查找,其冲突机制采用链表方式,哈希函数采用除法散列方式。

        InnoDB存储引擎的表空间都有一个space_id,用户所要查询的应该是某个表空间的某个连续16KB的页,即偏移量offset。InnoDB存储引擎将space_id左移20位,然后加上这个space_id和offset,即关键字K=space_id<<20+space_id+offset,然后通过除法散列到各个槽中去。

5.7.3 自适应哈希索引

        自适应哈希索引采用之前讨论的哈希表的方式实现。不同的是,这仅是数据库自身创建并使用的,DBA本身并不能对其进行干预。自适应哈希索引经哈希函数映射到一个哈希表中,因此对于字典类型的查找非常快速,如SELECT*FROM TABLE WHERE index_col='xxx'。但是对于范围查找就无能为力了。通过命令SHOW ENGINE INNODB STATUS可以看到当前自适应哈希索引的使用状况。

        需要注意的是,哈希索引只能用来搜索等值的查询,对于其他查找类型,如范围查找,是不能使用哈希索引的。因此,这里出现了non-hash searches/s的情况。通过hash searches:non-hash searches可以大概了解使用哈希索引后的效率。

        可以通过参数innodb_adaptive_hash_index来禁用或启动此特性,默认为开启。

5.8 全文检索

5.8.1 概述

        通过前面的介绍,已经知道B+树索引的特点,可以通过索引字段的前缀(prefix)进行查找。例如,对于下面的查询B+树索引是支持的:


SELECT*FROM blog WHERE content like'xxx%'


        上述SQL语句可以查询博客内容以xxx开头的文章,并且只要content添加了B+树索引,就能利用索引进行快速查询。然而实际这种查询不符合用户的要求,因为在更多的情况下,用户需要查询的是博客内容包含单词xxx的文章,即:


SELECT*FROM blog WHERE content like'%xxx%'


        根据B+树索引的特性,上述SQL语句即便添加了B+树索引也是需要进行索引的扫描来得到结果。

        全文检索(Full-Text Search)是将存储于数据库中的整本书或整篇文章中的任意内容信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。

        从InnoDB 1.2.x版本开始,InnoDB存储引擎开始支持全文检索。

5.8.2 倒排索引

        全文检索通常使用倒排索引(inverted index)来实现。倒排索引同B+树索引一样,也是一种索引结构。它在辅助表(auxiliary table)中存储了单词与单词自身在一个或多个文档中所在位置之间的映射。这通常利用关联数组实现,其拥有两种表现形式:

        ❑inverted file index,其表现形式为{单词,单词所在文档的ID}

        ❑full invertedindex,其表现形式为{单词,(单词所在文档的ID,在具体文档中的位置)}

        例如,对于下面这个例子,表t存储的内容如表5-6所示。

        DocumentId表示进行全文检索文档的Id,Text表示存储的内容,用户需要对存储的这些文档内容进行全文检索。例如,查找出现过Some单词的文档Id,又或者查找单个文档中出现过两个Some单词的文档Id,等等。

        对于inverted file index的关联数组,其存储的内容如表5-7所示。

        可以看到单词code存在于文档1和4中,单词days存在与文档3和6中。之后再要进行全文查询就简单了,可以直接根据Documents得到包含查询关键字的文档。对于inverted file index,其仅存取文档Id,而full inverted index存储的是对(pair),即(DocumentId,Position),因此其存储的倒排索引如表5-8所示。

        full inverted index还存储了单词所在的位置信息,如code这个单词出现在(1∶6),即文档1的第6个单词为code。相比之下,full inverted index占用更多的空间,但是能更好地定位数据,并扩充一些其他的搜索特性。

5.8.3 InnoDB全文检索

        InnoDB存储引擎从1.2.x版本开始支持全文检索的技术,其采用full inverted index的方式。在InnoDB存储引擎中,将(DocumentId,Position)视为一个“ilist”。因此在全文检索的表中,有两个列,一个是word字段,另一个是ilist字段,并且在word字段上有设有索引。此外,由于InnoDB存储引擎在ilist字段中存放了Position信息,故可以进行Proximity Search,而MyISAM存储引擎不支持该特性。

        倒排索引需要将word存放到一张表中,这个表称为Auxiliary Table(辅助表)。在InnoDB存储引擎中,为了提高全文检索的并行性能,共有6张Auxiliary Table,目前每张表根据word的Latin编码进行分区。

        Auxiliary Table是持久的表,存放于磁盘上。然而在InnoDB存储引擎的全文索引中,还有另外一个重要的概念FTS Index Cache(全文检索索引缓存),其用来提高全文检索的性能。

        FTS Index Cache是一个红黑树结构,其根据(word,ilist)进行排序。这意味着插入的数据已经更新了对应的表,但是对全文索引的更新可能在分词操作后还在FTS Index Cache中,Auxiliary Table可能还没有更新。InnoDB存储引擎会批量对Auxiliary Table进行更新,而不是每次插入后更新一次Auxiliary Table。当对全文检索进行查询时,Auxiliary Table首先会将在FTS Index Cache中对应的word字段合并到Auxiliary Table中,然后再进行查询。这种merge操作非常类似之前介绍的Insert Buffer的功能,不同的是Insert Buffer是一个持久的对象,并且其是B+树的结构。然而FTS Index Cache的作用又和Insert Buffer是类似的,它提高了InnoDB存储引擎的性能,并且由于其根据红黑树排序后进行批量插入,其产生的Auxiliary Table相对较小。

        InnoDB存储引擎允许用户查看指定倒排索引的Auxiliary Table中分词的信息,可以通过设置参数innodb_ft_aux_table来观察倒排索引的Auxiliary Table。

        当数据库关闭时,在FTS Index Cache中的数据库会同步到磁盘上的Auxiliary Table中。然而,如果当数据库发生宕机时,一些FTS Index Cache中的数据库可能未被同步到磁盘上。那么下次重启数据库时,当用户对表进行全文检索(查询或者插入操作)时,InnoDB存储引擎会自动读取未完成的文档,然后进行分词操作,再将分词的结果放入到FTS Index Cache中。

        参数innodb_ft_cache_size用来控制FTS Index Cache的大小,默认值为32M。当该缓存满时,会将其中的(word,ilist)分词信息同步到磁盘的Auxiliary Table中。增大该参数可以提高全文检索的性能,但是在宕机时,未同步到磁盘中的索引信息可能需要更长的时间进行恢复。

        FTS Document ID是另外一个重要的概念。在InnoDB存储引擎中,为了支持全文检索,必须有一个列与word进行映射,在InnoDB中这个列被命名为FTS_DOC_ID,其类型必须是BIGINT UNSIGNED NOT NULL,并且InnoDB存储引擎自动会在该列上加入一个名为FTS_DOC_ID_INDEX的Unique Index。上述这些操作都由InnoDB存储引擎自己完成,用户也可以在建表时自动添加FTS_DOC_ID,以及相应的Unique Index。由于列名为FTS_DOC_ID的列具有特殊意义,因此创建时必须注意相应的类型,否则MySQL数据库会抛出错误。     

        由于文档的DML操作实际并不删除索引中的数据,相反还会在对应的DELETED表中插入记录,因此随着应用程序的允许,索引会变得非常大,即使索引中的有些数据已经被删除,查询也不会选择这类记录。为此,InnoDB存储引擎提供了一种方式,允许用户手工地将已经删除的记录从索引中彻底删除,该命令就是OPTIMIZE TABLE。因为OPTIMIZE TABLE还会进行一些其他的操作,如Cardinality的重新统计,若用户希望仅对倒排索引进行操作,那么可以通过参数innodb_optimize_fulltext_only进行设置,如:


mysql>SET GLOBAL innodb_optimize_fulltext_only=1;

mysql>OPTIMIZE TABLEfts_a;


        若被删除的文档非常多,那么OPTIMIZE TABLE操作可能需要占用非常多的时间,这会影响应用程序的并发性,并极大地降低用户的响应时间。用户可以通过参数innodb_ft_num_word_optimize来限制每次实际删除的分词数量。该参数的默认值为2000。

        当前InnoDB存储引擎的全文检索还存在以下的限制:

        ❑每张表只能有一个全文检索的索引。

        ❑由多列组合而成的全文检索的索引列必须使用相同的字符集与排序规则。

        ❑不支持没有单词界定符(delimiter)的语言,如中文、日语、韩语等。

5.8.4 全文检索

        MySQL数据库支持全文检索(Full-Text Search)的查询,其语法为:


MATCH(col1,col2,...)AGAINST(expr[search_modifier])

search_modifier:

{

IN NATURAL LANGUAGE MODE

|IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION

|IN BOOLEAN MODE

|WITH QUERY EXPANSION

}


        MySQL数据库通过MATCH()…AGAINST()语法支持全文检索的查询,MATCH指定了需要被查询的列,AGAINST指定了使用何种方法去进行查询。下面将对各种查询模式进行详细的介绍。

1.Natural Language

        全文检索通过MATCH函数进行查询,默认采用Natural Language模式,其表示查询带有指定word的文档。

        对于InnoDB存储引擎的全文检索,还需要考虑以下的因素:

        ❑查询的word在stopword列中,忽略该字符串的查询。

        ❑查询的word的字符长度是否在区间[innodb_ft_min_token_size,innodb_ft_max_token_size]内。

2.Boolean

        MySQL数据库允许使用IN BOOLEAN MODE修饰符来进行全文检索。当使用该修饰符时,查询字符串的前后字符会有特殊的含义,例如下面的语句要求查询有字符串Pease但没有hot的文档,其中+和-分别表示这个单词必须出现,或者一定不存在。

        Boolean全文检索支持以下几种操作符:

        ❑+表示该word必须存在。

        ❑-表示该word必须被排除。

        ❑(no operator)表示该word是可选的,但是如果出现,其相关性会更高

        ❑@distance表示查询的多个单词之间的距离是否在distance之内,distance的单位是字节。这种全文检索的查询也称为Proximity Search。如MATCH(body)AGAINST('"Pease pot"@30'IN BOOLEAN MODE)表示字符串Pease和pot之间的距离需在30字节内。

        ❑>表示出现该单词时增加相关性。

        ❑<表示出现该单词时降低相关性。

        ❑~表示允许出现该单词,但是出现时相关性为负(全文检索查询允许负相关性)。

        ❑*表示以该单词开头的单词,如lik*,表示可以是lik、like,又或者likes。

        ❑"表示短语。

3.Query Expansion

        MySQL数据库还支持全文检索的扩展查询。这种查询通常在查询的关键词太短,用户需要implied knowledge(隐含知识)时进行。例如,对于单词database的查询,用户可能希望查询的不仅仅是包含database的文档,可能还指那些包含MySQL、Oracle、DB2、RDBMS的单词。而这时可以使用Query Expansion模式来开启全文检索的implied knowledge。

        通过在查询短语中添加WITH QUERY EXPANSION或IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION可以开启blind query expansion(又称为automatic relevance feedback)。该查询分为两个阶段。

        ❑第一阶段:根据搜索的单词进行全文索引查询。

        ❑第二阶段:根据第一阶段产生的分词再进行一次全文检索的查询。

        由于Query Expansion的全文检索可能带来许多非相关性的查询,因此在使用时,用户可能需要非常谨慎。

第6章 锁

6.1 什么是锁

        锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。InnoDB存储引擎会在行级别上对表数据上锁,也会在数据库内部其他多个地方使用锁,从而允许对多种不同资源提供并发访问。例如,操作缓冲池中的LRU列表,删除、添加、移动LRU列表中的元素,为了保证一致性,必须有锁的介入。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。

        对于MyISAM引擎,其锁是表锁设计。并发情况下的读没有问题,但是并发插入时的性能就要差一些了,若插入是在“底部”,MyISAM存储引擎还是可以有一定的并发写入操作。

        InnoDB存储引擎锁的实现和Oracle数据库非常类似,提供一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。

6.2 lock与latch

        在数据库中,lock与latch都可以被称为“锁”。

        latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。

        lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放,是有死锁机制的。

        对于InnoDB存储引擎中的latch,可以通过命令SHOW ENGINE INNODB MUTEX来进行查看,如图6-1所示。

图 6-1 通过命令SHOW ENGINE INNODB MUTEX查看latch

        在Debug版本下,通过命令SHOW ENGINE INNODB MUTEX可以看到latch的更多信息,如图6-2所示。

图 6-2 在Debug版本下查看到的latch

        通过上述的例子可以看出,列Type显示的总是InnoDB,列Name显示的是latch的信息以及所在源码的位置(行数)。列Status比较复杂,在Debug模式下,除了显示os_waits,还会显示count、spin_waits、spin_rounds、os_yields、os_wait_times等信息。其具体含义见表6-2。

        相对于latch的查看,lock信息就显得直观多了。用户可以通过命令SHOW ENGINE INNODB STATUS及information_schema架构下的表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS来观察锁的信息。

6.3 InnoDB存储引擎中的锁

6.3.1 锁的类型

        InnoDB存储引擎实现了如下两种标准的行级锁:

        ❑共享锁(S Lock),允许事务读一行数据。

        ❑排他锁(X Lock),允许事务删除或更新一行数据。

        如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容(Lock Compatible)。但若有其他的事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁——这种情况称为锁不兼容。表6-3显示了共享锁和排他锁的兼容性。

        InnoDB存储引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁,如图6-3所示。

图 6-3 层次结构

        若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。例如图6-3,如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。

        InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:

        1)意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁

        2)意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁

        由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如表6-4所示。

        在InnoDB 1.0版本之前,用户只能通过命令SHOW FULL PROCESSLIST,SHOW ENGINE INNODB STATUS等来查看当前数据库中锁的请求,然后再判断事务锁的情况。从InnoDB1.0开始,在INFORMATION_SCHEMA架构下添加了表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS。通过这三张表,用户可以更简单地监控当前事务并分析可能存在的锁问题。

        如果需要查看锁,则还需要访问表INNODB_LOCKS,该表的字段组成如表6-6所示。

        通过表INNODB_LOCK_WAITS,可以很直观地反映当前事务的等待。表INNODB_LOCK_WAITS由4个字段组成,如表6-7所示。

6.3.2 一致性非锁定读

        一致性的非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。如图6-4所示。

图 6-4 InnoDB存储引擎非锁定的一致性读

        图6-4直观地展现了InnoDB存储引擎一致性的非锁定读。之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。

        非锁定读机制极大地提高了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。

        快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。

        在事务隔离级别READ COMMITTED和REPEATABLE READ(InnoDB存储引擎的默认事务隔离级别)下,InnoDB存储引擎使用非锁定的一致性读。在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

6.3.3 一致性锁定读

        在默认配置下,即事务的隔离级别为REPEATABLE READ模式下,InnoDB存储引擎的SELECT操作使用一致性非锁定读。但是在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性,这要求数据库支持加锁语句,即使是对于SELECT的只读操作。InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读(locking read)操作:

        ❑SELECT…FOR UPDATE

        ❑SELECT…LOCK IN SHARE MODE

        SELECT…FOR UPDATE对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。SELECT…LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则会被阻塞。

        对于一致性非锁定读,即使读取的行已被执行了SELECT…FOR UPDATE,也是可以进行读取的。此外,SELECT…FOR UPDATE,SELECT…LOCK IN SHARE MODE必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上述两句SELECT锁定语句时,务必加上BEGIN,START TRANSACTION或者SET AUTOCOMMIT=0。

6.3.4 自增长与锁

        自增长在数据库中是非常常见的一种属性,也是很多DBA或开发人员首选的主键方式。在InnoDB存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter)。当对含有自增长的计数器的表进行插入操作时,这个计数器会被初始化,执行如下的语句来得到计数器的值:

SELECT MAX(auto_inc_col)FROM t FOR UPDATE;

        插入操作会依据这个自增长的计数器值加1赋予自增长列。这个实现方式称做AUTO-INC Locking。这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放。

        虽然AUTO-INC Locking从一定程度上提高了并发插入的效率,但还是存在一些性能上的问题。首先,对于有自增长值的列的并发插入性能较差,事务必须等待前一个插入的完成(虽然不用等待事务的完成)。其次,对于INSERT…SELECT的大数据量的插入会影响插入的性能,因为另一个事务中的插入会被阻塞。

        从MySQL 5.1.22版本开始,InnoDB存储引擎中提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能。并且从该版本开始,InnoDB存储引擎提供了一个参数innodb_autoinc_lock_mode来控制自增长的模式,该参数的默认值为1。在继续讨论新的自增长实现方式之前,需要对自增长的插入进行分类,如表6-9所示。

        接着来分析参数innodb_autoinc_lock_mode以及各个设置下对自增的影响,其总共有三个有效值可供设定,即0、1、2,具体说明如表6-10所示。

        需要注意的是InnoDB存储引擎中自增长的实现和MyISAM不同,MyISAM存储引擎是表锁设计,自增长不用考虑并发插入的问题。因此在master上用InnoDB存储引擎,在slave上用MyISAM存储引擎的replication架构下,用户必须考虑这种情况。

        另外,在InnoDB存储引擎中,自增长值的列必须是索引,同时必须是索引的第一个列。如果不是第一个列,则MySQL数据库会抛出异常,而MyISAM存储引擎没有这个问题。

6.3.5 外键和锁

        外键主要用于引用完整性的约束检查。在InnoDB存储引擎中,对于一个外键列,如果没有显式地对这个列加索引,InnoDB存储引擎自动对其加一个索引,因为这样可以避免表锁。

        对于外键值的插入或更新,首先需要查询父表中的记录,即SELECT父表。但是对于父表的SELECT操作,不是使用一致性非锁定读的方式,因为这样会发生数据不一致的问题,因此这时使用的是SELECT…LOCK IN SHARE MODE方式,即主动对父表加一个S锁。如果这时父表上已经这样加X锁,子表上的操作会被阻塞,如表6-11所示。

6.4 锁的算法

6.4.1 行锁的3种算法

        InnoDB存储引擎有3种行锁的算法,其分别是:

        ❑Record Lock:单个行记录上的锁

        ❑Gap Lock:间隙锁,锁定一个范围,但不包含记录本身

        ❑Next-Key Lock∶Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身

        Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。

        Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。例如一个索引有10,11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:


(-∞,10]

(10,11]

(11,13]

(13,20]

(20,+∞)


        采用Next-Key Lock的锁定技术称为Next-Key Locking。其设计的目的是为了解决Phantom Problem。而利用这种锁定技术,锁定的不是单个值,而是一个范围,是谓词锁(predict lock)的一种改进。除了next-key locking,还有previous-key locking技术。同样上述的索引10、11、13和20,若采用previous-key locking技术,那么可锁定的区间为:


(-∞,10)

[10,11)

[11,13)

[13,20)

[20,+∞)


        当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。

        Next-Key Lock降级为Record Lock仅在查询的列是唯一索引的情况下。若是辅助索引,则情况会完全不同。

        用户可以通过以下两种方式来显式地关闭Gap Lock:

        ❑将事务的隔离级别设置为READ COMMITTED

        ❑将参数innodb_locks_unsafe_for_binlog设置为1

        在上述的配置下,除了外键约束和唯一性检查依然需要的Gap Lock,其余情况仅使用Record Lock进行锁定。但需要牢记的是,上述设置破坏了事务的隔离性,并且对于replication,可能会导致主从数据的不一致。此外,从性能上来看,READ COMMITTED也不会优于默认的事务隔离级别READ REPEATABLE。

        在InnoDB存储引擎中,对于INSERT的操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。

        对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用Next-Key Lock进行锁定。

6.4.2 解决Phantom Problem

        在默认的事务隔离级别下,即REPEATABLE READ下,InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem(幻像问题)。

        Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。

        InnoDB存储引擎默认的事务隔离级别是REPEATABLE READ,在该隔离级别下,其采用Next-Key Locking的方式来加锁。而在事务隔离级别READ COMMITTED下,其仅采用Record Lock。用户可以通过InnoDB存储引擎的Next-Key Locking机制在应用层面实现唯一性的检查。例如:


SELECT*FROM table WHERE col=xxx LOCK IN SHARE MODE;

If not found any row:

#unique for insert value

INSERT INTO table VALUES(...);


        如果用户通过索引查询一个值,并对该行加上一个SLock,那么即使查询的值不在,其锁定的也是一个范围,因此若没有返回任何行,那么新插入的值一定是唯一的。如果在进行第一步SELECT…LOCK IN SHARE MODE操作时,有多个事务并发操作,那么这种唯一性检查机制是否存在问题。其实并不会,因为这时会导致死锁,只有一个事务的插入操作会成功,而其余的事务会抛出死锁的错误。

6.5 锁问题

        通过锁定机制可以实现事务的隔离性要求,使得事务可以并发地工作。

6.5.1 脏读

        在理解脏读(Dirty Read)之前,需要理解脏数据的概念。但是脏数据和之前所介绍的脏页完全是两种不同的概念。脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中。脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。

        对于脏页的读取,是非常正常的。脏页是因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性(或者说两者最终会达到一致性,即当脏页都刷回到磁盘)。并且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。

        脏数据却截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性。

        脏读指的是在不同的事务下,当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据。

6.5.2 不可重复读

        不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了一些DML操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读

        不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了数据库事务一致性的要求。

        在InnoDB存储引擎中,通过使用Next-Key Lock算法来避免不可重复读的问题。在MySQL官方文档中将不可重复读的问题定义为Phantom Problem,即幻像问题。

6.5.3 丢失更新

        丢失更新是另一个锁导致的问题,简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。例如:

1)事务T1将行记录r更新为v1,但是事务T1并未提交。

2)与此同时,事务T2将行记录r更新为v2,事务T2未提交。

3)事务T1提交。

4)事务T2提交。

        在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。这是因为,即使是READ UNCOMMITTED的事务隔离级别,对于行的DML操作,需要对行或其他粗粒度级别的对象加锁。因此在上述步骤2)中,事务T2并不能对行记录r进行更新操作,其会被阻塞,直到事务T1提交。

        虽然数据库能阻止丢失更新问题的产生,但是在生产应用中还有另一个逻辑意义的丢失更新问题。

6.6 阻塞

        一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其是为了确保事务可以并发且正常地运行。

        在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来控制等待的时间(默认是50秒),innodb_rollback_on_timeout用来设定是否在等待超时时对进行中的事务进行回滚操作(默认是OFF,代表不回滚)。参数innodb_lock_wait_timeout是动态的,可以在MySQL数据库运行时进行调整,innodb_rollback_on_timeout是静态的,不可在启动时进行修改。

6.7 死锁

6.7.1 死锁的概念

        死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。解决死锁问题最简单的方式是不要有等待,将任何的等待都转化为回滚,并且事务重新开始。

        解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来设置超时的时间。

        超时机制虽然简单,但是其仅通过超时后对事务进行回滚的方式来处理,或者说其是根据FIFO的顺序选择回滚对象。但若超时的事务所占权重比较大,如事务操作更新了很多行,占用了较多的undo log,这时采用FIFO的方式,就显得不合适了,因为回滚这个事务的时间相对另一个事务所占用的时间可能会很多。

        因此,除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息:

        ❑锁的信息链表

        ❑事务等待链表

        通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。在wait-for graph中,事务为图中的节点。而在图中,事务T1指向T2边的定义为:

        ❑事务T1等待事务T2所占用的资源

        ❑事务T1最终等待T2所占用的资源,也就是事务之间在等待相同的资源,而事务T1发生在事务T2的后面

        下面来看一个例子,当前事务和锁的状态如图6-5所示。

图 6-5 示例事务状态和锁的信息

        在Transaction Wait Lists中可以看到共有4个事务t1、t2、t3、t4,故在wait-for graph中应有4个节点。而事务t2对row1占用x锁,事务t1对row2占用s锁。事务t1需要等待事务t2中row1的资源,因此在wait-for graph中有条边从节点t1指向节点t2。事务t2需要等待事务t1、t4所占用的row2对象,故而存在节点t2到节点t1、t4的边。同样,存在节点t3到节点t1、t2、t4的边,因此最终的wait-for graph如图6-6所示。

图 6-6 wait-for graph

        通过图6-6可以发现存在回路(t1,t2),因此存在死锁。通过上述的介绍,可以发现wait-for graph是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务。

        wait-for graph的死锁检测通常采用深度优先的算法实现,在InnoDB1.2版本之前,都是采用递归方式实现。而从1.2版本开始,对wait-for graph的死锁检测进行了优化,将递归用非递归的方式实现,从而进一步提高了InnoDB存储引擎的性能。

6.7.2 死锁概率

6.7.3 死锁的示例

6.8 锁升级

        锁升级(Lock Escalation)是指将当前锁的粒度降低。举例来说,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁。如果在数据库的设计中认为锁是一种稀有资源,而且想避免锁的开销,那数据库中会频繁出现锁升级现象。

        InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。

第7章 事务

        事务会把数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有修改都不保存。

        InnoDB存储引擎中的事务完全符合ACID的特性。ACID是以下4个词的缩写:

        ❑原子性(atomicity)❑一致性(consistency)❑隔离性(isolation)❑持久性(durability)

        InnoDB通过锁实现事务的隔离性的。

7.1 认识事务

7.1.1 概述

        事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。在事务中的操作,要么都做修改,要么都不做,这就是事务的目的。

        对于InnoDB存储引擎而言,其默认的事务隔离级别为READ REPEATABLE,完全遵循和满足事务的ACID特性。

        A(Atomicity),原子性。原子性指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。

        C(consistency),一致性。一致性指事务将数据库从一种状态转变为下一种一致的状态。

        I(isolation),隔离性。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。

        D(durability),持久性。事务一旦提交,其结果就是永久性的。

7.1.2 分类

        从事务理论的角度来说,可以把事务分为以下几种类型:

        ❑扁平事务(Flat Transactions)

        ❑带有保存点的扁平事务(Flat Transactions with Savepoints)

        ❑链事务(Chained Transactions)

        ❑嵌套事务(Nested Transactions)

        ❑分布式事务(Distributed Transactions)

    

7.2 事务的实现

        事务隔离性由锁来实现。原子性、一致性、持久性通过数据库的redo log和undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。

        redo和undo的作用都可以视为是一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录。

7.2.1 redo

1.基本概念

        重做日志用来实现事务的持久性,即事务ACID中的D。其由两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的。

        InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。这里的日志是指重做日志,在InnoDB存储引擎中,由两部分组成,即redo log和undo log。redo log用来保证事务的持久性,undo log用来帮助事务回滚及MVCC的功能。redo log基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。

        为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一次fsync操作。由于重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一次fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。

        InnoDB存储引擎允许用户手工设置非持久性的情况发生,以此提高数据库的性能。即当事务提交时,日志不写入重做日志文件,而是等待一个时间周期后再执行fsync操作。由于并非强制在事务提交时进行一次fsync操作,显然这可以显著提高数据库的性能。但是当数据库发生宕机时,由于部分日志未刷新到磁盘,因此会丢失最后一段时间的事务。

        参数innodb_flush_log_at_trx_commit用来控制重做日志刷新到磁盘的策略。该参数的默认值为1,表示事务提交时必须调用一次fsync操作。还可以设置该参数的值为0和2。0表示事务提交时不进行写入重做日志操作,这个操作仅在master thread中完成,而在master thread中每1秒会进行一次重做日志文件的fsync操作。2表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行fsync操作。在这个设置下,当MySQL数据库发生宕机而操作系统不发生宕机时,并不会导致事务的丢失。而当操作系统宕机时,重启数据库后会丢失未从文件系统缓存刷新到重做日志文件那部分事务。

        虽然用户可以通过设置参数innodb_flush_log_at_trx_commit为0或2来提高事务提交的性能,但这种设置方法丧失了事务的ACID特性。

        在MySQL数据库中还有一种二进制日志(binlog),其用来进行POINT-IN-TIME(PIT)的恢复及主从复制(Replication)环境的建立。从表面上看其和重做日志非常相似,都是记录了对于数据库操作的日志。然而,从本质上来看,两者有着非常大的不同。

        首先,重做日志是在InnoDB存储引擎层产生,而二进制日志是在MySQL数据库的上层产生的,并且二进制日志不仅仅针对于InnoDB存储引擎,MySQL数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。

        其次,两种日志记录的内容形式不同。MySQL数据库上层的二进制日志是一种逻辑日志,其记录的是对应的SQL语句。而InnoDB存储引擎层面的重做日志是物理格式日志,其记录的是对于每个页的修改。

        此外,两种日志记录写入磁盘的时间点不同,如图7-6所示。二进制日志只在事务提交完成后进行一次写入。而InnoDB存储引擎的重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序进行写入的。

图 7-6 二进制日志与重做日志的写入的时间点不同

        从图7-6中可以看到,二进制日志仅在事务提交时记录,并且对于每一个事务,仅包含对应事务的一个日志。而对于InnoDB存储引擎的重做日志,由于其记录的是物理操作日志,因此每个事务对应多个日志条目,并且事务的重做日志写入是并发的,并非在事务提交时写入,故其在文件中记录的顺序并非是事务开始的顺序。*T1、*T2、*T3表示的是事务提交时的日志。

2.log block

        在InnoDB存储引擎中,重做日志都是以512字节进行存储的。这意味着重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块的大小为512字节。

        若一个页中产生的重做日志数量大于512字节,那么需要分割为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要doublewrite技术。

        重做日志块除了日志本身之外,还由日志块头(log block header)及日志块尾(log block tailer)两部分组成。重做日志头一共占用12字节,重做日志尾占用8字节。故每个重做日志块实际可以存储的大小为492字节(512-12-8)。图7-7显示了重做日志块缓存的结构。

图 7-7 重做日志块缓存的结构

        图7-7显示了重做日志缓存的结构,可以发现重做日志缓存由每个为512字节大小的日志块所组成。日志块由三部分组成,依次为日志块头(log block header)、日志内容(log body)、日志块尾(log block tailer)。

        log block header由4部分组成,如表7-2所示。

        log buffer是由log block组成,在内部log buffer就好似一个数组,因此LOG_BLOCK_HDR_NO用来标记这个数组中的位置。其是递增并且循环使用的,占用4个字节,但是由于第一位用来判断是否是flush bit,所以最大的值为2G。

        LOG_BLOCK_HDR_DATA_LEN占用2字节,表示log block所占用的大小。当log block被写满时,该值为0x200,表示使用全部log block空间,即占用512字节。

        LOG_BLOCK_FIRST_REC_GROUP占用2个字节,表示log block中第一个日志所在的偏移量。如果该值的大小和LOG_BLOCK_HDR_DATA_LEN相同,则表示当前log block不包含新的日志。如事务T1的重做日志1占用762字节,事务T2的重做日志占用100字节。由于每个log block实际只能保存492个字节,因此其在log buffer中的情况应如图7-8所示。

图 7-8 LOG_BLOCK_FIRST_REC_GROUP的例子

        从图7-8中可以观察到,由于事务T1的重做日志占用792字节,因此需要占用两个log block。左侧的log block中LOG_BLOCK_FIRST_REC_GROUP为12,即log block中第一个日志的开始位置。在第二个log block中,由于包含了之前事务T1的重做日志,事务T2的日志才是log block中第一个日志,因此该log block的LOG_BLOCK_FIRST_REC_GROUP为282(270+12)。

        LOG_BLOCK_CHECKPOINT_NO占用4字节,表示该log block最后被写入时的检查点第4字节的值。

        log block tailer只由1个部分组成(如表7-3所示),且其值和LOG_BLOCK_HDR_NO相同,并在函数log_block_init中被初始化。

3.log group

        log group为重做日志组,其中有多个重做日志文件。InnoDB存储引擎实际只有一个log group。

        log group是一个逻辑上的概念,并没有一个实际存储的物理文件来表示log group信息。log group由多个重做日志文件组成,每个log group中的日志文件大小是相同的,且在InnoDB 1.2版本之前,重做日志文件的总大小要小于4GB(不能等于4GB)。从InnoDB 1.2版本开始重做日志文件总大小的限制提高为了512GB。InnoSQL版本的InnoDB存储引擎在1.1版本就支持大于4GB的重做日志。

        重做日志文件中存储的就是之前在log buffer中保存的log block,因此其也是根据块的方式进行物理存储的管理,每个块的大小与log block一样,同样为512字节。在InnoDB存储引擎运行过程中,log buffer根据一定的规则将内存中的log block刷新到磁盘。这个规则具体是:

❑事务提交时

❑当log buffer中有一半的内存空间已经被使用时

❑log checkpoint时

        对于log block的写入追加(append)在redo log file的最后部分,当一个redo log file被写满时,会接着写入下一个redo log file,其使用方式为round-robin。

        虽然log block总是在redo log file的最后部分进行写入,有的读者可能以为对redo log file的写入都是顺序的。其实不然,因为redo log file除了保存log buffer刷新到磁盘的log block,还保存了一些其他的信息,这些信息一共占用2KB大小,即每个redo log file的前2KB的部分不保存log block的信息。对于log group中的第一个redo log file,其前2KB的部分保存4个512字节大小的块,其中存放的内容如表7-4所示。

        需要特别注意的是,上述信息仅在每个log group的第一个redo log file中进行存储。log group中的其余redo log file仅保留这些空间,但不保存上述信息。正因为保存了这些信息,就意味着对redo log file的写入并不是完全顺序的。因为其除了log block的写入操作,还需要更新前2KB部分的信息,这些信息对于InnoDB存储引擎的恢复操作来说非常关键和重要。故log group与redo log file之间的关系如图7-9所示。

图 7-9 log group与redo log file之间的关系

        在log filer header后面的部分为InnoDB存储引擎保存的checkpoint(检查点)值,其设计是交替写入,这样的设计避免了因介质失败而导致无法找到可用的checkpoint的情况。

4.重做日志格式

        不同的数据库操作会有对应的重做日志格式。此外,由于InnoDB存储引擎的存储管理是基于页的,故其重做日志格式也是基于页的。虽然有着不同的重做日志格式,但是它们有着通用的头部格式,如图7-10所示。

图 7-10 重做日志格式

        通用的头部格式由以下3部分组成:

        ❑redo_log_type:重做日志的类型。

        ❑space:表空间的ID。

        ❑page_no:页的偏移量。

        之后redo log body的部分,根据重做日志类型的不同,会有不同的存储内容,例如,对于页上记录的插入和删除操作,分别对应如图7-11所示的格式:

图 7-11 插入和删除的重做日志格式

        到InnoDB1.2版本时,一共有51种重做日志类型。随着功能不断地增加,相信会加入越来越多的重做日志类型。

5.LSN

        LSN是Log Sequence Number的缩写,其代表的是日志序列号。在InnoDB存储引擎中,LSN占用8字节,并且单调递增。LSN表示的含义有:

        ❑重做日志写入的总量

        ❑checkpoint的位置

        ❑页的版本     

6.恢复

        InnoDB存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作。因为重做日志记录的是物理日志,因此恢复的速度比逻辑日志,如二进制日志,要快很多。与此同时,InnoDB存储引擎自身也对恢复进行了一定程度的优化,如顺序读取及并行应用重做日志,这样可以进一步地提高数据库恢复的速度。

        由于checkpoint表示已经刷新到磁盘页上的LSN,因此在恢复过程中仅需恢复checkpoint开始的日志部分。        

        由于重做日志是物理日志,因此其是幂等的。幂等的概念如下:

        INSERT操作在二进制日志中就不是幂等的,重复执行可能会插入多条重复的记录。而上述INSERT操作的重做日志是幂等的。

7.2.2 undo

1.基本概念

        重做日志记录了事务的行为,可以很好地通过其对页进行“重做”操作。但是事务有时还需要进行回滚操作,这时就需要undo。因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样如果用户执行的事务或语句由于某种原因失败了,又或者用户用一条ROLLBACK语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。

        redo存放在重做日志文件中,与redo不同,undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment)。undo段位于共享表空间内。可以通过py_innodb_page_info.py工具来查看当前共享表空间中undo的数量。

        undo是逻辑日志,只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。这是因为在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。

        除了回滚操作,undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。

        最后也是最为重要的一点是,undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。

2.undo存储管理

        InnoDB存储引擎对undo的管理同样采用段的方式。InnoDB存储引擎有rollback segment,每个回滚段种记录了1024个undo log segment,而在每个undo log segment段中进行undo页的申请。共享表空间偏移量为5的页(0,5)记录了所有rollback segment header所在的页,这个页的类型为FIL_PAGE_TYPE_SYS。

        在InnoDB1.1版本之前(不包括1.1版本),只有一个rollback segment,因此支持同时在线的事务限制为1024。从1.1版本开始InnoDB支持最大128个rollback segment,故其支持同时在线的事务限制提高到了128*1024。

        虽然InnoDB1.1版本支持了128个rollback segment,但是这些rollback segment都存储于共享表空间中。从InnoDB1.2版本开始,可通过参数对rollback segment做进一步的设置。这些参数包括:

❑innodb_undo_directory

❑innodb_undo_logs

❑innodb_undo_tablespaces

        参数innodb_undo_directory用于设置rollback segment文件所在的路径。

        参数innodb_undo_logs用来设置rollback segment的个数,默认值为128。

        参数innodb_undo_tablespaces用来设置构成rollback segment文件的数量,这样rollback segment可以较为平均地分布在多个文件中。

        事务在undo log segment分配页并写入undo log的这个过程同样需要写入重做日志。当事务提交时,InnoDB存储引擎会做以下两件事情:

        ❑将undo log放入列表中,以供之后的purge操作

        ❑判断undo log所在的页是否可以重用,若可以分配给下个事务使用

        事务提交后并不能马上删除undo log及undo log所在的页。这是因为可能还有其他事务需要通过undo log来得到行记录之前的版本。故事务提交时将undo log放入一个链表中,是否可以最终删除undo log及undo log所在页由purge线程来判断。

        此外,若为每一个事务分配一个单独的undo页会非常浪费存储空间,特别是对于OLTP的应用类型。因为在事务提交时,可能并不能马上释放页,因此,在InnoDB存储引擎的设计中对undo页可以进行重用。具体来说,当事务提交时,首先将undo log放入链表中,然后判断undo页的使用空间是否小于3/4,若是则表示该undo页可以被重用,之后新的undo log记录在当前undo log的后面。由于存放undo log的列表是以记录进行组织的,而undo页可能存放着不同事务的undo log,因此purge操作需要涉及磁盘的离散读取操作,是一个比较缓慢的过程。

3.undo log格式

        在InnoDB存储引擎中,undo log分为:

        ❑insert undo log

        ❑update undo log

        insert undo log是指在insert操作中产生的undo log。因为insert操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该undo log可以在事务提交后直接删除。不需要进行purge操作。insert undo log的格式如图7-14所示。

图 7-14 insert undo log的格式

        update undo log记录的是对delete和update操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。update undo log的结构如图7-15所示。

图 7-15 update undo log格式

        update undo log相对于之前介绍的insert undo log,记录的内容更多,所需占用的空间也更大。next、start、undo_no、table_id与之前介绍的insert undo log部分相同。这里的type_cmpl,由于update undo log本身还有分类,故其可能的值如下:

❑12 TRX_UNDO_UPD_EXIST_REC更新non-delete-mark的记录

❑13 TRX_UNDO_UPD_DEL_REC将delete的记录标记为not delete

❑14 TRX_UNDO_DEL_MARK_REC将记录标记为delete

        接着的部分记录update_vector信息,update_vector表示update操作导致发生改变的列。每个修改的列信息都要记录的undo log中。对于不同的undo log类型,可能还需要记录对索引列所做的修改。

4.查看undo信息

7.2.3 purge

        delete和update操作可能并不直接删除原有的数据。

        purge用于最终完成delete和update操作。这样设计是因为InnoDB存储引擎支持MVCC,所以记录不能在事务提交时立即进行处理。这时其他事物可能正在引用这行,故InnoDB存储引擎需要保存记录之前的版本。而是否可以删除该条记录通过purge来进行判断。若该行记录已不被任何其他事务引用,那么就可以进行真正的delete操作。可见,purge操作是清理之前的delete和update操作,将上述操作“最终”完成。而实际执行的操作为delete操作,清理之前行记录的版本。

        为了节省存储空间,InnoDB存储引擎的undo log设计是这样的:一个页上允许多个事务的undo log存在。虽然这不代表事务在全局过程中提交的顺序,但是后面的事务产生的undo log总在最后。此外,InnoDB存储引擎还有一个history列表,它根据事务提交的顺序,将undo log进行链接。InnoDB存储引擎这种先从history list中找undo log,然后再从undo page中找undo log的设计模式是为了避免大量的随机读取操作,从而提高purge的效率。

        全局动态参数innodb_purge_batch_size用来设置每次purge操作需要清理的undo page数量。在InnoDB1.2之前,该参数的默认值为20。而从1.2版本开始,该参数的默认值为300。通常来说,该参数设置得越大,每次回收的undo page也就越多,这样可供重用的undo page就越多,减少了磁盘存储空间与分配的开销。不过,若该参数设置得太大,则每次需要purge处理更多的undo page,从而导致CPU和磁盘IO过于集中于对undo log的处理,使性能下降。因此对该参数的调整需要由有经验的DBA来操作,并且需要长期观察数据库的运行的状态。

        当InnoDB存储引擎的压力非常大时,并不能高效地进行purge操作。那么history list的长度会变得越来越长。全局动态参数innodb_max_purge_lag用来控制history list的长度,若长度大于该参数时,其会“延缓”DML的操作。该参数默认值为0,表示不对history list做任何限制。当大于0时,就会延缓DML的操作,其延缓的算法为:


delay=((length(history_list)-innodb_max_purge_lag)*10)-5


        delay的单位是毫秒。此外,需要特别注意的是,delay的对象是行,而不是一个DML操作。例如当一个update操作需要更新5行数据时,每行数据的操作都会被delay,故总的延时时间为5*delay。而delay的统计会在每一次purge操作完成后,重新进行计算。

        InnoDB1.2版本引入了新的全局动态参数innodb_max_purge_lag_delay,其用来控制delay的最大毫秒数。也就是当上述计算得到的delay值大于该参数时,将delay设置为innodb_max_purge_lag_delay,避免由于purge操作缓慢导致其他SQL线程出现无限制的等待。

7.2.4 group commit

        若事务为非只读事务,则每次事务提交时需要进行一次fsync操作,以此保证重做日志都已经写入磁盘。当数据库发生宕机时,可以通过重做日志进行恢复。为了提高磁盘fsync的效率,当前数据库都提供了group commit的功能,即一次fsync可以刷新确保多个事务日志被写入文件。对于InnoDB存储引擎来说,事务提交时会进行两个阶段的操作:

        1)修改内存中事务对应的信息,并且将日志写入重做日志缓冲。

        2)调用fsync将确保日志都从重做日志缓冲写入磁盘。

        步骤2)相对步骤1)是一个较慢的过程,这是因为存储引擎需要与磁盘打交道。但当有事务进行这个过程时,其他事务可以进行步骤1)的操作,正在提交的事物完成提交操作后,再次进行步骤2)时,可以将多个事务的重做日志通过一次fsync刷新到磁盘,这样就大大地减少了磁盘的压力,从而提高了数据库的整体性能。对于写入或更新较为频繁的操作,group commit的效果尤为明显。

        然而在InnoDB1.2版本之前,在开启二进制日志后,InnoDB存储引擎的group commit功能会失效,从而导致性能的下降。并且在线环境多使用replication环境,因此二进制日志的选项基本都为开启状态,因此这个问题尤为显著。

        导致这个问题的原因是在开启二进制日志后,为了保证存储引擎层中的事务和二进制日志的一致性,二者之间使用了两阶段事务,其步骤如下:

        1)当事务提交时InnoDB存储引擎进行prepare操作。

        2)MySQL数据库上层写入二进制日志。

        3)InnoDB存储引擎层将日志写入重做日志文件。

                a)修改内存中事务对应的信息,并且将日志写入重做日志缓冲。

                b)调用fsync将确保日志都从重做日志缓冲写入磁盘。

        一旦步骤2)中的操作完成,就确保了事务的提交,即使在执行步骤3)时数据库发生了宕机。此外需要注意的是,每个步骤都需要进行一次fsync操作才能保证上下两层数据的一致性。步骤2)的fsync由参数sync_binlog控制,步骤3)的fsync由参数innodb_flush_log_at_trx_commit控制。因此上述整个过程如图7-18所示。

图 7-18 开启二进制日志后InnoDB存储引擎的提交过程

        为了保证MySQL数据库上层二进制日志的写入顺序和InnoDB层的事务提交顺序一致,MySQL数据库内部使用了prepare_commit_mutex这个锁。但是在启用这个锁之后,步骤3)中的步骤a)步不可以在其他事务执行步骤b)时进行,从而导致了group commit失效。

        然而,为什么需要保证MySQL数据库上层二进制日志的写入顺序和InnoDB层的事务提交顺序一致呢?这时因为备份及恢复的需要,例如通过工具xtrabackup或者ibbackup进行备份,并用来建立replication,如图7-19所示。

图 7-19 InnoDB存储引擎层事务提交的顺序与MySQL数据库上层的二进制日志不同

        可以看到若通过在线备份进行数据库恢复来重新建立replication,事务T1的数据会产生丢失。因为在InnoDB存储引擎层会检测事务T3在上下两层都完成了提交,不需要再进行恢复。因此通过锁prepare_commit_mutex以串行的方式来保证顺序性,然而这会使group commit无法生效,如图7-20所示。

图 7-20 通过锁prepare_commit_mutex保证InnoDB存储引擎层事务提交与MySQL数据库上层的二进制日志写入的顺序性

        MySQL 5.6 BLGC的实现方式是将事务提交的过程分为几个步骤来完成,如图7-21所示。

图 7-21 MySQL 5.6 BLGC的实现方式

        在MySQL数据库上层进行提交时首先按顺序将其放入一个队列中,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。BLGC的步骤分为以下三个阶段:

        ❑Flush阶段,将每个事务的二进制日志写入内存中。

        ❑Sync阶段,将内存中的二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的写入,这就是BLGC。

        ❑Commit阶段,leader根据顺序调用存储引擎层事务的提交,InnoDB存储引擎本就支持group commit,因此修复了原先由于锁prepare_commit_mutex导致group commit失效的问题。

        当有一组事务在进行Commit阶段时,其他新事物可以进行Flush阶段,从而使group commit不断生效。当然group commit的效果由队列中事务的数量决定,若每次队列中仅有一个事务,那么可能效果和之前差不多,甚至会更差。但当提交的事务越多时,group commit的效果越明显,数据库性能的提升也就越大。

        参数binlog_max_flush_queue_time用来控制Flush阶段中等待的时间,即使之前的一组事务完成提交,当前一组的事务也不马上进入Sync阶段,而是至少需要等待一段时间。这样做的好处是group commit的事务数量更多,然而这也可能会导致事务的响应时间变慢。该参数的默认值为0,且推荐设置依然为0。除非用户的MySQL数据库系统中有着大量的连接(如100个连接),并且不断地在进行事务的写入或更新操作。

7.3 事务控制语句

        在MySQL命令行的默认设置下,事务都是自动提交(auto commit)的,即执行SQL语句后就会马上执行COMMIT操作。因此要显式地开启一个事务需使用命令BEGIN、START TRANSACTION,或者执行命令SET AUTOCOMMIT=0,禁用当前会话的自动提交。

        ❑START TRANSACTION|BEGIN:显式地开启一个事务。

        ❑COMMIT:COMMIT会提交事务,并使得已对数据库做的所有修改成为永久性的。

        ❑ROLLBACK:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。

        ❑SAVEPOINT identifier∶SAVEPOINT允许在事务中创建一个保存点,一个事务中可以有多个SAVEPOINT。

        ❑RELEASE SAVEPOINT identifier:删除一个事务的保存点,当没有一个保存点执行这句语句时,会抛出一个异常。

        ❑ROLLBACK TO[SAVEPOINT]identifier:这个语句与SAVEPOINT命令一起使用。可以把事务回滚到标记点,而不回滚在此标记点之前的任何工作。

        ❑SET TRANSACTION:这个语句用来设置事务的隔离级别。InnoDB提供的事务隔离级别有:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。

7.4 隐式提交的SQL语句

        以下这些SQL语句会产生一个隐式的提交操作,即执行完这些语句后,会有一个隐式的COMMIT操作。

        ❑DDL语句:ALTER DATABASE...UPGRADE DATA DIRECTORY NAME,ALTER EVENT,ALTER PROCEDURE,ALTER TABLE,ALTER VIEW,CREATE DATABASE,CREATE EVENT,CREATE INDEX,CREATE PROCEDURE,CREATE TABLE,CREATE TRIGGER,CREATE VIEW,DROP DATABASE,DROP EVENT,DROP INDEX,DROP PROCEDURE,DROP TABLE,DROP TRIGGER,DROP VIEW,RENAME TABLE,TRUNCATE TABLE。

        ❑用来隐式地修改MySQL架构的操作:CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD。

        ❑管理语句:ANALYZE TABLE、CACHE INDEX、CHECK TABLE、LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE。

7.5 对于事务操作的统计

        由于InnoDB存储引擎是支持事务的,因此InnoDB存储引擎的应用需要在考虑每秒请求数(Question Per Second,QPS)的同时,应该关注每秒事务处理的能力(Transaction Per Second,TPS)。

        计算TPS的方法是(com_commit+com_rollback)/time。但是利用这种方法进行计算的前提是:所有的事务必须都是显式提交的,如果存在隐式地提交和回滚(默认autocommit=1),不会计算到com_commit和com_rollback变量中。如:

mysql>SHOW GLOBAL STATUS LIKE'com_commit'\G;

mysql>SHOW GLOBAL STATUS LIKE'com_commit'\G;

        MySQL数据库中另外还有两个参数handler_commit和handler_rollback用于事务的统计操作。

7.6 事务的隔离级别

        SQL标准定义的四个隔离级别为:

        ❑READ UNCOMMITTED

        ❑READ COMMITTED

        ❑REPEATABLE READ

        ❑SERIALIZABLE

             在InnoDB存储引擎中,可以使用以下命令来设置当前会话或全局的事务隔离级别:

SET[GLOBAL|SESSION]TRANSACTION ISOLATION LEVEL

{

READ UNCOMMITTED

|READ COMMITTED

|REPEATABLE READ

|SERIALIZABLE

}

        如果想在MySQL数据库启动时就设置事务的默认隔离级别,那就需要修改MySQL的配置文件,在[mysqld]中添加如下行:


[mysqld]

transaction-isolation=READ-COMMITTED


        查看当前会话的事务隔离级别,可以使用:


mysql>SELECT@@tx_isolation\G;


        查看全局的事务隔离级别,可以使用:


mysql>SELECT@@global.tx_isolation\G;


        在SERIALIABLE的事务隔离级别,InnoDB存储引擎会对每个SELECT语句后自动加上LOCK IN SHARE MODE,即为每个读取操作加一个共享锁。因此在这个事务隔离级别下,读占用了锁,对一致性的非锁定读不再予以支持。

7.7 分布式事务

7.7.1 MySQL数据库分布式事务

        InnoDB存储引擎提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的ACID要求又有了提高。另外,在使用分布式事务时,InnoDB存储引擎的事务隔离级别必须设置为SERIALIZABLE。

        XA事务允许不同数据库之间的分布式事务,如一台服务器是MySQL数据库的,另一台是Oracle数据库的,又可能还有一台服务器是SQL Server数据库的,只要参与在全局事务中的每个节点都支持XA事务。

        XA事务由一个或多个资源管理器(Resource Managers)、一个事务管理器(Transaction Manager)以及一个应用程序(Application Program)组成。

        ❑资源管理器:提供访问事务资源的方法。通常一个数据库就是一个资源管理器。

        ❑事务管理器:协调参与全局事务中的各个事务。需要和参与全局事务的所有资源管理器进行通信。

        ❑应用程序:定义事务的边界,指定全局事务中的操作。

        在MySQL数据库的分布式事务中,资源管理器就是MySQL数据库,事务管理器为连接MySQL服务器的客户端。图7-22显示了一个分布式事务的模型。

图 7-22 分布式事务模型

        分布式事务使用两段式提交(two-phase commit)的方式。在第一阶段,所有参与全局事务的节点都开始准备(PREPARE),告诉事务管理器它们准备好提交了。在第二阶段,事务管理器告诉资源管理器执行ROLLBACK还是COMMIT。如果任何一个节点显示不能提交,则所有的节点都被告知需要回滚。可见与本地事务不同的是,分布式事务需要多一次的PREPARE操作,待收到所有节点的同意信息后,再进行COMMIT或是ROLLBACK操作。

MySQL数据库XA事务的SQL语法如下:


XA{START|BEGIN}xid[JOIN|RESUME]

XA END xid[SUSPEND[FOR MIGRATE]]

XA PREPARE xid

XA COMMIT xid[ONE PHASE]

XA ROLLBACK xid

XA RECOVER


        通过参数innodb_support_xa可以查看是否启用了XA事务的支持(默认为ON):


mysql>SHOW VARIABLES LIKE'innodb_support_xa'\G;


7.7.2 内部XA事务

        之前讨论的分布式事务是外部事务,即资源管理器是MySQL数据库本身。在MySQL数据库中还存在另外一种分布式事务,其在存储引擎与插件之间,又或者在存储引擎与存储引擎之间,称之为内部XA事务。

        最为常见的内部XA事务存在于binlog与InnoDB存储引擎之间。由于复制的需要,因此目前绝大多数的数据库都开启了binlog功能。在事务提交时,先写二进制日志,再写InnoDB存储引擎的重做日志。对上述两个操作的要求也是原子的,即二进制日志和重做日志必须同时写入。若二进制日志先写了,而在写入InnoDB存储引擎时发生了宕机,那么slave可能会接收到master传过去的二进制日志并执行,最终导致了主从不一致的情况。如图7-23所示。

图 7-23 宕机导致replication主从不一致的情况

        在图7-23中,如果执行完①、②后在步骤③之前MySQL数据库发生了宕机,则会发生主从不一致的情况。为了解决这个问题,MySQL数据库在binlog与InnoDB存储引擎之间采用XA事务。当事务提交时,InnoDB存储引擎会先做一个PREPARE操作,将事务的xid写入,接着进行二进制日志的写入,如图7-24所示。如果在InnoDB存储引擎提交前,MySQL数据库宕机了,那么MySQL数据库在重启后会先检查准备的UXID事务是否已经提交,若没有,则在存储引擎层再进行一次提交操作。

图 7-24 MySQL数据库通过内部XA事务保证主从数据一致

7.8 不好的事务习惯

7.8.1 在循环中提交

7.8.2 使用自动提交

7.8.3 使用自动回滚

7.9 长事务

        长事务(Long-Lived Transactions),顾名思义,就是执行时间较长的事务。

        对于长事务的问题,有时可以通过转化为小批量(mini batch)的事务来进行处理。当事务发生错误时,只需要回滚一部分数据,然后接着上次已完成的事务继续进行。

第8章 备份与恢复

        对InnoDB存储引擎的备份,MySQL数据库提供的大多数工具(如mysqldump、ibbackup、replication)都能很好地完成备份的工作,当然也可以通过第三方的一些工具来完成,如xtrabacup、LVM快照备份等。

8.1 备份与恢复概述

        可以根据不同的类型来划分备份的方法。根据备份的方法不同可以将备份分为:

        ❑Hot Backup(热备)

        ❑Cold Backup(冷备)

        ❑Warm Backup(温备)

        Hot Backup是指数据库运行中直接备份,对正在运行的数据库操作没有任何的影响。这种方式在MySQL官方手册中称为Online Backup(在线备份)。Cold Backup是指备份操作是在数据库停止的情况下,这种备份最为简单,一般只需要复制相关的数据库物理文件即可。这种方式在MySQL官方手册中称为Offline Backup(离线备份)。Warm Backup备份同样是在数据库运行中进行的,但是会对当前数据库的操作有所影响,如加一个全局读锁以保证备份数据的一致性。

        按照备份后文件的内容,备份又可以分为:

        ❑逻辑备份

        ❑裸文件备份

        在MySQL数据库中,逻辑备份是指备份出的文件内容是可读的,一般是文本文件。内容一般是由一条条SQL语句,或者是表内实际数据组成。如mysqldump和SELECT*INTO OUTFILE的方法。这类方法的好处是可以观察导出文件的内容,一般适用于数据库的升级、迁移等工作。但其缺点是恢复所需要的时间往往较长。

        裸文件备份是指复制数据库的物理文件,既可以是在数据库运行中的复制(如ibbackup、xtrabackup这类工具),也可以是在数据库停止运行时直接的数据文件复制。这类备份的恢复时间往往较逻辑备份短很多。

        若按照备份数据库的内容来分,备份又可以分为:

        ❑完全备份

        ❑增量备份

        ❑日志备份

        完全备份是指对数据库进行一个完整的备份。增量备份是指在上次完全备份的基础上,对于更改的数据进行备份。日志备份主要是指对MySQL数据库二进制日志的备份,通过对一个完全备份进行二进制日志的重做(replay)来完成数据库的point-in-time的恢复工作。MySQL数据库复制(replication)的原理就是异步实时地将二进制日志重做传送并应用到从(slave/standby)数据库。

        对于MySQL数据库来说,官方没有提供真正的增量备份的方法,大部分是通过二进制日志完成增量备份的工作。

        对于InnoDB存储引擎来说,因为其支持MVCC功能,因此实现一致的备份比较简单。用户可以先开启一个事务,然后导出一组相关的表,最后提交。当然用户的事务隔离级别必须设置为REPEATABLE READ,这样的做法就可以给出一个完美的一致性备份。然而这个方法的前提是需要用户正确地设计应用程序

        对于mysqldump备份工具来说,可以通过添加--single-transaction选项获得InnoDB存储引擎的一致性备份,原理和之前所说的相同。需要了解的是,这时的备份是在一个执行时间很长的事务中完成的。另外,对于InnoDB存储引擎的备份,务必加上--single-transaction的选项(虽然是mysqldump的一个可选选项,但是我找不出任何不加的理由)。

        最后,任何时候都需要做好远程异地备份,也就是容灾的防范

8.2 冷备

        对于InnoDB存储引擎的冷备非常简单,只需要备份MySQL数据库的frm文件,共享表空间文件,独立表空间文件(*.ibd),重做日志文件。另外建议定期备份MySQL数据库的配置文件my.cnf,这样有利于恢复的操作。

        冷备的优点是:

        ❑备份简单,只要复制相关文件即可。

        ❑备份文件易于在不同操作系统,不同MySQL版本上进行恢复。

        ❑恢复相当简单,只需要把文件恢复到指定位置即可。

        ❑恢复速度快,不需要执行任何SQL语句,也不需要重建索引。

        冷备的缺点是:

        ❑InnoDB存储引擎冷备的文件通常比逻辑文件大很多,因为表空间中存放着很多其他的数据,如undo段,插入缓冲等信息。

        ❑冷备也不总是可以轻易地跨平台。操作系统、MySQL的版本、文件大小写敏感和浮点数格式都会成为问题。

8.3 逻辑备份

8.3.1 mysqldump

        mysqldump备份工具最初由Igor Romanenko编写完成,通常用来完成转存(dump)数据库的备份及不同数据库之间的移植,如从MySQL低版本数据库升级到MySQL高版本数据库,又或者从MySQL数据库移植到Oracle、Microsoft SQL Server数据库等。

        mysqldump的语法如下:


shell>mysqldump[arguments]>fle_name


        如果想要备份所有的数据库,可以使用--all-databases选项:


shell>mysqldump--all-databases>dump.sql


        如果想要备份指定的数据库,可以使用--databases选项:


shell>mysqldump--databases db1 db2 db3>dump.sql


        如果想要对test这个架构进行备份,可以使用如下语句:


[root@xen-server~]#mysqldump--single-transaction test>test_backup.sql


        上述操作产生了一个对test架构的备份,使用--single-transaction选项来保证备份的一致性。备份出的test_backup.sql是文本文件,通过文本查看命令cat就可以得到文件的内容:


[root@xen-server~]#cat test_backup.sql


        可以看到,备份出的文件内容就是表结构和数据,所有这些都是用SQL语句方式表示。文件开始和结束的注释部分是用来设置MySQL数据库的各项参数,一般用来使还原工作更有效和准确地进行。之后的部分先是CREATE TABLE语句,接着就是INSERT的SQL语句了。

        mysqldump的参数选项很多,可以通过使用mysqldump--help命令来查看所有的参数,有些参数有缩写形式,如--lock-tables的缩写形式-l。这里列举一些比较重要的参数。

        ❑--single-transaction:在备份开始前,先执行START TRANSACTION命令,以此来获得备份的一致性,当前该参数只对InnoDB存储引擎有效。当启用该参数并进行备份时,确保没有其他任何的DDL语句执行,因为一致性读并不能隔离DDL操作。

        ❑--lock-tables(-l):在备份中,依次锁住每个架构下的所有表。一般用于MyISAM存储引擎,当备份时只能对数据库进行读取操作,不过备份依然可以保证一致性。对于InnoDB存储引擎,不需要使用该参数,用--single-transaction即可。并且--lock-tables和--single-transaction是互斥(exclusive)的,不能同时使用。如果用户的MySQL数据库中,既有MyISAM存储引擎的表,又有InnoDB存储引擎的表,那么这时用户的选择只有--lock-tables了。此外,正如前面所说的那样,--lock-tables选项是依次对每个架构中的表上锁的,因此只能保证每个架构下表备份的一致性,而不能保证所有架构下表的一致性。

        ❑--lock-all-tables(-x):在备份过程中,对所有架构中的所有表上锁。这个可以避免之前说的--lock-tables参数不能同时锁住所有表的问题。

        ❑--add-drop-database:在CREATE DATABASE前先运行DROP DATABASE。这个参数需要和--all-databases或者--databases选项一起使用。

8.3.2 SELECT...INTO OUTFILE

        SELECT...INTO语句也是一种逻辑备份的方法,更准确地说是导出一张表中的数据。SELECT...INTO的语法如下:


SELECT[column 1],[column 2]...

INTO

OUTFILE'file_name'

[{FIELDS|COLUMNS}

[TERMINATED BY'string']

[[OPTIONALLY]ENCLOSED BY'char']

[ESCAPED BY'char']

]

[LINES

[STARTING BY'string']

[TERMINATED BY'string']

]

FROM TABLE WHERE......


        其中FIELDS[TERMINATED BY'string']表示每个列的分隔符,[[OPTIONALLY]ENCLOSED BY'char']表示对于字符串的包含符,[ESCAPED BY'char']表示转义符。[STARTING BY'string']表示每行的开始符号,TERMINATED BY'string'表示每行的结束符号。如果没有指定任何的FIELDS和LINES的选项,默认使用以下的设置:


FIELDS TERMINATED BY'\t'ENCLOSED BY''ESCAPED BY'\\'

LINES TERMINATED BY'\n'STARTING BY''


        file_name表示导出的文件,但文件所在的路径的权限必须是mysql∶mysql的,否则MySQL会报没有权限导出。

8.3.3 逻辑备份的恢复

        mysqldump的恢复操作比较简单,因为备份的文件就是导出的SQL语句,一般只需要执行这个文件就可以了,可以通过以下的方法:


[root@xen-server~]#mysql-uroot-p<test_backup.sql


        如果在导出时包含了创建和删除数据库的SQL语句,那必须确保删除架构时,架构目录下没有其他与数据库相关的文件,否则可能会得到以下的错误:


mysql>drop database test;


        因为逻辑备份的文件是由SQL语句组成的,也可以通过SOURCE命令来执行导出的逻辑备份文件,如下:


mysql>source/home/mysql/test_backup.sql;


        通过mysqldump可以恢复数据库,但是经常发生的一个问题是,mysqldump可以导出存储过程、导出触发器、导出事件、导出数据,但是却不能导出视图。因此,如果用户的数据库中还使用了视图,那么在用mysqldump备份完数据库后还需要导出视图的定义,或者备份视图定义的frm文件,并在恢复时进行导入,这样才能保证mysqldump数据库的完全恢复。

8.3.4 LOAD DATA INFILE

        若通过mysqldump-tab,或者通过SELECT INTO OUTFILE导出的数据需要恢复,这时可以通过命令LOAD DATA INFILE来进行导入。LOAD DATA INFILE的语法如下:


LOAD DATA INTO TABLE a IGNORE 1 LINES INFILE'/home/mysql/a.txt'

[REPLACE|IGNORE]

INTO TABLE tbl_name

[CHARACTER SET charset_name]

[{FIELDS|COLUMNS}

[TERMINATED BY'string']

[[OPTIONALLY]ENCLOSED BY'char']

[ESCAPED BY'char']

]

[LINES

[STARTING BY'string']

[TERMINATED BY'string']

]

[IGNORE number LINES]

[(col_name_or_user_var,...)]

[SET col_name=expr,...]


        要对服务器文件使用LOAD DATA INFILE,必须拥有FILE权。其中对于导入格式的选项和之前介绍的SELECT INTO OUTFILE命令完全一样。IGNORE number LINES选项可以忽略导入的前几行。

8.3.5 mysqlimport

        mysqlimport是MySQL数据库提供的一个命令行程序,从本质上来说,是LOAD DATA INFILE的命令接口,而且大多数的选项都和LOAD DATA INFILE语法相同。其语法格式如下:


shell>mysqlimport[options]db_name textfile1[textfile2...]


        和LOAD DATA INFILE不同的是,mysqlimport命令可以用来导入多张表。并且通过--user-thread参数并发地导入不同的文件。这里的并发是指并发导入多个文件,而不是指mysqlimport可以并发地导入一个文件,这是有明显区别的。此外,通常来说并发地对同一张表进行导入,其效果一般都不会比串行的方式好。

8.4 二进制日志备份与恢复

        二进制日志非常关键,用户可以通过它完成point-in-time的恢复工作。MySQL数据库的replication同样需要二进制日志。在默认情况下并不启用二进制日志,要使用二进制日志首先必须启用它。如在配置文件中进行设置:


[mysqld]

log-bin=mysql-bin


        在备份二进制日志文件前,可以通过FLUSH LOGS命令来生成一个新的二进制日志文件,然后备份之前的二进制日志。

        要恢复二进制日志也是非常简单的,通过mysqlbinlog即可。mysqlbinlog的使用方法如下:


shell>mysqlbinlog[options]log_fle...


        如果需要恢复多个二进制日志文件,最正确的做法应该是同时恢复多个二进制日志文件,而不是一个一个地恢复,如:


shell>mysqlbinlog binlog.[0-10]*|mysql-u root-p test


        也可以先通过mysqlbinlog命令导出到一个文件,然后再通过SOURCE命令来导入,这种做法的好处是可以对导出的文件进行修改后再导入,如:


shell>mysqlbinlog binlog.000001>/tmp/statements.sql

shell>mysqlbinlog binlog.000002>>/tmp/statements.sql

shell>mysql-u root-p-e"source/tmp/statements.sql"


        --start-position和--stop-position选项可以用来指定从二进制日志的某个偏移量来进行恢复,这样可以跳过某些不正确的语句,如:


shell>mysqlbinlog--start-position=107856 binlog.0000001|mysql-uroot-p test


        --start-datetime和--stop-datetime选项可以用来指定从二进制日志的某个时间点来进行恢复,用法和--start-position和--stop-position选项基本相同。

8.5 热备

8.5.1 ibbackup

        ibbackup是InnoDB存储引擎官方提供的热备工具,可以同时备份MyISAM存储引擎和InnoDB存储引擎表。对于InnoDB存储引擎表其备份工作原理如下:

        1)记录备份开始时,InnoDB存储引擎重做日志文件检查点的LSN。

        2)复制共享表空间文件以及独立表空间文件。

        3)记录复制完表空间文件后,InnoDB存储引擎重做日志文件检查点的LSN。

        4)复制在备份时产生的重做日志。

        对于事务的数据库,如Microsoft SQL Server数据库和Oracle数据库,热备的原理大致和上述相同。可以发现,在备份期间不会对数据库本身有任何影响,所做的操作只是复制数据库文件,因此任何对数据库的操作都是允许的,不会阻塞任何操作。故ibbackup的优点如下:

        ❑在线备份,不阻塞任何的SQL语句。

        ❑备份性能好,备份的实质是复制数据库文件和重做日志文件。

        ❑支持压缩备份,通过选项,可以支持不同级别的压缩。

        ❑跨平台支持,ibbackup可以运行在Linux、Windows以及主流的UNIX系统平台上。

        ibbackup对InnoDB存储引擎表的恢复步骤为:

        ❑恢复表空间文件。

        ❑应用重做日志文件。

        ibbackup提供了一种高性能的热备方式,是InnoDB存储引擎备份的首选方式。不过它是收费软件,并非免费的软件。好在开源的魅力就在于社区的力量,Percona公司给用户带来了开源、免费的XtraBackup热备工具,它实现所有ibbackup的功能,并且扩展支持了真正的增量备份功能。因此,更好的选择是使用XtraBackup来完成热备的工作。

8.5.2 XtraBackup

        XtraBackup备份工具是由Percona公司开发的开源热备工具。支持MySQL5.0以上的版本。XtraBackup在GPL v2开源下发布,官网地址是:https://launchpad.net/percona-xtrabackup。

xtrabackup命令的使用方法如下:


xtrabackup--backup|--prepare[OPTIONS]


xtrabackup命令的可选参数如下:


(The defaults options should be given as the first argument)

--print-defaults Prints the program's argument list and exit.

--no-defaults Don't read the default options from any file.

--defaults-file=Read the default options from this file.

--defaults-extra-file=Read this file after the global options files have been read.

--target-dir=The destination directory for backups.

--backup Make a backup of a mysql instance.

--stats Calculate the statistic of the datadir(it is recommended you take mysqld offline).

--prepare Prepare a backup so you can start mysql server with your restore.

--export Create files to import to another database after it has been prepared.

--print-param Print the parameters of mysqld that you will need for a forcopyback.

--use-memory=This value is used instead of buffer_pool_size.

--suspend-at-end Creates a file called xtrabackup_suspended and waits until the user deletes that file at the end of the backup.

--throttle=(use with--backup)Limits the IO operations(pairs of reads and writes)per second to the values set here.

--log-stream outputs the contents of the xtrabackup_logfile to stdout.

--incremental-lsn=(use with--backup)Copy only.ibd pages newer than the specified LSN high:low.##ATTENTION##:checkpoint lsn*must*be used.Be Careful!

--incremental-basedir=(use with--backup)Copy only.ibd pages newer than the existing backup at the specified directory.

--incremental-dir=(use with--prepare)Apply.delta files and logfiles located in the specified directory.

--tables=name Regular Expression list of table names to be backed up.

--create-ib-logfile(NOT CURRENTLY IMPLEMENTED)will create ib_logfile*after a--prepare.

###If you want to create ib_logfile*only re-execute this

command using the same options.###

--datadir=name Path to the database root.

--tmpdir=name Path for temporary files.Several paths may be specified as a colon(:)separated string.

If you specify multiple paths they are used round-robin.

8.5.3 XtraBackup实现增量备份

        MySQL数据库本身提供的工具并不支持真正的增量备份,更准确地说,二进制日志的恢复应该是point-in-time的恢复而不是增量备份。而XtraBackup工具支持对于InnoDB存储引擎的增量备份,其工作原理如下:

        1)首选完成一个全备,并记录下此时检查点的LSN。

        2)在进行增量备份时,比较表空间中每个页的LSN是否大于上次备份时的LSN,如果是,则备份该页,同时记录当前检查点的LSN。

8.6 快照备份

        MySQL数据库本身并不支持快照功能,因此快照备份是指通过文件系统支持的快照功能对数据库进行备份。备份的前提是将所有数据库文件放在同一文件分区中,然后对该分区进行快照操作。支持快照功能的文件系统和设备包括FreeBSD的UFS文件系统,Solaris的ZFS文件系统,GNU/Linux的逻辑管理器(Logical Volume Manager,LVM)等。这里以LVM为例进行介绍,UFS和ZFS的快照实现大致和LVM相似。

        LVM是LINUX系统下对磁盘分区进行管理的一种机制。LVM在硬盘和分区之上建立一个逻辑层,来提高磁盘分区管理的灵活性。管理员可以通过LVM系统轻松管理磁盘分区,例如,将若干个磁盘分区连接为一个整块的卷组(Volume Group),形成一个存储池。管理员可以在卷组上随意创建逻辑卷(Logical Volumes),并进一步在逻辑卷上创建文件系统。管理员通过LVM可以方便地调整卷组的大小,并且可以对磁盘存储按照组的方式进行命名、管理和分配。简单地说,用户可以通过LVM由物理块设备(如硬盘等)创建物理卷,由一个或多个物理卷创建卷组,最后从卷组中创建任意个逻辑卷(不超过卷组大小),如图8-1所示。

图 8-1 LVM工作原理

        图8-2显示了由多块磁盘组成的逻辑卷LV0。

图 8-2 物理到逻辑卷的映射

8.7 复制

8.7.1 复制的工作原理

        复制(replication)是MySQL数据库提供的一种高可用高性能的解决方案,一般用来建立大型的应用。总体来说,replication的工作原理分为以下3个步骤:

        1)主服务器(master)把数据更改记录到二进制日志(binlog)中。

        2)从服务器(slave)把主服务器的二进制日志复制到自己的中继日志(relay log)中。

        3)从服务器重做中继日志中的日志,把更改应用到自己的数据库上,以达到数据的最终一致性。

        复制的工作原理并不复杂,其实就是一个完全备份加上二进制日志备份的还原。不同的是这个二进制日志的还原操作基本上实时在进行中。这里特别需要注意的是,复制不是完全实时地进行同步,而是异步实时。这中间存在主从服务器之间的执行延时,如果主服务器的压力很大,则可能导致主从服务器延时较大。复制的工作原理如图8-4所示。

图 8-4 MySQL数据库的复制工作原理

        从服务器有2个线程,一个是I/O线程,负责读取主服务器的二进制日志,并将其保存为中继日志;另一个是SQL线程,复制执行中继日志。MySQL4.0版本之前,从服务器只有1个线程,既负责读取二进制日志,又负责执行二进制日志中的SQL语句。这种方式不符合高性能的要求,目前已淘汰。

        通过SHOW SLAVE STATUS命令可以观察当前复制的运行状态,一些主要的变量如表8-1所示。

8.7.2 快照+复制的备份架构

        复制可以用来作为备份,但功能不仅限于备份,其主要功能如下:

        ❑数据分布。由于MySQL数据库提供的复制并不需要很大的带宽要求,因此可以在不同的数据中心之间实现数据的复制。

        ❑读取的负载平衡。通过建立多个从服务器,可将读取平均地分布到这些从服务器中,并且减少了主服务器的压力。一般通过DNS的Round-Robin和Linux的LVS功能都可以实现负载平衡。

        ❑数据库备份。复制对备份很有帮助,但是从服务器不是备份,不能完全代替备份。

        ❑高可用性和故障转移。通过复制建立的从服务器有助于故障转移,减少故障的停机时间和恢复时间。

        可见,复制的设计不是简简单单用来备份的,并且只是用复制来进行备份是远远不够的。假设当前应用采用了主从的复制架构,从服务器作为备份。这时,一个初级DBA执行了误操作,如DROP DATABASE或DROP TABLE,这时从服务器也跟着运行了。这时用户怎样从服务器进行恢复呢?

        因此,一个比较好的方法是通过对从服务器上的数据库所在分区做快照,以此来避免误操作对复制造成影响。当发生主服务器上的误操作时,只需要将从服务器上的快照进行恢复,然后再根据二进制日志进行point-in-time的恢复即可。因此快照+复制的备份架构如图8-5所示。

图 8-5 快照+复制的备份架构

        还有一些其他的方法来调整复制,比如采用延时复制,即间歇性地开启从服务器上的同步,保证大约一小时的延时。这的确也是一个方法,只是数据库在高峰和非高峰期间每小时产生的二进制日志量是不同的,用户很难精准地控制。另外,这种方法也不能完全起到对误操作的防范作用。

        此外,建议在从服务上启用read-only选项,这样能保证从服务器上的数据仅与主服务器进行同步,避免其他线程修改数据。

        在启用read-only选项后,如果操作从服务器的用户没有SUPER权限,则对从服务器进行任何的修改操作会抛出一个错误。

第9章 性能调优

        本章将从以下几个方面集中讲解InnoDB存储引擎的性能问题:

        ❑选择合适的CPU

        ❑内存的重要性

        ❑硬盘对数据库性能的影响

        ❑合理地设置RAID

        ❑操作系统的选择也很重要

        ❑不同文件系统对数据库的影响

        ❑选择合适的基准测试工具

9.1 选择合适的CPU

        用户首先需要清楚当前数据库的应用类型。一般而言,可分为两大类:OLTP(Online Transaction Processing,在线事务处理)和OLAP(Online Analytical Processing,在线分析处理)OLAP多用在数据仓库或数据集市中,一般需要执行复杂的SQL语句来进行查询;OLTP多用在日常的事物处理应用中,如银行交易、在线商品交易、Blog、网络游戏等应用。相对于OLAP,数据库的容量较小。

        InnoDB存储引擎一般都应用于OLTP的数据库应用,这种应用的特点如下:

❑用户操作的并发量大

❑事务处理的时间一般比较短

❑查询的语句较为简单,一般都走索引

❑复杂的查询较少

        可以看出,OLTP的数据库应用本身对CPU的要求并不是很高,因为复杂的查询可能需要执行比较、排序、连接等非常耗CPU的操作,这些操作在OLTP的数据库应用中较少发生。因此,可以说OLAP是CPU密集型的操作,而OLTP是IO密集型的操作。

9.2 内存的重要性

        内存的大小是最能直接反映数据库的性能。已经了解到InnoDB存储引擎既缓存数据,又缓存索引,并且将它们缓存于一个很大的缓冲池中,即InnoDB Buffer Pool。因此,内存的大小直接影响了数据库的性能。

9.3 硬盘对数据库性能的影响

9.3.1 传统机械硬盘

        机械硬盘有两个重要的指标:一个是寻道时间,另一个是转速。机械硬盘的访问需要耗费长时间的磁头旋转和定位来查找,因此顺序访问的速度要远高于随机访问。传统关系数据库的很多设计也都是在尽量充分地利用顺序访问的特性。

        通常来说,可以将多块机械硬盘组成RAID来提高数据库的性能,也可以将数据文件分布在不同硬盘上来达到访问负载的均衡。

9.3.2 固态硬盘

        固态硬盘,更准确地说是基于闪存的固态硬盘,其内部由闪存(Flash Memory)组成。

        对于固态硬盘在InnoDB存储引擎中的优化,可以增加innodb_io_capacity变量的值达到充分利用固态硬盘带来的高IOPS特性。

9.4 合理地设置RAID

9.4.1 RAID类型

        RAID(Redundant Array of Independent Disks,独立磁盘冗余数组)的基本思想就是把多个相对便宜的硬盘组合起来,成为一个磁盘数组,使性能达到甚至超过一个价格昂贵、容量巨大的硬盘。由于将多个硬盘组合成为一个逻辑扇区,RAID看起来就像一个单独的硬盘或逻辑存储单元,因此操作系统只会把它当作一个硬盘。

        RAID的作用是:

        ❑增强数据集成度

        ❑增强容错功能

        ❑增加处理量或容量

        根据不同磁盘的组合方式,常见的RAID组合方式可分为RAID 0、RAID 1、RAID 5、RAID 10和RAID 50等。

        RAID 0:将多个磁盘合并成一个大的磁盘,不会有冗余,并行I/O,速度最快。RAID 0亦称为带区集,它将多个磁盘并列起来,使之成为一个大磁盘,如图9-4所示。

图 9-4 RAID 0结构

        RAID 1:两组以上的N个磁盘相互作为镜像(如图9-5所示),在一些多线程操作系统中能有很好的读取速度,但写入速度略有降低。RAID 1就是镜像,其原理为在主硬盘上存放数据的同时也在镜像硬盘上写相同的数据。当主硬盘(物理)损坏时,镜像硬盘则代替主硬盘的工作。因为有镜像硬盘做数据备份,所以RAID 1的数据安全性在所有的RAID级别上来说是最好的。但是,无论用多少磁盘作为RAID 1,仅算一个磁盘的容量,是所有RAID中磁盘利用率最低的一个级别。

图 9-5 RAID 1结构

        RAID 5:是一种存储性能、数据安全和存储成本兼顾的存储解决方案。它使用的是Disk Striping(硬盘分区)技术。RAID 5至少需要三个硬盘,RAID 5不对存储的数据进行备份,而是把数据和相对应的奇偶校验信息存储到组成RAID 5的各个磁盘上,并且奇偶校验信息和相对应的数据分别存储于不同的磁盘上。当RAID 5的一个磁盘数据发生损坏后,利用剩下的数据和相应的奇偶校验信息去恢复被损坏的数据。RAID 5可以理解为是RAID 0和RAID 1的折中方案。RAID 5可以为系统提供数据安全保障,但保障程度要比镜像低而磁盘空间利用率要比镜像高。RAID 5具有和RAID 0相近似的数据读取速度,只是多了一个奇偶校验信息,写入数据的速度相当慢,若使用Write Back可以让性能改善不少。同时,由于多个数据对应一个奇偶校验信息,RAID 5的磁盘空间利用率要比RAID 1高,存储成本相对较低。RAID 5的结构如图9-6所示。

图 9-6 RAID 5结构

        RAID 10和RAID 01:RAID 10是先镜像再分区数据,将所有硬盘分为两组,视为RAID 0的最低组合,然后将这两组各自视为RAID 1运作。RAID 10有着不错的读取速度,而且拥有比RAID 0更高的数据保护性。RAID 01则与RAID 10的程序相反,先分区再将数据镜射到两组硬盘。RAID 01将所有的硬盘分为两组,变成RAID 1的最低组合,而将两组硬盘各自视为RAID 0运作。RAID 01比RAID 10有着更快的读写速度,不过也多了一些会让整个硬盘组停止运转的几率,因为只要同一组的硬盘全部损毁,RAID 01就会停止运作,而RAID 10可以在牺牲RAID 0的优势下正常运作。RAID 10巧妙地利用了RAID 0的速度及RAID 1的安全(保护)两种特性,它的缺点是需要较多的硬盘,因为至少必须拥有四个以上的偶数硬盘才能使用。RAID 10和RAID 01的结构如图9-7所示。

图 9-7 RAID 10和RAID 01结构

        RAID 50:RAID 50也被称为镜像阵列条带,由至少六块硬盘组成,像RAID 0一样,数据被分区成条带,在同一时间内向多块磁盘写入;像RAID 5一样,也是以数据的校验位来保证数据的安全,且校验条带均匀分布在各个磁盘上,其目的在于提高RAID 5的读写性能。

图 9-8 RAID 50结构

        对于数据库应用来说,RAID 10是最好的选择,它同时兼顾了RAID 1和RAID 0的特性。但是,当一个磁盘失效时,性能可能会受到很大的影响,因为条带(strip)会成为瓶颈。我曾在生产环境下遇到过的情况是,两台负载基本相同的数据库,一台正常的服务器磁盘IO负载为20%左右,而另一台服务器IO负载却高达90%。

9.4.2 RAID Write Back功能

        RAID Write Back功能是指RAID控制器能够将写入的数据放入自身的缓存中,并把它们安排到后面再执行。这样做的好处是,不用等待物理磁盘实际写入的完成,因此写入变得更快了。对于数据库来说,这显得十分重要。例如,对重做日志的写入,在将sync_binlog设为1的情况下二进制日志的写入、脏页的刷新等都可以使性能得到明显的提升。

9.4.3 RAID配置工具

        对RAID卡进行配置可以在服务器启动时进入一个类似于BIOS的配置界面,然后再对其进行各种设置。此外,很多厂商都开发了各种操作系统下的软件对RAID进行配置,如果用户使用的是LSI公司生产提供的RAID卡,则可以使用MegaCLI工具来进行配置。

        MegaCLI为多个操作系统提供了支持,对Windows操作系统还提供了GUI界面的配置环境,因此相对来说比较简单。这里主要介绍命令行下MegaCLI的使用,在Windows下同样可以使用命令MegaCLI.exe。

9.5 操作系统的选择

9.6 不同的文件系统对数据库性能的影响

9.7 选择合适的基准测试工具

        基准测试工具可以用来对数据库或操作系统调优后的性能进行对比。MySQL数据库本身提供了一些比较优秀的工具,这里将介绍另外两款更为优秀和常用的基准测试工具:sysbench和mysql-tpcc。

9.7.1 sysbench

        sysbench是一个模块化的、跨平台的多线程基准测试工具,主要用于测试各种不同系统参数下的数据库负载情况。它主要包括以下几种测试方式:

❑CPU性能

❑磁盘IO性能

❑调度程序性能

❑内存分配及传输速度

❑POSIX线程性能

❑数据库OLTP基准测试

        sysbench的数据库OLTP测试支持MySQL、PostgreSQL和Oracle。目前sysbench主要用于Linux操作系统,开源社区已经将sysbench移植到Windows,并支持对Microsoft SQL Server数据库的测试。

9.7.2 mysql-tpcc

        TPC(Transaction Processing Performance Council,事务处理性能协会)是一个用来评价大型数据库系统软硬件性能的非盈利组织。TPC-C是TPC协会制定的,用来测试典型的复杂OLTP(在线事务处理)系统的性能。

        TPC-C的性能度量单位是tpmC,tpm是transaction per minute的缩写,C代表TPC的C基准测试。该值越大,代表事务处理的性能越高。

        tpcc-mysql由以下两个工具组成。

        ❑tpcc_load:根据仓库数量,生成9张表中的数据。

        ❑tpcc_start:根据不同选项进行TPC-C测试。

第10章 InnoDB存储引擎源代码的编译和调试

《完》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会编程的小猴子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值