MySQL
2024-08-01 18:38:57 5 举报
AI智能生成
MySQL是一个开源的关系型数据库管理系统,它使用SQL(结构化查询语言)来管理数据。MySQL支持大量的数据库类型,包括事务安全的InnoDB、用于高可用性和灾难恢复的Galera Cluster,以及用于高速缓存和查询的Memcached。它具有高性能、高可靠性和易于使用的特点,被广泛应用于各种规模的企业和项目中。MySQL可以通过命令行界面或图形用户界面进行操作,支持多种操作系统,包括Linux、Windows和macOS。
作者其他创作
大纲/内容
如何看待NULL
关于Null的特别说明。
MySQL在处理为Null的字段的时候是很分裂的.很多时候都是根据不同情况来进行区分的.但是不外乎以下三种:
1.nulls_equal
认为所有的NULL值都是相等的.这个值也是innodb_stats_method的默认值.
如果某个索引列中NULL值特别多的化,这种统计方式会让优化器认为某个列中平均一个值重复次数特别多,所以倾向于不使用索引进行访问.
2.nulls_unequal
认为所有的NULL值都是不相等的.
如果某个索引列中NULL值特别多的化,这种统计方式会让优化器认为某个列中平均一个值重复次数特别少,所以倾向于使用索引进行访问.
3.nulls_ignored
直接把NULL值忽略掉.
有迹象表明,在MySQL5.7以后的版本中,对这个innodb_stats_method的修改不起作用,MySQL把这个值在代码里面写死为nulls_equal.也就是说MySQL在进行索引列的数据统计行为又把null视为第二种情况(NULL值在业务上就是代表没有,所有的NULL值和起来算一份),看起来,MySQL对Null值得处理也很分裂,总的来说,对于列的生命尽可能地不要允许为null.
MySQL在处理为Null的字段的时候是很分裂的.很多时候都是根据不同情况来进行区分的.但是不外乎以下三种:
1.nulls_equal
认为所有的NULL值都是相等的.这个值也是innodb_stats_method的默认值.
如果某个索引列中NULL值特别多的化,这种统计方式会让优化器认为某个列中平均一个值重复次数特别多,所以倾向于不使用索引进行访问.
2.nulls_unequal
认为所有的NULL值都是不相等的.
如果某个索引列中NULL值特别多的化,这种统计方式会让优化器认为某个列中平均一个值重复次数特别少,所以倾向于使用索引进行访问.
3.nulls_ignored
直接把NULL值忽略掉.
有迹象表明,在MySQL5.7以后的版本中,对这个innodb_stats_method的修改不起作用,MySQL把这个值在代码里面写死为nulls_equal.也就是说MySQL在进行索引列的数据统计行为又把null视为第二种情况(NULL值在业务上就是代表没有,所有的NULL值和起来算一份),看起来,MySQL对Null值得处理也很分裂,总的来说,对于列的生命尽可能地不要允许为null.
举个例子。上图是本人的MySQL的版本号
系统默认是nulls_equal,也就是所有的null值都是相等的.
```c
SHOW VARIABLES LIKE 'innodb_stats_method';
SELECT VERSION();
```
```c
CREATE TABLE `test_null` (
`field` varchar(8) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=gbk
```
```c
SHOW VARIABLES LIKE 'innodb_stats_method';
SELECT VERSION();
```
```c
CREATE TABLE `test_null` (
`field` varchar(8) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=gbk
```
SELECT COUNT(*) FROM test_null;# 认为每个null都是不同的 4
SELECT COUNT(`field`) FROM test_null; # 认为null是无效的 2
SELECT * FROM test_null WHERE `field` IS NULL;# 认为每个null都是不同的 2
SELECT COUNT(`field`) FROM test_null; # 认为null是无效的 2
SELECT * FROM test_null WHERE `field` IS NULL;# 认为每个null都是不同的 2
一棵B+树可以存放多少条数据
前提:在B+树中,非叶子节点里面存储的是主键值+指针,叶子节点存储的是真实的行数据
假设B+树的深度为2,B+树的存储总记录数= 根节点指针数 * 单个叶子节点记录条数
假设主键ID为bigint类型,长度为8字节,而指针大小在InnoDB源码种设置为6字节,这样总共一共14字节。
在MySQL中InnoDB存储引擎的最小存储单元是页(大小为16KB,可以通过参数设置),
那么一个页中能存放多少这样的组合,就代表有多少指针,即16 * 1024 / 14 = 1170.
可以算出一棵高度为2的B+树,能存放1170*16=18720条这样的数据记录。
同理,高度为3的B+树可以存放的行数:1170 * 1170 * 16 = 21902400(2190w条)
千万级的数据存储只需要约3层B+树,查询数据时,每加载一页(page)代表一次IO,所以说,
根据主键id索引查询约3次IO便可以找到目标结果
假设B+树的深度为2,B+树的存储总记录数= 根节点指针数 * 单个叶子节点记录条数
假设主键ID为bigint类型,长度为8字节,而指针大小在InnoDB源码种设置为6字节,这样总共一共14字节。
在MySQL中InnoDB存储引擎的最小存储单元是页(大小为16KB,可以通过参数设置),
那么一个页中能存放多少这样的组合,就代表有多少指针,即16 * 1024 / 14 = 1170.
可以算出一棵高度为2的B+树,能存放1170*16=18720条这样的数据记录。
同理,高度为3的B+树可以存放的行数:1170 * 1170 * 16 = 21902400(2190w条)
千万级的数据存储只需要约3层B+树,查询数据时,每加载一页(page)代表一次IO,所以说,
根据主键id索引查询约3次IO便可以找到目标结果
什么是松散索引扫描
https://juejin.cn/post/6863770705897455623
COUNT(*)、COUNT(1)有什么区别
倒排索引和正排索引的区别
倒排索引
词条A:文档1,文档2
词条B:文档1,文档3
词条C:文档1,文档3
词条D:文档2
词条E:文档3
倒排表以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字表段,它记录该文档的ID和字符在该文档中出现的位置情况。
由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂,但是在查询的时候由于可以一次得到查询关键字的所有文档,所以效率高于正排表。
在全文检索中,检索的快速响应是一个最为关键的性能,而索引建立由于在后台进行,尽管效率相对低一些,但不会影响整个搜索引擎的效率。
词条A:文档1,文档2
词条B:文档1,文档3
词条C:文档1,文档3
词条D:文档2
词条E:文档3
倒排表以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字表段,它记录该文档的ID和字符在该文档中出现的位置情况。
由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂,但是在查询的时候由于可以一次得到查询关键字的所有文档,所以效率高于正排表。
在全文检索中,检索的快速响应是一个最为关键的性能,而索引建立由于在后台进行,尽管效率相对低一些,但不会影响整个搜索引擎的效率。
正排索引
文档1:词条A,词条B,词条C
文档2:词条A,词条D
文档3:词条B,词条C,词条E
正排表是以文档的ID为关键字,表中记录文档中的每个字的位置信息,查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档。
正排表的结构如图所示,这种组织方法在建立索引的时候结构比较简单,建立比较方便且易于维护;因为索引是基于文档建立的,若是有新的文档加入,
直接为文档建立一个新的索引块,挂接在原来的索引文件的后面,如果是有文档删除,则直接找到该文档号对应的索引信息,将其直接删除。
尽管正排表的工作原理非常的简单,但是由于其检索效率太低,除非在特定情况下,否则实用性价值不大。
文档1:词条A,词条B,词条C
文档2:词条A,词条D
文档3:词条B,词条C,词条E
正排表是以文档的ID为关键字,表中记录文档中的每个字的位置信息,查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档。
正排表的结构如图所示,这种组织方法在建立索引的时候结构比较简单,建立比较方便且易于维护;因为索引是基于文档建立的,若是有新的文档加入,
直接为文档建立一个新的索引块,挂接在原来的索引文件的后面,如果是有文档删除,则直接找到该文档号对应的索引信息,将其直接删除。
尽管正排表的工作原理非常的简单,但是由于其检索效率太低,除非在特定情况下,否则实用性价值不大。
倒排索引和全文索引有什么区别?
倒排索引和全文索引之间存在一些关键的区别,尽管它们在某些方面可能有重叠,以下是它们的主要区别:
1.定义和目的
1.1倒排索引
定义:倒排索引是一种索引数据结构,用于存储文档中的词条及其在文档中的位置。它的核心是一个映射,将词条映射到包含这些词条的文档ID列表
目的:主要用于快速检索包含特定词条的文档,非常适合搜索引擎中的关键词查询
1.2 全文索引
定义:全文索引是一种用于加速对文本文档内容进行搜索的索引结构。它通常包括倒排索引,但可能还包含其他结构和优化技术,如位置索引、词频等
目的:提供对文本文档的全文搜索能力,支持复杂查询,如布尔搜索、短语搜索、相似度搜索等
2.索引结构
倒排索引:包含一个或多个词条,每个词条关联一个文档ID列表。文档ID列表可能还包含位置信息(即词条在文档中的具体位置)。例如
```
词条 "apple" -> [文档1,文档2,文档5]
词条"banana" -> [文档2,文档3]
```
全文索引:除了倒排索引外,全文索引可能还包含其他数据结构和信息,哟关于优化查询性能和支持复杂查询。可能包含c词条的词频信息、词条的位置索引、同义词处理、词干处理等。例如:
```
词条"apple" -> [文档1(位置:5,20), 文档2(位置:3, 15), 文档5(位置7)]
词条"banana" -> [文档2(位置:8,22),文档3(位置:11)]
```
3.功能和查询能力
倒排索引:主要支持关键词查询,即查找包含某个或某些特定词条的文档。查询速度块,适合简单的词条存在性查询
全文索引:支持复杂查询,如布尔查询、短语查询、前缀查询、模糊查询、相似度查询等。提供更丰富的查询功能,能够处理自然语言查询,进行排序和相关性评分
4.使用场景
倒排索引:通常用于搜索引擎和信息检索系统,用于快速查找包含特定关键词的文档。适合于大规模文本数据的关键词检索
全文索引:广泛用于数据库管理系统、内容管理系统和搜索引擎,提供高级的全文搜索功能。适用于需要进行复杂文本搜索和自然语言处理的应用场景
5.总结
倒排索引是全文索引的一部分,是一种具体的数据结构,主要用于支持关键词查询。
全文索引则是一个更广泛的概念,包含倒排索引以及其他用于支持复杂文本搜索的技术和数据结构
倒排索引和全文索引之间存在一些关键的区别,尽管它们在某些方面可能有重叠,以下是它们的主要区别:
1.定义和目的
1.1倒排索引
定义:倒排索引是一种索引数据结构,用于存储文档中的词条及其在文档中的位置。它的核心是一个映射,将词条映射到包含这些词条的文档ID列表
目的:主要用于快速检索包含特定词条的文档,非常适合搜索引擎中的关键词查询
1.2 全文索引
定义:全文索引是一种用于加速对文本文档内容进行搜索的索引结构。它通常包括倒排索引,但可能还包含其他结构和优化技术,如位置索引、词频等
目的:提供对文本文档的全文搜索能力,支持复杂查询,如布尔搜索、短语搜索、相似度搜索等
2.索引结构
倒排索引:包含一个或多个词条,每个词条关联一个文档ID列表。文档ID列表可能还包含位置信息(即词条在文档中的具体位置)。例如
```
词条 "apple" -> [文档1,文档2,文档5]
词条"banana" -> [文档2,文档3]
```
全文索引:除了倒排索引外,全文索引可能还包含其他数据结构和信息,哟关于优化查询性能和支持复杂查询。可能包含c词条的词频信息、词条的位置索引、同义词处理、词干处理等。例如:
```
词条"apple" -> [文档1(位置:5,20), 文档2(位置:3, 15), 文档5(位置7)]
词条"banana" -> [文档2(位置:8,22),文档3(位置:11)]
```
3.功能和查询能力
倒排索引:主要支持关键词查询,即查找包含某个或某些特定词条的文档。查询速度块,适合简单的词条存在性查询
全文索引:支持复杂查询,如布尔查询、短语查询、前缀查询、模糊查询、相似度查询等。提供更丰富的查询功能,能够处理自然语言查询,进行排序和相关性评分
4.使用场景
倒排索引:通常用于搜索引擎和信息检索系统,用于快速查找包含特定关键词的文档。适合于大规模文本数据的关键词检索
全文索引:广泛用于数据库管理系统、内容管理系统和搜索引擎,提供高级的全文搜索功能。适用于需要进行复杂文本搜索和自然语言处理的应用场景
5.总结
倒排索引是全文索引的一部分,是一种具体的数据结构,主要用于支持关键词查询。
全文索引则是一个更广泛的概念,包含倒排索引以及其他用于支持复杂文本搜索的技术和数据结构
DoubleWriteBuffer的作用
概述。
InnoDB是MySQL中一种常用的事务性存储引擎,它具有很多优秀的特性。其中,Doublewrite Buffer(双写缓冲区)是InnoDB的一个重要特性之一
InnoDB是MySQL中一种常用的事务性存储引擎,它具有很多优秀的特性。其中,Doublewrite Buffer(双写缓冲区)是InnoDB的一个重要特性之一
为什么需要DoubleWrite Buffer?
我们常见的服务器一般都是Linux操作系统,Linux文件系统页(OS page)的大小默认是4KB。而MySQL的页(Page)大小默认是16KB,可以使用如下命令查看MySQL的Page大小:
```c
mysql> SHOW VARIABLES LIKE 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.01 sec)
```
一般情况下,其余程序因为需要跟操作系统交互,它们的页(Page)都会大于等于操作系统的页大小,为整数倍。比如,Oracle的Page大小为8KB。MySQL程序是跑在Linux操作系统上的,需要跟操作系统交互,所以MySQL中一页数据刷到磁盘,要写4个文件系统里的页。如图所示。
需要注意的是,这个操作并非原子操作,比如我操作系统写到第二个页的时候,Linux及其断电了,这时候就会出现问题了。造成"页数据损坏"。并且这种"页数据损坏"靠redo日志是无法修复的。重做日志中记录的是对页的物理操作,而不是页面的全量记录,而如果发生Parial Page Write(部分页写入)问题时,出现问题的是未修改过的数据,此时重做日志(Redo Log)无能为力。写double write buffer成功了,这个问题就不用担心了。
DoubleWriteBuffer的出现就是为了解决上面的这种情况,虽然名字带了Buffer,但实际上DoubleWriteBuffer是内存+磁盘的结构。
DoubleWriteBuffer是一种特殊文件flush技术,带给InnoDB存储引擎的是数据页的可靠性。它的作用是,在把页写道数据文件之前,InnoDB先把它们写道一个叫double write buffer完成后,InnoDB才会把页写道数据文件的适当的位置。如果在写页的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在double write buffer中找到完好的page副本用于恢复。
我们常见的服务器一般都是Linux操作系统,Linux文件系统页(OS page)的大小默认是4KB。而MySQL的页(Page)大小默认是16KB,可以使用如下命令查看MySQL的Page大小:
```c
mysql> SHOW VARIABLES LIKE 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.01 sec)
```
一般情况下,其余程序因为需要跟操作系统交互,它们的页(Page)都会大于等于操作系统的页大小,为整数倍。比如,Oracle的Page大小为8KB。MySQL程序是跑在Linux操作系统上的,需要跟操作系统交互,所以MySQL中一页数据刷到磁盘,要写4个文件系统里的页。如图所示。
需要注意的是,这个操作并非原子操作,比如我操作系统写到第二个页的时候,Linux及其断电了,这时候就会出现问题了。造成"页数据损坏"。并且这种"页数据损坏"靠redo日志是无法修复的。重做日志中记录的是对页的物理操作,而不是页面的全量记录,而如果发生Parial Page Write(部分页写入)问题时,出现问题的是未修改过的数据,此时重做日志(Redo Log)无能为力。写double write buffer成功了,这个问题就不用担心了。
DoubleWriteBuffer的出现就是为了解决上面的这种情况,虽然名字带了Buffer,但实际上DoubleWriteBuffer是内存+磁盘的结构。
DoubleWriteBuffer是一种特殊文件flush技术,带给InnoDB存储引擎的是数据页的可靠性。它的作用是,在把页写道数据文件之前,InnoDB先把它们写道一个叫double write buffer完成后,InnoDB才会把页写道数据文件的适当的位置。如果在写页的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在double write buffer中找到完好的page副本用于恢复。
Double Write Buffer原理。
如图所示,当有页数据要刷盘时:
1.页数据先通过memcpy函数拷贝至内存中的Doublewrite buffer中
2.Doublewrite buffer的内存里的数据页,会fsync刷到Doublewrite buffer的磁盘上,分两次写入磁盘共享表空间中(连续存储,顺序写,性能很高),每次写1MB
3.Doublewrite buffer的内存里的数据页,再刷到数据磁盘存储.ibd文件上(离散写)
Doublewrite buffer内存结构由128个页(Page)构成,大小是2MB。DoublewriteBuffer磁盘结构再系统表空间上是128个页(2个区,extend1和extend2),大小事2MB.如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的Doublewrite中找到该页的一个副本,将其复制到表空间文件,再应用重做日志。MySQL会检查double write的数据的完整性,如果不完整直接丢弃double write buffer内容,重新执行那条redo log,如果double write buffer的数据是完整的,用double write buffer的数据更新该数据页,跳过该redo log.所以在正常的情况下,MySQL写数据页时,会写两遍到磁盘上,第一遍是写到double write buffer,第二遍是写到真正的数据文件中,这就是"Doublewrite"的由来。在数据库异常关闭的情况下启动时,都会做数据库恢复(redo)操作,恢复的过程中,数据库都会检查页面是不是合法(校验等等),如果发现一个页面校验结果不一致,则此时会用到双鞋这个功能。我们可以通过如下命令来监控Doublewrite buffer工作负载
```sql
mysql> SHOW GLOBAL status LIKE '%dblwr%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Innodb_dblwr_pages_written | 1961 |
| Innodb_dblwr_writes | 67 |
+----------------------------+-------+
2 rows in set (0.00 sec)
```
如图所示,当有页数据要刷盘时:
1.页数据先通过memcpy函数拷贝至内存中的Doublewrite buffer中
2.Doublewrite buffer的内存里的数据页,会fsync刷到Doublewrite buffer的磁盘上,分两次写入磁盘共享表空间中(连续存储,顺序写,性能很高),每次写1MB
3.Doublewrite buffer的内存里的数据页,再刷到数据磁盘存储.ibd文件上(离散写)
Doublewrite buffer内存结构由128个页(Page)构成,大小是2MB。DoublewriteBuffer磁盘结构再系统表空间上是128个页(2个区,extend1和extend2),大小事2MB.如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的Doublewrite中找到该页的一个副本,将其复制到表空间文件,再应用重做日志。MySQL会检查double write的数据的完整性,如果不完整直接丢弃double write buffer内容,重新执行那条redo log,如果double write buffer的数据是完整的,用double write buffer的数据更新该数据页,跳过该redo log.所以在正常的情况下,MySQL写数据页时,会写两遍到磁盘上,第一遍是写到double write buffer,第二遍是写到真正的数据文件中,这就是"Doublewrite"的由来。在数据库异常关闭的情况下启动时,都会做数据库恢复(redo)操作,恢复的过程中,数据库都会检查页面是不是合法(校验等等),如果发现一个页面校验结果不一致,则此时会用到双鞋这个功能。我们可以通过如下命令来监控Doublewrite buffer工作负载
```sql
mysql> SHOW GLOBAL status LIKE '%dblwr%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Innodb_dblwr_pages_written | 1961 |
| Innodb_dblwr_writes | 67 |
+----------------------------+-------+
2 rows in set (0.00 sec)
```
Doublewrite Buffer相关参数
1.innodb_doublewrite:Doublewrite Buffer是否启用开关,默认是开启状态,InnoDB将所有数据存储两次,首先到双写缓冲区,然后到实际数据文件
2.innodb_dblwr_pages_written:记录写到DWB中的页数量
3.innodb_dblwr_writes:记录DWB写操作的次数
1.innodb_doublewrite:Doublewrite Buffer是否启用开关,默认是开启状态,InnoDB将所有数据存储两次,首先到双写缓冲区,然后到实际数据文件
2.innodb_dblwr_pages_written:记录写到DWB中的页数量
3.innodb_dblwr_writes:记录DWB写操作的次数
总结.
InnoDB Doublewrite Buffer是InnoDB的一个重要特性,用于保证MySQL数据的可靠性和一致性。它的实现原理是通过将要写入磁盘的数据先写入到DoublewriteBuffer中的内存缓存区域,然后再写入到磁盘的两个不同位置,来避免由于磁盘损坏等因素导致数据丢失或不一致的问题。DoublewriteBuffer对于保证MySQL数据的安全性和一致性具有重要意义
InnoDB Doublewrite Buffer是InnoDB的一个重要特性,用于保证MySQL数据的可靠性和一致性。它的实现原理是通过将要写入磁盘的数据先写入到DoublewriteBuffer中的内存缓存区域,然后再写入到磁盘的两个不同位置,来避免由于磁盘损坏等因素导致数据丢失或不一致的问题。DoublewriteBuffer对于保证MySQL数据的安全性和一致性具有重要意义
不可重复读和幻读的区别
https://github.com/Snailclimb/JavaGuide/issues/1763
不可重复读:读的结果的行数不变或减少,结果的内容发生变化
幻读:读的结果的行数变多了
不可重复读:读的结果的行数不变或减少,结果的内容发生变化
幻读:读的结果的行数变多了
MySQL的默认排序规则
https://blog.csdn.net/u010648555/article/details/124011964
MySQL查看数据库各个表的占用空间大小
```sql
SELECT
table_name AS `Table`,
ROUND((data_length + index_length) / 1024 / 1024 / 1024, 2) AS `Size (GB)`
FROM
information_schema.tables
WHERE
table_schema =' your_database_name'
ORDER BY `Size (GB)` DESC;
```
SELECT
table_name AS `Table`,
ROUND((data_length + index_length) / 1024 / 1024 / 1024, 2) AS `Size (GB)`
FROM
information_schema.tables
WHERE
table_schema =' your_database_name'
ORDER BY `Size (GB)` DESC;
```
扇区数和一簇
https://blog.csdn.net/ZK_J1994/article/details/72676862
MySQL如果要执行一个事务,这个事务的操作日志什么时候会被记录到二进制日志中?
MySQL中,事务的操作日志,也就是对数据库所做的更改,通常是在事务提交时被记录到二进制日志(Binary Log)中的。二进制日志是一个重要的功能,它记录了所有更改数据的操作语句,不包括查询语句。这些日志对于数据库的备份、恢复、以及主从复制(Master-Slave Replication)等操作至关重要。
在默认的`STATEMENT`格式下,MySQL服务器会在事务提交时将事务中的SQL语句记录到二进制日志中。如果使用的是`ROW`格式,那么改变的每一行数据都会被记录。还有一种格式是`MIXED`,这种格式是前两种格式的混合,MySQL会根据执行的SQL语句来决定使用哪种格式进行记录。
二进制日志的写入时机可以通过配置项`sync_binlog`进行控制。如果`sync_binlog`设置为1,则每次事务提交时,MySQL都会将二进制日志同步到磁盘上,确保日志的持久性。如果设置为0,则依赖于操作系统的刷新机制,这可能会带来数据丢失的风险,因为如果在日志同步到磁盘之前发生系统故障,那么未同步的日志将会丢失。
综上所述,事务的操作日志默认情况下会在事务提交时被记录到二进制日志中,具体时机还受到二进制日志格式和`sync_binlog`参数的影响。
在默认的`STATEMENT`格式下,MySQL服务器会在事务提交时将事务中的SQL语句记录到二进制日志中。如果使用的是`ROW`格式,那么改变的每一行数据都会被记录。还有一种格式是`MIXED`,这种格式是前两种格式的混合,MySQL会根据执行的SQL语句来决定使用哪种格式进行记录。
二进制日志的写入时机可以通过配置项`sync_binlog`进行控制。如果`sync_binlog`设置为1,则每次事务提交时,MySQL都会将二进制日志同步到磁盘上,确保日志的持久性。如果设置为0,则依赖于操作系统的刷新机制,这可能会带来数据丢失的风险,因为如果在日志同步到磁盘之前发生系统故障,那么未同步的日志将会丢失。
综上所述,事务的操作日志默认情况下会在事务提交时被记录到二进制日志中,具体时机还受到二进制日志格式和`sync_binlog`参数的影响。
RedoLog日志和Binlog日志哪个先写的问题
MySQL中六种日志文件,分别是重做日志(redo log)、回滚日志(undo log)、二进制日志(binlog)、错误日志(errorlog)、慢查询日志(slow query log)、一般查询日志(general log)、中继日志(relay log).其中重做日志和回滚日志与事务操作息息相关,二进制日志与事务操作有一定的关系,这三种日志,对理解MySQL的事务操作有着重要的意义。
首先我们知道在MySQL中,二进制日志(binlog)是server层的,主要用来做主从复制和即时点恢复时使用的。而事务日志(redolog)是InnoDB存储引擎层的,用来保证事务安全的。你可能会有这样的疑问。
1.为什么MySQL有binlog,还有redo log?
2.事务是如何提交的?事务提交先写binlog还是redo log? 如何保证这两部分的日志做到顺序一致性?
3.为了保障主从复制安全,故障恢复是如何做的?
4.为什么需要保证二进制日志的写入顺序和InnoDB层事务提交顺序一致性呢?
简单总结e二者关系:
1.redolog用来恢复当前及其上的存储引擎中的数据,例如innodb
2.binlog用来做复制同步的,让别的机器上的实例进行数据库操作
首先我们知道在MySQL中,二进制日志(binlog)是server层的,主要用来做主从复制和即时点恢复时使用的。而事务日志(redolog)是InnoDB存储引擎层的,用来保证事务安全的。你可能会有这样的疑问。
1.为什么MySQL有binlog,还有redo log?
2.事务是如何提交的?事务提交先写binlog还是redo log? 如何保证这两部分的日志做到顺序一致性?
3.为了保障主从复制安全,故障恢复是如何做的?
4.为什么需要保证二进制日志的写入顺序和InnoDB层事务提交顺序一致性呢?
简单总结e二者关系:
1.redolog用来恢复当前及其上的存储引擎中的数据,例如innodb
2.binlog用来做复制同步的,让别的机器上的实例进行数据库操作
1.为什么MySQL有binlog,还有redo log?
这个是因为MySQL体系结构的原因,MySQL是多存储引擎的,不管使用哪种存储引擎,都会有binlog,而不一定有redolog,简单地说,binlog是MySQL Server层的,redo log是InnoDB层的。
redolog
在MySQL InnoDB中,redo log是用来实现事务的是就行,即当事务在提交时,必须先将该事务的所有操作日志写到磁盘上的redo log file进行持久化,这也就是我们常说的Write Ahead Log策略。有了redo log,在数据库发生宕机时,即时内存中的数据还没来得及持久化到磁盘上,我们也可以通过redo log完成数据的恢复,这样就避免了数据的丢失
binlog
在MySQL中,binlog记录了数据库系统所有的更新操作,主要是用来实现数据恢复和主从复制的。一方面,主从配置的MySQL集群可以利用binlog将主库中的更新操作传递到从库中,依次来实现主从数据的一致性;另一方面,数据库还可以利用binlog来进行数据的恢复
这个是因为MySQL体系结构的原因,MySQL是多存储引擎的,不管使用哪种存储引擎,都会有binlog,而不一定有redolog,简单地说,binlog是MySQL Server层的,redo log是InnoDB层的。
redolog
在MySQL InnoDB中,redo log是用来实现事务的是就行,即当事务在提交时,必须先将该事务的所有操作日志写到磁盘上的redo log file进行持久化,这也就是我们常说的Write Ahead Log策略。有了redo log,在数据库发生宕机时,即时内存中的数据还没来得及持久化到磁盘上,我们也可以通过redo log完成数据的恢复,这样就避免了数据的丢失
binlog
在MySQL中,binlog记录了数据库系统所有的更新操作,主要是用来实现数据恢复和主从复制的。一方面,主从配置的MySQL集群可以利用binlog将主库中的更新操作传递到从库中,依次来实现主从数据的一致性;另一方面,数据库还可以利用binlog来进行数据的恢复
2.事务是如何提交的?事务提交先写binlog还是redo log?如何保证这两部分的日志做到顺序一致性?
MySQL Binary Log在MySQL5.1版本后推出主要用于主备复制的搭建,我们回顾下MySQL在开启/关闭Binary Log功能时是如何工作的。
2.1 MySQL没有开启Binary log的情况下
正式环境一般是开启的。首先看一下什么是CrashSafe?CrashSafe指MySQL服务器宕机重启后,能够保证:
2.1.1 所有已经提交的事务的数据仍然存在
2.1.2 所有没有提交的事务的数据自动回滚
InnoDB通过Redo Log和Undo Log可以保证以上两点。为了保证严格的CrashSafe,必须要在每隔事务提交的时候,将Redo Log写入硬件存储。这样做会牺牲一些性能,但是可靠性最好。为了平衡两者,InnoDB提供了一个innodb_flush_log_at_trx_commit(控制的是Redo Log文件的写入)系统变量,用户可以根据应用的需求自行调整.
innodb_flush_log_at_trx_comit = 0|1|2 (默认是1)
0 - 每N秒将Redo Log Buffer的记录写入Redo Log 文件,并且将文件刷入硬件存储1次,N由innodb_flush_log_at_timeout控制。默认是1s
1 - 每个事务提交时,将记录从Redo Log Buffer写入Redo Log文件,并且将文件刷入硬件存储
2 - 每个事务提交奥时,仅将记录从Redo Log Buffer写入Redo Log文件,也就是写入到操作系统缓存,但不保证它们被同步到磁盘,操作系统可能会定期将缓存中的数据写入磁盘。这种设置在操作系统崩溃时提供了一定程度的持久性保证,但在MySQL数据库崩溃时可能会丢失数据
通过redo日志将所有已经在存储以前引擎内部提交的事务应用redolog恢复,所有已经prepare但是没有commit的transaction将会应用undo log做rollback。然后客户端连接时就能看到已经提交的数据存在数据库内,未提交被回滚的数据需要重新执行。
2.2MySQL开启Binary LOg的情况下?
MySQL引入二阶段提交(two phase commit or 2pc),MySQL内部会自动将普通事务当作一个XA事务(内部分布式事务)来处理:
(在这里提到的两段式提交是MySQL 5.6以前的实现方式,但是此过程存在严重缺陷:此过程为了保证MySQL Server层binlog的写入顺序和InnoDB层的事务提交顺序是一致的,MySQL数据库内部使用了prepare_commit_mutext这个锁。但是在启用了这个锁之后,并不能并发写入binlog,从而导致group commit失效,这个问题在MySQL 5.6中的Binary Log Group Commit(BLGC)得到解决)
先写binglog还是先写redolog的呢?
针对这个疑问,我们可以做出两个假设.
1.假设一:先写redo log再写binlog
想象以下,如果数据库系统在写完一个事务的redolog时发生crash,而此时这个事务的binlog还没有持久化。在数据库恢复后,主库会根据redolog中区完成此事务的重做,主库中就有这个事务的数据。但是,由于此事务并没有产生binlog,即时主库恢复后,关于此事务的数据修改也不会同步到从库上,这样就产生了主从不一致的错误
2.假设而:先写binlog再写redo log
想象以下,如果数据库系统在写完一个事务的binlog时发生crash,而此时这个事务的redo log还没有持久化,或者说此事务的redolog还没记录完(至少没有记录commitlog)。在数据库恢复后,从库会根据主库中记录的binlog去回放此事务的数据修改。但是,由于此事务并没有产生完整提交的redo log,主库在恢复后会回滚事务,这样也会产生主从不一致的错误。
通过上面的假设和分析,我们可以看出,不管时先写redo log还是先写binlog,都有可能会产生主从不一致的错误,那么MySQL又是怎么做到binlog和redolog的一致性的呢?
MySQL Binary Log在MySQL5.1版本后推出主要用于主备复制的搭建,我们回顾下MySQL在开启/关闭Binary Log功能时是如何工作的。
2.1 MySQL没有开启Binary log的情况下
正式环境一般是开启的。首先看一下什么是CrashSafe?CrashSafe指MySQL服务器宕机重启后,能够保证:
2.1.1 所有已经提交的事务的数据仍然存在
2.1.2 所有没有提交的事务的数据自动回滚
InnoDB通过Redo Log和Undo Log可以保证以上两点。为了保证严格的CrashSafe,必须要在每隔事务提交的时候,将Redo Log写入硬件存储。这样做会牺牲一些性能,但是可靠性最好。为了平衡两者,InnoDB提供了一个innodb_flush_log_at_trx_commit(控制的是Redo Log文件的写入)系统变量,用户可以根据应用的需求自行调整.
innodb_flush_log_at_trx_comit = 0|1|2 (默认是1)
0 - 每N秒将Redo Log Buffer的记录写入Redo Log 文件,并且将文件刷入硬件存储1次,N由innodb_flush_log_at_timeout控制。默认是1s
1 - 每个事务提交时,将记录从Redo Log Buffer写入Redo Log文件,并且将文件刷入硬件存储
2 - 每个事务提交奥时,仅将记录从Redo Log Buffer写入Redo Log文件,也就是写入到操作系统缓存,但不保证它们被同步到磁盘,操作系统可能会定期将缓存中的数据写入磁盘。这种设置在操作系统崩溃时提供了一定程度的持久性保证,但在MySQL数据库崩溃时可能会丢失数据
通过redo日志将所有已经在存储以前引擎内部提交的事务应用redolog恢复,所有已经prepare但是没有commit的transaction将会应用undo log做rollback。然后客户端连接时就能看到已经提交的数据存在数据库内,未提交被回滚的数据需要重新执行。
2.2MySQL开启Binary LOg的情况下?
MySQL引入二阶段提交(two phase commit or 2pc),MySQL内部会自动将普通事务当作一个XA事务(内部分布式事务)来处理:
(在这里提到的两段式提交是MySQL 5.6以前的实现方式,但是此过程存在严重缺陷:此过程为了保证MySQL Server层binlog的写入顺序和InnoDB层的事务提交顺序是一致的,MySQL数据库内部使用了prepare_commit_mutext这个锁。但是在启用了这个锁之后,并不能并发写入binlog,从而导致group commit失效,这个问题在MySQL 5.6中的Binary Log Group Commit(BLGC)得到解决)
先写binglog还是先写redolog的呢?
针对这个疑问,我们可以做出两个假设.
1.假设一:先写redo log再写binlog
想象以下,如果数据库系统在写完一个事务的redolog时发生crash,而此时这个事务的binlog还没有持久化。在数据库恢复后,主库会根据redolog中区完成此事务的重做,主库中就有这个事务的数据。但是,由于此事务并没有产生binlog,即时主库恢复后,关于此事务的数据修改也不会同步到从库上,这样就产生了主从不一致的错误
2.假设而:先写binlog再写redo log
想象以下,如果数据库系统在写完一个事务的binlog时发生crash,而此时这个事务的redo log还没有持久化,或者说此事务的redolog还没记录完(至少没有记录commitlog)。在数据库恢复后,从库会根据主库中记录的binlog去回放此事务的数据修改。但是,由于此事务并没有产生完整提交的redo log,主库在恢复后会回滚事务,这样也会产生主从不一致的错误。
通过上面的假设和分析,我们可以看出,不管时先写redo log还是先写binlog,都有可能会产生主从不一致的错误,那么MySQL又是怎么做到binlog和redolog的一致性的呢?
innodb_flush_log_at_timeout
innodb_flush_log_at_trx_commit
在MySQL内部,在事务提交时利用两阶段提交(内部XA的两阶段提交)很好地解决了上面提到的binlog和redo log的一致性:
1.第一阶段:InnoDB Prepare阶段。此时SQL已经成功执行,并生成事务ID(xid)信息及redo和undo的内存日志。此阶段InnoDB会写事务的redo log,但要注意的时,此时redo log只是记录了事务的所有操作日志,并没有记录提交(commit)日志,因此事务此时的状态为Prepare。此阶段对binlog不会有任何操作
2.第二阶段:commit节点,这个阶段又分成两个步骤。第一步写binlog(先调用write()将binlog日志数据写入文件系统缓存,再调用fsync()将binlog文件系统缓存日志数据永久写入磁盘);第二步完成事务的提交(commit),此时在redo log中记录此事务的提交日志(增加commit标签)。还要注意的是,在这个过程中是以第二阶段中binlog的写入与否作为事务是否成功提交的标识,第一步写binlog完成,第二步redo log的commit未提交未完成,也算完成。
可以看出,此过程中是先写redolog再写binlog的。但需要注意的是,在第一阶段并没有记录完整的redolog(不包含事务的commit标签),而是在第二阶段记录完binlog后再写入redolog的commit标签。
当innodb的redolog记录,但是状态为Prepare时,尚未commit时,用户是查询不到该数据的
1.第一阶段:InnoDB Prepare阶段。此时SQL已经成功执行,并生成事务ID(xid)信息及redo和undo的内存日志。此阶段InnoDB会写事务的redo log,但要注意的时,此时redo log只是记录了事务的所有操作日志,并没有记录提交(commit)日志,因此事务此时的状态为Prepare。此阶段对binlog不会有任何操作
2.第二阶段:commit节点,这个阶段又分成两个步骤。第一步写binlog(先调用write()将binlog日志数据写入文件系统缓存,再调用fsync()将binlog文件系统缓存日志数据永久写入磁盘);第二步完成事务的提交(commit),此时在redo log中记录此事务的提交日志(增加commit标签)。还要注意的是,在这个过程中是以第二阶段中binlog的写入与否作为事务是否成功提交的标识,第一步写binlog完成,第二步redo log的commit未提交未完成,也算完成。
可以看出,此过程中是先写redolog再写binlog的。但需要注意的是,在第一阶段并没有记录完整的redolog(不包含事务的commit标签),而是在第二阶段记录完binlog后再写入redolog的commit标签。
当innodb的redolog记录,但是状态为Prepare时,尚未commit时,用户是查询不到该数据的
此时的崩溃恢复过程如下:
1.如果数据库在记录此事务的binlog之前和过程中发生crash那么没有此条redolog记录,并且后续的binlog也不会记录。数据库在恢复后认为此事务并没有成功提交,则会回滚此事务的操作。与此同时,因为在binlog中
也没有此事务的记录,所以从库也不会有此事务的数据修改。
2.如果数据库在记录此事务的binlog之后发生crash,此时redolog新增一条,binlog新增一条,但是redolog的commit标识为false.此时,即使是redo log中还没有记录此事务的commit标签,数据库在恢复后也会认为此事务提交成功.
因为在上述两阶段中,binlog写入成功就认为事务成功提交了,原理如下:
数据库恢复后它会先扫描最后一个redolog文件,并提取其中的事务ID(xid),InnoDB会将那些状态为Prepare的事务(redo log没有记录commit标签)的xid和binlog中提取的xid做比较,如果在binlog中存在,则重新提交该事务,
否则回滚该事务。这也就是说,binlog中记录的事务,在恢复时都会被认为是已提交事务,会在redolog中重新写入commit标志,并完成此事务的重做(主库中有此事务的数据修改),在当前机器的innodb实例上补做事务的提交标识。
与此同时,因为在binlog中有了此事务的记录,所有从库也会此事务的数据修改。简单来说,即使redo log中还没有记录此事务的commit标签,只要记录了binlog,系统重启后,就会去扫描binglog,重新不上redolog的commit
知识点总结:
1.自动为每个事务分配一个唯一的ID(XID)。这个ID很重要,是两阶段提交的核心
2.COMMIT会被自动的分成Prepare和Commit两个阶段
3.Binlog会被当作事务协调者(Transaction Coordinator) Binlog Event会被当作协调者日志
binlog在2PC中充当了事务的协调者(Transaction Coordinator)由binlog来通知InnoDB引擎来执行prepare,commit或者rollback的步骤。事务提交的整个过程如图所示:
1.engine是存储引擎实例,这里指innodb,第一个箭头处标识准备阶段,第二个箭头表示二次提交阶段
2.server是server层
1.如果数据库在记录此事务的binlog之前和过程中发生crash那么没有此条redolog记录,并且后续的binlog也不会记录。数据库在恢复后认为此事务并没有成功提交,则会回滚此事务的操作。与此同时,因为在binlog中
也没有此事务的记录,所以从库也不会有此事务的数据修改。
2.如果数据库在记录此事务的binlog之后发生crash,此时redolog新增一条,binlog新增一条,但是redolog的commit标识为false.此时,即使是redo log中还没有记录此事务的commit标签,数据库在恢复后也会认为此事务提交成功.
因为在上述两阶段中,binlog写入成功就认为事务成功提交了,原理如下:
数据库恢复后它会先扫描最后一个redolog文件,并提取其中的事务ID(xid),InnoDB会将那些状态为Prepare的事务(redo log没有记录commit标签)的xid和binlog中提取的xid做比较,如果在binlog中存在,则重新提交该事务,
否则回滚该事务。这也就是说,binlog中记录的事务,在恢复时都会被认为是已提交事务,会在redolog中重新写入commit标志,并完成此事务的重做(主库中有此事务的数据修改),在当前机器的innodb实例上补做事务的提交标识。
与此同时,因为在binlog中有了此事务的记录,所有从库也会此事务的数据修改。简单来说,即使redo log中还没有记录此事务的commit标签,只要记录了binlog,系统重启后,就会去扫描binglog,重新不上redolog的commit
知识点总结:
1.自动为每个事务分配一个唯一的ID(XID)。这个ID很重要,是两阶段提交的核心
2.COMMIT会被自动的分成Prepare和Commit两个阶段
3.Binlog会被当作事务协调者(Transaction Coordinator) Binlog Event会被当作协调者日志
binlog在2PC中充当了事务的协调者(Transaction Coordinator)由binlog来通知InnoDB引擎来执行prepare,commit或者rollback的步骤。事务提交的整个过程如图所示:
1.engine是存储引擎实例,这里指innodb,第一个箭头处标识准备阶段,第二个箭头表示二次提交阶段
2.server是server层
如上图绿色显示,只要步骤执行到写binlog时,就算作提交了,当然,最后redolog的commit算是锦上添花,即使局部失败,也不影响整体的成功。上面的图片可以看到,事务的提交主要分为两个主要步骤:
1.InnoDB Prepare准备阶段(Storage Engine (InnoDB) Transaction Prepare Phase)
此时SQL已经成功执行,并生成xid信息及redo和undo的内存日志。然后调用prepare方法完成第一阶段,prepare方法实际上什么也没做,将事务状态设为TRX_PREPARED,并将redo log刷磁盘
2.commit提交阶段(Storage Engine(InnoDB) Commit Phase)
记录协调者日志,即binlog。如果事务设计的所有存储引擎的prepare都执行成功,则调用TC_LOG_BINLOG::log_xid方法将SQL语句写到binlog(write()将binnary log内存日志数据写入文件系统才能,fsync()将binnary log文件系统缓存日志数据永久写入磁盘)。此时,事务已经算作要提交了告诉引擎做commit.最后调用引擎的commit完成事务的提交。会清楚undo信息,刷redo日志(写入redo log 的commit标签),将事务设为TRX_NOT_STARTED状态。
PS:记录binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后,这点至关重要。通过上述MySQL内部XA的两阶段提交就可以解决binlog和redolog的一致性问题。数据库在上述任何阶段crash,主从库都不会产生不一致的错误。
简单来说,当系统挂掉,redo log里面可能包含无效数据(待回滚数据),当系统再恢复后,首先会处理binlog,根据binlog会回滚redolog(处理完后,redolog的数据都是有效数据),最后才会读取redolog恢复bufferpool
1.InnoDB Prepare准备阶段(Storage Engine (InnoDB) Transaction Prepare Phase)
此时SQL已经成功执行,并生成xid信息及redo和undo的内存日志。然后调用prepare方法完成第一阶段,prepare方法实际上什么也没做,将事务状态设为TRX_PREPARED,并将redo log刷磁盘
2.commit提交阶段(Storage Engine(InnoDB) Commit Phase)
记录协调者日志,即binlog。如果事务设计的所有存储引擎的prepare都执行成功,则调用TC_LOG_BINLOG::log_xid方法将SQL语句写到binlog(write()将binnary log内存日志数据写入文件系统才能,fsync()将binnary log文件系统缓存日志数据永久写入磁盘)。此时,事务已经算作要提交了告诉引擎做commit.最后调用引擎的commit完成事务的提交。会清楚undo信息,刷redo日志(写入redo log 的commit标签),将事务设为TRX_NOT_STARTED状态。
PS:记录binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后,这点至关重要。通过上述MySQL内部XA的两阶段提交就可以解决binlog和redolog的一致性问题。数据库在上述任何阶段crash,主从库都不会产生不一致的错误。
简单来说,当系统挂掉,redo log里面可能包含无效数据(待回滚数据),当系统再恢复后,首先会处理binlog,根据binlog会回滚redolog(处理完后,redolog的数据都是有效数据),最后才会读取redolog恢复bufferpool
为什么MySQL不能用binlog来进行崩溃恢复?
在MySQL中一条SQL语句的执行流程
InnoDB和MyISAM的区别
MySQL架构与历史
概述。
和其他数据库系统相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥好的作用,但同时也会带来一点选择上的困难。MySQL并不完美,却足够灵活,能够适应高要求的环境,例如Web类应用。同时,MySQL既可以嵌入到应用程序中,也可以支持数据仓库、内容索引和部署软件、高可用的冗余系统、在线事务处理系统(OLTP)等各种应用类型。
为了充分发徽MySQL的性能并顺利地使用,就必须理解其设计。MySQL的灵活性体现在很多方面。例如,你可以通过配置使它在不同的硬件上都运行得很好,也可以支持多种不同得数据类型。但是,MySQL最重要、最与众不同的特性是它的存储引擎架构,这种架构的设计将查询处理(Query Processing)及其他系统任务(Server Task)和数据的存储/提取相分离。这种处理和存储分离的设计可以在使用时根据性能、特性,以及其他需求啦i选择数据存储的方式。
和其他数据库系统相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥好的作用,但同时也会带来一点选择上的困难。MySQL并不完美,却足够灵活,能够适应高要求的环境,例如Web类应用。同时,MySQL既可以嵌入到应用程序中,也可以支持数据仓库、内容索引和部署软件、高可用的冗余系统、在线事务处理系统(OLTP)等各种应用类型。
为了充分发徽MySQL的性能并顺利地使用,就必须理解其设计。MySQL的灵活性体现在很多方面。例如,你可以通过配置使它在不同的硬件上都运行得很好,也可以支持多种不同得数据类型。但是,MySQL最重要、最与众不同的特性是它的存储引擎架构,这种架构的设计将查询处理(Query Processing)及其他系统任务(Server Task)和数据的存储/提取相分离。这种处理和存储分离的设计可以在使用时根据性能、特性,以及其他需求啦i选择数据存储的方式。
MySQL逻辑架构。
如果能在头脑中构建出一幅MySQL各组件之间如何协同工作的架构图,就会有助于深入理解MySQL服务器,如图所示,展示了MySQL逻辑架构图。
最上层的服务并不是MySQL所独有,大多数基于网络的客户端/服务器的工具或者服务器都有类似的架构。比如连接处理、授权认证、安全等等。
第二层架构是MySQL比较有意思的部分.大多数MySQL的核心服务功能都在这一层,包括查询解析、分析、优化、缓存以及所有的内置函数(例如,日期、
时间、数学和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。
第三层包含了存储引擎。存储引擎负责MySQL中数据的存储和提取。和GNU/Linux下的各种文件系统一样,每个存储引擎都有它的优势和劣势。服务器通过API与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。存储引擎API包含几十个底层函数,用于执行诸如"开始一个事务"或者"根据主键提取一行记录"等操作。但存储引擎不会去解析SQL(InnoDB是一个例外,它会解析外键定义,因为MySQL服务器本身没有实现该功能),不同存储引擎之间也不会相互通信,而是简单地响应上层服务器的请求。
如果能在头脑中构建出一幅MySQL各组件之间如何协同工作的架构图,就会有助于深入理解MySQL服务器,如图所示,展示了MySQL逻辑架构图。
最上层的服务并不是MySQL所独有,大多数基于网络的客户端/服务器的工具或者服务器都有类似的架构。比如连接处理、授权认证、安全等等。
第二层架构是MySQL比较有意思的部分.大多数MySQL的核心服务功能都在这一层,包括查询解析、分析、优化、缓存以及所有的内置函数(例如,日期、
时间、数学和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。
第三层包含了存储引擎。存储引擎负责MySQL中数据的存储和提取。和GNU/Linux下的各种文件系统一样,每个存储引擎都有它的优势和劣势。服务器通过API与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。存储引擎API包含几十个底层函数,用于执行诸如"开始一个事务"或者"根据主键提取一行记录"等操作。但存储引擎不会去解析SQL(InnoDB是一个例外,它会解析外键定义,因为MySQL服务器本身没有实现该功能),不同存储引擎之间也不会相互通信,而是简单地响应上层服务器的请求。
连接管理与安全性。
每个客户端连接都会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或者CPU中运行,服务器会负责缓存线程,因此不需要为每一个新键的连接创建或者销毁线程(MySQL5.5版本后提供了一个API,支持线程池(Thread-Pooling)插件,可以使用池中少量的线程来服务大量的连接)。
当客户端(应用)连接到MySQL服务器时,服务器需要对其进行认证。认证基于用户名、原始主机信息和密码。如果使用了安全套接字(SSL)的方式连接,还可以使用X.509证书认证。一旦客户端连接成功,服务器会继续验证该客户端是否具有执行某个特定查询的权限(例如,是否允许客户端对world数据库的Country表执行SELECT语句)
每个客户端连接都会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或者CPU中运行,服务器会负责缓存线程,因此不需要为每一个新键的连接创建或者销毁线程(MySQL5.5版本后提供了一个API,支持线程池(Thread-Pooling)插件,可以使用池中少量的线程来服务大量的连接)。
当客户端(应用)连接到MySQL服务器时,服务器需要对其进行认证。认证基于用户名、原始主机信息和密码。如果使用了安全套接字(SSL)的方式连接,还可以使用X.509证书认证。一旦客户端连接成功,服务器会继续验证该客户端是否具有执行某个特定查询的权限(例如,是否允许客户端对world数据库的Country表执行SELECT语句)
优化与执行。
MySQL会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询、决定表的读取顺序,以及选择合适的索引等。用户可以通过特殊的关键字提示(hint)优化器,影响它的决策过程。也可以请求优化器解释(explain)优化过程的各个因素,使用户可以知道服务器是如何进行优化决策的,并提供一个参考基准,便于用户重构查询和schema、修改相关配置,使应用尽可能高效运行。
优化器并不关心表使用的是什么存储引擎,但存储引擎对于优化查询是有影响的。优化器会请求存储引擎提供容量或某个具体操作的开销信息,以及表数据的统计信息等。例如,某些存储引擎的某种索引,可能对一些特定的查询有优化。
对于SELECT语句,在解析查询之前,服务器会先检查查询缓存(Query Cache),如果能够在其中找到对应的查询,服务器就不必再执行查询解析、优化和执行的整个过程,而是直接返回查询缓存中的结果集
MySQL会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询、决定表的读取顺序,以及选择合适的索引等。用户可以通过特殊的关键字提示(hint)优化器,影响它的决策过程。也可以请求优化器解释(explain)优化过程的各个因素,使用户可以知道服务器是如何进行优化决策的,并提供一个参考基准,便于用户重构查询和schema、修改相关配置,使应用尽可能高效运行。
优化器并不关心表使用的是什么存储引擎,但存储引擎对于优化查询是有影响的。优化器会请求存储引擎提供容量或某个具体操作的开销信息,以及表数据的统计信息等。例如,某些存储引擎的某种索引,可能对一些特定的查询有优化。
对于SELECT语句,在解析查询之前,服务器会先检查查询缓存(Query Cache),如果能够在其中找到对应的查询,服务器就不必再执行查询解析、优化和执行的整个过程,而是直接返回查询缓存中的结果集
并发控制。
无论何时,只要有多个查询需要在同一时刻修改数据,都会产生并发控制的问题。这里我们讨论的只是MySQL的两个层面的并发控制:服务器层与存储引擎层。并发控制是一个内容庞大的话题,我们只是简要地分析MySQL如何控制并发读写。
以Unix系统的email box为例,典型的mbox文件格式是非常简单的。一个mbox邮箱中的所有邮件都串行在一起,彼此首尾相连。这种格式对于读取和分析邮件信息非常友好,同时投递邮件也很容易,只要在文件末尾附加新的邮件内容即可。但如果两个进程在同一时刻对同一个邮箱投递邮件,会发生什么情况?显然,邮箱的数据会被破坏,两封邮件的内容会交叉地附加在邮箱文件的末尾。设计良好的邮箱邮递系统会通过锁(lock)来防止数据损坏。如果客户视图投递邮件,而邮箱已经被其他客户端锁住,那就必须等待,知道锁释放才能进行投递。
这种锁的方案在实际应用环境中虽然工作良好,但并不支持并发处理。因为在任意一个时刻,只有一个进程可以修改邮箱的数据,这在大容量的邮箱系统中是个问题
无论何时,只要有多个查询需要在同一时刻修改数据,都会产生并发控制的问题。这里我们讨论的只是MySQL的两个层面的并发控制:服务器层与存储引擎层。并发控制是一个内容庞大的话题,我们只是简要地分析MySQL如何控制并发读写。
以Unix系统的email box为例,典型的mbox文件格式是非常简单的。一个mbox邮箱中的所有邮件都串行在一起,彼此首尾相连。这种格式对于读取和分析邮件信息非常友好,同时投递邮件也很容易,只要在文件末尾附加新的邮件内容即可。但如果两个进程在同一时刻对同一个邮箱投递邮件,会发生什么情况?显然,邮箱的数据会被破坏,两封邮件的内容会交叉地附加在邮箱文件的末尾。设计良好的邮箱邮递系统会通过锁(lock)来防止数据损坏。如果客户视图投递邮件,而邮箱已经被其他客户端锁住,那就必须等待,知道锁释放才能进行投递。
这种锁的方案在实际应用环境中虽然工作良好,但并不支持并发处理。因为在任意一个时刻,只有一个进程可以修改邮箱的数据,这在大容量的邮箱系统中是个问题
读写锁。
从邮箱中读取数据没有这样的麻烦,即使同一个hi上课多个用户并发读取也不会有什么问题,因为读取不会修改数据,所以不会出错。但如果某个客户正在读取邮箱,同时另外一个用户视图删除编号为25的邮件,会产生什么结果?结论是不确定,读的客户可能会报错退出,也可能读取到不一致的邮箱数据。所以,为安全起见,即使是读取邮箱也需要特别注意。
如果把上述的邮箱当称数据库的一张表,把邮件当成表中的一行记录,就很容易看出,同样的问题依然存在。从很多方面来说,邮箱就是一张简单的数据库表。修改数据库中的记录,和删除或者修改邮箱中的邮件信息,十分类似。
解决这类经典问题的方法就是并发控制,其实非常简单。在处理并发读或者写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁(shared lock)和排他锁(exclusive lock),也叫读锁(read lock)和写锁(write lock),
这里先不讨论锁的具体实现,描述一下锁的概念如下:读锁是共享的,或者说是相互不阻塞的。多个客户端在同一时刻可以同时读取同一个资源,而互不干扰。写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,这是出于安全策略的考虑,只有这样,才能确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源。
在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时,MySQL会通过锁定防止其他用户读取同一数据。大多数时候,MySQL锁的内部管理是透明的
从邮箱中读取数据没有这样的麻烦,即使同一个hi上课多个用户并发读取也不会有什么问题,因为读取不会修改数据,所以不会出错。但如果某个客户正在读取邮箱,同时另外一个用户视图删除编号为25的邮件,会产生什么结果?结论是不确定,读的客户可能会报错退出,也可能读取到不一致的邮箱数据。所以,为安全起见,即使是读取邮箱也需要特别注意。
如果把上述的邮箱当称数据库的一张表,把邮件当成表中的一行记录,就很容易看出,同样的问题依然存在。从很多方面来说,邮箱就是一张简单的数据库表。修改数据库中的记录,和删除或者修改邮箱中的邮件信息,十分类似。
解决这类经典问题的方法就是并发控制,其实非常简单。在处理并发读或者写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁(shared lock)和排他锁(exclusive lock),也叫读锁(read lock)和写锁(write lock),
这里先不讨论锁的具体实现,描述一下锁的概念如下:读锁是共享的,或者说是相互不阻塞的。多个客户端在同一时刻可以同时读取同一个资源,而互不干扰。写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,这是出于安全策略的考虑,只有这样,才能确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源。
在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时,MySQL会通过锁定防止其他用户读取同一数据。大多数时候,MySQL锁的内部管理是透明的
锁粒度。
一种提高共享资源并发性的方式就是让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是所有的资源。更理想的方式是,只对会修改的数据片进行精确的锁定。任何时候,在给定的资源山,锁定的数据量越少,则系统的并发程度越高,只要相互间不发生冲突即可。
问题是加锁也需要消耗资源,锁的各种操作,包括获得锁、检查锁是否已经解除、释放锁等,都会增加系统的开销。如果系统花费大量的时间来管理锁,而不是存取数据,那么系统的性能可能会因此受到影响。
所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡当然也会影响到性能。大多数商业数据库系统没有提供更多的选择,一般是在表上施加行级锁(row-level lock),并以各种复杂的方式来实现,以便在锁比较多的秦广下尽可能地提供更好的性能。
而MySQL则提供了多种选择。每种MySQL存储引擎都可以实现自己的锁策略和锁粒度。在存储引擎的设计中,锁管理是个非常重要的决定。将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时却会失去对另外一些应用场景的良好支持。好在MySQL支持多个存储引擎的架构,所以不需要单一的通用解决方案
一种提高共享资源并发性的方式就是让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是所有的资源。更理想的方式是,只对会修改的数据片进行精确的锁定。任何时候,在给定的资源山,锁定的数据量越少,则系统的并发程度越高,只要相互间不发生冲突即可。
问题是加锁也需要消耗资源,锁的各种操作,包括获得锁、检查锁是否已经解除、释放锁等,都会增加系统的开销。如果系统花费大量的时间来管理锁,而不是存取数据,那么系统的性能可能会因此受到影响。
所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡当然也会影响到性能。大多数商业数据库系统没有提供更多的选择,一般是在表上施加行级锁(row-level lock),并以各种复杂的方式来实现,以便在锁比较多的秦广下尽可能地提供更好的性能。
而MySQL则提供了多种选择。每种MySQL存储引擎都可以实现自己的锁策略和锁粒度。在存储引擎的设计中,锁管理是个非常重要的决定。将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时却会失去对另外一些应用场景的良好支持。好在MySQL支持多个存储引擎的架构,所以不需要单一的通用解决方案
表锁(table lock).
表锁是MySQL种最基本的锁策略,并且是开销最小的策略。表锁非常类似前文描述的邮箱加锁机制:它会锁定整张表。一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的。
在特定的场景种,表锁也可能由良好的性能。例如,READ LOCAL表锁支持某些类型的并发写操作。另外,写锁也比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面(写锁可以插入到锁队列种读锁的前面,反之读锁则不能插入到写锁的前面)。
尽管存储引擎可以管理自己的锁,MySQL本身还是会使用各种有效的表锁来实现不同的目的。例如,服务器会为诸如ALTER TABLE之类的语句使用表锁,而忽略存储引擎的锁机制。
表锁是MySQL种最基本的锁策略,并且是开销最小的策略。表锁非常类似前文描述的邮箱加锁机制:它会锁定整张表。一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的。
在特定的场景种,表锁也可能由良好的性能。例如,READ LOCAL表锁支持某些类型的并发写操作。另外,写锁也比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面(写锁可以插入到锁队列种读锁的前面,反之读锁则不能插入到写锁的前面)。
尽管存储引擎可以管理自己的锁,MySQL本身还是会使用各种有效的表锁来实现不同的目的。例如,服务器会为诸如ALTER TABLE之类的语句使用表锁,而忽略存储引擎的锁机制。
行级锁(row lock)。
行级锁可以最大程度地支持并发处理(同时也带来了最大的锁开销)。众所周知,在InnoDB和XtraDB,以及其他一些存储引擎种实现了行级锁。行级锁只在存储引擎层实现,而MySQL服务器层没有实现。服务器层完全不了解存储引擎中的锁实现
行级锁可以最大程度地支持并发处理(同时也带来了最大的锁开销)。众所周知,在InnoDB和XtraDB,以及其他一些存储引擎种实现了行级锁。行级锁只在存储引擎层实现,而MySQL服务器层没有实现。服务器层完全不了解存储引擎中的锁实现
事务。
在理解事务的概念之前,解除数据库系统的其他高级特性还言之过早。事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎还能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或者其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。
银行应用是解释事务必要性的一个经典例子。假设一个银行的数据库有两张表,支票表(checking)和储蓄表(savings)。现在要从用户Jane的支票账户转移200美元到她的储蓄账户,那么需要至少三个步骤
1.检查支票账户的余额高于200美元
2.从支票账户余额中减去200美元
3.在储蓄账户余额中增加200美元。
上述三个步骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。
可以用START TRANSACTION 语句开始一个事务,然后要么使用COMMIT提交事务将修改的数据持久保留,要么使用ROLLBACK撤销所有的修改。事务SQL的样本如下:
```c
1.START TRANSACTION;
2.SELECT balance FROM checking WHERE customer_id = 10233276;
3.UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
4.UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
5.COMMIT
```
单纯的事务概念并不是故事的全部,试想一下,如果执行到第四条语句时服务器崩溃了会发生什么?天知道,用户可能会损失200美元。再假如,在执行到第三条语句和第四条语句之间,另外一个进程要删除支票账户的所有余额,那么结果可能就是银行在不知道这个逻辑的情况下白白给了Jane200美元。
除非系统通过严格的ACID测试,否则空谈事务的概念是不够的。ACID表示原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Duration).一个运行良好的事务处理系统,必须具备这些标准特征。
原子性(Actomicity).
一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性,
一致性(Consistency).
数据库总是从一个一致性的状态转换到另一个一致性的状态。在前面的例子中,一致性确保了,即使在执行第三、第四语句之间时系统崩溃,支票账户中也不会损失200美元,因为事务最终没有提交,所以事务中所做的修改也不会保存到数据库中
隔离性(Isolation).
通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。在前面的例子中,当执行完第三条语句、第四条语句还没开始时,此时有另外一个账户汇总程序开始运行,则其看到的支票账户的余额并没有减去200美元。这和事务之间的隔离级别(Isolation level)有关系,你会发现为什么我们要说"通常来说"是不可见的
持久性(Durability).
一旦事务提交,则其所做的修改就会永久保存到数据库中,此时即使系统崩溃,修改的数据也不会丢失。持久性是个有点模糊的概念,因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。而且不可能有能做到100%的持久性保证的策略(如果数据库本身就能做到真正的持久性,那么备份又怎么能持久性呢?)。
事务的ACID特性可以确保银行不会弄丢你的钱。而在应用逻辑中,要实现这一点非常难,甚至可以说是不可能完成的任务。一个兼容ACID的数据库系统,需要做很多复杂但可能用户并没有觉察到的工作,才能确保ACID的实现。
就像锁粒度的升级会增加系统开销一样,这种事务处理过程中额外的安全性,也会需要数据库系统做更多的额外工作。一个实现了ACID的数据库,相比没有实现ACID的数据库,通常会需要更强的CPU处理能力、更大的内存和更多的磁盘空间。这也正是MySQL的存储引擎架构可以发挥优势的地方。用户可以根据业务是否需要事务处理,来选择合适的存储引擎。对于一些不需要事务的查询类应用,选择一个非事务型的存储以前宁,可以获得更高的性能。即使存储引擎不支持事务,也可以通过LOCK TABLES语句为应用提供一定程度的保护。
在理解事务的概念之前,解除数据库系统的其他高级特性还言之过早。事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎还能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或者其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。
银行应用是解释事务必要性的一个经典例子。假设一个银行的数据库有两张表,支票表(checking)和储蓄表(savings)。现在要从用户Jane的支票账户转移200美元到她的储蓄账户,那么需要至少三个步骤
1.检查支票账户的余额高于200美元
2.从支票账户余额中减去200美元
3.在储蓄账户余额中增加200美元。
上述三个步骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。
可以用START TRANSACTION 语句开始一个事务,然后要么使用COMMIT提交事务将修改的数据持久保留,要么使用ROLLBACK撤销所有的修改。事务SQL的样本如下:
```c
1.START TRANSACTION;
2.SELECT balance FROM checking WHERE customer_id = 10233276;
3.UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
4.UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
5.COMMIT
```
单纯的事务概念并不是故事的全部,试想一下,如果执行到第四条语句时服务器崩溃了会发生什么?天知道,用户可能会损失200美元。再假如,在执行到第三条语句和第四条语句之间,另外一个进程要删除支票账户的所有余额,那么结果可能就是银行在不知道这个逻辑的情况下白白给了Jane200美元。
除非系统通过严格的ACID测试,否则空谈事务的概念是不够的。ACID表示原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Duration).一个运行良好的事务处理系统,必须具备这些标准特征。
原子性(Actomicity).
一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性,
一致性(Consistency).
数据库总是从一个一致性的状态转换到另一个一致性的状态。在前面的例子中,一致性确保了,即使在执行第三、第四语句之间时系统崩溃,支票账户中也不会损失200美元,因为事务最终没有提交,所以事务中所做的修改也不会保存到数据库中
隔离性(Isolation).
通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。在前面的例子中,当执行完第三条语句、第四条语句还没开始时,此时有另外一个账户汇总程序开始运行,则其看到的支票账户的余额并没有减去200美元。这和事务之间的隔离级别(Isolation level)有关系,你会发现为什么我们要说"通常来说"是不可见的
持久性(Durability).
一旦事务提交,则其所做的修改就会永久保存到数据库中,此时即使系统崩溃,修改的数据也不会丢失。持久性是个有点模糊的概念,因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。而且不可能有能做到100%的持久性保证的策略(如果数据库本身就能做到真正的持久性,那么备份又怎么能持久性呢?)。
事务的ACID特性可以确保银行不会弄丢你的钱。而在应用逻辑中,要实现这一点非常难,甚至可以说是不可能完成的任务。一个兼容ACID的数据库系统,需要做很多复杂但可能用户并没有觉察到的工作,才能确保ACID的实现。
就像锁粒度的升级会增加系统开销一样,这种事务处理过程中额外的安全性,也会需要数据库系统做更多的额外工作。一个实现了ACID的数据库,相比没有实现ACID的数据库,通常会需要更强的CPU处理能力、更大的内存和更多的磁盘空间。这也正是MySQL的存储引擎架构可以发挥优势的地方。用户可以根据业务是否需要事务处理,来选择合适的存储引擎。对于一些不需要事务的查询类应用,选择一个非事务型的存储以前宁,可以获得更高的性能。即使存储引擎不支持事务,也可以通过LOCK TABLES语句为应用提供一定程度的保护。
隔离级别。
隔离性其实比想象的要复杂。在SQL标准中定义了四种隔离几倍,每一种隔离级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低。
每种存储引擎实现的隔离级别不尽相同。下面简单地介绍一下四种隔离级别。
1.READ UNCOMMITED(未提交读)
在READ UNCOMMITTED级别,事务中地修改,即使没有提交,对其他事务也都是可见地。事务可以读取未提交的数据,这也被称为脏读(Dirty Read).这个级别会导致很多问题,从性能上来说,READ UNCOMMITTED不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真的非常有必要,在实际应用中一般很少使用
2.READ COMMITTED(提交读)
大多数数据库系统的默认隔离级别都是READ COMMITTED(但MySQL 不是)。READ COMMITTED满足前面提到的隔离性的简单定义:一个事务开始时,只能"看见"已经提交的事务所做的修改。换句话说,一个事务从开始知道提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。
3.REPETABLE READ(可重复读)
REPETABLE READ解决了脏读的问题,该级别保证了同一个事务中多次读取同样的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另一个幻读(Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决了幻读的问题。可重复读是MySQL的默认事务隔离级别
4.SERIALIZABLE(可串行化)
SERIALIZABLE是最高的隔离几倍。她通过强制事务串行执行,避免了前面说的幻读问题。简单来说,SERIALIZABLE会在读取的每一行数据上都加上所,所以可能导致大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别
隔离性其实比想象的要复杂。在SQL标准中定义了四种隔离几倍,每一种隔离级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低。
每种存储引擎实现的隔离级别不尽相同。下面简单地介绍一下四种隔离级别。
1.READ UNCOMMITED(未提交读)
在READ UNCOMMITTED级别,事务中地修改,即使没有提交,对其他事务也都是可见地。事务可以读取未提交的数据,这也被称为脏读(Dirty Read).这个级别会导致很多问题,从性能上来说,READ UNCOMMITTED不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真的非常有必要,在实际应用中一般很少使用
2.READ COMMITTED(提交读)
大多数数据库系统的默认隔离级别都是READ COMMITTED(但MySQL 不是)。READ COMMITTED满足前面提到的隔离性的简单定义:一个事务开始时,只能"看见"已经提交的事务所做的修改。换句话说,一个事务从开始知道提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。
3.REPETABLE READ(可重复读)
REPETABLE READ解决了脏读的问题,该级别保证了同一个事务中多次读取同样的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另一个幻读(Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决了幻读的问题。可重复读是MySQL的默认事务隔离级别
4.SERIALIZABLE(可串行化)
SERIALIZABLE是最高的隔离几倍。她通过强制事务串行执行,避免了前面说的幻读问题。简单来说,SERIALIZABLE会在读取的每一行数据上都加上所,所以可能导致大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别
死锁。
死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定一个资源时,也会产生死锁。例如,设想下面两个事务同时处理StockPrice表:
事务1:
```sql
START TRANSACTION;
UPDATE StockPrice SET close =45.50 WHERE stock_id = 4 and date = '2002-05-01';
UPDATE StockPrice SET close = 19.80 WHERE stock_id = 3 and date = '2002-05-02';
COMMIT;
```
事务2:
```sql
START TRANSACTION;
UPDATE StockPrice SET high = 20.12 WHERE stock_id = 3 and date = '2002-05-02';
UPDATE StockPrice SET high = 47.20 WHERE stock_id = 4 and date = '2002-05-01';
COMMIT;
```
如果凑巧,两个事务都执行了第一条UPDATE语句,更新了一行数据,同时也锁定了该行数据,接着每个事务都尝试去执行第二条UPDATE语句,却发现该行已经被对方锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,则现如死循环。除非有外部因素介入才可能解除死锁。
为了解决这种问题,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,比如InnoDB存储引擎,越能检测到死锁的循环一来,并立即返回一个错误。这种解决方式很有效,否则死锁会导致出现非常慢的查询。还有一种解决方式,就是当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。InnoDB目前处理死锁的方法时,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。
锁的行为和顺序是和存储引擎相关的。以同样的顺序执行语句,有些存储引擎会产生死锁,有些则不会。死锁的产生有双重原因:有些是因为真正的数据冲突,这种情况通常很难避免,但有些完全是由于存储引擎的实现方式导致的。死锁放生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。对于事务型的系统,这是无法避免的,所以应用程序在设计时必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。
死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定一个资源时,也会产生死锁。例如,设想下面两个事务同时处理StockPrice表:
事务1:
```sql
START TRANSACTION;
UPDATE StockPrice SET close =45.50 WHERE stock_id = 4 and date = '2002-05-01';
UPDATE StockPrice SET close = 19.80 WHERE stock_id = 3 and date = '2002-05-02';
COMMIT;
```
事务2:
```sql
START TRANSACTION;
UPDATE StockPrice SET high = 20.12 WHERE stock_id = 3 and date = '2002-05-02';
UPDATE StockPrice SET high = 47.20 WHERE stock_id = 4 and date = '2002-05-01';
COMMIT;
```
如果凑巧,两个事务都执行了第一条UPDATE语句,更新了一行数据,同时也锁定了该行数据,接着每个事务都尝试去执行第二条UPDATE语句,却发现该行已经被对方锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,则现如死循环。除非有外部因素介入才可能解除死锁。
为了解决这种问题,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,比如InnoDB存储引擎,越能检测到死锁的循环一来,并立即返回一个错误。这种解决方式很有效,否则死锁会导致出现非常慢的查询。还有一种解决方式,就是当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。InnoDB目前处理死锁的方法时,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。
锁的行为和顺序是和存储引擎相关的。以同样的顺序执行语句,有些存储引擎会产生死锁,有些则不会。死锁的产生有双重原因:有些是因为真正的数据冲突,这种情况通常很难避免,但有些完全是由于存储引擎的实现方式导致的。死锁放生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。对于事务型的系统,这是无法避免的,所以应用程序在设计时必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。
事务日志。
事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改内存拷贝,再把修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久化到磁盘。事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以次啊用事务日志的方式相对来说要快得多。事务日志持久化以后,内存中被修改得数据在后台可以慢慢地刷回到磁盘。目前大多数存储引擎都是这样实现地,我们通常称之为预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。
如果数据地修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动回复这部分修改地数据。具体的回复方式则视存储引擎而定。
事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改内存拷贝,再把修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久化到磁盘。事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以次啊用事务日志的方式相对来说要快得多。事务日志持久化以后,内存中被修改得数据在后台可以慢慢地刷回到磁盘。目前大多数存储引擎都是这样实现地,我们通常称之为预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。
如果数据地修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动回复这部分修改地数据。具体的回复方式则视存储引擎而定。
MySQL中的事务。
MySQL提供了两种事务型的存储引擎:InnoDB和NDB Cluster。另外还有一些第三方存储引擎也支持事务,比较知名的包括XtraDB和PBXT.
1.自动提交(AUTOCOMMIT):
MySQL默认采用自动提交(AUTOCOMMIT)模式。也就是说,如果不是显式地开始一个事务,则每个查询都被当作一个事务执行提交操作。在当前连接中,可以通过设置AUTOCOMMIT变量来启用或者禁用自动提交模式:
```c
mysql > SHOW VARIABLES LIKE 'AUTOCOMMIT';
Variable_name:autocommit
Value: ON
mysql> SET AUTOCOMMIT = 1;
```
1或者ON表示启用,0或者OFF表示禁用。当AUTOCOMMIT=0时,所有地查询都是在一个事务中,直到显式地执行COMMIT提交或者ROLLBACK回滚,该事务结束,同时又开始了另一个新事务。修改AUTOCOMMIT对非事务型的表,比如MyISAM或者内存表,不会又任何影响。对这类表来说,没有COMMIT或者ROLLBACK的概念,也可以说是相当于一直处于AUTOCOMMIT启用的模式。
另外还有一些命令,在执行之前回强制执行COMMIT提交当前的活动事务。典型的例子,在数据定义语言(DDL)中,如果是回导致大量数据改变的操作,比如ALTER TABLE,就是如此。另外还有LOCK TABLES等其他语句也会导致同样的结果。如果有需要,请检查对应版本的官方文档来确认所有可能导致自动提交的语句列表。MySQL可以通过执行SET TRANSACTION ISOLATION LEVEL命令来设置隔离级别。新的隔离级别会在下一次事务开始的时候生效。可以在配置文件中设置整个数据库的隔离界别,也可以只改变当前会话的隔离级别:
```sql
mysql>SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
```
MySQL能够识别所有的4个ANSI隔离级别,InnoDB尹千影也支持所有的隔离界别。
2.在事务中混合使用存储引擎:
MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。所以在同一个事务中,使用多种存储引擎是不可靠的。
如果在事务中混合使用了事务型和非事务型的表(例如InnoDB和MyISAM表),在正常提交的情况下不会有什么问题,但如果该事务需要回滚,非事务型表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以为每张表选择合适的存储引擎非常重要。
在非事务型的表上执行事务相关操作的时候,MySQL通常不会发出提醒,也不会报错。有时候只有回滚的时候才会发出一个警告:"某些非事务型的表上的变更不能被回滚"。但大多数情况下,对非事务型表的操作都不会有提示。
3.隐式和显式锁定:
InnoDB采用的是两阶段锁定协议(two-phase locking protocol)。在事务执行过程中,随时都可以执行锁定,锁只有执行COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB会根据隔离级别在需要的时候自动加锁。另外,InnoDB也支持通过特定的语句进行显式锁定,这些语句不属于SQL规范(这些锁定提示经常被滥用,实际上应当尽量避免使用)
3.1 SELECT ... LOCK IN SHARE MODE
3.2 SELECT ... FOR UPDATE
MySQL也支持LOCK TABLES和UNLOCK TABLES语句,这是在服务器层实现的,和存储引擎无关。它们有自己的用途,但并不能替代事务处理。如果应用需要用到事务,还是应该选择事务型存储引擎。经常可以发现,应用已经将表从MyISAM转换到InnoDB,但还是显式地使用LOCK TABLES语句。这不但没有必要,还会严重影响性能,实际上InnoDB的行级锁工作得更好。
LOCK TABLES和事务之间相互影响得化,情况会变得非常复杂,在某些MySQL版本中甚至会产生无法预料的结果。因此,建议除了事务中禁用AUTOCOMMIT,可以使用LOCK TABLES之外,其他任何时候都不要显式地执行LOCK TABLES,不管使用的是什么存储引擎。
MySQL提供了两种事务型的存储引擎:InnoDB和NDB Cluster。另外还有一些第三方存储引擎也支持事务,比较知名的包括XtraDB和PBXT.
1.自动提交(AUTOCOMMIT):
MySQL默认采用自动提交(AUTOCOMMIT)模式。也就是说,如果不是显式地开始一个事务,则每个查询都被当作一个事务执行提交操作。在当前连接中,可以通过设置AUTOCOMMIT变量来启用或者禁用自动提交模式:
```c
mysql > SHOW VARIABLES LIKE 'AUTOCOMMIT';
Variable_name:autocommit
Value: ON
mysql> SET AUTOCOMMIT = 1;
```
1或者ON表示启用,0或者OFF表示禁用。当AUTOCOMMIT=0时,所有地查询都是在一个事务中,直到显式地执行COMMIT提交或者ROLLBACK回滚,该事务结束,同时又开始了另一个新事务。修改AUTOCOMMIT对非事务型的表,比如MyISAM或者内存表,不会又任何影响。对这类表来说,没有COMMIT或者ROLLBACK的概念,也可以说是相当于一直处于AUTOCOMMIT启用的模式。
另外还有一些命令,在执行之前回强制执行COMMIT提交当前的活动事务。典型的例子,在数据定义语言(DDL)中,如果是回导致大量数据改变的操作,比如ALTER TABLE,就是如此。另外还有LOCK TABLES等其他语句也会导致同样的结果。如果有需要,请检查对应版本的官方文档来确认所有可能导致自动提交的语句列表。MySQL可以通过执行SET TRANSACTION ISOLATION LEVEL命令来设置隔离级别。新的隔离级别会在下一次事务开始的时候生效。可以在配置文件中设置整个数据库的隔离界别,也可以只改变当前会话的隔离级别:
```sql
mysql>SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
```
MySQL能够识别所有的4个ANSI隔离级别,InnoDB尹千影也支持所有的隔离界别。
2.在事务中混合使用存储引擎:
MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。所以在同一个事务中,使用多种存储引擎是不可靠的。
如果在事务中混合使用了事务型和非事务型的表(例如InnoDB和MyISAM表),在正常提交的情况下不会有什么问题,但如果该事务需要回滚,非事务型表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以为每张表选择合适的存储引擎非常重要。
在非事务型的表上执行事务相关操作的时候,MySQL通常不会发出提醒,也不会报错。有时候只有回滚的时候才会发出一个警告:"某些非事务型的表上的变更不能被回滚"。但大多数情况下,对非事务型表的操作都不会有提示。
3.隐式和显式锁定:
InnoDB采用的是两阶段锁定协议(two-phase locking protocol)。在事务执行过程中,随时都可以执行锁定,锁只有执行COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB会根据隔离级别在需要的时候自动加锁。另外,InnoDB也支持通过特定的语句进行显式锁定,这些语句不属于SQL规范(这些锁定提示经常被滥用,实际上应当尽量避免使用)
3.1 SELECT ... LOCK IN SHARE MODE
3.2 SELECT ... FOR UPDATE
MySQL也支持LOCK TABLES和UNLOCK TABLES语句,这是在服务器层实现的,和存储引擎无关。它们有自己的用途,但并不能替代事务处理。如果应用需要用到事务,还是应该选择事务型存储引擎。经常可以发现,应用已经将表从MyISAM转换到InnoDB,但还是显式地使用LOCK TABLES语句。这不但没有必要,还会严重影响性能,实际上InnoDB的行级锁工作得更好。
LOCK TABLES和事务之间相互影响得化,情况会变得非常复杂,在某些MySQL版本中甚至会产生无法预料的结果。因此,建议除了事务中禁用AUTOCOMMIT,可以使用LOCK TABLES之外,其他任何时候都不要显式地执行LOCK TABLES,不管使用的是什么存储引擎。
多版本并发控制。
MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC).不仅是MySQL,包括Oracle、PostgreSQL等其他数据库系统也都实现了MVCC,但各自实现的机制不尽相同,因为MVCC没有一个统一的实现标准.
可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。如果之前没有这方面的概念,这句话听起来就有点迷惑。熟悉了以后会发现,这句话其实还是很容易理解的。前面说到不同存储引擎的MVCC实现是不同的,典型的有乐观(optimistic)并发控制和悲观(pessimitsitc)并发控制。下面我们通过InnoDB的简化版行为来说明MVCC是如何工作的.
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number),没开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。
1.SELECT
InnoDB会根据一下两个条件检查每行记录:
a.InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本好小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
b.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除
只有符合上述两个条件的记录,才能返回作为查询结果
2.INSERT
InnoDB为新插入的每一行保存当前系统版本号作为行版本号
3.DELETE
InnoDB为删除的每一行保存当前系统版本号作为行删除标识
4.UPDATE
InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处使每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
MVCC只在REPETABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容(MVCC并没有正式的规范,所以各个存储引擎和数据库系统的实现都是各异的,没有人能说其他的实现方式是错误的)因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC).不仅是MySQL,包括Oracle、PostgreSQL等其他数据库系统也都实现了MVCC,但各自实现的机制不尽相同,因为MVCC没有一个统一的实现标准.
可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。如果之前没有这方面的概念,这句话听起来就有点迷惑。熟悉了以后会发现,这句话其实还是很容易理解的。前面说到不同存储引擎的MVCC实现是不同的,典型的有乐观(optimistic)并发控制和悲观(pessimitsitc)并发控制。下面我们通过InnoDB的简化版行为来说明MVCC是如何工作的.
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number),没开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。
1.SELECT
InnoDB会根据一下两个条件检查每行记录:
a.InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本好小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
b.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除
只有符合上述两个条件的记录,才能返回作为查询结果
2.INSERT
InnoDB为新插入的每一行保存当前系统版本号作为行版本号
3.DELETE
InnoDB为删除的每一行保存当前系统版本号作为行删除标识
4.UPDATE
InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处使每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
MVCC只在REPETABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容(MVCC并没有正式的规范,所以各个存储引擎和数据库系统的实现都是各异的,没有人能说其他的实现方式是错误的)因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
MySQL的存储引擎。
这里只是概要地描述MySQL的存储引擎,而不会设计太多细节。
在文件系统中,MySQL将每个数据库(也可以称之为schema)保存为数据目录下的一个子目录。创建表时,MySQL会在数据库子目录下创建一个和表同名的.frm文件保存的定义。例如创建一个名为MyTable的表,MySQL会在MyTable.frm文件中保存该表的定义。因为MySQL使用文件系统的目录和文件来保存数据库和表的定义,大小写敏感性和具体的平台密切相关。在Windows中,大小写是不敏感的;而在类Unix中则是敏感的。不同的存储引擎保存数据和索引的方式是不同的,但表的定义则是在MySQL服务层统一处理。
可以使用SHOW TABLE STATUS命令(在MySQL 5.0以后的版本中,也可以查询INFORMATION_SCHEMA中对应的表)显式表的相关信息。例如,对于mysql数据库中的user表:
```sql
mysql> SHOW TABLE STATUS LIKE 'user' \G
*************************** 1. row ***************************
Name: user
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 6
Avg_row_length: 2730
Data_length: 16384
Max_data_length: 0
Index_length: 0
Data_free: 0
Auto_increment: NULL
Create_time: 2023-08-15 23:04:22
Update_time: NULL
Check_time: NULL
Collation: utf8_bin
Checksum: NULL
Create_options: stats_persistent=0
Comment: Users and global privileges
1 row in set (0.13 sec)
```
输出的结果表明,这是一个InnoDB表。输出中还有其他信息以及统计信息。
这里只是概要地描述MySQL的存储引擎,而不会设计太多细节。
在文件系统中,MySQL将每个数据库(也可以称之为schema)保存为数据目录下的一个子目录。创建表时,MySQL会在数据库子目录下创建一个和表同名的.frm文件保存的定义。例如创建一个名为MyTable的表,MySQL会在MyTable.frm文件中保存该表的定义。因为MySQL使用文件系统的目录和文件来保存数据库和表的定义,大小写敏感性和具体的平台密切相关。在Windows中,大小写是不敏感的;而在类Unix中则是敏感的。不同的存储引擎保存数据和索引的方式是不同的,但表的定义则是在MySQL服务层统一处理。
可以使用SHOW TABLE STATUS命令(在MySQL 5.0以后的版本中,也可以查询INFORMATION_SCHEMA中对应的表)显式表的相关信息。例如,对于mysql数据库中的user表:
```sql
mysql> SHOW TABLE STATUS LIKE 'user' \G
*************************** 1. row ***************************
Name: user
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 6
Avg_row_length: 2730
Data_length: 16384
Max_data_length: 0
Index_length: 0
Data_free: 0
Auto_increment: NULL
Create_time: 2023-08-15 23:04:22
Update_time: NULL
Check_time: NULL
Collation: utf8_bin
Checksum: NULL
Create_options: stats_persistent=0
Comment: Users and global privileges
1 row in set (0.13 sec)
```
输出的结果表明,这是一个InnoDB表。输出中还有其他信息以及统计信息。
InnoDB三大特性:
1.AHI 自适应哈希
2.双写机制
3.Buffer Pool
1.AHI 自适应哈希
2.双写机制
3.Buffer Pool
字段含义:
Name:表名
Engine:表的存储引擎类型。在旧版本中,该列的名字叫Type,而不是Engine.
Row_format:行的格式。可选的值为Dynamic、Fixed或者Compressed.Dynamic的行长度是可变,一般包含可变长度的字段,如VARCHAR或BLOB.Fixed的行长度则是固定的,只包含固定长度的列,如CHAR和INTERGER.Compressed的行则只在压缩表中存在。
Rows:表中的行数。对于MyISAM和其他一些存储引擎,该值是精确的,但对于InnoDB该值是估计值。
Avg_row_length:平均每行包含的字节数
Data_length:表数据的大小(以字节为单位)
Max_data_length:表数据的最大容量,该值和存储引擎有关
Index_length:索引的大小(以字节为单位)
Data_free:对于MyISAM表,表示已分配但目前没有使用的空间。这部分空间包括了之前删除的行,以及后续可以被INSERT利用到的空间
Auto_increment:下一个AUTO_INCREMENT的值
Create_time:表的创建时间
Update_time:表数据的最后修改时间
Check_time:使用CHECK TABLE命令或者myisamchk工具最后一次检查表的时间
Collation:表的默认字符集和字符列排序规则
Checksum:如果启用,保存的是整个表 的实时校验和
Create_options:创建表时指定的其他选项
Comment:该列包含了一些其他的额外信息。对于MyISAM表,保存的时表在创建时带的注释。对于InnoDB表,则保存的时InnoDB表空间的剩余空间信息。如果是一个视图,则该列包含"VIEW"的文本字样
Name:表名
Engine:表的存储引擎类型。在旧版本中,该列的名字叫Type,而不是Engine.
Row_format:行的格式。可选的值为Dynamic、Fixed或者Compressed.Dynamic的行长度是可变,一般包含可变长度的字段,如VARCHAR或BLOB.Fixed的行长度则是固定的,只包含固定长度的列,如CHAR和INTERGER.Compressed的行则只在压缩表中存在。
Rows:表中的行数。对于MyISAM和其他一些存储引擎,该值是精确的,但对于InnoDB该值是估计值。
Avg_row_length:平均每行包含的字节数
Data_length:表数据的大小(以字节为单位)
Max_data_length:表数据的最大容量,该值和存储引擎有关
Index_length:索引的大小(以字节为单位)
Data_free:对于MyISAM表,表示已分配但目前没有使用的空间。这部分空间包括了之前删除的行,以及后续可以被INSERT利用到的空间
Auto_increment:下一个AUTO_INCREMENT的值
Create_time:表的创建时间
Update_time:表数据的最后修改时间
Check_time:使用CHECK TABLE命令或者myisamchk工具最后一次检查表的时间
Collation:表的默认字符集和字符列排序规则
Checksum:如果启用,保存的是整个表 的实时校验和
Create_options:创建表时指定的其他选项
Comment:该列包含了一些其他的额外信息。对于MyISAM表,保存的时表在创建时带的注释。对于InnoDB表,则保存的时InnoDB表空间的剩余空间信息。如果是一个视图,则该列包含"VIEW"的文本字样
InnoDB存储引擎.
InnoDB是MySQL的默认事务型引擎,也是最重要、使用最广泛的存储引擎。它被设计用来处理大量的短期(short-lived)事务,短期事务大部分情况是正常提交的,很少会被回滚。InnoDB的性能和自动崩溃回复特性,使得它在非事务型存储的需求中也很流行。除非有特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎,如果要学习存储引擎,InnoDB也是一个非常好的值得花最多的时间去深入学习的对象,收益肯定比将时间平均花在每个存储引擎的学习上要高得多。
InnoDB概览:InnoDB的数据存储在表空间(tablespace)中,表空间是InnoDB管理的一个黑盒子,由一系列的数据文件组成。在MySQL4.1以后的版本中,InnoDB可以将每个表的数据和索引存放在单独的文件中。InnoDB也可以使用裸设备作为表空间的存储节值,但现代的文件系统使得裸设备不再是必要的选择。
InnoDB采用MVCC来支持高并发,并且实现了四个标准的隔离级别。其默认级别是REPEATABLE READ(可重复读),并且通过间隙锁(next-key- locking)策略防止出现幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。
InnoDB表是基于聚簇索引建立的,InnoDB的索引结构和MySQL的其他存储引擎有很大的不同,聚簇索引对主键查询有很高的性能。不过它的二级索引(secondary index,非逐渐索引)中必须包含主键列,所以如果主键列很大的话,其他的索引都会很大。因此,若表上的索引较多的话,主键应当尽可能的小。InnoDB的存储格式是平台独立的,也就是说可以将数据和索引文件从Intel平台复制到PowerPC或者Sun SPARC平台。
InnoDB内部做了很多优化,包括从磁盘读取数据时采用的可预测性读,能够自动在内存中创建hash索引以加速读操作的自适应哈希索引(adaptive hash index),以及能够加速插入的插入缓冲区(insert buffer)等。
InnoDB的行为是非常复杂的,不容易理解,如果使用了InnoDBi年轻,强烈建议阅读官方手册中的"InnoDB事务模型和锁"一节。如果应用程序基于InnoDB构建,则实现了解一下InnoDB的MVCC架构带来的一些微妙和细节之处是非常有必要的。存储引擎要为所有用户甚至包括修改数据的用户维持一致性的视图,是非常复杂的工作。
作为事务型的存储引擎,InnoDB通过一些机制和工具支持真正的热备份,Oracle提供的MySQL Enterprise Backup、Percona提供的开源的XtraBackup都可以做到这一点。MySQL的其他存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取
InnoDB是MySQL的默认事务型引擎,也是最重要、使用最广泛的存储引擎。它被设计用来处理大量的短期(short-lived)事务,短期事务大部分情况是正常提交的,很少会被回滚。InnoDB的性能和自动崩溃回复特性,使得它在非事务型存储的需求中也很流行。除非有特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎,如果要学习存储引擎,InnoDB也是一个非常好的值得花最多的时间去深入学习的对象,收益肯定比将时间平均花在每个存储引擎的学习上要高得多。
InnoDB概览:InnoDB的数据存储在表空间(tablespace)中,表空间是InnoDB管理的一个黑盒子,由一系列的数据文件组成。在MySQL4.1以后的版本中,InnoDB可以将每个表的数据和索引存放在单独的文件中。InnoDB也可以使用裸设备作为表空间的存储节值,但现代的文件系统使得裸设备不再是必要的选择。
InnoDB采用MVCC来支持高并发,并且实现了四个标准的隔离级别。其默认级别是REPEATABLE READ(可重复读),并且通过间隙锁(next-key- locking)策略防止出现幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。
InnoDB表是基于聚簇索引建立的,InnoDB的索引结构和MySQL的其他存储引擎有很大的不同,聚簇索引对主键查询有很高的性能。不过它的二级索引(secondary index,非逐渐索引)中必须包含主键列,所以如果主键列很大的话,其他的索引都会很大。因此,若表上的索引较多的话,主键应当尽可能的小。InnoDB的存储格式是平台独立的,也就是说可以将数据和索引文件从Intel平台复制到PowerPC或者Sun SPARC平台。
InnoDB内部做了很多优化,包括从磁盘读取数据时采用的可预测性读,能够自动在内存中创建hash索引以加速读操作的自适应哈希索引(adaptive hash index),以及能够加速插入的插入缓冲区(insert buffer)等。
InnoDB的行为是非常复杂的,不容易理解,如果使用了InnoDBi年轻,强烈建议阅读官方手册中的"InnoDB事务模型和锁"一节。如果应用程序基于InnoDB构建,则实现了解一下InnoDB的MVCC架构带来的一些微妙和细节之处是非常有必要的。存储引擎要为所有用户甚至包括修改数据的用户维持一致性的视图,是非常复杂的工作。
作为事务型的存储引擎,InnoDB通过一些机制和工具支持真正的热备份,Oracle提供的MySQL Enterprise Backup、Percona提供的开源的XtraBackup都可以做到这一点。MySQL的其他存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取
MyISAM存储引擎。
在MySQL5.1及之前的版本,MyISAM是默认的存储引擎。MyISAM是默认的存储引擎。MyISAM提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但MyISAM不支持事务和行级锁,而且有一个毫无疑问的缺陷就是崩溃后无法安全回复。正是由于MyISAM引擎的缘故,即使MySQL支持事务已经很长时间了,在很多人的概念中MySQL还是非事务型的数据库。尽管MyISAM引擎不支持事务、不支持崩溃后的安全回复,但它绝不是一无是处的。对于只读的数据,或者表比较小、可以忍受修复(repair)操作,则依然可以继续使用MySQL(但请不要默认使用MyISAM,而是应当默认使用InnoDB)
在MySQL5.1及之前的版本,MyISAM是默认的存储引擎。MyISAM是默认的存储引擎。MyISAM提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但MyISAM不支持事务和行级锁,而且有一个毫无疑问的缺陷就是崩溃后无法安全回复。正是由于MyISAM引擎的缘故,即使MySQL支持事务已经很长时间了,在很多人的概念中MySQL还是非事务型的数据库。尽管MyISAM引擎不支持事务、不支持崩溃后的安全回复,但它绝不是一无是处的。对于只读的数据,或者表比较小、可以忍受修复(repair)操作,则依然可以继续使用MySQL(但请不要默认使用MyISAM,而是应当默认使用InnoDB)
存储。
MyISAM会将表存储在两个文件中:数据文件和索引文件,分别以.MYD和MYI为扩展名.MyISAM表可以包含动态或者静态(长度固定)行。MySQL会根据表的定义来决定采用何种行格式。MyISAM表可以存储的行记录数,一般受限于可用的磁盘空间,或者操作系统中单个文件的最大尺寸。
在MySQL5.0中,MyISAM表如果是变长行,则默认配置只能处理256TB的数据,因为指向数据记录的指针长度是6个字节。而在更早的版本中,指针长度默认是4字节,所以只能处理4GB的数据,而所有的MySQL版本都支持8字节的指针。要改变MyISAM表指针的长度(调高或者调低),可以通过修改表的MAX_ROWS和AVG_ROW_LENGTH选项的值来实现,两者相乘就是表可能达到的最大大小。修改这两个参数会导致重建整个表和表的所有所i你,这可能需要很长的时间才能完成。
MyISAM会将表存储在两个文件中:数据文件和索引文件,分别以.MYD和MYI为扩展名.MyISAM表可以包含动态或者静态(长度固定)行。MySQL会根据表的定义来决定采用何种行格式。MyISAM表可以存储的行记录数,一般受限于可用的磁盘空间,或者操作系统中单个文件的最大尺寸。
在MySQL5.0中,MyISAM表如果是变长行,则默认配置只能处理256TB的数据,因为指向数据记录的指针长度是6个字节。而在更早的版本中,指针长度默认是4字节,所以只能处理4GB的数据,而所有的MySQL版本都支持8字节的指针。要改变MyISAM表指针的长度(调高或者调低),可以通过修改表的MAX_ROWS和AVG_ROW_LENGTH选项的值来实现,两者相乘就是表可能达到的最大大小。修改这两个参数会导致重建整个表和表的所有所i你,这可能需要很长的时间才能完成。
MyISAM特性。
作为MySQL最早的存储引擎之一,MyISAM有一些已经开发出来很多年的特性,可以满足用户的实际需求。
1.加锁与并发.
MyISAM对整张表加锁,而不是针对行。读取时需要读到的所有表加共享锁,写入时则对表加排他锁。但是在表有读取查询的同时,也可以往表中插入新的记录(这被称为并发插入,CONCURRENT INSERT)
2.修复.
对于MyISAM表,MySQL可以手工或者自动执行检查和修复操作,但这里说的修复和事务回复以及崩溃恢复是不同的概念。执行表的修复可能导致一些数据丢失,而且修复操作是非常慢的。可以通过CHECK TABLE mytable检查表的错误,如果有错误可以通过执行REPAIR TABLE mytable进行修复。另外,如果MySQL服务器已经关闭,也可以通过myisamchk命令行工具进行检查和修复操作
3.索引特性.
对于MyISAM表,即使BLOB和TEXT等长字段,也可以基于前500个字符创建索引。MyISAM也支持全文索引,这是一种基于分词创建的索引,可以支持复杂的查询
4.延迟更新索引键(Delayed Key Write)。
创建MyISAM表的时候,如果指定了DELAY_KEY_WRITE选项,在每次修改执行完成时,不会立刻将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区(in-memory key buffer),只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入到磁盘。这种方式可以极大地提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。延迟更新索引键的特性,可以在全局设置,也可以为单个表设置。
作为MySQL最早的存储引擎之一,MyISAM有一些已经开发出来很多年的特性,可以满足用户的实际需求。
1.加锁与并发.
MyISAM对整张表加锁,而不是针对行。读取时需要读到的所有表加共享锁,写入时则对表加排他锁。但是在表有读取查询的同时,也可以往表中插入新的记录(这被称为并发插入,CONCURRENT INSERT)
2.修复.
对于MyISAM表,MySQL可以手工或者自动执行检查和修复操作,但这里说的修复和事务回复以及崩溃恢复是不同的概念。执行表的修复可能导致一些数据丢失,而且修复操作是非常慢的。可以通过CHECK TABLE mytable检查表的错误,如果有错误可以通过执行REPAIR TABLE mytable进行修复。另外,如果MySQL服务器已经关闭,也可以通过myisamchk命令行工具进行检查和修复操作
3.索引特性.
对于MyISAM表,即使BLOB和TEXT等长字段,也可以基于前500个字符创建索引。MyISAM也支持全文索引,这是一种基于分词创建的索引,可以支持复杂的查询
4.延迟更新索引键(Delayed Key Write)。
创建MyISAM表的时候,如果指定了DELAY_KEY_WRITE选项,在每次修改执行完成时,不会立刻将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区(in-memory key buffer),只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入到磁盘。这种方式可以极大地提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。延迟更新索引键的特性,可以在全局设置,也可以为单个表设置。
MyISAM压缩表。
如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表或许适合采用MyISAM压缩表。
可以使用myisampack对MyISAM表进行压缩(也叫打包pack).压缩表是不能进行修改的(除非先将表解除压缩,修改数据,然后再次压缩)。压缩表可以极大地减少磁盘空间占用,因此也可以减少磁盘I/O,从而提升查询性能。压缩表也支持索引,但索引也是只读的。以现在的硬件能力,对大多数应用场景,读取压缩表数据时的解压带来的开销影响并不大,而减少I/O带来的好处则要大得多。压缩时表中的记录时独立压缩的,所以读取单行的时候不需要去解压整个表(甚至也不解压行所在的整个页面)
如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表或许适合采用MyISAM压缩表。
可以使用myisampack对MyISAM表进行压缩(也叫打包pack).压缩表是不能进行修改的(除非先将表解除压缩,修改数据,然后再次压缩)。压缩表可以极大地减少磁盘空间占用,因此也可以减少磁盘I/O,从而提升查询性能。压缩表也支持索引,但索引也是只读的。以现在的硬件能力,对大多数应用场景,读取压缩表数据时的解压带来的开销影响并不大,而减少I/O带来的好处则要大得多。压缩时表中的记录时独立压缩的,所以读取单行的时候不需要去解压整个表(甚至也不解压行所在的整个页面)
MyISAM性能。
MyISAM引擎设计简单,数据以紧密格式存储,所以在某些场景下的性能很好。MyISAM有一些服务器级别的性能扩展限制,比如对索引键缓冲区(key cache)的Mutex锁,MariaDB基于段(segment)的索引键缓冲区机制来避免该问题。但MySIAM最典型的性能问题还是表锁的问题,如果你发现所有的查询都长期处于"Locked"状态,那么毫无疑问表锁就是罪魁祸首
MyISAM引擎设计简单,数据以紧密格式存储,所以在某些场景下的性能很好。MyISAM有一些服务器级别的性能扩展限制,比如对索引键缓冲区(key cache)的Mutex锁,MariaDB基于段(segment)的索引键缓冲区机制来避免该问题。但MySIAM最典型的性能问题还是表锁的问题,如果你发现所有的查询都长期处于"Locked"状态,那么毫无疑问表锁就是罪魁祸首
为什么MyISAM中的崩溃不能安全恢复?
回答:MyISAM是MySQL数据库管理系统种的一个存储引擎,他曾是最常会用的默认存储引擎之一,早期版本中。MyISAM存储引擎的特点是速度快,对读取操作进行了优化,但在某些方面不如其他存储引擎(如InnoDB)健壮。MyISAM的一个已知问题是在崩溃恢复方面的脆弱性。
MyISAM存储引擎的崩溃安全恢复问题主要源于一下几个方面:
1.没有事务支持:MyISAM存储引擎不支持事务,这意味着如果在数据写入过程中发生系统崩溃或断电, 那么未完成的数据操作将会丢失,从而导致数据不一致
2.表级锁定:MyISAM使用表级锁定,而不是行级锁定。在多用户环境中,这可能导致在一个长时间运行的写入操作过程中,如果发生崩溃,那么整个表可能会处于不一致状态
3.更新操作:MyISAM在执行更新操作时,通常会直接在原数据文件上进行修改。如果更新操作未完成而系统崩溃,那么可能会留下部分更新过的数据,这会导致数据完整性问题
4.缺乏自动崩溃恢复机制:与InnoDB等存储引擎不同,MyISAM没有自动的崩溃恢复机制。InnoDB使用事务日志(redo log)来保证即使在崩溃恢复后也能恢复到一致状态。MyISAM没有这样的机制,因此在崩溃后需要依赖MySQL的修复工具(如myisamchk)来恢复数据一致性,这个过程可能是手动执行的,并且有一定的风险
5.数据文件和索引文件分离:MyISAM将数据文件和索引文件分开存储,这可能会导致在特定情况下,比如突然的电源故障,数据文件和索引文件之间的同步出现问题
由于这些原因,MyISAM存储引擎子在需要高可靠性和自动崩溃恢复的场景中通常不是最佳选择。对于这些场景,InnoDB存储引擎提供了更好的支持,
因为它支持事务、行级锁定、并发控制和自动崩溃恢复。随着MySQL的发展,MyISAM逐渐被InnoDB取代,成为新的默认存储引擎
回答:MyISAM是MySQL数据库管理系统种的一个存储引擎,他曾是最常会用的默认存储引擎之一,早期版本中。MyISAM存储引擎的特点是速度快,对读取操作进行了优化,但在某些方面不如其他存储引擎(如InnoDB)健壮。MyISAM的一个已知问题是在崩溃恢复方面的脆弱性。
MyISAM存储引擎的崩溃安全恢复问题主要源于一下几个方面:
1.没有事务支持:MyISAM存储引擎不支持事务,这意味着如果在数据写入过程中发生系统崩溃或断电, 那么未完成的数据操作将会丢失,从而导致数据不一致
2.表级锁定:MyISAM使用表级锁定,而不是行级锁定。在多用户环境中,这可能导致在一个长时间运行的写入操作过程中,如果发生崩溃,那么整个表可能会处于不一致状态
3.更新操作:MyISAM在执行更新操作时,通常会直接在原数据文件上进行修改。如果更新操作未完成而系统崩溃,那么可能会留下部分更新过的数据,这会导致数据完整性问题
4.缺乏自动崩溃恢复机制:与InnoDB等存储引擎不同,MyISAM没有自动的崩溃恢复机制。InnoDB使用事务日志(redo log)来保证即使在崩溃恢复后也能恢复到一致状态。MyISAM没有这样的机制,因此在崩溃后需要依赖MySQL的修复工具(如myisamchk)来恢复数据一致性,这个过程可能是手动执行的,并且有一定的风险
5.数据文件和索引文件分离:MyISAM将数据文件和索引文件分开存储,这可能会导致在特定情况下,比如突然的电源故障,数据文件和索引文件之间的同步出现问题
由于这些原因,MyISAM存储引擎子在需要高可靠性和自动崩溃恢复的场景中通常不是最佳选择。对于这些场景,InnoDB存储引擎提供了更好的支持,
因为它支持事务、行级锁定、并发控制和自动崩溃恢复。随着MySQL的发展,MyISAM逐渐被InnoDB取代,成为新的默认存储引擎
为什么MyISAM的读取速度快呢?
MyISAM的读取速度快主要是由于它的设计理念和存储特性,这些特性使得MyISAM在读取操作上非常高效,以下是一些关键因素:
1.存储结构:MyISAM将数据存储在两个文件中,一个用于数据,另一个用户索引,这种分离使得MyISAM在读取数据时可以并行访问索引和数据文件,从而提高读取速度
2.表级锁定:MyISAM使用表级锁定,这意味着在读取操作时,整个表会被锁定,从而避免了复杂的行级锁定带来的开销。在大量读取操作的场景中,表级锁定可以减少锁的开销,提高读取效率
3.缓存机制:MyISAM有自己的缓存机制,可以缓存数据和索引。这意味着频繁访问的数据和索引可以保持在内存中,从而减少磁盘IO操作,提高读取速度
4.优化读取操作:MyISAM存储引擎对读取操作进行了优化,例如,它支持全表扫描和索引扫描,这些操作在MyISAM中非常高效
5.无事务支持:由于MyISAM不支持事务,它不需要维护事务日志,这减少了写入操作的开销,从而在读取操作上可以更加专注于性能优化
6.简单性:MyISAM的设计相对简单,没有InnoDB那样的复杂机制(如MVCC多版本并发控制),因为MyISAM不都不支持行级锁,也不支持事务,也就没有所谓的事务版本号之说,
这使得MyISAM在执行读取操作时可以更加直接和快速
尽管MyISAM在读取操作上表现出色,但由于不支持事务、行级锁定和崩溃恢复的局限性,它在需要高并发、数据完整性和可靠性的应用场景中并不是最佳选择。对于这些场景,
InnoDB通常是一个更好的选择,因为它提供了事务支持、行级锁定、并发控制和自动崩溃恢复等功能
MyISAM的读取速度快主要是由于它的设计理念和存储特性,这些特性使得MyISAM在读取操作上非常高效,以下是一些关键因素:
1.存储结构:MyISAM将数据存储在两个文件中,一个用于数据,另一个用户索引,这种分离使得MyISAM在读取数据时可以并行访问索引和数据文件,从而提高读取速度
2.表级锁定:MyISAM使用表级锁定,这意味着在读取操作时,整个表会被锁定,从而避免了复杂的行级锁定带来的开销。在大量读取操作的场景中,表级锁定可以减少锁的开销,提高读取效率
3.缓存机制:MyISAM有自己的缓存机制,可以缓存数据和索引。这意味着频繁访问的数据和索引可以保持在内存中,从而减少磁盘IO操作,提高读取速度
4.优化读取操作:MyISAM存储引擎对读取操作进行了优化,例如,它支持全表扫描和索引扫描,这些操作在MyISAM中非常高效
5.无事务支持:由于MyISAM不支持事务,它不需要维护事务日志,这减少了写入操作的开销,从而在读取操作上可以更加专注于性能优化
6.简单性:MyISAM的设计相对简单,没有InnoDB那样的复杂机制(如MVCC多版本并发控制),因为MyISAM不都不支持行级锁,也不支持事务,也就没有所谓的事务版本号之说,
这使得MyISAM在执行读取操作时可以更加直接和快速
尽管MyISAM在读取操作上表现出色,但由于不支持事务、行级锁定和崩溃恢复的局限性,它在需要高并发、数据完整性和可靠性的应用场景中并不是最佳选择。对于这些场景,
InnoDB通常是一个更好的选择,因为它提供了事务支持、行级锁定、并发控制和自动崩溃恢复等功能
MySQL内键的其他存储引擎。
MySQL还有一些有特殊用途的存储引擎。在新版本中,有些可能因为一些原因已经不再支持;另外还有些会继续支持,但是需要明确地启用后才能使用。
1.Archive引擎
Archive存储引擎只支持INSERT和SELECT操作,在MySQL5.1之前也不支持索引。Archive引擎会缓存所有的写并利用zlib对插入的行进行压缩,所以比MySQL表的磁盘I/O更少。但是每次SELECT查询都需要执行全表扫描。所以Archive表适合日志和数据采集类应用,这类应用做数据分析时往往需要全表扫描。或者在一些需要更快速的INSERT操作的场合下也可以使用.
Archive引擎支持行级锁和专用的缓冲区,所以可以实现高并发的插入。在一个查询开始直到返回表中存在的所有行数之前,Archive引擎会阻止其他的SELECT执行,以实现一致性读。另外,也实现了批量插入在完成之后对读操作是不可见的。这种机制模仿了事务和MVCC的一些特性,但Archive引擎不是一个事务型的引擎,而是一个针对告诉插入和压缩做了优化的简单引擎
2.Blackhole引擎。
Blackhole没有实现任何的存储机制,它会丢弃所有插入的数据,不做任何保存。但是服务器会记录Blackhole表的日志,所以可以用于复制数据到悲苦,或者只是简单地记录到日志。这种特殊的存储引擎可以在一些特殊的复制架构和日志审核时发徽作用。但这种应用方式会碰到很多问题,因此并不推荐
3.CSV引擎.
CSV引擎可以将普通的CSV文件(逗号分割值得文件)作为MySQL的表来处理,但这种表不支持索引。CSV引擎可以在数据库运行时拷入或者拷出文件。可以将Excel等电子表格软件中的数据存储为CSV文件,然后复制到MySQL数据目录下,就能在MySQL中打开使用。同样的,如果将数据写入到一个CSV引擎表,其他的外部程序也能立即从表的数据文件中读取CSV格式的数据,因此CSV引擎可以作为一种数据交换的机制,非常有用
4.Federated引擎。
Federated引擎是访问其他MySQL服务器的一个代理,它会创建一个到远程MySQL服务器的客户端连接,并将查询传输到远程服务器执行,然后提取或者发送需要的数据。最初设计该存储引擎是为了和企业级数据库如Microsoft SQL Server和Oracle的类似特性竞争的,可以说更多的是一种市场行为。尽管该引擎看起来提供了一种很好的跨服务器的灵活性,但也经常带来问题,因此默认是禁止的。MariaDB使用了它的一个后续改进版本,叫作FederatedX.
5.Memory引擎。
如果需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没关系,那么使用Memory表(以前也叫做HEAP表)是非常有用的。Memory表至少比MyISAM表要快一个数量级,因为所有的数据都存在内存中,不需要进行磁盘I/O,Memory表的结构在重启以后还会保留,但数据会丢失
Memory表在很多场景可以发挥好的作用:
5.1 用于查找(lookup)或者映射(mapping)表,例如将邮编和州名映射的表
5.2 用于缓存周期性聚合数据(periodically aggregated data)的结果
5.3 用于保存数据分析中产生的中间数据
Memory表支持Hash索引,因此查询操作非常快。虽然Memory表的速度非常快,但还是无法取代传统的基于磁盘的表。Memory表是表级锁,因此并发写入的性能较低。它不支持BLOCK或TEXT类型的列,并且每行的长度是固定的,所以即使指定了VARCHAR列,实际存储时也会转换成CHAR;这可能导致部分内存的浪费(其中一些限制在Percona版本已经解决)
如果MySQL在执行查询的过程中需要使用临时表来保存中间结果,内部使用的临时表就是Memory表。如果中间结果太大超出了Memory表的限制,或者含有BLOB或TEXT字段,则临时表会转换成MyISAM表。
人们经常混淆Memory表和临时表,临时表是指使用CREATE TEMPORARY TABLE语句创建的表,它可以使用任何存储引擎,因此和Memory 表不是一回事。临时表只在单个连接中可见,当连接断开时,临时表也将不复存在。
6.Merge引擎。
Merge引擎是MyISAM引擎的一个变种。Merge表是由多个MyISAM表合并而来的虚拟表。如果将MySQL用于日志或者数据仓库类应用,该引擎可以发挥作用,但是引入分区功能后,该引擎已经被放弃
7.NDB集群引擎
2003年,当时的MySQL AB公司从索尼爱立信公司收购了NDB数据库,然后开发了NDB集群存储引擎,作为SQL和NDB原生协议之间的解耦。MySQL服务器、NDB集群存储引擎,以及分布式、share-noting的、容灾的、高可用的NDB数据库的组合,被称为MySQL集群(MySQL Cluster)
MySQL还有一些有特殊用途的存储引擎。在新版本中,有些可能因为一些原因已经不再支持;另外还有些会继续支持,但是需要明确地启用后才能使用。
1.Archive引擎
Archive存储引擎只支持INSERT和SELECT操作,在MySQL5.1之前也不支持索引。Archive引擎会缓存所有的写并利用zlib对插入的行进行压缩,所以比MySQL表的磁盘I/O更少。但是每次SELECT查询都需要执行全表扫描。所以Archive表适合日志和数据采集类应用,这类应用做数据分析时往往需要全表扫描。或者在一些需要更快速的INSERT操作的场合下也可以使用.
Archive引擎支持行级锁和专用的缓冲区,所以可以实现高并发的插入。在一个查询开始直到返回表中存在的所有行数之前,Archive引擎会阻止其他的SELECT执行,以实现一致性读。另外,也实现了批量插入在完成之后对读操作是不可见的。这种机制模仿了事务和MVCC的一些特性,但Archive引擎不是一个事务型的引擎,而是一个针对告诉插入和压缩做了优化的简单引擎
2.Blackhole引擎。
Blackhole没有实现任何的存储机制,它会丢弃所有插入的数据,不做任何保存。但是服务器会记录Blackhole表的日志,所以可以用于复制数据到悲苦,或者只是简单地记录到日志。这种特殊的存储引擎可以在一些特殊的复制架构和日志审核时发徽作用。但这种应用方式会碰到很多问题,因此并不推荐
3.CSV引擎.
CSV引擎可以将普通的CSV文件(逗号分割值得文件)作为MySQL的表来处理,但这种表不支持索引。CSV引擎可以在数据库运行时拷入或者拷出文件。可以将Excel等电子表格软件中的数据存储为CSV文件,然后复制到MySQL数据目录下,就能在MySQL中打开使用。同样的,如果将数据写入到一个CSV引擎表,其他的外部程序也能立即从表的数据文件中读取CSV格式的数据,因此CSV引擎可以作为一种数据交换的机制,非常有用
4.Federated引擎。
Federated引擎是访问其他MySQL服务器的一个代理,它会创建一个到远程MySQL服务器的客户端连接,并将查询传输到远程服务器执行,然后提取或者发送需要的数据。最初设计该存储引擎是为了和企业级数据库如Microsoft SQL Server和Oracle的类似特性竞争的,可以说更多的是一种市场行为。尽管该引擎看起来提供了一种很好的跨服务器的灵活性,但也经常带来问题,因此默认是禁止的。MariaDB使用了它的一个后续改进版本,叫作FederatedX.
5.Memory引擎。
如果需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没关系,那么使用Memory表(以前也叫做HEAP表)是非常有用的。Memory表至少比MyISAM表要快一个数量级,因为所有的数据都存在内存中,不需要进行磁盘I/O,Memory表的结构在重启以后还会保留,但数据会丢失
Memory表在很多场景可以发挥好的作用:
5.1 用于查找(lookup)或者映射(mapping)表,例如将邮编和州名映射的表
5.2 用于缓存周期性聚合数据(periodically aggregated data)的结果
5.3 用于保存数据分析中产生的中间数据
Memory表支持Hash索引,因此查询操作非常快。虽然Memory表的速度非常快,但还是无法取代传统的基于磁盘的表。Memory表是表级锁,因此并发写入的性能较低。它不支持BLOCK或TEXT类型的列,并且每行的长度是固定的,所以即使指定了VARCHAR列,实际存储时也会转换成CHAR;这可能导致部分内存的浪费(其中一些限制在Percona版本已经解决)
如果MySQL在执行查询的过程中需要使用临时表来保存中间结果,内部使用的临时表就是Memory表。如果中间结果太大超出了Memory表的限制,或者含有BLOB或TEXT字段,则临时表会转换成MyISAM表。
人们经常混淆Memory表和临时表,临时表是指使用CREATE TEMPORARY TABLE语句创建的表,它可以使用任何存储引擎,因此和Memory 表不是一回事。临时表只在单个连接中可见,当连接断开时,临时表也将不复存在。
6.Merge引擎。
Merge引擎是MyISAM引擎的一个变种。Merge表是由多个MyISAM表合并而来的虚拟表。如果将MySQL用于日志或者数据仓库类应用,该引擎可以发挥作用,但是引入分区功能后,该引擎已经被放弃
7.NDB集群引擎
2003年,当时的MySQL AB公司从索尼爱立信公司收购了NDB数据库,然后开发了NDB集群存储引擎,作为SQL和NDB原生协议之间的解耦。MySQL服务器、NDB集群存储引擎,以及分布式、share-noting的、容灾的、高可用的NDB数据库的组合,被称为MySQL集群(MySQL Cluster)
MySQL查看存储引擎的命令
```sql
mysql> SHOW ENGINES;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine | Support | Comment | Transactions | XA | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| CSV | YES | CSV storage engine | NO | NO | NO |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
| Sequence | YES | Sequence Storage Engine Helper | NO | NO | NO |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
10 rows in set (0.11 sec)
```
```sql
mysql> SHOW ENGINES;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine | Support | Comment | Transactions | XA | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| CSV | YES | CSV storage engine | NO | NO | NO |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
| Sequence | YES | Sequence Storage Engine Helper | NO | NO | NO |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
10 rows in set (0.11 sec)
```
查看建表使用的存储引擎
mysql> SHOW CREATE TABLE `tjc_project`.`app_config`;
+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| app_config | CREATE TABLE `app_config` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键id',
`environment_type` varchar(63) DEFAULT NULL COMMENT '环境类型',
`config_key` varchar(127) DEFAULT NULL COMMENT '配置类型',
`config_value` varchar(255) DEFAULT NULL COMMENT '配置的值',
`status` tinyint(1) DEFAULT NULL COMMENT '开启状态 0:关闭 1:开启',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |
+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.13 sec)
mysql> SHOW CREATE TABLE `tjc_project`.`app_config`;
+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| app_config | CREATE TABLE `app_config` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键id',
`environment_type` varchar(63) DEFAULT NULL COMMENT '环境类型',
`config_key` varchar(127) DEFAULT NULL COMMENT '配置类型',
`config_value` varchar(255) DEFAULT NULL COMMENT '配置的值',
`status` tinyint(1) DEFAULT NULL COMMENT '开启状态 0:关闭 1:开启',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |
+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.13 sec)
查看默认的存储引擎配置
mysql> SHOW VARIABLES LIKE '%storage_engine%';
+----------------------------------+-----------------------+
| Variable_name | Value |
+----------------------------------+-----------------------+
| default_storage_engine | InnoDB |
| default_tmp_storage_engine | InnoDB |
| disabled_storage_engines | myisam,memory,archive |
| internal_tmp_disk_storage_engine | InnoDB |
+----------------------------------+-----------------------+
4 rows in set (0.12 sec)
mysql> SHOW VARIABLES LIKE '%storage_engine%';
+----------------------------------+-----------------------+
| Variable_name | Value |
+----------------------------------+-----------------------+
| default_storage_engine | InnoDB |
| default_tmp_storage_engine | InnoDB |
| disabled_storage_engines | myisam,memory,archive |
| internal_tmp_disk_storage_engine | InnoDB |
+----------------------------------+-----------------------+
4 rows in set (0.12 sec)
第三方存储引擎。
MySQL从2007年开始提供了插件式的存储引擎API,从此用处了一系列为不同目的而设计的存储引擎。其中有一些已经合并到MySQL服务器,但大多数还是第三方产品或者开源项目。
MySQL从2007年开始提供了插件式的存储引擎API,从此用处了一系列为不同目的而设计的存储引擎。其中有一些已经合并到MySQL服务器,但大多数还是第三方产品或者开源项目。
OLTP类引擎。
Percona的XtraDB存储疫情是基于InnoDB引擎的一个改进版本,已经包含在Percona Server和MariaDB中,它的改进点主要集中在性能、可测量性和操作灵活性方面。XtraDB可以作为InnoDB的一个完全的替代产品,甚至可以兼容地读写InnoDB地数据文件,并支持InnoDB的所有查询。
另外还有一些和InnoDB地一个完全地替代产品,比如都支持ACID事务和MVCC。其中一个就是PBXT,由Paul McCullagh 和Primebase GMBH开发。它支持引擎级别的复制、外键约束,并且以一种比较复杂的架构对固态存储(SSD)提供了适当的支持,还对较大的值类型如BLOB也做了优化。PBXT是一款社区支持的存储引擎,MariaDB包含了该引擎。
TokuDB引擎使用了一种新的叫作分形树(Fractal Trees)的索引数据结构。该结构是缓存无关的,因此即使其大小超过内存性能也不会下降,也就没有内存生命周期和碎片的问题。TokuDB是一种大数据(Big Data)存储引擎,因为其拥有很高的压缩比,可以在很大的数据量上创建大量索引。目前其最适合在需要大量插入数据的分析型数据集的场景中使用。
RRethinkDB最初是为固态存储(SSD)而设计的,然而随者时间的推移,目前看起来和最初的目标有一定的差距。该引擎比较特币的地方在于采用了一种只能追加的写时复制B树(append-only copyon-write B-Tree)作为索引的数据结构。
在Sum收购MySQL AB后,Falcon存储引擎曾经作为下一代存储引擎被寄予期望,但现在该项目已经被取消很久了
Percona的XtraDB存储疫情是基于InnoDB引擎的一个改进版本,已经包含在Percona Server和MariaDB中,它的改进点主要集中在性能、可测量性和操作灵活性方面。XtraDB可以作为InnoDB的一个完全的替代产品,甚至可以兼容地读写InnoDB地数据文件,并支持InnoDB的所有查询。
另外还有一些和InnoDB地一个完全地替代产品,比如都支持ACID事务和MVCC。其中一个就是PBXT,由Paul McCullagh 和Primebase GMBH开发。它支持引擎级别的复制、外键约束,并且以一种比较复杂的架构对固态存储(SSD)提供了适当的支持,还对较大的值类型如BLOB也做了优化。PBXT是一款社区支持的存储引擎,MariaDB包含了该引擎。
TokuDB引擎使用了一种新的叫作分形树(Fractal Trees)的索引数据结构。该结构是缓存无关的,因此即使其大小超过内存性能也不会下降,也就没有内存生命周期和碎片的问题。TokuDB是一种大数据(Big Data)存储引擎,因为其拥有很高的压缩比,可以在很大的数据量上创建大量索引。目前其最适合在需要大量插入数据的分析型数据集的场景中使用。
RRethinkDB最初是为固态存储(SSD)而设计的,然而随者时间的推移,目前看起来和最初的目标有一定的差距。该引擎比较特币的地方在于采用了一种只能追加的写时复制B树(append-only copyon-write B-Tree)作为索引的数据结构。
在Sum收购MySQL AB后,Falcon存储引擎曾经作为下一代存储引擎被寄予期望,但现在该项目已经被取消很久了
面向列的存储引擎。
MySQL默认是面向行的,每一行的数据是一起存储的,服务器的查询也是以行为单位处理的。而在大数据量处理时,面向列的方式可能效率更高。如果不需要整行的数据,面向列的方式可以传输更少的数据。如果每一列都单独存储,那么压缩的效率也会更高。
Infobright是最有名的面向列的存储引擎,在非常大的数据量(数十TB)时,该引擎工作良好。Infobright是为数据分析和数据仓库应用设计的。数据高度压缩,按照块进行排序,每个块都对应一组元数据。在处理查询时,访问元数据可决定跳过该块,甚至可能只需要元数据即可满足查询的需求。但该引擎不支持索引,不过在这么大的数据量级,即使有索引也很难发挥作用,而且块结构也是一种准索引(quasi-index).Infobright需要对MySQL服务器做定制,因为一些地方需要修改以适应面向列存储的需要。如果查询无法在存储层使用面向列的模式执行,则需要在服务器层转换成按行处理,这个过程会很慢,Infobright有社区版和商业版两个版本。
另外一个面向列的存储引擎时Calpont公司的InfiniDB,也有社区版和商业版。InfiniDB可以在一组机器集群间作分布式查询。
MySQL默认是面向行的,每一行的数据是一起存储的,服务器的查询也是以行为单位处理的。而在大数据量处理时,面向列的方式可能效率更高。如果不需要整行的数据,面向列的方式可以传输更少的数据。如果每一列都单独存储,那么压缩的效率也会更高。
Infobright是最有名的面向列的存储引擎,在非常大的数据量(数十TB)时,该引擎工作良好。Infobright是为数据分析和数据仓库应用设计的。数据高度压缩,按照块进行排序,每个块都对应一组元数据。在处理查询时,访问元数据可决定跳过该块,甚至可能只需要元数据即可满足查询的需求。但该引擎不支持索引,不过在这么大的数据量级,即使有索引也很难发挥作用,而且块结构也是一种准索引(quasi-index).Infobright需要对MySQL服务器做定制,因为一些地方需要修改以适应面向列存储的需要。如果查询无法在存储层使用面向列的模式执行,则需要在服务器层转换成按行处理,这个过程会很慢,Infobright有社区版和商业版两个版本。
另外一个面向列的存储引擎时Calpont公司的InfiniDB,也有社区版和商业版。InfiniDB可以在一组机器集群间作分布式查询。
社区存储引擎。
如果要列举社区提供的所有存储引擎,可能会有两位数,甚至三位数。但是负责地说,其中大部分影响力有限,很多可能都没有听说过,或者只有极少人在使用。在这里列举了一些,也大都没有在生产环境中应用过,慎用,后果字符。
1.Aria.
之前地名字是Maria,是MySQL创建者计划用来替代MyISAM的一款引擎。MariaDB包含了该引擎,之前计划开发的很多特性,有些因为在MariaDB服务器层实现,所以引擎层就取消了。可以说Aria就是解决了崩溃安全恢复问题的MyISAM,当然也还有一些特性是MyISAM不具备的,比如数据的缓存(MyISAM只能缓存索引)
2.Groonga.
这是一款全文索引引擎,号称可以提供准备准确而高效的全文索引
3.OQGraph
该引擎由Open Query研发,支持图操作(比如查找两点之间的最短路径),用SQL很难实现该类操作
4.Q4M
该引擎在MySQL内部实现了队列操作,而用SQL很难在一个语句实现这类队列操作
5.SphinxSE.
该引擎为Sphinx全文索引搜索服务器提供了SQL结构
6.Spider
该引擎可以将数据切分成不同的分区,比较高效透明地实现了分片(shard),并且可以针对分片执行并行查询(分片可以分布在不同的服务器上)
7.VPForMySQL.
该引擎支持垂直分区,通过一系列的代理存储引擎实现。垂直分区指的是可以将表分成不同列的组合,并且单独存储。但对查询来说,看到的还是一张表,该引擎和Spider的作者是同一人
如果要列举社区提供的所有存储引擎,可能会有两位数,甚至三位数。但是负责地说,其中大部分影响力有限,很多可能都没有听说过,或者只有极少人在使用。在这里列举了一些,也大都没有在生产环境中应用过,慎用,后果字符。
1.Aria.
之前地名字是Maria,是MySQL创建者计划用来替代MyISAM的一款引擎。MariaDB包含了该引擎,之前计划开发的很多特性,有些因为在MariaDB服务器层实现,所以引擎层就取消了。可以说Aria就是解决了崩溃安全恢复问题的MyISAM,当然也还有一些特性是MyISAM不具备的,比如数据的缓存(MyISAM只能缓存索引)
2.Groonga.
这是一款全文索引引擎,号称可以提供准备准确而高效的全文索引
3.OQGraph
该引擎由Open Query研发,支持图操作(比如查找两点之间的最短路径),用SQL很难实现该类操作
4.Q4M
该引擎在MySQL内部实现了队列操作,而用SQL很难在一个语句实现这类队列操作
5.SphinxSE.
该引擎为Sphinx全文索引搜索服务器提供了SQL结构
6.Spider
该引擎可以将数据切分成不同的分区,比较高效透明地实现了分片(shard),并且可以针对分片执行并行查询(分片可以分布在不同的服务器上)
7.VPForMySQL.
该引擎支持垂直分区,通过一系列的代理存储引擎实现。垂直分区指的是可以将表分成不同列的组合,并且单独存储。但对查询来说,看到的还是一张表,该引擎和Spider的作者是同一人
选择合适的引擎。
这么多存储引擎,我们怎么选择?大部分情况下,InnoDB都是正确的选择,所以Oracle在MySQL5.5版本时终于将InnoDB作为默认的存储引擎了。对于如何选择存储引擎,可以简单概括为一句话"除非需要用到某些InnoDB不具备的特性,并且没有其他办法可以替代,否则都应该优先选择InnoDB引擎"。例如,如果要用到全文索引,建议优先考虑InnoDB加上Sphinx的组合,而不是使用支持全文索引的MyISAM。当然如果不需要用到InnoDB的特性,同时其他引擎的特性能够更好地满足需求,也可以考虑一下其他存储引擎。举个例子,如果不在乎可扩展能力和并发能力,也不在乎崩溃后的数据丢失问题,却对InnoDB的空间占用过多比较敏感,这种场合下选择MyISAM就比较合适。
除非万不得已,否则建议不要混合使用多种存储引擎,否则可能带来的一系列复杂的问题已经一些潜在的buf和边界问题。存储引擎层和服务器层的交互已经比较复杂,更不用说混合多个存储引擎了。至少,混合存储对一致性备份和服务器参数配置都带来了一些困难。
如果应用需要不同的存储疫情,请先考虑一下几个因素。
1.事务
如果应用需要事务支持,那么InnoDB(或者XtraDB)是目前最稳定并且经过验证的选择。如果不需要事务,并且主要是SELECT和INSERT操作,那么MyISAM是不同的选择。一般日志行的应用比较符合这一特性。
2.备份
备份的需求也会影响存储引擎的选择。如果可以定期地关闭服务器来执行备份,那么备份的因素可以忽略。反之,如果需要在线热备份,那么选择InnoDB就是基本的要求
3.崩溃恢复。
数据量比较大的时候,系统崩溃后如何快速地恢复是一个需要考虑地问题。相对而言,MyISAM崩溃后发生损坏地概率比InnoDB要高很多,而且恢复速度也要慢。因此,即使不需要事务支持,很多人也选择InnoDB引擎,这是一个非常重要的因素。
4.特有的特性.
最后,有些应用可能一来一些存储引擎所独有的特性或者优化,比如很多应用一来聚簇索引的优化。另外,MySQL中也只有MyISAM支持地理空间搜索。如果一个存储引擎拥有一些关键的特性,同时却又缺乏一些必要的特性,那么有时候不得不做折中的考虑,或者在架构设计上做一些取舍。某些存储引擎无法直接支持的特性,有时候通过变通也可以满足需求。
如果不了解具体的应用,上面提到的这些概念都是比较抽象的。所以接下来会讨论一些常见的应用场景,在这些场景中会涉及很多的表,以及这些表如何选用合适的存储引擎。
这么多存储引擎,我们怎么选择?大部分情况下,InnoDB都是正确的选择,所以Oracle在MySQL5.5版本时终于将InnoDB作为默认的存储引擎了。对于如何选择存储引擎,可以简单概括为一句话"除非需要用到某些InnoDB不具备的特性,并且没有其他办法可以替代,否则都应该优先选择InnoDB引擎"。例如,如果要用到全文索引,建议优先考虑InnoDB加上Sphinx的组合,而不是使用支持全文索引的MyISAM。当然如果不需要用到InnoDB的特性,同时其他引擎的特性能够更好地满足需求,也可以考虑一下其他存储引擎。举个例子,如果不在乎可扩展能力和并发能力,也不在乎崩溃后的数据丢失问题,却对InnoDB的空间占用过多比较敏感,这种场合下选择MyISAM就比较合适。
除非万不得已,否则建议不要混合使用多种存储引擎,否则可能带来的一系列复杂的问题已经一些潜在的buf和边界问题。存储引擎层和服务器层的交互已经比较复杂,更不用说混合多个存储引擎了。至少,混合存储对一致性备份和服务器参数配置都带来了一些困难。
如果应用需要不同的存储疫情,请先考虑一下几个因素。
1.事务
如果应用需要事务支持,那么InnoDB(或者XtraDB)是目前最稳定并且经过验证的选择。如果不需要事务,并且主要是SELECT和INSERT操作,那么MyISAM是不同的选择。一般日志行的应用比较符合这一特性。
2.备份
备份的需求也会影响存储引擎的选择。如果可以定期地关闭服务器来执行备份,那么备份的因素可以忽略。反之,如果需要在线热备份,那么选择InnoDB就是基本的要求
3.崩溃恢复。
数据量比较大的时候,系统崩溃后如何快速地恢复是一个需要考虑地问题。相对而言,MyISAM崩溃后发生损坏地概率比InnoDB要高很多,而且恢复速度也要慢。因此,即使不需要事务支持,很多人也选择InnoDB引擎,这是一个非常重要的因素。
4.特有的特性.
最后,有些应用可能一来一些存储引擎所独有的特性或者优化,比如很多应用一来聚簇索引的优化。另外,MySQL中也只有MyISAM支持地理空间搜索。如果一个存储引擎拥有一些关键的特性,同时却又缺乏一些必要的特性,那么有时候不得不做折中的考虑,或者在架构设计上做一些取舍。某些存储引擎无法直接支持的特性,有时候通过变通也可以满足需求。
如果不了解具体的应用,上面提到的这些概念都是比较抽象的。所以接下来会讨论一些常见的应用场景,在这些场景中会涉及很多的表,以及这些表如何选用合适的存储引擎。
常见的引擎应用场景。
1.日志型应用
假设你需要实时地记录一台中心电话交换机的每一通电话的日志到MySQL中,或者通过Apache的mod_log_sql模块将网站的所有访问信息直接记录到表中。这一类应用的插入速度有很高的要求,数据库不能称为瓶颈.MyISAM或者Archive存储引擎对这类应用比较合适,因为它们开销低,而且插入速度非常快。
如果需要对记录的日志做分析报表,那么事情就会变得有趣了。生成报表的SQL很有可能会导致插入效率明显降低,这时候该怎么办呢?
一种解决办法,是利用MySQL内置的复制方案将数据复制一份到悲苦,然后在备库上执行比较消耗时间和CPU的查询。这样主库只用于高效的插入工作。而备库上执行的查询也无须担心影响到日志的插入性能。当然也可以在系统负载较低的时候执行报表查询操作,但应用在不断变化,如果依赖这个策略可能以后会导致问题。
另外一种办法,在日志记录表的名字中包含年和月的信息,比如web_logs_2012_01或者web_logs_2012_jan.这样可以在已经没有插入操作的历史表上做频繁的查询操作,而不会干扰到最新的当前表上的插入操作
2.只读或者大部分情况下只读的表。
有些表的数据用于编制类目或者分列清单(如工作岗位、竞拍、不动产等),折中应用场景是典型的读多写少的业务。如果不介意MyISAM的崩溃恢复问题,选用MyISAM引擎是合适的。不过不要低估崩溃恢复问题的重要性,有些存储引擎不会保证将数据安全地写入到磁盘中,而许多用户实际上并不清楚这样有多大的风险(MyISAM只将数据写到内存中,然后等待操作系统定期地将数据刷出到磁盘上)
一个值得推荐的方式,是在性能测试环境模拟真实的环境,芸一行应用,然后拔下电源模拟崩溃测试。对崩溃恢复的第一手测试经验是无价之宝,可以避免真的碰到崩溃时手足无措
不要轻信"MyISAM比InnoDB快"之类的经验之谈,这个结论往往不是绝对的。在很多我们已知的场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是使用到聚簇索引,或者需要访问到数据都可以放入内存的应用。当设计上述类型的应用时,建议采用InnoDB。MyISAM引擎在一开始可能没有任何问题,但随者应用压力的上升,则可能迅速恶化。各种锁争用、崩溃后的数据丢失等问题都会随之而来。
3.订单处理
如果涉及订单处理,那么支持事务就是必要选项。半完成的订单是无法用来吸引用户的。另外一个重要的考虑点是存储引擎对外键的支持情况,InnoDB是订单处理类应用的最佳选择
4.电子公告牌和主题讨论论坛。
对于MySQL用户,主题讨论是个很有意思的话题。当前有成百上千的基于PHP或者Perl的免费系统可以支持主题讨论。其中大部分的数据库操作效率都不高,因为它们大多倾向于在一次请求中执行尽可能多的查询语句。另外还有部分系统设计为不采用数据库,当然也就无法利用到数据库提供的一些方便的特性。主题讨论区一般都有更新计数器,并且会为各个主题计算访问统计信息。多数应用只设计了几张表来保存所有的数据,所以核心表的读写压力可能非常大。为保证这些核心表的数据一致性,锁称为资源争用的主要因素。
尽管有这些设计缺陷,但大多数应用在中低负载时可以工作得很好。如果Web站点得规模迅速扩展,流量随之猛增,则数据库访问可能变得非常慢。此时一个典型得解决方案是更改为支持更高读写得存储引擎,但有时用户会发现这么做反而导致系统变得更慢了。用户可能没有意识到这是由于某些特殊查询得缘故,典型的如:
```sql
mysql> SELECT COUNT(*) FROM table;
```
问题就在于,不是所有的存储引擎运行上述查询都非常快:对于MyISAM确实会很快,但其他的可能都不行。每种存储引擎都能找出类似的对自己有利的例子
5.CD-ROM应用
如果要发布一个基于CD-ROM或者DVD-ROM并且使用MySQL数据文件的应用,可以考虑使用MyISAM表或者MyISAM压缩表,这样表之间可以隔离并且可以在不同介质上相互拷贝。MyISAM压缩表比未压缩的表要节约很多空间,但压缩表是只读的。在某些应用中这可能是个大问题。但如果数据放到只读介质的场景下,压缩表的只读特性就不是问题,就没有理由不采用压缩表了。
6.大数据量
什么样的数据量算大?我们创建或者管理的很多InnoDB数据库的数据量在3~5TB之间,或者更大,这是单台机器上的两,不是一个分片(shard)的量。这些系统运行得还不错,要做到这一点需要合理地选择硬件,做好物理设计,并未服务器地IO瓶颈做好规划。在这样的数据量下,如果采用MyISAM,崩溃后的恢复就是一个噩梦。
如果数据量继续增长到10TB以上的级别,可能就需要建立数据仓库。Infobright是MySQL数据仓库最成功的解决方案,也有一些大数据库不适合Infobright,却可能适合TokuDB
1.日志型应用
假设你需要实时地记录一台中心电话交换机的每一通电话的日志到MySQL中,或者通过Apache的mod_log_sql模块将网站的所有访问信息直接记录到表中。这一类应用的插入速度有很高的要求,数据库不能称为瓶颈.MyISAM或者Archive存储引擎对这类应用比较合适,因为它们开销低,而且插入速度非常快。
如果需要对记录的日志做分析报表,那么事情就会变得有趣了。生成报表的SQL很有可能会导致插入效率明显降低,这时候该怎么办呢?
一种解决办法,是利用MySQL内置的复制方案将数据复制一份到悲苦,然后在备库上执行比较消耗时间和CPU的查询。这样主库只用于高效的插入工作。而备库上执行的查询也无须担心影响到日志的插入性能。当然也可以在系统负载较低的时候执行报表查询操作,但应用在不断变化,如果依赖这个策略可能以后会导致问题。
另外一种办法,在日志记录表的名字中包含年和月的信息,比如web_logs_2012_01或者web_logs_2012_jan.这样可以在已经没有插入操作的历史表上做频繁的查询操作,而不会干扰到最新的当前表上的插入操作
2.只读或者大部分情况下只读的表。
有些表的数据用于编制类目或者分列清单(如工作岗位、竞拍、不动产等),折中应用场景是典型的读多写少的业务。如果不介意MyISAM的崩溃恢复问题,选用MyISAM引擎是合适的。不过不要低估崩溃恢复问题的重要性,有些存储引擎不会保证将数据安全地写入到磁盘中,而许多用户实际上并不清楚这样有多大的风险(MyISAM只将数据写到内存中,然后等待操作系统定期地将数据刷出到磁盘上)
一个值得推荐的方式,是在性能测试环境模拟真实的环境,芸一行应用,然后拔下电源模拟崩溃测试。对崩溃恢复的第一手测试经验是无价之宝,可以避免真的碰到崩溃时手足无措
不要轻信"MyISAM比InnoDB快"之类的经验之谈,这个结论往往不是绝对的。在很多我们已知的场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是使用到聚簇索引,或者需要访问到数据都可以放入内存的应用。当设计上述类型的应用时,建议采用InnoDB。MyISAM引擎在一开始可能没有任何问题,但随者应用压力的上升,则可能迅速恶化。各种锁争用、崩溃后的数据丢失等问题都会随之而来。
3.订单处理
如果涉及订单处理,那么支持事务就是必要选项。半完成的订单是无法用来吸引用户的。另外一个重要的考虑点是存储引擎对外键的支持情况,InnoDB是订单处理类应用的最佳选择
4.电子公告牌和主题讨论论坛。
对于MySQL用户,主题讨论是个很有意思的话题。当前有成百上千的基于PHP或者Perl的免费系统可以支持主题讨论。其中大部分的数据库操作效率都不高,因为它们大多倾向于在一次请求中执行尽可能多的查询语句。另外还有部分系统设计为不采用数据库,当然也就无法利用到数据库提供的一些方便的特性。主题讨论区一般都有更新计数器,并且会为各个主题计算访问统计信息。多数应用只设计了几张表来保存所有的数据,所以核心表的读写压力可能非常大。为保证这些核心表的数据一致性,锁称为资源争用的主要因素。
尽管有这些设计缺陷,但大多数应用在中低负载时可以工作得很好。如果Web站点得规模迅速扩展,流量随之猛增,则数据库访问可能变得非常慢。此时一个典型得解决方案是更改为支持更高读写得存储引擎,但有时用户会发现这么做反而导致系统变得更慢了。用户可能没有意识到这是由于某些特殊查询得缘故,典型的如:
```sql
mysql> SELECT COUNT(*) FROM table;
```
问题就在于,不是所有的存储引擎运行上述查询都非常快:对于MyISAM确实会很快,但其他的可能都不行。每种存储引擎都能找出类似的对自己有利的例子
5.CD-ROM应用
如果要发布一个基于CD-ROM或者DVD-ROM并且使用MySQL数据文件的应用,可以考虑使用MyISAM表或者MyISAM压缩表,这样表之间可以隔离并且可以在不同介质上相互拷贝。MyISAM压缩表比未压缩的表要节约很多空间,但压缩表是只读的。在某些应用中这可能是个大问题。但如果数据放到只读介质的场景下,压缩表的只读特性就不是问题,就没有理由不采用压缩表了。
6.大数据量
什么样的数据量算大?我们创建或者管理的很多InnoDB数据库的数据量在3~5TB之间,或者更大,这是单台机器上的两,不是一个分片(shard)的量。这些系统运行得还不错,要做到这一点需要合理地选择硬件,做好物理设计,并未服务器地IO瓶颈做好规划。在这样的数据量下,如果采用MyISAM,崩溃后的恢复就是一个噩梦。
如果数据量继续增长到10TB以上的级别,可能就需要建立数据仓库。Infobright是MySQL数据仓库最成功的解决方案,也有一些大数据库不适合Infobright,却可能适合TokuDB
转换表的引擎。
1.ALTER TABLE
将表从一个引擎修改为另一个引擎最简单的办法是使用ALTER TABLE语句。下面的语句将mytable的引擎修改为InnoDB:
```sql
mysql>ALTER TABLE mytable ENGINE = InnoDB;
```
上述语法可以使用任何存储引擎。但有一个问题:需要执行很长时间。MySQL会按行将数据从原表复制到一张新的表中,在复制期间可能会消耗系统所有的I/O能力,同时原表上回加上读锁。所以,在繁忙的表上执行此操作要特别小心。一个替代方案是采用导入与导出的方法,手工进行表的复制。如果转换表的存储引擎,将会失去和原引擎相关的所有特性。例如,将一张InnoDB表转换为MyISAM,然后再转换回InnoDB,原InnoDB表上所有的外键将丢失。
2.导出与导入
为了更好地控制转换的过程,可以使用mysqldump工具将数据导出到文件,然后修改文件中CREATE TABLE语句的存储引擎选项,注意同时修改表名,因为同一个数据库中不能存在相同的表ing,即使它们使用的是不同的存储引擎。同时要注意mysqldump默认回自动再CREATE TABLE语句前加上DROP TABLE语句,不注意这一点可能回导致数据丢失.
3.创建与查询(CREATE 和SELECT)
第三种转换的技术综合了第一种方法的高效和第二种方法的安全。不需要导出整个表的数据,而是先创建一个新的存储引擎的表,然后利用INSERT...SELECT语法来导数据:
```sql
mysql>START TRANSACTION;
mysql>INSERT INTO innodb_table SELECT * FROM myisam_table WHERE id BETWEEN x AND y;
mysql>COMMIT;
```
这样操作完成以后,新表是原表的一个全量复制,原表还在,如果需要可以删除原表。如果有必要,可以在执行的过程中对原表加锁,以确保新表和原表的数据一致。
1.ALTER TABLE
将表从一个引擎修改为另一个引擎最简单的办法是使用ALTER TABLE语句。下面的语句将mytable的引擎修改为InnoDB:
```sql
mysql>ALTER TABLE mytable ENGINE = InnoDB;
```
上述语法可以使用任何存储引擎。但有一个问题:需要执行很长时间。MySQL会按行将数据从原表复制到一张新的表中,在复制期间可能会消耗系统所有的I/O能力,同时原表上回加上读锁。所以,在繁忙的表上执行此操作要特别小心。一个替代方案是采用导入与导出的方法,手工进行表的复制。如果转换表的存储引擎,将会失去和原引擎相关的所有特性。例如,将一张InnoDB表转换为MyISAM,然后再转换回InnoDB,原InnoDB表上所有的外键将丢失。
2.导出与导入
为了更好地控制转换的过程,可以使用mysqldump工具将数据导出到文件,然后修改文件中CREATE TABLE语句的存储引擎选项,注意同时修改表名,因为同一个数据库中不能存在相同的表ing,即使它们使用的是不同的存储引擎。同时要注意mysqldump默认回自动再CREATE TABLE语句前加上DROP TABLE语句,不注意这一点可能回导致数据丢失.
3.创建与查询(CREATE 和SELECT)
第三种转换的技术综合了第一种方法的高效和第二种方法的安全。不需要导出整个表的数据,而是先创建一个新的存储引擎的表,然后利用INSERT...SELECT语法来导数据:
```sql
mysql>START TRANSACTION;
mysql>INSERT INTO innodb_table SELECT * FROM myisam_table WHERE id BETWEEN x AND y;
mysql>COMMIT;
```
这样操作完成以后,新表是原表的一个全量复制,原表还在,如果需要可以删除原表。如果有必要,可以在执行的过程中对原表加锁,以确保新表和原表的数据一致。
服务器性能剖析
概述。
最长碰到的三个性能相关的服务请求是:如何确认服务器是否达到了性能最佳的状态、找出某条语句为什么执行不够快、诊断被人们描述成"停顿"、"堆积"或者"卡死"的某些间歇性疑难故障。
问10个人关于性能的问题,可能会得到10个不同的回答,比如"每秒查询次数"、"CPU利用率"、"可扩展性"之类。这其实也没有什么问题,每个人在不同场景下对性能有不同的理解,但是比较正式的定义是完成某件任务所需要的时间度量,换句话说,性能即响应时间,这是一个非常重要的原则。通过任务和时间而不是资源来测试性能。数据库服务器的目的是执行SQL语句,所以它关注的任务是查询或者语句,如SEELCT、UPDATE、DELETE等。数据库服务器的性能用查询的响应时间来度量,单位是每个查询花费的时间。
还有另外一个问题:什么是优化?暂时不讨论这个问题,而是假设性能优化就是在一定的工作负载下尽可能地降低响应时间。很多人对此很迷茫。假如你认为性能优化是降低CPU利用率,那么可以减少对资源的使用。但这是一个陷阱,资源是用来消耗并用来工作的,所以有时候消耗更多的资源能够加快查询速度。很多时候使用老版本InnoDB引擎的MySQL升级到新版本后,CPU利用率会上升得很厉害,这并不代表性能出现了问题,反而说明新版本得InnoDB对资源的利用率上升了。查询的响应时间则更能体现升级后的性能是不是变得更好。版本升级有时候会带来一些bug,比如不能利用某些索引从而导致CPU利用率上升。CPU利用率只是一种现象,而不是很好的可度量的目标。
同样,如果把性能优化仅仅堪称是提升每秒查询量,这其实只是吞吐量优化。吞吐量的提升可以看作性能优化的副产品,对查询的优化可以让服务器每秒执行更多的查询,因为每条查询执行的时间更短了(吞吐量的定义是单位时间内的查询数量,这正好是前面对性能定义的倒数)。
所以如果目标是降低响应时间,那么就需要理解为什么服务器执行查询需要这么多时间,然后去减少或者消除哪些对获得查询结果来说不必要的工作。也就是说,先要搞清楚时间花在哪里。这就引申出优化的第二个原则:无法测量就无法有效地优化,所以第一步应该测量时间花在哪里
很多人在优化时,都将精力放在修改一些东西上,却很少去进行精确地测量。我们地做法完全相反,将花费非常多,甚至90%的时间来测量响应时间花在哪里。如果通过测量没有找到答案,那要么是测量的方式错了,要么是测量得不够完整。如果测量了系统中完整而且正确得数据,性能问题一般都能暴露出来,对症下要得解决方案也就比较明了。测量是一项很有挑战性的工作,并且分析结果也同样有挑战性,测出时间花在哪里,和知道为什么花在那里,是两码事。
前面提到需要合适的测量范围,其实合理的测量范围是说只测量需要优化的活动。有两种比较常见的情况会导致不合适的测量:
1.在错误的时间启动和停止测量
2.测量的是聚合后的信息,而不是目标互动本身。
例如,一个常见的错误是先看慢查询,然后又去排查整个服务器的情况来判断问题在那里。如果确认有慢查询,那么就应该测量慢查询,而不是测量整个服务器。测量的应该是从慢查询的开始到结束的时间,而不是查询之前或查询之后的时间。
完成一项任务所需要的时间可以分成两部分:执行时间和等待时间。如果要优化任务的执行时间,最好的办法是通过测量定位不同的子任务花费的时间,然后优化去掉一些子任务、降低子任务的执行频率或者提升子任务的效率。而优化任务的等待时间则相对要复杂一些,因为等待有可能是由其他系统间接影响导致,任务之间也可能由于争用磁盘或者CPU资源而相互影响。根据时间是花在执行还是等待上的不同,诊断也需要不同的工具和技术
最长碰到的三个性能相关的服务请求是:如何确认服务器是否达到了性能最佳的状态、找出某条语句为什么执行不够快、诊断被人们描述成"停顿"、"堆积"或者"卡死"的某些间歇性疑难故障。
问10个人关于性能的问题,可能会得到10个不同的回答,比如"每秒查询次数"、"CPU利用率"、"可扩展性"之类。这其实也没有什么问题,每个人在不同场景下对性能有不同的理解,但是比较正式的定义是完成某件任务所需要的时间度量,换句话说,性能即响应时间,这是一个非常重要的原则。通过任务和时间而不是资源来测试性能。数据库服务器的目的是执行SQL语句,所以它关注的任务是查询或者语句,如SEELCT、UPDATE、DELETE等。数据库服务器的性能用查询的响应时间来度量,单位是每个查询花费的时间。
还有另外一个问题:什么是优化?暂时不讨论这个问题,而是假设性能优化就是在一定的工作负载下尽可能地降低响应时间。很多人对此很迷茫。假如你认为性能优化是降低CPU利用率,那么可以减少对资源的使用。但这是一个陷阱,资源是用来消耗并用来工作的,所以有时候消耗更多的资源能够加快查询速度。很多时候使用老版本InnoDB引擎的MySQL升级到新版本后,CPU利用率会上升得很厉害,这并不代表性能出现了问题,反而说明新版本得InnoDB对资源的利用率上升了。查询的响应时间则更能体现升级后的性能是不是变得更好。版本升级有时候会带来一些bug,比如不能利用某些索引从而导致CPU利用率上升。CPU利用率只是一种现象,而不是很好的可度量的目标。
同样,如果把性能优化仅仅堪称是提升每秒查询量,这其实只是吞吐量优化。吞吐量的提升可以看作性能优化的副产品,对查询的优化可以让服务器每秒执行更多的查询,因为每条查询执行的时间更短了(吞吐量的定义是单位时间内的查询数量,这正好是前面对性能定义的倒数)。
所以如果目标是降低响应时间,那么就需要理解为什么服务器执行查询需要这么多时间,然后去减少或者消除哪些对获得查询结果来说不必要的工作。也就是说,先要搞清楚时间花在哪里。这就引申出优化的第二个原则:无法测量就无法有效地优化,所以第一步应该测量时间花在哪里
很多人在优化时,都将精力放在修改一些东西上,却很少去进行精确地测量。我们地做法完全相反,将花费非常多,甚至90%的时间来测量响应时间花在哪里。如果通过测量没有找到答案,那要么是测量的方式错了,要么是测量得不够完整。如果测量了系统中完整而且正确得数据,性能问题一般都能暴露出来,对症下要得解决方案也就比较明了。测量是一项很有挑战性的工作,并且分析结果也同样有挑战性,测出时间花在哪里,和知道为什么花在那里,是两码事。
前面提到需要合适的测量范围,其实合理的测量范围是说只测量需要优化的活动。有两种比较常见的情况会导致不合适的测量:
1.在错误的时间启动和停止测量
2.测量的是聚合后的信息,而不是目标互动本身。
例如,一个常见的错误是先看慢查询,然后又去排查整个服务器的情况来判断问题在那里。如果确认有慢查询,那么就应该测量慢查询,而不是测量整个服务器。测量的应该是从慢查询的开始到结束的时间,而不是查询之前或查询之后的时间。
完成一项任务所需要的时间可以分成两部分:执行时间和等待时间。如果要优化任务的执行时间,最好的办法是通过测量定位不同的子任务花费的时间,然后优化去掉一些子任务、降低子任务的执行频率或者提升子任务的效率。而优化任务的等待时间则相对要复杂一些,因为等待有可能是由其他系统间接影响导致,任务之间也可能由于争用磁盘或者CPU资源而相互影响。根据时间是花在执行还是等待上的不同,诊断也需要不同的工具和技术
通过性能剖析进行优化。
一旦掌握并时间面向响应时间的优化方法,就会发现需要不断地对系统进行性能剖析(profiling).
性能剖析是测量和分析时间花费在哪里的主要方法。性能剖析一般有两个步骤:测量任务所花费的时间;然后对结果进行统计和排序,将重要的任务排到前面。性能剖析工具的工作方式基本相同。在任务开始时启动计时器,在任务结束时停止计时器,然后用结束时间减去启动时间得到响应时间。也有些工具会记录任务的夫人吴。这些结果数据可以用来绘制调用关系图,但对于哦我们的目标来说更重要的时,可以将相似的任务分组并进行汇总。对相似的任务分组并进行汇总可以帮助对那些分到一组的任务做更复杂的统计分析,但至少需要知道每一组有多少任务,并计算出总的响应时间。通过性能剖析报告(profile report)可以获得需要的结果。性能剖析报告会列出所有的任务列表。每行记录一个任务,包括任务名、任务的执行时间、任务的消耗时间、任务的平均执行时间,以及该任务执行时间占全部时间的百分比。性能剖析报告会按照任务的消耗时间进行降序排序。
我们将实际地讨论两种类型的性能剖析:基于执行时间的分析和基于等待的分析。基于执行时间的分析研究的是什么任务的执行时间最长,而基于等待的分析则是判断任务在什么地方被阻塞的时间最长。如果任务执行时间长是因为消耗了太多的资源且大部分时间花费在执行上,等待的时间不多,这种情况基于等待的分析作用就不大。反之亦然,如果任务一直在等待,没有消耗什么资源,去分析执行时间就不会有什么结果。如果不能确认问题是出在执行还是等待上,那么两种方式都需要试试。
事实上,当基于执行时间的分析发现一个任务需要花费太多时间的时候,应该深入去分析一下,可能会发现某些"执行时间"实际上是在等待。例如,SELECT查询花费了大量时间,如果深入研究,则可能会发现时间都花费在等待IO上。
在对系统进行性能剖析前,必须先要能够进行而苍凉,这需要系统测量化的支持。可测量的系统一般会有多个测量点可以捕获并手机数据,但实际系统很少可以做到可测量化。大部分系统都没有多少可测量点,即使有也只提供一些活动的技术,而没有活动花费的时间统计。
```c
# Profile
# Rank Query ID Response time Calls R/Call V/M Ite
# ==== ============================= ============== ===== ====== ===== ===
# 1 0xFFFCA4D67EA0A788813031B8... 328.2315 90.4% 30520 0.0108 0.01 COMMIT
# 2 0xB2249CB854EE3C2AD30AD7E3... 8.0186 2.2% 1208 0.0066 0.01 UPDATE sbtest?
# 3 0xE81D0B3DB4FB31BC558CAEF5... 6.6346 1.8% 1639 0.0040 0.01 SELECT sbtest?
# 4 0xDDBF88031795EC65EAB8A8A8... 5.5093 1.5% 756 0.0073 0.02 DELETE sbtest?
# MISC 0xMISC 14.6011 4.0% 3334 0.0044 0.0 <13 ITEMS>
```
一旦掌握并时间面向响应时间的优化方法,就会发现需要不断地对系统进行性能剖析(profiling).
性能剖析是测量和分析时间花费在哪里的主要方法。性能剖析一般有两个步骤:测量任务所花费的时间;然后对结果进行统计和排序,将重要的任务排到前面。性能剖析工具的工作方式基本相同。在任务开始时启动计时器,在任务结束时停止计时器,然后用结束时间减去启动时间得到响应时间。也有些工具会记录任务的夫人吴。这些结果数据可以用来绘制调用关系图,但对于哦我们的目标来说更重要的时,可以将相似的任务分组并进行汇总。对相似的任务分组并进行汇总可以帮助对那些分到一组的任务做更复杂的统计分析,但至少需要知道每一组有多少任务,并计算出总的响应时间。通过性能剖析报告(profile report)可以获得需要的结果。性能剖析报告会列出所有的任务列表。每行记录一个任务,包括任务名、任务的执行时间、任务的消耗时间、任务的平均执行时间,以及该任务执行时间占全部时间的百分比。性能剖析报告会按照任务的消耗时间进行降序排序。
我们将实际地讨论两种类型的性能剖析:基于执行时间的分析和基于等待的分析。基于执行时间的分析研究的是什么任务的执行时间最长,而基于等待的分析则是判断任务在什么地方被阻塞的时间最长。如果任务执行时间长是因为消耗了太多的资源且大部分时间花费在执行上,等待的时间不多,这种情况基于等待的分析作用就不大。反之亦然,如果任务一直在等待,没有消耗什么资源,去分析执行时间就不会有什么结果。如果不能确认问题是出在执行还是等待上,那么两种方式都需要试试。
事实上,当基于执行时间的分析发现一个任务需要花费太多时间的时候,应该深入去分析一下,可能会发现某些"执行时间"实际上是在等待。例如,SELECT查询花费了大量时间,如果深入研究,则可能会发现时间都花费在等待IO上。
在对系统进行性能剖析前,必须先要能够进行而苍凉,这需要系统测量化的支持。可测量的系统一般会有多个测量点可以捕获并手机数据,但实际系统很少可以做到可测量化。大部分系统都没有多少可测量点,即使有也只提供一些活动的技术,而没有活动花费的时间统计。
```c
# Profile
# Rank Query ID Response time Calls R/Call V/M Ite
# ==== ============================= ============== ===== ====== ===== ===
# 1 0xFFFCA4D67EA0A788813031B8... 328.2315 90.4% 30520 0.0108 0.01 COMMIT
# 2 0xB2249CB854EE3C2AD30AD7E3... 8.0186 2.2% 1208 0.0066 0.01 UPDATE sbtest?
# 3 0xE81D0B3DB4FB31BC558CAEF5... 6.6346 1.8% 1639 0.0040 0.01 SELECT sbtest?
# 4 0xDDBF88031795EC65EAB8A8A8... 5.5093 1.5% 756 0.0073 0.02 DELETE sbtest?
# MISC 0xMISC 14.6011 4.0% 3334 0.0044 0.0 <13 ITEMS>
```
理解性能剖析。
MySQL的性能剖析(profile)将最重要的任务展示在前面,但有时候没显式出来的信息也很重要。不幸的是,尽管性能剖析输出了排名、总计和平均值,但还是有很多需要的信息是却是的,如下所示:
1.值得优化的查询(worthwhile query)
性能剖析不会自当给出那些查询值得花时间去优化。这把我们带回到优化的不呢一,如果你读过Cary Millsap的书,对此就会有很多的理解。这里我们要再次强调两点:
第一,一些只占总响应时间比重很小的查询是不值得优化的。根据阿姆达尔定律(Amdahl's Law),对一个占总响应时间不超过5%的查询进行优化,无论如何努力,收益也不会超过5%.
第二,如果花费了1000美元去优化一个任务,但业务的收入没有任何增加,那么可以说反而导致业务被逆优化了1000美元。如果优化的成本大于收益,就应当停止优化
2.异常情况
某些任务即使没有出现在性能剖析输出的前面也需要优化。比如某些任务执行次数很少,但每次执行都非常慢,严重影响用户体验。因为其执行频率低,所以总的响应时间占比并不突出。
3.未知的未知
一款好的性能剖析工具会显式可能的"丢失的时间"。丢失的时间指的是任务的总时间和实际测量到的时间之间的差。例如,如果处理器的CPU时间是10秒,二剖析到的任务总时间是9.7秒,那么就有300毫秒的丢失时间。这可能是有些任务没有测量到,也可能是由于测量的误差和精度问题的缘故。如果工具发现了这类问题,则要引起重视,因为有可能错过了某些重要的事情。即使性能剖析没有发现丢失时间,也需要注意考虑这类问题存在的可能性,这样才不会错过重要的信息。
4.被掩藏的细节
性能剖析无法显式所有响应时间的分布。只相信平均值是非常危险的,它会隐藏很多信息,而且无法表达全部情况。Peter经常举例说医院所有病人的平均提问没有任何价值。假如在前面的性能剖析的例子中,如果有两次查询的响应时间是1秒,而另外12771次查询的响应时间是几十微妙,结果会怎样?只从平均值里是无法发现两次1秒的查询的。要做出最好的决策,需要为性能剖析里输出的这一样中包含的12773次查询提供更多的信息,尤其是更多响应时间的信息,比如直方图、百分比、标准差、偏差指数。
MySQL的性能剖析(profile)将最重要的任务展示在前面,但有时候没显式出来的信息也很重要。不幸的是,尽管性能剖析输出了排名、总计和平均值,但还是有很多需要的信息是却是的,如下所示:
1.值得优化的查询(worthwhile query)
性能剖析不会自当给出那些查询值得花时间去优化。这把我们带回到优化的不呢一,如果你读过Cary Millsap的书,对此就会有很多的理解。这里我们要再次强调两点:
第一,一些只占总响应时间比重很小的查询是不值得优化的。根据阿姆达尔定律(Amdahl's Law),对一个占总响应时间不超过5%的查询进行优化,无论如何努力,收益也不会超过5%.
第二,如果花费了1000美元去优化一个任务,但业务的收入没有任何增加,那么可以说反而导致业务被逆优化了1000美元。如果优化的成本大于收益,就应当停止优化
2.异常情况
某些任务即使没有出现在性能剖析输出的前面也需要优化。比如某些任务执行次数很少,但每次执行都非常慢,严重影响用户体验。因为其执行频率低,所以总的响应时间占比并不突出。
3.未知的未知
一款好的性能剖析工具会显式可能的"丢失的时间"。丢失的时间指的是任务的总时间和实际测量到的时间之间的差。例如,如果处理器的CPU时间是10秒,二剖析到的任务总时间是9.7秒,那么就有300毫秒的丢失时间。这可能是有些任务没有测量到,也可能是由于测量的误差和精度问题的缘故。如果工具发现了这类问题,则要引起重视,因为有可能错过了某些重要的事情。即使性能剖析没有发现丢失时间,也需要注意考虑这类问题存在的可能性,这样才不会错过重要的信息。
4.被掩藏的细节
性能剖析无法显式所有响应时间的分布。只相信平均值是非常危险的,它会隐藏很多信息,而且无法表达全部情况。Peter经常举例说医院所有病人的平均提问没有任何价值。假如在前面的性能剖析的例子中,如果有两次查询的响应时间是1秒,而另外12771次查询的响应时间是几十微妙,结果会怎样?只从平均值里是无法发现两次1秒的查询的。要做出最好的决策,需要为性能剖析里输出的这一样中包含的12773次查询提供更多的信息,尤其是更多响应时间的信息,比如直方图、百分比、标准差、偏差指数。
对应用程序进行性能剖析。
对任何需要消耗时间的任务都可以做性能pixie,当然也包括应用程序。实际上,剖析应用程序一般比剖析数据库服务器容易,而且回报更多。虽然前面都是针对MySQL服务器的剖析,但对系统进行性能剖析还是建议自上而下地进行,这样可以追踪自用户发起到服务器响应的整个流程。虽然性能问题大多数情况下都和数据库有关,但应用导致的性能问题也不少。性能瓶颈可能有很多影响因素:
1.外部资源,比如调用了外部的Web服务或者搜索引擎
2.应用需要处理大量的数据,比如分析一个超大的XML文件
3.在循环中执行昂贵的操作,比如滥用正则表达式
4.使用了低效的算法,比如使用暴力搜索算法(naive search algorithm)来查找列表中的项
幸运的是,确定MySQL的问题没有这么复杂,只需要一款应用程序的剖析工具即可(作为回报,一旦拥有这样的工具,就可以从一开始就写出高效的代码)。
建议在所有的新项目中都考虑包含性能剖析得代码。往已有的项目中假如性能剖析代码也许很困难,新项目就简单一些。
对任何需要消耗时间的任务都可以做性能pixie,当然也包括应用程序。实际上,剖析应用程序一般比剖析数据库服务器容易,而且回报更多。虽然前面都是针对MySQL服务器的剖析,但对系统进行性能剖析还是建议自上而下地进行,这样可以追踪自用户发起到服务器响应的整个流程。虽然性能问题大多数情况下都和数据库有关,但应用导致的性能问题也不少。性能瓶颈可能有很多影响因素:
1.外部资源,比如调用了外部的Web服务或者搜索引擎
2.应用需要处理大量的数据,比如分析一个超大的XML文件
3.在循环中执行昂贵的操作,比如滥用正则表达式
4.使用了低效的算法,比如使用暴力搜索算法(naive search algorithm)来查找列表中的项
幸运的是,确定MySQL的问题没有这么复杂,只需要一款应用程序的剖析工具即可(作为回报,一旦拥有这样的工具,就可以从一开始就写出高效的代码)。
建议在所有的新项目中都考虑包含性能剖析得代码。往已有的项目中假如性能剖析代码也许很困难,新项目就简单一些。
性能剖析本身会导致服务器变慢吗?
说"是的",是因为性能剖析确实会导致应用慢一点;说"不是",是因为性能剖析可以帮助应用运行得更快。
性能剖析和定期检测都会带来额外开销。问题在于这部分得开销有多少,并且由此获得得收益是否能够抵消这些开销。大多数设计和构建过高性能应用程序得人相信,应该尽可能地测量一切可以测量地地方,并且接受这些测量带来的额外开销,这些开销应该被当称应用程序的一部分。Oracle的性能优化大师Tom Kyte曾被问到Oracle中的测量点的开销,他的回答是,测量点至少为性能优化贡献了10%。对此我们深表赞同,而且大多数应用并不需要要每天都运行详细的性能测量,所以实际贡献甚至要超过10%。即使不同这个观点,为应用构建一些可以永久使用的轻量级的性能剖析也是有意义的。如果系统没有每天变化的性能统计,则碰到无法提前预知的性能瓶颈就是一件头痛的事情,发现问题的时候,如果有历史数据,则这些历史数据价值是无限的。而且性能数据还可以帮助规划好硬件采购、资源分配、以及预测周期性的性能尖峰。那么何谓"轻量级"的性能剖析?比如可以为所有的SQL语句计时,加上脚本总时间统计,这样做的代价不高,而且不需要在每次页面查看(page view)时都执行.如果流量趋势比较稳定,随机采样也可以,随机采用可以通过在应用程序中设置实现:
```php
<?php
$profiling_enabled = rand(0,100) >99;
?>
```
这样只有1%的会话会执行性能采样,来帮助定位一些严重的问题。这种策略在生产环境中尤其有用,可以发现一些其他方法无法发现的问题。
说"是的",是因为性能剖析确实会导致应用慢一点;说"不是",是因为性能剖析可以帮助应用运行得更快。
性能剖析和定期检测都会带来额外开销。问题在于这部分得开销有多少,并且由此获得得收益是否能够抵消这些开销。大多数设计和构建过高性能应用程序得人相信,应该尽可能地测量一切可以测量地地方,并且接受这些测量带来的额外开销,这些开销应该被当称应用程序的一部分。Oracle的性能优化大师Tom Kyte曾被问到Oracle中的测量点的开销,他的回答是,测量点至少为性能优化贡献了10%。对此我们深表赞同,而且大多数应用并不需要要每天都运行详细的性能测量,所以实际贡献甚至要超过10%。即使不同这个观点,为应用构建一些可以永久使用的轻量级的性能剖析也是有意义的。如果系统没有每天变化的性能统计,则碰到无法提前预知的性能瓶颈就是一件头痛的事情,发现问题的时候,如果有历史数据,则这些历史数据价值是无限的。而且性能数据还可以帮助规划好硬件采购、资源分配、以及预测周期性的性能尖峰。那么何谓"轻量级"的性能剖析?比如可以为所有的SQL语句计时,加上脚本总时间统计,这样做的代价不高,而且不需要在每次页面查看(page view)时都执行.如果流量趋势比较稳定,随机采样也可以,随机采用可以通过在应用程序中设置实现:
```php
<?php
$profiling_enabled = rand(0,100) >99;
?>
```
这样只有1%的会话会执行性能采样,来帮助定位一些严重的问题。这种策略在生产环境中尤其有用,可以发现一些其他方法无法发现的问题。
剖析MySQL查询。
对查询进行性能剖析有两种方式,每种方式都有各自的问题,可以剖析整个数据库服务器,这样可以分析出哪些查询时主要的压力来源.定位到具体需要优化的查询后,也可以钻取下去对这些查询进行单独的剖析,分析哪些子任务是响应时间的主要消耗者。
对查询进行性能剖析有两种方式,每种方式都有各自的问题,可以剖析整个数据库服务器,这样可以分析出哪些查询时主要的压力来源.定位到具体需要优化的查询后,也可以钻取下去对这些查询进行单独的剖析,分析哪些子任务是响应时间的主要消耗者。
剖析服务器负载。
服务器端的剖析很有价值,因为在服务器端可以有效地审计效率低下的查询。定位和优化"坏"查询能够显著地提升应用的性能,也能解决某些特定的难题。还可以降低服务器的整体压力,这样所有的查询都将因为减少了对共享资源的争用而收益("间接的好处")。降低服务器的负载也可以推迟或者避免升级更昂贵硬件的需求,还可以发现和定位糟糕的用户体验,比如某些极端情况.
MySQL的每一个新版本中都增加了更多的可测量点。如果当前的趋势可靠的话,那么在性能方面比较重要的测量需求很快能够在全球范围内得到支持。但如果只是需要剖析并找出代价高的查询,就不需要如此复杂。有一个工具很早之前就能帮到我们了,这就是慢查询日志。
服务器端的剖析很有价值,因为在服务器端可以有效地审计效率低下的查询。定位和优化"坏"查询能够显著地提升应用的性能,也能解决某些特定的难题。还可以降低服务器的整体压力,这样所有的查询都将因为减少了对共享资源的争用而收益("间接的好处")。降低服务器的负载也可以推迟或者避免升级更昂贵硬件的需求,还可以发现和定位糟糕的用户体验,比如某些极端情况.
MySQL的每一个新版本中都增加了更多的可测量点。如果当前的趋势可靠的话,那么在性能方面比较重要的测量需求很快能够在全球范围内得到支持。但如果只是需要剖析并找出代价高的查询,就不需要如此复杂。有一个工具很早之前就能帮到我们了,这就是慢查询日志。
捕获MySQL的查询到日志文件中。
在MySQL中,慢查询最初只是捕获比较慢的查询,而性能剖析却需要针对所有的查询。而且在MySQL5.0及之前的版本中,慢查询日志的响应时间的单位是秒,粒度太粗了。幸运的是,这些限制都已经成为历史了。在MySQL5.1及更新的版本中,慢日志的功能已经被加强,可以通过设置long_query_time为0来捕获所有的查询,而且查询的响应时间单位已经可以做到微秒级。如果使用的是Percona Server,那么5.0版本就具备了这些特性,而且Percona Server提供了对日志内容和查询捕获的更多控制能力。
在MySQL的当前版本中,慢查询日志是开销最低,精度最高的测量查询时间的工具。如果还在担心开启慢查询日志会带来额外的I/O开销,那大可放心。已经在IO密集型场景做过基准测试,慢查询日志带来的开销可以忽略不计(实际上在CPU密集型场景的影响还稍微大一些)。更需要担心的是日志可能消耗大量的磁盘空间。如果长期开启慢查询日志,注意要部署日志轮转(log rotation)工具。或者不要长期启用慢查询日志,只在需要手机负载样本的期间开启即可。
MySQL还有另外一种查询日志,被称之为"通用日志",但很少用于分析和剖析服务器性能。通用日志在查询请求服务器时进行记录,所以不包含响应时间和执行计划等重要信息。MySQL5.1之后支持将日志记录到数据库的表中,但多数情况下这样做没什么必要。这不但对性能有较大影响,而且MySQL5.1在将慢查询记录到文件中时已经支持微秒级别的信息,然而将慢查询记录到表中会导致时间粒度退化为只能到秒级。而秒级别的慢查询日志没有太大的意义。
Percona Server得慢查询日志比MySQL官方版本记录了更多细节且有价值得信息,如查询执行计划、锁、IO活动等。这些特性都是随着处理各种不同得优化场景得需求而慢慢加进来得。另外在可管理性上也进行了增强。比如全局修改针对每个连接的long_query_time的阈值,这样当应用使用连接池或者持久连接的时候,可以不用重置会话级别的变量而启动或者停止连接的查询日志。总的来说,慢查询日志是一种轻量而且功能全面的性能剖析工具,是优化服务器查询的利器。
有时因为某些原因如权限不足等,无法在服务器上记录查询。这样的限制也常常碰到,所以官方开发了两种替代的技术,都集成到了Percona Toolkit中的
pt-query-digest中。
1.第一种是通过--processlist选项不断查看SHOW FULL PROCESSLIST的输出,记录查询第一次出现的时间和消失的时间。某些情况下这样的精度也足够发现问题,但却无法捕获所有的查询。一些执行较快的查询可能在两次执行的间隙就执行完成了,从而无法捕获到。
2.第二种技术是通过抓取TCP网络包,然后根据MySQL的客户端/服务端通信协议进行解析。可以先通过tcpdump将网络包数据保存到磁盘,然后使用pt-query-digest的--type=tcpdump选项来解决并分析查询.此方法的精度比较高,并且可以捕获所有查询。还可以解析更高级的协议特性,比如可以解析二进制协议,从而创建并执行服务器预解析的语句(prepared statement)及压缩协议。另外还有一种方法,就是通过MySQL Proxy代理曾的脚本来记录所有查询,但在实践中很少这样做
在MySQL中,慢查询最初只是捕获比较慢的查询,而性能剖析却需要针对所有的查询。而且在MySQL5.0及之前的版本中,慢查询日志的响应时间的单位是秒,粒度太粗了。幸运的是,这些限制都已经成为历史了。在MySQL5.1及更新的版本中,慢日志的功能已经被加强,可以通过设置long_query_time为0来捕获所有的查询,而且查询的响应时间单位已经可以做到微秒级。如果使用的是Percona Server,那么5.0版本就具备了这些特性,而且Percona Server提供了对日志内容和查询捕获的更多控制能力。
在MySQL的当前版本中,慢查询日志是开销最低,精度最高的测量查询时间的工具。如果还在担心开启慢查询日志会带来额外的I/O开销,那大可放心。已经在IO密集型场景做过基准测试,慢查询日志带来的开销可以忽略不计(实际上在CPU密集型场景的影响还稍微大一些)。更需要担心的是日志可能消耗大量的磁盘空间。如果长期开启慢查询日志,注意要部署日志轮转(log rotation)工具。或者不要长期启用慢查询日志,只在需要手机负载样本的期间开启即可。
MySQL还有另外一种查询日志,被称之为"通用日志",但很少用于分析和剖析服务器性能。通用日志在查询请求服务器时进行记录,所以不包含响应时间和执行计划等重要信息。MySQL5.1之后支持将日志记录到数据库的表中,但多数情况下这样做没什么必要。这不但对性能有较大影响,而且MySQL5.1在将慢查询记录到文件中时已经支持微秒级别的信息,然而将慢查询记录到表中会导致时间粒度退化为只能到秒级。而秒级别的慢查询日志没有太大的意义。
Percona Server得慢查询日志比MySQL官方版本记录了更多细节且有价值得信息,如查询执行计划、锁、IO活动等。这些特性都是随着处理各种不同得优化场景得需求而慢慢加进来得。另外在可管理性上也进行了增强。比如全局修改针对每个连接的long_query_time的阈值,这样当应用使用连接池或者持久连接的时候,可以不用重置会话级别的变量而启动或者停止连接的查询日志。总的来说,慢查询日志是一种轻量而且功能全面的性能剖析工具,是优化服务器查询的利器。
有时因为某些原因如权限不足等,无法在服务器上记录查询。这样的限制也常常碰到,所以官方开发了两种替代的技术,都集成到了Percona Toolkit中的
pt-query-digest中。
1.第一种是通过--processlist选项不断查看SHOW FULL PROCESSLIST的输出,记录查询第一次出现的时间和消失的时间。某些情况下这样的精度也足够发现问题,但却无法捕获所有的查询。一些执行较快的查询可能在两次执行的间隙就执行完成了,从而无法捕获到。
2.第二种技术是通过抓取TCP网络包,然后根据MySQL的客户端/服务端通信协议进行解析。可以先通过tcpdump将网络包数据保存到磁盘,然后使用pt-query-digest的--type=tcpdump选项来解决并分析查询.此方法的精度比较高,并且可以捕获所有查询。还可以解析更高级的协议特性,比如可以解析二进制协议,从而创建并执行服务器预解析的语句(prepared statement)及压缩协议。另外还有一种方法,就是通过MySQL Proxy代理曾的脚本来记录所有查询,但在实践中很少这样做
分析查询日志
强烈建议大家从现在起就利用慢查询日志捕获服务器上的所有查询,并且进行分析。可以在一些典型的时间窗口如业务高峰期的一个小时内记录查询。如果业务趋势比较均衡,那么一分钟甚至更短的时间内捕获需要优化的低效查询也是可以的。
不要直接打开整个慢查询日志进行分析,这样做只会浪费时间和金钱。首先应该生成一个剖析报告,如果需要则,则可以再查看日志中需要特别关注的部分。自顶向下是比价好的方式,否则有可能像前面提到的,反而导致业务的逆优化。
从慢查询日志中生成剖析报告需要有一款好工具,这里建议使用pt-query-digest,这毫无疑问是分析MySQL查询日志最有力的工具。该工具功能强大,包括可以将查询报告保存到数据库中,以及追踪工作负载随时间的变化。
一般情况下,只需要将慢查询日志文件作为参数传递给pt-query-digest,就可以正确地工作了,它将查询的剖析报告打印出来,并且能够选择将"重要"的查询逐条打印出更详细的信息。输出的报告细节详尽,绝对可以让生活更美好。
强烈建议大家从现在起就利用慢查询日志捕获服务器上的所有查询,并且进行分析。可以在一些典型的时间窗口如业务高峰期的一个小时内记录查询。如果业务趋势比较均衡,那么一分钟甚至更短的时间内捕获需要优化的低效查询也是可以的。
不要直接打开整个慢查询日志进行分析,这样做只会浪费时间和金钱。首先应该生成一个剖析报告,如果需要则,则可以再查看日志中需要特别关注的部分。自顶向下是比价好的方式,否则有可能像前面提到的,反而导致业务的逆优化。
从慢查询日志中生成剖析报告需要有一款好工具,这里建议使用pt-query-digest,这毫无疑问是分析MySQL查询日志最有力的工具。该工具功能强大,包括可以将查询报告保存到数据库中,以及追踪工作负载随时间的变化。
一般情况下,只需要将慢查询日志文件作为参数传递给pt-query-digest,就可以正确地工作了,它将查询的剖析报告打印出来,并且能够选择将"重要"的查询逐条打印出更详细的信息。输出的报告细节详尽,绝对可以让生活更美好。
剖析单条查询。
在定位到需要优化的单条查询后,可以针对查询"钻取"更多的信息,确认为什么会花费这么长的时间执行,以及需要如何去优化。不幸的是,MySQL目前大多数的测量点对于剖析查询都没有什么帮助。当然这种状况正在改善,大多数生产环境的服务器还没有使用包含最新剖析特性的版本。所以在实际应用中,除了SHOW STATUS、SHOW PROFILE、检查慢查询日志的条目(这还要求必须是Percona Server,官方MySQL版本的慢查询缺失了很多附加信息)这三种方法外就没有什么更好的办法了。
在定位到需要优化的单条查询后,可以针对查询"钻取"更多的信息,确认为什么会花费这么长的时间执行,以及需要如何去优化。不幸的是,MySQL目前大多数的测量点对于剖析查询都没有什么帮助。当然这种状况正在改善,大多数生产环境的服务器还没有使用包含最新剖析特性的版本。所以在实际应用中,除了SHOW STATUS、SHOW PROFILE、检查慢查询日志的条目(这还要求必须是Percona Server,官方MySQL版本的慢查询缺失了很多附加信息)这三种方法外就没有什么更好的办法了。
使用SHOW PROFILE。
SHOW PROFILE命令是在MySQL 5.1以后的版本中引入的,来源于开源社区中的Jeremy Cole的贡献。这是唯一一个在GA版本中包含的真正的查询剖析工具。默认是禁用地,还可以通过服务器变量在会话(连接)级别动态地修改。
```sql
mysql> SELECT VERSION();
+------------+
| VERSION() |
+------------+
| 5.7.42-log |
+------------+
1 row in set (0.11 sec)
mysql> SHOW VARIABLES LIKE 'profiling';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling | OFF |
+---------------+-------+
1 row in set (0.07 sec)
mysql> SHOW PROFILE
-> ;
Empty set
```
然后再服务器上执行的所有语句,都会测量其耗费的时间和其他一些查询执行状态变更相关的数据。这个功能有一定的作用,而且最初的设计功能更强大,但未来版本中可能会被Performance Schema所取代,尽管如此,这个工具最有用的作用还是在语句执行期间剖析服务器的具体工作。
当一条查询提交给服务器时,此工具会记录剖析信息到一张临时表,并且给查询赋予一个从1开始的整数标识符。
SHOW PROFILE命令是在MySQL 5.1以后的版本中引入的,来源于开源社区中的Jeremy Cole的贡献。这是唯一一个在GA版本中包含的真正的查询剖析工具。默认是禁用地,还可以通过服务器变量在会话(连接)级别动态地修改。
```sql
mysql> SELECT VERSION();
+------------+
| VERSION() |
+------------+
| 5.7.42-log |
+------------+
1 row in set (0.11 sec)
mysql> SHOW VARIABLES LIKE 'profiling';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling | OFF |
+---------------+-------+
1 row in set (0.07 sec)
mysql> SHOW PROFILE
-> ;
Empty set
```
然后再服务器上执行的所有语句,都会测量其耗费的时间和其他一些查询执行状态变更相关的数据。这个功能有一定的作用,而且最初的设计功能更强大,但未来版本中可能会被Performance Schema所取代,尽管如此,这个工具最有用的作用还是在语句执行期间剖析服务器的具体工作。
当一条查询提交给服务器时,此工具会记录剖析信息到一张临时表,并且给查询赋予一个从1开始的整数标识符。
举个例子。
```sql
mysql> SELECT * FROM chat_room ORDER BY id DESC LIMIT 10;
10 rows in set (0.18 sec)
mysql> SHOW PROFILES;
+----------+------------+-------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+-------------------------------------------------------+
| 1 | 0.00397750 | SHOW VARIABLES LIKE 'profiling' |
| 2 | 0.00010750 | SELECT * FROM chat ORDER BY id DESC LIMIT 100 |
| 3 | 0.00010100 | use chat |
| 4 | 0.00020625 | SELECT * FROM chat ORDER BY id DESC LIMIT 100 |
| 5 | 0.15971150 | SELECT * FROM chat_message ORDER BY id DESC LIMIT 100 |
| 6 | 0.00036200 | SELECT * FROM chat_message ORDER BY id DESC LIMIT 10 |
| 7 | 0.00037850 | SELECT * FROM chat_room ORDER BY id DESC LIMIT 10 |
+----------+------------+-------------------------------------------------------+
7 rows in set (0.17 sec)
```
该查询返回了10行记录,花费了0.18秒。接下来是SHOW PROFILES。
首先可以看到的是以很高的精度显示了查询的响应时间,这很好。MySQL客户端显示的时间只有两位小数,对于一些执行得很快的查询这样的精度是不够的。接下来继续看接下来的输出:
```sql
mysql> SHOW PROFILE FOR QUERY 7;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000077 |
| checking permissions | 0.000007 |
| Opening tables | 0.000071 |
| init | 0.000016 |
| System lock | 0.000006 |
| optimizing | 0.000003 |
| statistics | 0.000008 |
| preparing | 0.000009 |
| Sorting result | 0.000003 |
| executing | 0.000002 |
| Sending data | 0.000123 |
| end | 0.000004 |
| query end | 0.000005 |
| closing tables | 0.000005 |
| freeing items | 0.000024 |
| cleaning up | 0.000016 |
+----------------------+----------+
16 rows in set (0.17 sec)
```
剖析报告给出了查询执行的每个步骤及其花费的时间,看结果很难快速地确定哪个步骤花费地时间最多。因为输出是按照执行顺序排序,而不是按花费的时间排序的——而实际上我们更关心的是花费了多少多少时间,这样才能知道哪些开销比较打。但不幸的是无法通过诸如ORDER BY 之类的命令重新排序。假如不适用SHOW PROFILE命令而是这届查询INFORMATION_SHCEMA中对应的表,则可以按照需要格式化输出
如下方所示
```sql
mysql> SELECT * FROM chat_room ORDER BY id DESC LIMIT 10;
10 rows in set (0.18 sec)
mysql> SHOW PROFILES;
+----------+------------+-------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+-------------------------------------------------------+
| 1 | 0.00397750 | SHOW VARIABLES LIKE 'profiling' |
| 2 | 0.00010750 | SELECT * FROM chat ORDER BY id DESC LIMIT 100 |
| 3 | 0.00010100 | use chat |
| 4 | 0.00020625 | SELECT * FROM chat ORDER BY id DESC LIMIT 100 |
| 5 | 0.15971150 | SELECT * FROM chat_message ORDER BY id DESC LIMIT 100 |
| 6 | 0.00036200 | SELECT * FROM chat_message ORDER BY id DESC LIMIT 10 |
| 7 | 0.00037850 | SELECT * FROM chat_room ORDER BY id DESC LIMIT 10 |
+----------+------------+-------------------------------------------------------+
7 rows in set (0.17 sec)
```
该查询返回了10行记录,花费了0.18秒。接下来是SHOW PROFILES。
首先可以看到的是以很高的精度显示了查询的响应时间,这很好。MySQL客户端显示的时间只有两位小数,对于一些执行得很快的查询这样的精度是不够的。接下来继续看接下来的输出:
```sql
mysql> SHOW PROFILE FOR QUERY 7;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000077 |
| checking permissions | 0.000007 |
| Opening tables | 0.000071 |
| init | 0.000016 |
| System lock | 0.000006 |
| optimizing | 0.000003 |
| statistics | 0.000008 |
| preparing | 0.000009 |
| Sorting result | 0.000003 |
| executing | 0.000002 |
| Sending data | 0.000123 |
| end | 0.000004 |
| query end | 0.000005 |
| closing tables | 0.000005 |
| freeing items | 0.000024 |
| cleaning up | 0.000016 |
+----------------------+----------+
16 rows in set (0.17 sec)
```
剖析报告给出了查询执行的每个步骤及其花费的时间,看结果很难快速地确定哪个步骤花费地时间最多。因为输出是按照执行顺序排序,而不是按花费的时间排序的——而实际上我们更关心的是花费了多少多少时间,这样才能知道哪些开销比较打。但不幸的是无法通过诸如ORDER BY 之类的命令重新排序。假如不适用SHOW PROFILE命令而是这届查询INFORMATION_SHCEMA中对应的表,则可以按照需要格式化输出
如下方所示
```sql
mysql> SET @query_id = 7;
mysql>mysql> SELECT
STATE,
SUM( DURATION ) AS Total_R,
ROUND( 100 * SUM( DURATION ) / ( SELECT SUM( DURATION ) FROM INFORMATION_SCHEMA.PROFILING WHERE QUERY_ID = @query_id ), 2 ) AS Pct_R,
COUNT(*) AS Calls,
SUM( DURATION )/ COUNT(*) AS `R/Call`
FROM
INFORMATION_SCHEMA.PROFILING
WHERE
QUERY_ID = @query_id
GROUP BY
STATE
ORDER BY
Total_R DESC;
+----------------------+----------+-------+-------+--------------+
| STATE | Total_R | Pct_R | Calls | R/Call |
+----------------------+----------+-------+-------+--------------+
| Sending data | 0.000123 | 32.45 | 1 | 0.0001230000 |
| starting | 0.000077 | 20.32 | 1 | 0.0000770000 |
| Opening tables | 0.000071 | 18.73 | 1 | 0.0000710000 |
| freeing items | 0.000024 | 6.33 | 1 | 0.0000240000 |
| init | 0.000016 | 4.22 | 1 | 0.0000160000 |
| cleaning up | 0.000016 | 4.22 | 1 | 0.0000160000 |
| preparing | 0.000009 | 2.37 | 1 | 0.0000090000 |
| statistics | 0.000008 | 2.11 | 1 | 0.0000080000 |
| checking permissions | 0.000007 | 1.85 | 1 | 0.0000070000 |
| System lock | 0.000006 | 1.58 | 1 | 0.0000060000 |
| closing tables | 0.000005 | 1.32 | 1 | 0.0000050000 |
| query end | 0.000005 | 1.32 | 1 | 0.0000050000 |
| end | 0.000004 | 1.06 | 1 | 0.0000040000 |
| Sorting result | 0.000003 | 0.79 | 1 | 0.0000030000 |
| optimizing | 0.000003 | 0.79 | 1 | 0.0000030000 |
| executing | 0.000002 | 0.53 | 1 | 0.0000020000 |
+----------------------+----------+-------+-------+--------------+
16 rows in set (0.17 sec)
```
效果好多了,通过这个结果可以很容易看到查询时间太长主要是因为花了一大半的时间是"发送数据(Sending data)",这个状态代表的原因非常多,可能是各种不同的服务器活动,包括在关联时搜索匹配的行记录等,这部分很难说能优化节省多少消耗时间。还有一种可能导致查询时间太长的原因,是数据复制到临时表这一步。如果是这种情况,则就要考虑如何改写查询以避免使用临时表,或者提升临时表的使用效率,另外还有一种状态是"结果排序()Sorting result",如果花费的时间占比非常低。那这部分是不值得去优化的。这是一个比较典型的问题,所以一般我们都不建议用户在"优化排序缓冲区(tunning sort buffer)"或者类似的活动上花时间。
尽管剖析报告帮助我们定位到哪些活动花费了最多的时间,但并不会告诉我们为什么会这样,要弄清除为什么状态花费这么多时间,就需要深入下去,继续剖析这一步的子任务。
mysql> SET @query_id = 7;
mysql>mysql> SELECT
STATE,
SUM( DURATION ) AS Total_R,
ROUND( 100 * SUM( DURATION ) / ( SELECT SUM( DURATION ) FROM INFORMATION_SCHEMA.PROFILING WHERE QUERY_ID = @query_id ), 2 ) AS Pct_R,
COUNT(*) AS Calls,
SUM( DURATION )/ COUNT(*) AS `R/Call`
FROM
INFORMATION_SCHEMA.PROFILING
WHERE
QUERY_ID = @query_id
GROUP BY
STATE
ORDER BY
Total_R DESC;
+----------------------+----------+-------+-------+--------------+
| STATE | Total_R | Pct_R | Calls | R/Call |
+----------------------+----------+-------+-------+--------------+
| Sending data | 0.000123 | 32.45 | 1 | 0.0001230000 |
| starting | 0.000077 | 20.32 | 1 | 0.0000770000 |
| Opening tables | 0.000071 | 18.73 | 1 | 0.0000710000 |
| freeing items | 0.000024 | 6.33 | 1 | 0.0000240000 |
| init | 0.000016 | 4.22 | 1 | 0.0000160000 |
| cleaning up | 0.000016 | 4.22 | 1 | 0.0000160000 |
| preparing | 0.000009 | 2.37 | 1 | 0.0000090000 |
| statistics | 0.000008 | 2.11 | 1 | 0.0000080000 |
| checking permissions | 0.000007 | 1.85 | 1 | 0.0000070000 |
| System lock | 0.000006 | 1.58 | 1 | 0.0000060000 |
| closing tables | 0.000005 | 1.32 | 1 | 0.0000050000 |
| query end | 0.000005 | 1.32 | 1 | 0.0000050000 |
| end | 0.000004 | 1.06 | 1 | 0.0000040000 |
| Sorting result | 0.000003 | 0.79 | 1 | 0.0000030000 |
| optimizing | 0.000003 | 0.79 | 1 | 0.0000030000 |
| executing | 0.000002 | 0.53 | 1 | 0.0000020000 |
+----------------------+----------+-------+-------+--------------+
16 rows in set (0.17 sec)
```
效果好多了,通过这个结果可以很容易看到查询时间太长主要是因为花了一大半的时间是"发送数据(Sending data)",这个状态代表的原因非常多,可能是各种不同的服务器活动,包括在关联时搜索匹配的行记录等,这部分很难说能优化节省多少消耗时间。还有一种可能导致查询时间太长的原因,是数据复制到临时表这一步。如果是这种情况,则就要考虑如何改写查询以避免使用临时表,或者提升临时表的使用效率,另外还有一种状态是"结果排序()Sorting result",如果花费的时间占比非常低。那这部分是不值得去优化的。这是一个比较典型的问题,所以一般我们都不建议用户在"优化排序缓冲区(tunning sort buffer)"或者类似的活动上花时间。
尽管剖析报告帮助我们定位到哪些活动花费了最多的时间,但并不会告诉我们为什么会这样,要弄清除为什么状态花费这么多时间,就需要深入下去,继续剖析这一步的子任务。
使用SHOW STATUS。
MySQL的SHOW STATUS命令返回了一些计数器。既有服务器级别的全局计数器,也有基于某个连接的会话级别的计数器。例如其中的Queries在会话开始时为0,每提交一条查询增加1.如果执行SHOW GLOBAL STATUS(注意到新加额GLOBAL关键字),则可以查看服务器级别的从服务器启动时开始计算的查询次数统计。不同计数器的可见范围不一样,不过全局的计数器也会出现SHOW STATUS的结果中,容易被误认为时会话级别的,千万不要搞迷糊了。在使用这个命令时要注意几点,就像前面所讨论的,收集合适级别的测量值是很关键的。如果打算优化从某些特定连接观察到的东西,测量的却是全局级别的数据,就会导致胡乱.MySQL官方手册中对所有变量是会话级还是全局级做了详细的说明。
SHOW STATUS是一个有用的工具,但并不是一款剖析工具。SHOW STATUS的大部分结果都只是一个计数器,可以显示某些互动如读索引的频繁程度,但无法给出消耗了多少时间。SHOW STATUS的结果中只有一条指的是操作时间(Innodb_row_lock_time),而且只能是全局级的,所以还是无法测量绘画级别的工作。尽管SHOW STATUS无法提供基于时间的统计,但对于在执行完查询后观察某些计数器的值还是有帮助的。有时候可以猜测哪些操作代价较高或者消耗的时间较多。最有用的计数器包括句柄计数器(handler counter)、临时文件和表计数器等。下面的例子演示了如何将会话级别的计数器重置为0,然后查询前面(SHOW PROFILE)提到的视图,再检查计数器的结果
MySQL的SHOW STATUS命令返回了一些计数器。既有服务器级别的全局计数器,也有基于某个连接的会话级别的计数器。例如其中的Queries在会话开始时为0,每提交一条查询增加1.如果执行SHOW GLOBAL STATUS(注意到新加额GLOBAL关键字),则可以查看服务器级别的从服务器启动时开始计算的查询次数统计。不同计数器的可见范围不一样,不过全局的计数器也会出现SHOW STATUS的结果中,容易被误认为时会话级别的,千万不要搞迷糊了。在使用这个命令时要注意几点,就像前面所讨论的,收集合适级别的测量值是很关键的。如果打算优化从某些特定连接观察到的东西,测量的却是全局级别的数据,就会导致胡乱.MySQL官方手册中对所有变量是会话级还是全局级做了详细的说明。
SHOW STATUS是一个有用的工具,但并不是一款剖析工具。SHOW STATUS的大部分结果都只是一个计数器,可以显示某些互动如读索引的频繁程度,但无法给出消耗了多少时间。SHOW STATUS的结果中只有一条指的是操作时间(Innodb_row_lock_time),而且只能是全局级的,所以还是无法测量绘画级别的工作。尽管SHOW STATUS无法提供基于时间的统计,但对于在执行完查询后观察某些计数器的值还是有帮助的。有时候可以猜测哪些操作代价较高或者消耗的时间较多。最有用的计数器包括句柄计数器(handler counter)、临时文件和表计数器等。下面的例子演示了如何将会话级别的计数器重置为0,然后查询前面(SHOW PROFILE)提到的视图,再检查计数器的结果
```sql
mysql> FLUSH STATUS;
Query OK, 0 rows affected (0.09 sec)
mysql> SELECT * FROM chat_room ORDER BY id DESC LIMIT 10;
10 rows in set (0.15 sec)
mysql> SHOW STATUS WHERE Variable_name LIKE '%Handler%' OR Variable_name LIKE 'Created%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Created_tmp_disk_tables | 0 |
| Created_tmp_files | 0 |
| Created_tmp_tables | 0 |
| Handler_commit | 1 |
| Handler_delete | 0 |
| Handler_discover | 0 |
| Handler_external_lock | 2 |
| Handler_mrr_init | 0 |
| Handler_prepare | 0 |
| Handler_read_first | 0 |
| Handler_read_key | 1 |
| Handler_read_last | 1 |
| Handler_read_next | 0 |
| Handler_read_prev | 9 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
| Handler_rollback | 0 |
| Handler_savepoint | 0 |
| Handler_savepoint_rollback | 0 |
| Handler_update | 0 |
| Handler_write | 0 |
+----------------------------+-------+
21 rows in set (0.19 sec)
```
从结果中可以看到Created_tmp_tables创建临时表的次数。以及读取记录的前一个指针Handler_read_prev的次数。仅从结果来推测,我们可以判断出,上面的SELECT查询在找到最新一条行记录时,通过读取前一个节点指针来获取记录的。
使用这个技术的时候,要注意SHOW STATUS本身也会创建一个临时表,而且也会通过句柄操作访问此临时表,这会影响到SHOW STATUS结果中对应的数字,而且不同的版本可能行为也不尽相同。
你可能会注意到通过EXPLAIN 查询查询的执行计划也可以获得大部分相同的信息,但EXPLAIN是通过估计得到的结果,而通过计数器则是实际的测量结果。例如EXPLAIN 无法告诉你临时表是否是磁盘表,这和内存临时表的性能差别是很大的。
mysql> FLUSH STATUS;
Query OK, 0 rows affected (0.09 sec)
mysql> SELECT * FROM chat_room ORDER BY id DESC LIMIT 10;
10 rows in set (0.15 sec)
mysql> SHOW STATUS WHERE Variable_name LIKE '%Handler%' OR Variable_name LIKE 'Created%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Created_tmp_disk_tables | 0 |
| Created_tmp_files | 0 |
| Created_tmp_tables | 0 |
| Handler_commit | 1 |
| Handler_delete | 0 |
| Handler_discover | 0 |
| Handler_external_lock | 2 |
| Handler_mrr_init | 0 |
| Handler_prepare | 0 |
| Handler_read_first | 0 |
| Handler_read_key | 1 |
| Handler_read_last | 1 |
| Handler_read_next | 0 |
| Handler_read_prev | 9 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
| Handler_rollback | 0 |
| Handler_savepoint | 0 |
| Handler_savepoint_rollback | 0 |
| Handler_update | 0 |
| Handler_write | 0 |
+----------------------------+-------+
21 rows in set (0.19 sec)
```
从结果中可以看到Created_tmp_tables创建临时表的次数。以及读取记录的前一个指针Handler_read_prev的次数。仅从结果来推测,我们可以判断出,上面的SELECT查询在找到最新一条行记录时,通过读取前一个节点指针来获取记录的。
使用这个技术的时候,要注意SHOW STATUS本身也会创建一个临时表,而且也会通过句柄操作访问此临时表,这会影响到SHOW STATUS结果中对应的数字,而且不同的版本可能行为也不尽相同。
你可能会注意到通过EXPLAIN 查询查询的执行计划也可以获得大部分相同的信息,但EXPLAIN是通过估计得到的结果,而通过计数器则是实际的测量结果。例如EXPLAIN 无法告诉你临时表是否是磁盘表,这和内存临时表的性能差别是很大的。
使用慢查询
Percona Server对慢查询日志做了哪些改进?比如"使用SHOW PROFILE"执行相同查询后可以抓取到的结果
```c
# Time:110905 17:03:18
# User@Host:root[root] @localhost[127.0.01]
# Thread_id:7 Schema:saklia Last_errono:0 Killed:0
# Query_time:0.166872 Lock_time:0.000552 Rows_sent:997 Rows_examined:24861
Rows_affected:0 Rows_read:997
# Bytes_sent:216528 Tmp_tables:3 Tmp_disk_tables:2 Tmp_table_sizes:11627188
# InnoDB_trx_id:191E
# QC_Hit:NO Full_scan:Yes Full_jooin:No Tmp_table:Yes Tmp_table_on_disk:Yes
# Filesort:yes Filesort_on_disk:No Merge_passes:0
# InnoDB_IO_r_ops:0 InnoDB_IO_r_bytes:0 InnoDB_IO_r_wait:0.000000
# InnoDB_rec_lock_wait:0.000000 InnoDB_queue_wait:0.000000
# InnoDB_pages_distinct:20
# PROFILE_VALUES ...Copying to tmp table:0.090623...
SET timestamp=1315256598
SELECT * FROM sakila.nice_but_slower_film_list;
```
从这里可以看到查询确实以供创建了三个临时表,其中两个是磁盘临时表。而SHOW PROFILE看起来则隐藏了信息(可能由于服务i去执行查询的方式有不一样地方造成的)。最后对该查询执行SHOW PROFILE的数据也会写入到日志中,所以在Percona Server中甚至可以记录SHOW PROFILE的细节信息。
另外可以看到,慢查询日志中详细记录的条目包含了SHOW PROFILE和SHOW STATUS所有的输出,并且还有更多的信息。所以通过pt-query-digest发现"坏"查询后,在慢查询日志中可以获得足够有用的信息。查看pt-query-digest的报告时其标题部分一般会有如下输出:
```c
# Query 1:0 OPS, 0x concurrency, ID0xEE758C5E0D7EADEE at byte 3214——
```
可以通过这里的字节偏移值(3214)直接跳转到日志的对应部分,例如用下面这样的命令即可:
```c
tail -c +3214 /path/to/query.log | head -n100
```
这样就可以直接跳转到细节部分了,另外pt-query-digest能够处理Percona Server在慢查询日志中增加的所有键值对,并且会自动在报告中打印更多的细节信息
Percona Server对慢查询日志做了哪些改进?比如"使用SHOW PROFILE"执行相同查询后可以抓取到的结果
```c
# Time:110905 17:03:18
# User@Host:root[root] @localhost[127.0.01]
# Thread_id:7 Schema:saklia Last_errono:0 Killed:0
# Query_time:0.166872 Lock_time:0.000552 Rows_sent:997 Rows_examined:24861
Rows_affected:0 Rows_read:997
# Bytes_sent:216528 Tmp_tables:3 Tmp_disk_tables:2 Tmp_table_sizes:11627188
# InnoDB_trx_id:191E
# QC_Hit:NO Full_scan:Yes Full_jooin:No Tmp_table:Yes Tmp_table_on_disk:Yes
# Filesort:yes Filesort_on_disk:No Merge_passes:0
# InnoDB_IO_r_ops:0 InnoDB_IO_r_bytes:0 InnoDB_IO_r_wait:0.000000
# InnoDB_rec_lock_wait:0.000000 InnoDB_queue_wait:0.000000
# InnoDB_pages_distinct:20
# PROFILE_VALUES ...Copying to tmp table:0.090623...
SET timestamp=1315256598
SELECT * FROM sakila.nice_but_slower_film_list;
```
从这里可以看到查询确实以供创建了三个临时表,其中两个是磁盘临时表。而SHOW PROFILE看起来则隐藏了信息(可能由于服务i去执行查询的方式有不一样地方造成的)。最后对该查询执行SHOW PROFILE的数据也会写入到日志中,所以在Percona Server中甚至可以记录SHOW PROFILE的细节信息。
另外可以看到,慢查询日志中详细记录的条目包含了SHOW PROFILE和SHOW STATUS所有的输出,并且还有更多的信息。所以通过pt-query-digest发现"坏"查询后,在慢查询日志中可以获得足够有用的信息。查看pt-query-digest的报告时其标题部分一般会有如下输出:
```c
# Query 1:0 OPS, 0x concurrency, ID0xEE758C5E0D7EADEE at byte 3214——
```
可以通过这里的字节偏移值(3214)直接跳转到日志的对应部分,例如用下面这样的命令即可:
```c
tail -c +3214 /path/to/query.log | head -n100
```
这样就可以直接跳转到细节部分了,另外pt-query-digest能够处理Percona Server在慢查询日志中增加的所有键值对,并且会自动在报告中打印更多的细节信息
使用Performance Schema
在MySQL5.5中新增的Performance Schema表还不支持查询级别的剖析信息。Performance Schema还是非常新的特性,并且还在快速开发中,未来的版本中将会包含更多的功能。尽管如此,MySQL 5.5的初始版本已经包含了狠毒偶有去的信息。例如,下面的查询显示了系统中等待的主要原因:
```sql
mysql> SELECT event_name, count_star, sum_timer_wait FROM `performance_schema`.`events_waits_summary_global_by_event_name` ORDER BY sum_timer_wait DESC LIMIT 5;
```
目前还有一些限制,使得Performance Schema还无法被当作一个通用的剖析工具,首先它还无法提供查询执行阶段的细节信息和计时信息,而前面提供的很多现有的工具都已经能做到这些了。其次,还没有经过长事件、大规模使用的验证,并且自身的开销也还比较大,多数比较保守的用户还对此有疑问。
最后,对大多数用户来说,直接通过Performance Schema的裸数据获得有用的结果相对来说过于复杂和底层。到目前为止实现的这个特性,主要是为了测量当为提升服务器性能而修改MySQL源代码时使用,包括等待和互斥锁。MySQL5.5中的特性读与高级用户也很有价值,而不仅仅为开发者使用。在MySQL5.6或者以后的版本中,Performance Schema将会包含更多的功能,再加上一些方便使用的工具。而且Oracle将其实现成表的形式,可以通过SQL访问,这样用户可以方便地访问有用的数据.但其目前还无法立即取代慢查询日志等其他工具用于服务器和查询的性能优化。
在MySQL5.5中新增的Performance Schema表还不支持查询级别的剖析信息。Performance Schema还是非常新的特性,并且还在快速开发中,未来的版本中将会包含更多的功能。尽管如此,MySQL 5.5的初始版本已经包含了狠毒偶有去的信息。例如,下面的查询显示了系统中等待的主要原因:
```sql
mysql> SELECT event_name, count_star, sum_timer_wait FROM `performance_schema`.`events_waits_summary_global_by_event_name` ORDER BY sum_timer_wait DESC LIMIT 5;
```
目前还有一些限制,使得Performance Schema还无法被当作一个通用的剖析工具,首先它还无法提供查询执行阶段的细节信息和计时信息,而前面提供的很多现有的工具都已经能做到这些了。其次,还没有经过长事件、大规模使用的验证,并且自身的开销也还比较大,多数比较保守的用户还对此有疑问。
最后,对大多数用户来说,直接通过Performance Schema的裸数据获得有用的结果相对来说过于复杂和底层。到目前为止实现的这个特性,主要是为了测量当为提升服务器性能而修改MySQL源代码时使用,包括等待和互斥锁。MySQL5.5中的特性读与高级用户也很有价值,而不仅仅为开发者使用。在MySQL5.6或者以后的版本中,Performance Schema将会包含更多的功能,再加上一些方便使用的工具。而且Oracle将其实现成表的形式,可以通过SQL访问,这样用户可以方便地访问有用的数据.但其目前还无法立即取代慢查询日志等其他工具用于服务器和查询的性能优化。
使用性能剖析。
当获得服务器或者查询的剖析报告后,怎么使用?好的剖析报告能够将潜在的问题显示出来,但最终的解决方案还需要用户来决定(尽管报告可能会给出建议)。优化查询时,用户需要对服务器如何执行查询有较深的了解。剖析报告能够尽可能多地收集需要的信息、给出诊断问题的正确方向,以及为其他诸如EXPLAIN等工具提供基础信息。
尽管一个拥有完美测量信息的剖析报告可以i让事情变得简单,但现有系统通常都没有完美的测量支持。例如,我们虽然推断出是临时表和没有索引的读导致查询的响应事件过长,但却没有明确的证据。因为无法测量所有需要的信息,或者测量的范围不正确,有些问题就很难解决。例如,可能没有集中在需要优化的地方测量,而是测量了服务器层面的活动,或者测量的是查询开始之前的计数器,而不是查询开始后的数据。
也有其他的可能性。设想一下正在分析慢查询日志,发现了一个很简单的查询正常情况下都非常快,却有几次非常不合理地执行了很长事件。手工重新执行一遍,发现也非常快,然后使用EXPLAIN 查询其执行计划,也正确地使用了索引。然后尝试修改WHERE条件中使用不同的值,以排除缓存命中的可能,也没有发现什么问题,这可能是什么原因呢?
如果使用官方版本的MySQL,慢查询日志中没有执行计划或者详细的时间信息,对于偶尔记录到的这几次查询异常慢的问题,很难知道其原因在哪里,因为信息优先,可能是系统中有其他东西消耗了资源,比如正在备份,也可能是某种类型的所或者争用阻塞了查询的进度。
当获得服务器或者查询的剖析报告后,怎么使用?好的剖析报告能够将潜在的问题显示出来,但最终的解决方案还需要用户来决定(尽管报告可能会给出建议)。优化查询时,用户需要对服务器如何执行查询有较深的了解。剖析报告能够尽可能多地收集需要的信息、给出诊断问题的正确方向,以及为其他诸如EXPLAIN等工具提供基础信息。
尽管一个拥有完美测量信息的剖析报告可以i让事情变得简单,但现有系统通常都没有完美的测量支持。例如,我们虽然推断出是临时表和没有索引的读导致查询的响应事件过长,但却没有明确的证据。因为无法测量所有需要的信息,或者测量的范围不正确,有些问题就很难解决。例如,可能没有集中在需要优化的地方测量,而是测量了服务器层面的活动,或者测量的是查询开始之前的计数器,而不是查询开始后的数据。
也有其他的可能性。设想一下正在分析慢查询日志,发现了一个很简单的查询正常情况下都非常快,却有几次非常不合理地执行了很长事件。手工重新执行一遍,发现也非常快,然后使用EXPLAIN 查询其执行计划,也正确地使用了索引。然后尝试修改WHERE条件中使用不同的值,以排除缓存命中的可能,也没有发现什么问题,这可能是什么原因呢?
如果使用官方版本的MySQL,慢查询日志中没有执行计划或者详细的时间信息,对于偶尔记录到的这几次查询异常慢的问题,很难知道其原因在哪里,因为信息优先,可能是系统中有其他东西消耗了资源,比如正在备份,也可能是某种类型的所或者争用阻塞了查询的进度。
其他剖析工具。
1.使用USER_STATISTICS表。
Percona Server和MariaDB都引入了一些额外的对象级别使用统计的INFORMATION_SCHEMA表,这些最初是由Google开发的。这些表对于查询服务器各部分的实际使用情况非常有帮助。在一个大型企业中,DBA负责管理数据库,但其对开发缺少话语权,那么通过这些表就可以对数据库活动那个进行测量和审计,并且强制执行使用策略。对于像共享主机环境这样的多租户环境也同样有用。另外,在查找性能问题时,这些表也可以帮助找出数据库中什么地方花费了最多的时间,或者什么表或索引表使用得最皮肤那你,抑或最不频繁,下面就是这些表:
```sql
mysql> SHOW TABLES FROM information_schema LIKE '%_STATISTICS';
+---------------------------------------------+
| Tables_in_information_schema (%_STATISTICS) |
+---------------------------------------------+
| TABLE_STATISTICS |
| INDEX_STATISTICS |
| IO_STATISTICS |
| PERF_STATISTICS |
+---------------------------------------------+
4 rows in set (0.15 sec)
```
有几个要点要说明一下:
1.可以查找使用得最多或者使用得最少的表和索引,通过读取次数或者更新次数,或者两者一起排序
2.可以查找出从未使用的索引,可以考虑删除之
3.可以看看复制用户的CONNECTED_TIME和BUSY_TIME,以确认复制是否会很难跟上主库的进度
2.使用strace
strace工具可以调查系统调用的情况,有好几种可以使用的方法,其中一种是计算系统调用的时间并打印出来:
```c
strace -cfp $(pidof mysqld)
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00 0.012014 6 202 12 read
00.00 0.000000 0 2 0 write
00.00 0.000000 0 4 0 ioctl
00.00 0.000000 0 1 0 mmap
------ ----------- ----------- --------- --------- ----------------
100.00 0.012014 209 12 total
```
read 系统调用被调用了 202 次,总共耗时大约 0.012 秒,每次调用平均用时 6 微秒,并且有 12 次错误。
write 系统调用被调用了 2 次,没有错误发生。
ioctl 系统调用被调用了 4 次,没有错误发生。
mmap 系统调用被调用了 1 次,没有错误发生。
这种用法和oprofile有点像。但是oprofile还可以剖析程序的内部符号,而不仅仅是系统调用。另外,strace拦截系统调用使用的是不同oprofile的技术,这会有一些不可预期性,开销也更大些。strace度量时使用的实际时间,而oprofile使用的是花费CPU周期。
举个例子,当IO等待出现问题的时候,strace能将它们希纳是出来,因为它诸如read或者pread64这样的系统调用开始计时,直到调用结束。但opfile不会这样,因为IO系统调用并不会真正地消耗CPU周期,而只是等待IO完成而已。
strace对像mysqld这样有大量线程的场景会产生一些副作用。当strace附加上去后,mysqld的运行会变得很慢,因此不适合在产品环境中使用。但在某些场景下还是相当有用的,Percona Toolkit中有一个叫作pt-ioprofile的工具就是使用strace来生成IO活动的剖析报告的。
1.使用USER_STATISTICS表。
Percona Server和MariaDB都引入了一些额外的对象级别使用统计的INFORMATION_SCHEMA表,这些最初是由Google开发的。这些表对于查询服务器各部分的实际使用情况非常有帮助。在一个大型企业中,DBA负责管理数据库,但其对开发缺少话语权,那么通过这些表就可以对数据库活动那个进行测量和审计,并且强制执行使用策略。对于像共享主机环境这样的多租户环境也同样有用。另外,在查找性能问题时,这些表也可以帮助找出数据库中什么地方花费了最多的时间,或者什么表或索引表使用得最皮肤那你,抑或最不频繁,下面就是这些表:
```sql
mysql> SHOW TABLES FROM information_schema LIKE '%_STATISTICS';
+---------------------------------------------+
| Tables_in_information_schema (%_STATISTICS) |
+---------------------------------------------+
| TABLE_STATISTICS |
| INDEX_STATISTICS |
| IO_STATISTICS |
| PERF_STATISTICS |
+---------------------------------------------+
4 rows in set (0.15 sec)
```
有几个要点要说明一下:
1.可以查找使用得最多或者使用得最少的表和索引,通过读取次数或者更新次数,或者两者一起排序
2.可以查找出从未使用的索引,可以考虑删除之
3.可以看看复制用户的CONNECTED_TIME和BUSY_TIME,以确认复制是否会很难跟上主库的进度
2.使用strace
strace工具可以调查系统调用的情况,有好几种可以使用的方法,其中一种是计算系统调用的时间并打印出来:
```c
strace -cfp $(pidof mysqld)
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00 0.012014 6 202 12 read
00.00 0.000000 0 2 0 write
00.00 0.000000 0 4 0 ioctl
00.00 0.000000 0 1 0 mmap
------ ----------- ----------- --------- --------- ----------------
100.00 0.012014 209 12 total
```
read 系统调用被调用了 202 次,总共耗时大约 0.012 秒,每次调用平均用时 6 微秒,并且有 12 次错误。
write 系统调用被调用了 2 次,没有错误发生。
ioctl 系统调用被调用了 4 次,没有错误发生。
mmap 系统调用被调用了 1 次,没有错误发生。
这种用法和oprofile有点像。但是oprofile还可以剖析程序的内部符号,而不仅仅是系统调用。另外,strace拦截系统调用使用的是不同oprofile的技术,这会有一些不可预期性,开销也更大些。strace度量时使用的实际时间,而oprofile使用的是花费CPU周期。
举个例子,当IO等待出现问题的时候,strace能将它们希纳是出来,因为它诸如read或者pread64这样的系统调用开始计时,直到调用结束。但opfile不会这样,因为IO系统调用并不会真正地消耗CPU周期,而只是等待IO完成而已。
strace对像mysqld这样有大量线程的场景会产生一些副作用。当strace附加上去后,mysqld的运行会变得很慢,因此不适合在产品环境中使用。但在某些场景下还是相当有用的,Percona Toolkit中有一个叫作pt-ioprofile的工具就是使用strace来生成IO活动的剖析报告的。
总结。
1.定义性能最有效的方法是响应时间
2.如果无法测量就无法有效地优化,所以性能优化工作需要基于高质量、全方位及完整的响应时间测量
3.测量的最佳开始点是应用程序,而不是数据库。即使问题出在底层的数据库,借助良好的测量也可以很容易地发现问题
4.大多数系统无法完整地测量,测量有时候也会有错误的结果。但也可以想办法绕过一些限制,并得到好的结果(但是要能意识到所使用的方法的缺陷和不确定性在哪里)
5.完整的测量会产生大量需要分析的数据,所以需要用到剖析器。这是最佳的工具,可以帮助将重要的问题冒泡到前面,这样就可以决定从哪里开始分析会比较好
6.剖析报告是一种汇总信息,掩盖和丢弃了太多细节。而且它不会告诉你缺少了什么,所以完全依赖剖析报告也是不明智的
7.有两种消耗时间的操作:工作或者等待。大多数剖析器只能测量因为工作而消耗的时间,所以等待分析有时候是很有用的补充,尤其是当CPU利用率很低但工作却一直无法完成的时候
8.优化和提升是两回事。当继续提升的成本超过收益的时候,应当停止优化
9.注意你的直觉,但应该只根据直觉来指导解决问题的思路,而不是用于确定系统的问题。决策应当尽量基于数据而不是感觉。
总的来说,解决性能问题的方法首先是要澄清问题,然后选择合适的技术来解答这些问题。如果你想尝试提升服务器的总体性能,那么一个比较好的七点是将所有查询记录到日志中,然后利用pt-query-digest工具生成系统级别的剖析报告。如果是要追查某些性能低下的查询,记录和剖析得方法也会有帮助。可以把精力放在寻找哪些消耗时间最多的、导致了糟糕的用户体验的,或者那些高度变化的,抑或有奇怪的响应时间直方图的查询。当找到了这些"坏"查询时,要钻取pt-query-digest报告中包含的该查询的详细信息,或者使用SHOW PROFILE及其他诸如EXPLAIN这样的工具。
如果找不到这些查询性能低下的原因,那么也可能时遇到了服务器级别的性能问题。这是,可以较高精度测量和回直服务器状态计数器的细节信息。如果通过这样的分析重现了问题,则应该通过同样的数据制定一个可靠的触发条件,来收集更多的诊断数据。多花费一点时间来确定可靠的触发条件,尽量避免漏检或者误报。如果已经可以捕获故障活动期间的数据,但还是无法找到其根本原因,则要么尝试捕获更多的数据,要么尝试寻求帮助。
我们无法完整地测量工作系统,但说到底它们都是某种状态机,所以只要足够细心,逻辑清晰并且坚持下去,通常来说都能得到想要的结果。要注意的时不要把原因和结果搞混了,而且在确认问题之前也不要随便针对系统做变动。
理论上纯粹的自顶向下的方法分析和详尽的测量只是理想的情况,而我们常常需要处理的是真实系统。真实系统是复杂且无法充分测量的,所以我们只能根据情况尽力而为。使用诸如pt-query-digest和MySQL企业监控器的查询分析其这样的工具并不完美,通常都不会给出问题根源的直接证据。但真的掌握了以后,已经足以完成大部分的优化诊断工作了。
1.定义性能最有效的方法是响应时间
2.如果无法测量就无法有效地优化,所以性能优化工作需要基于高质量、全方位及完整的响应时间测量
3.测量的最佳开始点是应用程序,而不是数据库。即使问题出在底层的数据库,借助良好的测量也可以很容易地发现问题
4.大多数系统无法完整地测量,测量有时候也会有错误的结果。但也可以想办法绕过一些限制,并得到好的结果(但是要能意识到所使用的方法的缺陷和不确定性在哪里)
5.完整的测量会产生大量需要分析的数据,所以需要用到剖析器。这是最佳的工具,可以帮助将重要的问题冒泡到前面,这样就可以决定从哪里开始分析会比较好
6.剖析报告是一种汇总信息,掩盖和丢弃了太多细节。而且它不会告诉你缺少了什么,所以完全依赖剖析报告也是不明智的
7.有两种消耗时间的操作:工作或者等待。大多数剖析器只能测量因为工作而消耗的时间,所以等待分析有时候是很有用的补充,尤其是当CPU利用率很低但工作却一直无法完成的时候
8.优化和提升是两回事。当继续提升的成本超过收益的时候,应当停止优化
9.注意你的直觉,但应该只根据直觉来指导解决问题的思路,而不是用于确定系统的问题。决策应当尽量基于数据而不是感觉。
总的来说,解决性能问题的方法首先是要澄清问题,然后选择合适的技术来解答这些问题。如果你想尝试提升服务器的总体性能,那么一个比较好的七点是将所有查询记录到日志中,然后利用pt-query-digest工具生成系统级别的剖析报告。如果是要追查某些性能低下的查询,记录和剖析得方法也会有帮助。可以把精力放在寻找哪些消耗时间最多的、导致了糟糕的用户体验的,或者那些高度变化的,抑或有奇怪的响应时间直方图的查询。当找到了这些"坏"查询时,要钻取pt-query-digest报告中包含的该查询的详细信息,或者使用SHOW PROFILE及其他诸如EXPLAIN这样的工具。
如果找不到这些查询性能低下的原因,那么也可能时遇到了服务器级别的性能问题。这是,可以较高精度测量和回直服务器状态计数器的细节信息。如果通过这样的分析重现了问题,则应该通过同样的数据制定一个可靠的触发条件,来收集更多的诊断数据。多花费一点时间来确定可靠的触发条件,尽量避免漏检或者误报。如果已经可以捕获故障活动期间的数据,但还是无法找到其根本原因,则要么尝试捕获更多的数据,要么尝试寻求帮助。
我们无法完整地测量工作系统,但说到底它们都是某种状态机,所以只要足够细心,逻辑清晰并且坚持下去,通常来说都能得到想要的结果。要注意的时不要把原因和结果搞混了,而且在确认问题之前也不要随便针对系统做变动。
理论上纯粹的自顶向下的方法分析和详尽的测量只是理想的情况,而我们常常需要处理的是真实系统。真实系统是复杂且无法充分测量的,所以我们只能根据情况尽力而为。使用诸如pt-query-digest和MySQL企业监控器的查询分析其这样的工具并不完美,通常都不会给出问题根源的直接证据。但真的掌握了以后,已经足以完成大部分的优化诊断工作了。
Schema与数据类型优化
概述。
良好的逻辑设计和物理设计是高性能的基石,应该根据系统将要执行的查询语句来设计schema,这往往需要权衡各种因素。例如,反范式的设计可以加快某些类型的查询,但同时可能使另一些类型的查询变慢。比如添加计数器和汇总表是一种很好的优化查询的方式,但这些表的维护成本可能会很高。MySQL独有的特性和实现细节对性能的影响也很大。
良好的逻辑设计和物理设计是高性能的基石,应该根据系统将要执行的查询语句来设计schema,这往往需要权衡各种因素。例如,反范式的设计可以加快某些类型的查询,但同时可能使另一些类型的查询变慢。比如添加计数器和汇总表是一种很好的优化查询的方式,但这些表的维护成本可能会很高。MySQL独有的特性和实现细节对性能的影响也很大。
选择优化的数据类型。
MySQL支持的数据类型非常多,选择正确的数据类型对于获取高性能至关重要。不管存储哪种类型的数据,下面几个简单的原则都有助于做出更好的选择。
1.更好的通常更好
一般情况下,应该尽量使用可以正确存储数据更小的数据类型(例如只需要存0~200,tinyint unsigned更好)。更小的数据类型通常更快,因为它们占用更少的磁盘、内存和CPU缓存,并且处理时需要的CPU周期也更少。
但是要确保没有嘀咕需要存储的值范围,因为在schema中的多个地方增加数据类型的范围是一个非常耗时和痛苦的操作。如果无法确定哪个数据类型是最好的,就选择你认为不会超过范围的最小类型。(如果系统不是很忙或者存储的数据量不多,或者是在可以轻易修改设计的早期阶段,那之后修改数据类型也比较容易)
2.简单就好
简单数据类型的操作通常需要更少的CPU周期。例如,整型比字符操作代价更低,因为字符集和校对规则(排序规则)使字符比较比整型比较更复杂。有两个例子:一个是应该使用mySQL的内建类型(date,time,datetime)而不是字符串来存储日期和时间,另外一个是应该使用整型存储IP地址。
3.尽量避免NULL
很多表都包含可为NULL(空值)的列,即使应用程序并不需要保存NULL也是如此,这是因为可为NULL是列的默认属性。通常情况下最好指定列为NOT NULL,除非真的需要存储NULL值。如果查询中包含可为NULL的列,对MySQL来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊处理。当可为NULl的列被索引时,每个索引记录需要一个额外的字节,在MyISAM里甚至还可能导致固定大小的索引(例如只有一个整数列的索引)变成可变大小的索引。
通常把可为NULl的列改为NOT NULL带来的性能提升比较小,所以(调优时)没有必要首先在现有schema中查找并修改掉这种情况,除非确定这回导致问题。但是,如果计划在列上键索引,就应该尽量避免设计成可为NULL的列。
当然也有例外,例如值得一提的时,InnoDB使用单独的位(bit)存储NULL值,所以对于稀疏数据(很多值为NULL,只有少数行的列有非NULL值)有很好的空间效率。但这一点不适用于MyISAM
在为列选择数据类型时,第一步需要确定合适的大类型:数字、字符串、时间等。这通常是很简单的。但是我们会提到一些特殊的不是那么直观的例子。
下一步是选择具体类型。很多MySQL的数据类型可以存储相同类型的数据,这是存储的长度和范围不一样、允许的精度不同,或者需要的物理空间(磁盘和内存空间)不同,相同大类型的不同子类型数据有时也有一些特殊的行为和属性。
然而TIMESTAMP只使用DATETIME一半的存储空间,并且会根据时区变化,具有特殊的自动更新能力。另一方面,TIMESTAMP允许的时间范围要小得多,有时候它的特殊能力会成为阻碍。
MySQL支持的数据类型非常多,选择正确的数据类型对于获取高性能至关重要。不管存储哪种类型的数据,下面几个简单的原则都有助于做出更好的选择。
1.更好的通常更好
一般情况下,应该尽量使用可以正确存储数据更小的数据类型(例如只需要存0~200,tinyint unsigned更好)。更小的数据类型通常更快,因为它们占用更少的磁盘、内存和CPU缓存,并且处理时需要的CPU周期也更少。
但是要确保没有嘀咕需要存储的值范围,因为在schema中的多个地方增加数据类型的范围是一个非常耗时和痛苦的操作。如果无法确定哪个数据类型是最好的,就选择你认为不会超过范围的最小类型。(如果系统不是很忙或者存储的数据量不多,或者是在可以轻易修改设计的早期阶段,那之后修改数据类型也比较容易)
2.简单就好
简单数据类型的操作通常需要更少的CPU周期。例如,整型比字符操作代价更低,因为字符集和校对规则(排序规则)使字符比较比整型比较更复杂。有两个例子:一个是应该使用mySQL的内建类型(date,time,datetime)而不是字符串来存储日期和时间,另外一个是应该使用整型存储IP地址。
3.尽量避免NULL
很多表都包含可为NULL(空值)的列,即使应用程序并不需要保存NULL也是如此,这是因为可为NULL是列的默认属性。通常情况下最好指定列为NOT NULL,除非真的需要存储NULL值。如果查询中包含可为NULL的列,对MySQL来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊处理。当可为NULl的列被索引时,每个索引记录需要一个额外的字节,在MyISAM里甚至还可能导致固定大小的索引(例如只有一个整数列的索引)变成可变大小的索引。
通常把可为NULl的列改为NOT NULL带来的性能提升比较小,所以(调优时)没有必要首先在现有schema中查找并修改掉这种情况,除非确定这回导致问题。但是,如果计划在列上键索引,就应该尽量避免设计成可为NULL的列。
当然也有例外,例如值得一提的时,InnoDB使用单独的位(bit)存储NULL值,所以对于稀疏数据(很多值为NULL,只有少数行的列有非NULL值)有很好的空间效率。但这一点不适用于MyISAM
在为列选择数据类型时,第一步需要确定合适的大类型:数字、字符串、时间等。这通常是很简单的。但是我们会提到一些特殊的不是那么直观的例子。
下一步是选择具体类型。很多MySQL的数据类型可以存储相同类型的数据,这是存储的长度和范围不一样、允许的精度不同,或者需要的物理空间(磁盘和内存空间)不同,相同大类型的不同子类型数据有时也有一些特殊的行为和属性。
然而TIMESTAMP只使用DATETIME一半的存储空间,并且会根据时区变化,具有特殊的自动更新能力。另一方面,TIMESTAMP允许的时间范围要小得多,有时候它的特殊能力会成为阻碍。
整数类型。
有两种类型的数字:整数(whole number)和实数(real number)。如果存储整数,可以使用这几种整数类型:TINYINT,SMALLINT, MEDIUMINT,INT,BIGINT.分别使用8,16,24,32,64存储空间。它们可以存储的值的范围从-2^(N-1)到2^(N-1)-1,其中N是存储空间的位数。
整数类型有可选的UNSIGNED属性,表示不允许负值,这大致可以使正数的上限提高已被。例如TINYINT UNSIGNED可以存储的范围是0~255,而TINYINY的存储范围是-128~127.
有符号和无符号类型使用相同的存储空间,并具有相同的性能,因此可以根据实际情况选择合适的类型。你的选择决定MySQL是怎么在内存和磁盘中保存数据的,然而,整数计算一半使用64位的BIGINT整数,即使在32位环境也是如此。(一些聚合函数是例外,它们使用DECIMAL或者DOUBLE进行计算)。
MySQL可以为整数类型指定宽度,例如INT(11),对大多数应用这是没有意义的,他不会限制值得合法范围,只是规定了MySQL的一些交互工具(例如MySQL命令行客户端)用来显示字符的个数。对于存储和计算来说INT(1)和INT(20)是相同的。
有两种类型的数字:整数(whole number)和实数(real number)。如果存储整数,可以使用这几种整数类型:TINYINT,SMALLINT, MEDIUMINT,INT,BIGINT.分别使用8,16,24,32,64存储空间。它们可以存储的值的范围从-2^(N-1)到2^(N-1)-1,其中N是存储空间的位数。
整数类型有可选的UNSIGNED属性,表示不允许负值,这大致可以使正数的上限提高已被。例如TINYINT UNSIGNED可以存储的范围是0~255,而TINYINY的存储范围是-128~127.
有符号和无符号类型使用相同的存储空间,并具有相同的性能,因此可以根据实际情况选择合适的类型。你的选择决定MySQL是怎么在内存和磁盘中保存数据的,然而,整数计算一半使用64位的BIGINT整数,即使在32位环境也是如此。(一些聚合函数是例外,它们使用DECIMAL或者DOUBLE进行计算)。
MySQL可以为整数类型指定宽度,例如INT(11),对大多数应用这是没有意义的,他不会限制值得合法范围,只是规定了MySQL的一些交互工具(例如MySQL命令行客户端)用来显示字符的个数。对于存储和计算来说INT(1)和INT(20)是相同的。
实数类型。
实数是带有小数部分的数字。然而,它们不只是为了存储小数部分;也可以使用DECIMAL存储比BIGINT还大的整数。MySQL既支持精确类型,也支持不精确类型。
FLOAT和DOUBLE类型支持使用标准的浮点运算进行近似计算。如果需要直到浮点运算时怎么计算的,则需要研究所使用的平台的浮点数的具体实现。DECIMAL类型用于存储精确的小数。在MySQL 5.0和更高版本,DECIMAL类型支持精确计算。MySQL4.1及更早版本则使用浮点运算来实现DECIMAL的计算,这样做会因为精度损失导致一些奇怪的结果。在这些版本的MySQL中,DECIMAL只是一个"存储类型"。
因为CPU不支持对DECIMAL的直接计算,所以在MySQL5.0以及更高版本中,MySQL服务器自身实现了DECIMAL的高精度计算。相对而言,CPU直接支持原生浮点计算,所以浮点运算明显更快。
浮点和DECIMAL类型都可以指定精度。对DECIMAL列,可以指定小数点前后所允许的最大位数。这会影响列的空间消耗。MySQL5.0和更高版本将数字打包保存到一个二进制字符串中(每4个字节存9个数字)。例如DECIMAL(18,9)小数点两边将个存储9个数字,一共使用9个字节:小数点前的数字用4个字节,小数点后的数字用4个字节,小数点本身占1个字节。
MySQL5.0和更高版本中的DECIMAL类型允许最多65个数字。而早期的MySQL版本中,这个限制时254个数字,并且保存为未压缩的字符串(每个数字一个字节)。然而,这些(早期)版本实际上并不能在计算中使用这么大的数字,因为DECIMAL只是一种存储格式,在计算中DECIMAL会转换为DOUBLE类型。
有多种方法可以指定浮点列所需要的精度,这回使得MySQL悄悄选择不同的数据类型,或者在存储时对值进行取舍。这些精度定义时非标准的,所以建议只指定数据类型,不指定精度。
浮点类型在存储同样范围的值时,通常比DECIMAL使用更少的空间。FLOAT使用4个字节存储。DOUBLE只能用8个字节,相比FLOAT有更高的精度和更大的范围。和整数类型一样,能选择的只是存储类型;MySQL使用DOUBLE作为内部浮点计算的类型。
因为需要额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用DECIMAL——例如存储财务数据。但在数据量比较大的时候,可以高铝使用BIGINT代替DECIMAL,将需要存储的货币单位根据小数的位数乘以相应的倍数即可。假设要存储财务数据精确到万分之一,则可以把所有金额乘以一百万,然后将结果存储在BIGINT里,这样就可以同时避免浮点存储计算不精确和DECIMAL精确计算代价高的问题
实数是带有小数部分的数字。然而,它们不只是为了存储小数部分;也可以使用DECIMAL存储比BIGINT还大的整数。MySQL既支持精确类型,也支持不精确类型。
FLOAT和DOUBLE类型支持使用标准的浮点运算进行近似计算。如果需要直到浮点运算时怎么计算的,则需要研究所使用的平台的浮点数的具体实现。DECIMAL类型用于存储精确的小数。在MySQL 5.0和更高版本,DECIMAL类型支持精确计算。MySQL4.1及更早版本则使用浮点运算来实现DECIMAL的计算,这样做会因为精度损失导致一些奇怪的结果。在这些版本的MySQL中,DECIMAL只是一个"存储类型"。
因为CPU不支持对DECIMAL的直接计算,所以在MySQL5.0以及更高版本中,MySQL服务器自身实现了DECIMAL的高精度计算。相对而言,CPU直接支持原生浮点计算,所以浮点运算明显更快。
浮点和DECIMAL类型都可以指定精度。对DECIMAL列,可以指定小数点前后所允许的最大位数。这会影响列的空间消耗。MySQL5.0和更高版本将数字打包保存到一个二进制字符串中(每4个字节存9个数字)。例如DECIMAL(18,9)小数点两边将个存储9个数字,一共使用9个字节:小数点前的数字用4个字节,小数点后的数字用4个字节,小数点本身占1个字节。
MySQL5.0和更高版本中的DECIMAL类型允许最多65个数字。而早期的MySQL版本中,这个限制时254个数字,并且保存为未压缩的字符串(每个数字一个字节)。然而,这些(早期)版本实际上并不能在计算中使用这么大的数字,因为DECIMAL只是一种存储格式,在计算中DECIMAL会转换为DOUBLE类型。
有多种方法可以指定浮点列所需要的精度,这回使得MySQL悄悄选择不同的数据类型,或者在存储时对值进行取舍。这些精度定义时非标准的,所以建议只指定数据类型,不指定精度。
浮点类型在存储同样范围的值时,通常比DECIMAL使用更少的空间。FLOAT使用4个字节存储。DOUBLE只能用8个字节,相比FLOAT有更高的精度和更大的范围。和整数类型一样,能选择的只是存储类型;MySQL使用DOUBLE作为内部浮点计算的类型。
因为需要额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用DECIMAL——例如存储财务数据。但在数据量比较大的时候,可以高铝使用BIGINT代替DECIMAL,将需要存储的货币单位根据小数的位数乘以相应的倍数即可。假设要存储财务数据精确到万分之一,则可以把所有金额乘以一百万,然后将结果存储在BIGINT里,这样就可以同时避免浮点存储计算不精确和DECIMAL精确计算代价高的问题
字符串类型。
MySQL支持多种字符串类型,每种类型还有很多变种。这些数据类型在4.1和5.0版本发生了很大的变化,使得情况更加复杂。从MySQL4.1开始,每个字符串列可以定义自己的字符集和排序规则,或者说校对规则(collation).这些东西会很大程度上影响性能。
1.VARCHAR和CHAR类型
VARCHAR和CHAR时两种最主要的字符串类型。不幸的时,很静精确地解释这些值是怎么存储在磁盘和内存中地,因为这跟存储引擎地具体实现有关。下面的描述假设使用的存储引擎是InnoDB和/或者MyISAM。
先看看VARCHAR和CHAR值通常在磁盘上怎么存储。请注意,存储引擎存储CHAR或者VARCHAR值得方式在内存中和磁盘上可能不一样,所以MySQL服务器从存储引擎独处得值可能需要转换另一种存储格式。下面是关于两种类型的一些比较
VARCHAR:
VARCHAR类型用于存储可变长字符串,是最常见的字符串数据类型。它比定长类型更节省空间,因为它仅使用必要的空间(例如,越短的字符串使用越少的空间)。有一种情况例外,如果MySQL表使用ROW_FORMAT=FIXED创建的话,每一行都会使用定长存储,这会很浪费空间。
VARCHAR需要使用1或者2个额外字节记录字符串的长度:如果列的最大长度小于或等于255字节。则只使用1个字节表示,否则使用2个字节。假设采用latin1字符集,一个VARCHAR(10)的列需要11个字节的存储空间。VARCHAR(1000)的列则需要1002个字节,因为需要2个字节存储长度信息。
VARCHAR节省了存储空间,所以对性能也有帮助。但是,由于行是变长的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作,如果一个行占用的空间增长,并且在页内没有更多的的空间可以存储,在这种情况下,不同的存储引擎的处理方式是不一样的。例如,MyISAM会将行拆成不同的片段存储,InnoDB则需要分裂页来使行可以放进页内。,其他一些存储引擎也许从不在原数据为止更新数据。
下面这些情况下使用VARCHAR是合适的:字符串列的最大长度比平均长度大很多;列的更新很少,所以碎片不是问题;使用了像UTF-8这样的复杂的字符集,每个字符都使用不同的字节数进行存储。在5.0或者更高版本。MySQL在存储和检索时会保留末尾空格。但在4.1或更老版本,MySQL会剔除末尾空格。
InnoDB则更灵活,它可以把过长的VARCHAR存储为BLOB
CHAR:
CHAR类型是定长的:MySQL总是根据定义的字符串长度分配足够的空间。当存储CHAR值时,MySQL会删除所有的末尾空格(在MySQL4.1和更老版本中VARCHAR也是这样实现的——也就是说这些版本中CHAR和VARCHAR在逻辑上是一样的,区别只是在存储格式上)。CHAR会根据需要采用空格进行填充以方便比较。
CHAR适合存储很短的字符串,或者所有的值都接近同一个长度。例如CHAR非常适合存储密码的MD5值,因为这是一个定长的值。对于经常变更的数据,CHAR也比VARCHAR更好,因为定长的CHAR类型不容易产生碎片。对于非常短的列,CHAR比VARCHAR在存储空间上也更有效率。例如使用CHAR(1)来存储只有Y和N的值,如果采用单字节字符集(记住字符串长度不是字节数,是字符数,多字节字符集会需要更多的空间存储单个字符)只需要一个字节,但是VARCHAR(1)却需要两个字节,因为还有一个记录长度的额外字节。
CHAR类型的这些行为可能有一点难以理解,下面通过一个具体的例子来说明。
MySQL支持多种字符串类型,每种类型还有很多变种。这些数据类型在4.1和5.0版本发生了很大的变化,使得情况更加复杂。从MySQL4.1开始,每个字符串列可以定义自己的字符集和排序规则,或者说校对规则(collation).这些东西会很大程度上影响性能。
1.VARCHAR和CHAR类型
VARCHAR和CHAR时两种最主要的字符串类型。不幸的时,很静精确地解释这些值是怎么存储在磁盘和内存中地,因为这跟存储引擎地具体实现有关。下面的描述假设使用的存储引擎是InnoDB和/或者MyISAM。
先看看VARCHAR和CHAR值通常在磁盘上怎么存储。请注意,存储引擎存储CHAR或者VARCHAR值得方式在内存中和磁盘上可能不一样,所以MySQL服务器从存储引擎独处得值可能需要转换另一种存储格式。下面是关于两种类型的一些比较
VARCHAR:
VARCHAR类型用于存储可变长字符串,是最常见的字符串数据类型。它比定长类型更节省空间,因为它仅使用必要的空间(例如,越短的字符串使用越少的空间)。有一种情况例外,如果MySQL表使用ROW_FORMAT=FIXED创建的话,每一行都会使用定长存储,这会很浪费空间。
VARCHAR需要使用1或者2个额外字节记录字符串的长度:如果列的最大长度小于或等于255字节。则只使用1个字节表示,否则使用2个字节。假设采用latin1字符集,一个VARCHAR(10)的列需要11个字节的存储空间。VARCHAR(1000)的列则需要1002个字节,因为需要2个字节存储长度信息。
VARCHAR节省了存储空间,所以对性能也有帮助。但是,由于行是变长的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作,如果一个行占用的空间增长,并且在页内没有更多的的空间可以存储,在这种情况下,不同的存储引擎的处理方式是不一样的。例如,MyISAM会将行拆成不同的片段存储,InnoDB则需要分裂页来使行可以放进页内。,其他一些存储引擎也许从不在原数据为止更新数据。
下面这些情况下使用VARCHAR是合适的:字符串列的最大长度比平均长度大很多;列的更新很少,所以碎片不是问题;使用了像UTF-8这样的复杂的字符集,每个字符都使用不同的字节数进行存储。在5.0或者更高版本。MySQL在存储和检索时会保留末尾空格。但在4.1或更老版本,MySQL会剔除末尾空格。
InnoDB则更灵活,它可以把过长的VARCHAR存储为BLOB
CHAR:
CHAR类型是定长的:MySQL总是根据定义的字符串长度分配足够的空间。当存储CHAR值时,MySQL会删除所有的末尾空格(在MySQL4.1和更老版本中VARCHAR也是这样实现的——也就是说这些版本中CHAR和VARCHAR在逻辑上是一样的,区别只是在存储格式上)。CHAR会根据需要采用空格进行填充以方便比较。
CHAR适合存储很短的字符串,或者所有的值都接近同一个长度。例如CHAR非常适合存储密码的MD5值,因为这是一个定长的值。对于经常变更的数据,CHAR也比VARCHAR更好,因为定长的CHAR类型不容易产生碎片。对于非常短的列,CHAR比VARCHAR在存储空间上也更有效率。例如使用CHAR(1)来存储只有Y和N的值,如果采用单字节字符集(记住字符串长度不是字节数,是字符数,多字节字符集会需要更多的空间存储单个字符)只需要一个字节,但是VARCHAR(1)却需要两个字节,因为还有一个记录长度的额外字节。
CHAR类型的这些行为可能有一点难以理解,下面通过一个具体的例子来说明。
首先我们创建一张只有一个CHAR(10)字段的表并且往里面插入一些值
```sql
mysql> CREATE TABLE char_test(char_col CHAR(10));
Query OK, 0 rows affected (0.09 sec)
mysql> INSERT INTO char_test(char_col) VALUES ('string1'),(' string2'),('string3 ');
Query OK, 3 rows affected (0.09 sec)
Records: 3 Duplicates: 0 Warnings: 0
```
当检索这些值得时候,会发现string3末尾的空格被截断了
```sql
mysql> SELECT CONCAT("'", char_col, "'") FROM char_test;
+----------------------------+
| CONCAT("'", char_col, "'") |
+----------------------------+
| 'string1' |
| ' string2' |
| 'string3' |
+----------------------------+
3 rows in set (0.12 sec)
```
如果使用VARCHAR(10)字段存储相同的值,可以得到如下结果(string3尾部的空格还在)
```sql
mysql> CREATE TABLE varchar_test(varchar_col VARCHAR(10));
Query OK, 0 rows affected (0.10 sec)
mysql> INSERT INTO varchar_test(varchar_col) VALUES ('string1'),(' string2'),('string3 ');
Query OK, 3 rows affected (0.09 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> SELECT CONCAT("'", varchar_col, "'") FROM varchar_test;
+-------------------------------+
| CONCAT("'", varchar_col, "'") |
+-------------------------------+
| 'string1' |
| ' string2' |
| 'string3 ' |
+-------------------------------+
3 rows in set (0.12 sec)
```
```sql
mysql> CREATE TABLE char_test(char_col CHAR(10));
Query OK, 0 rows affected (0.09 sec)
mysql> INSERT INTO char_test(char_col) VALUES ('string1'),(' string2'),('string3 ');
Query OK, 3 rows affected (0.09 sec)
Records: 3 Duplicates: 0 Warnings: 0
```
当检索这些值得时候,会发现string3末尾的空格被截断了
```sql
mysql> SELECT CONCAT("'", char_col, "'") FROM char_test;
+----------------------------+
| CONCAT("'", char_col, "'") |
+----------------------------+
| 'string1' |
| ' string2' |
| 'string3' |
+----------------------------+
3 rows in set (0.12 sec)
```
如果使用VARCHAR(10)字段存储相同的值,可以得到如下结果(string3尾部的空格还在)
```sql
mysql> CREATE TABLE varchar_test(varchar_col VARCHAR(10));
Query OK, 0 rows affected (0.10 sec)
mysql> INSERT INTO varchar_test(varchar_col) VALUES ('string1'),(' string2'),('string3 ');
Query OK, 3 rows affected (0.09 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> SELECT CONCAT("'", varchar_col, "'") FROM varchar_test;
+-------------------------------+
| CONCAT("'", varchar_col, "'") |
+-------------------------------+
| 'string1' |
| ' string2' |
| 'string3 ' |
+-------------------------------+
3 rows in set (0.12 sec)
```
数据如何存储取决于存储疫情,并非所有的存储引擎都会按照相同的方式处理定长和变长的字符串。Memory引擎只支持定长的行,即使有变长字段也会根据最大长度分配最大空间。不过,填充和截取空格的行为在不同存储引擎都是一样的,因为这是在MySQL服务器层进行处理的。
与CHAR和VARCHAR类似的类型还有BINARY和VARBINARY,它们存储的是二进制字符串。二进制字符串跟常规字符串非常相似,但是二进制字符串存储的是字节码而不是字符。填充也不一样:MySQL填充BINARY采用的\0(零字节)而不是空格,在检索时也不会去掉填充值。
当需要存储二进制数据,并且希望MySQL使用字节码而不是字符进行比较时,这些类型是非常有用的。二进制比较的有时并不仅仅体现在大小写敏感上。MySQL比较BINARY字符串时,每次按一个字节,并且根据该字节的数值进行比较。因此,二进制比较字符比较简单得多,所以也就更快
与CHAR和VARCHAR类似的类型还有BINARY和VARBINARY,它们存储的是二进制字符串。二进制字符串跟常规字符串非常相似,但是二进制字符串存储的是字节码而不是字符。填充也不一样:MySQL填充BINARY采用的\0(零字节)而不是空格,在检索时也不会去掉填充值。
当需要存储二进制数据,并且希望MySQL使用字节码而不是字符进行比较时,这些类型是非常有用的。二进制比较的有时并不仅仅体现在大小写敏感上。MySQL比较BINARY字符串时,每次按一个字节,并且根据该字节的数值进行比较。因此,二进制比较字符比较简单得多,所以也就更快
慷慨是不明智的。
使用VARCHAR(5)和VARCHAR(200)存储'hello'得空间开销是一样得。那么使用更短的列有什么优势吗?
事实证明有很大的优势。更长的列会消耗更多的内存,因为MySQL通常会分配固定大小的内存块来保存内部值。尤其是使用内存临时表进行排序或操作时会特别糟糕。在利用磁盘临时表进行排序时也同样糟糕。
所以最好的策略是只分配真正需要的空间
使用VARCHAR(5)和VARCHAR(200)存储'hello'得空间开销是一样得。那么使用更短的列有什么优势吗?
事实证明有很大的优势。更长的列会消耗更多的内存,因为MySQL通常会分配固定大小的内存块来保存内部值。尤其是使用内存临时表进行排序或操作时会特别糟糕。在利用磁盘临时表进行排序时也同样糟糕。
所以最好的策略是只分配真正需要的空间
BLOB和TEXT类型
BLOB和TEXT都是为存储很大的数据而设计的字符串数据类型,分别采用二进制和字符方式存储。
实际上它们分别属于两组不同的数据类型家族:字符类型是TINYTEXT,SMALLTEXT,TEXT,MEDIUMTEXT,LONGTEXT;对应的二进制类型是TINYBLOB,SMALLBLOB,BLOB,MEDIUMBLOB,LONGBLOB.BLOB是SMALLBLOB的同义词,TEXT是SMALLTEXT的同义词。
与其他类型不同,MySQL把每个BLOB和TEXT值当作一个独立的对象处理。存储引擎在存储时通常会做特殊处理。当BLOB和TEXT值太大时,InnoDB会使用专门的"外部"存储区域来进行存储,此时每个值在行内需要1~4个字节存储一个指针,然后再外部存储区域存储实际的值。
BLOB和TEXT家族之间仅有的不同时BLOB类型存储的是二进制数据,没有排序规则或字符集,而TEXT类型有字符集和排序规则。
MySQL对BLOB和TEXT列进行排序与其他类型是不同的:它只对每个列的最前max_sort_length字节而不是整个字符串做排序。如果只需要排序前面一小部分字符,则可以减小max_sort_length的配置,或者使用ORDER BY SUBSTRING(column, length).
MySQL不能将BLOB和TEXT列全部长度的字符串进行索引,也不能使用这些索引消除排序
BLOB和TEXT都是为存储很大的数据而设计的字符串数据类型,分别采用二进制和字符方式存储。
实际上它们分别属于两组不同的数据类型家族:字符类型是TINYTEXT,SMALLTEXT,TEXT,MEDIUMTEXT,LONGTEXT;对应的二进制类型是TINYBLOB,SMALLBLOB,BLOB,MEDIUMBLOB,LONGBLOB.BLOB是SMALLBLOB的同义词,TEXT是SMALLTEXT的同义词。
与其他类型不同,MySQL把每个BLOB和TEXT值当作一个独立的对象处理。存储引擎在存储时通常会做特殊处理。当BLOB和TEXT值太大时,InnoDB会使用专门的"外部"存储区域来进行存储,此时每个值在行内需要1~4个字节存储一个指针,然后再外部存储区域存储实际的值。
BLOB和TEXT家族之间仅有的不同时BLOB类型存储的是二进制数据,没有排序规则或字符集,而TEXT类型有字符集和排序规则。
MySQL对BLOB和TEXT列进行排序与其他类型是不同的:它只对每个列的最前max_sort_length字节而不是整个字符串做排序。如果只需要排序前面一小部分字符,则可以减小max_sort_length的配置,或者使用ORDER BY SUBSTRING(column, length).
MySQL不能将BLOB和TEXT列全部长度的字符串进行索引,也不能使用这些索引消除排序
磁盘临时表和文件排序
因为Memory引擎不支持BLOB和TEXT类型,所以,如果查询使用了BLOB或TEXT列并且需要使用隐式临时表,将不得不使用MyISAM磁盘临时表。即使只有几行数据也是如此(Percona Server的Memory引擎支持BLOB和TEXT类型,同样的场景下还是需要使用磁盘临时表)。这会导致严重的性能开销。即使配置MySQL将临时表存储再内存块设备上(RAM Disk),依然需要许多昂贵的系统调用。最好的解决方案是尽量避免使用BLOB和TEXT类型。如果实在无法避免,有一个技巧是在所有用到BLOB字段的地方都使用SUBSTRING(column, length)将列值转换为字符串(在ORDER BY 子句中也适用),这样就可以使用内存临时表了。但是要确保截取的子字符串足够短,不会使临时表的大小超过max_heap_table_size或tmp_table_size,超过以后MySQL会将内存临时表转换为MyISAM磁盘临时表。
最坏情况下的长度分配对于排序的时候也是一样的,所以这一招对于内存中创建大临时表和文件排序,以及在磁盘上创建大临时表和文件排序这两种情况都很有帮助。
例如,假设有一个1000万行的表,占用几个GB的磁盘空间。其中有一个uft8字符集的VARCHAR(1000)的列,每个字符最多使用3个字节,最坏情况下需要3000字节的空间。如果在ORDER BY 中用到这个列,并且查询扫描整个表,为了排序就需要超过30GB的临时表。
如果EXPLAIN执行计划的Extra列包含了"Using temporary",则说明这个查询使用了隐式临时表
因为Memory引擎不支持BLOB和TEXT类型,所以,如果查询使用了BLOB或TEXT列并且需要使用隐式临时表,将不得不使用MyISAM磁盘临时表。即使只有几行数据也是如此(Percona Server的Memory引擎支持BLOB和TEXT类型,同样的场景下还是需要使用磁盘临时表)。这会导致严重的性能开销。即使配置MySQL将临时表存储再内存块设备上(RAM Disk),依然需要许多昂贵的系统调用。最好的解决方案是尽量避免使用BLOB和TEXT类型。如果实在无法避免,有一个技巧是在所有用到BLOB字段的地方都使用SUBSTRING(column, length)将列值转换为字符串(在ORDER BY 子句中也适用),这样就可以使用内存临时表了。但是要确保截取的子字符串足够短,不会使临时表的大小超过max_heap_table_size或tmp_table_size,超过以后MySQL会将内存临时表转换为MyISAM磁盘临时表。
最坏情况下的长度分配对于排序的时候也是一样的,所以这一招对于内存中创建大临时表和文件排序,以及在磁盘上创建大临时表和文件排序这两种情况都很有帮助。
例如,假设有一个1000万行的表,占用几个GB的磁盘空间。其中有一个uft8字符集的VARCHAR(1000)的列,每个字符最多使用3个字节,最坏情况下需要3000字节的空间。如果在ORDER BY 中用到这个列,并且查询扫描整个表,为了排序就需要超过30GB的临时表。
如果EXPLAIN执行计划的Extra列包含了"Using temporary",则说明这个查询使用了隐式临时表
使用枚举类型(ENUM)代替字符串类型
有时候可以使用枚举列代替常用的字符串类型。枚举列可以把一些不重复的字符串存储成一个预定义的集合。MySQL在存储枚举时非常紧凑,会根据列表值得数量压缩到一个或者两个字节中。MySQL会在内部将每个值在列表中得为止保存为整数,并且在表的.frm文件中保存"数字-字符串"映射关系的"查找表",
例如,
```sql
mysql> CREATE TABLE enum_test(e ENUM('fish', 'apple', 'dog') NOT NULL);
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO enum_test(e) VALUES('fish'), ('dog'),('apple');
Query OK, 3 rows affected (0.02 sec)
Records: 3 Duplicates: 0 Warnings: 0
```
这三行数据实际存储为整数,而不是字符串。可以通过在数字上下问环境检索看到这个双重属性:
```sql
mysql> SELECT e+0 FROM enum_test;
+-----+
| e+0 |
+-----+
| 1 |
| 3 |
| 2 |
+-----+
3 rows in set (0.05 sec)
```
如果使用数字作为ENUM枚举常量,这种双重性很容易导致混乱,例如ENUM('1','2','3').建议尽量避免这么做。另外一个让人吃惊的地方时,枚举字段是按照内部存储的整数而不是定义的字符串进行排序的:
```sql
mysql> SELECT e FROM enum_test ORDER BY e;
+-------+
| e |
+-------+
| fish |
| apple |
| dog |
+-------+
3 rows in set (0.05 sec)
```
一种绕过这种限制的方式是按照需要的顺序来定义枚举列。另外也可以在查询中使用FIELD()函数显式地指定排序顺序,但这会导致MySQL无法利用索引消除排序。
```sql
mysql> SELECT e FROM enum_test ORDER BY FIELD(e, 'apple', 'dog','fish');
+-------+
| e |
+-------+
| apple |
| dog |
| fish |
+-------+
3 rows in set (0.07 sec)
```
有时候可以使用枚举列代替常用的字符串类型。枚举列可以把一些不重复的字符串存储成一个预定义的集合。MySQL在存储枚举时非常紧凑,会根据列表值得数量压缩到一个或者两个字节中。MySQL会在内部将每个值在列表中得为止保存为整数,并且在表的.frm文件中保存"数字-字符串"映射关系的"查找表",
例如,
```sql
mysql> CREATE TABLE enum_test(e ENUM('fish', 'apple', 'dog') NOT NULL);
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO enum_test(e) VALUES('fish'), ('dog'),('apple');
Query OK, 3 rows affected (0.02 sec)
Records: 3 Duplicates: 0 Warnings: 0
```
这三行数据实际存储为整数,而不是字符串。可以通过在数字上下问环境检索看到这个双重属性:
```sql
mysql> SELECT e+0 FROM enum_test;
+-----+
| e+0 |
+-----+
| 1 |
| 3 |
| 2 |
+-----+
3 rows in set (0.05 sec)
```
如果使用数字作为ENUM枚举常量,这种双重性很容易导致混乱,例如ENUM('1','2','3').建议尽量避免这么做。另外一个让人吃惊的地方时,枚举字段是按照内部存储的整数而不是定义的字符串进行排序的:
```sql
mysql> SELECT e FROM enum_test ORDER BY e;
+-------+
| e |
+-------+
| fish |
| apple |
| dog |
+-------+
3 rows in set (0.05 sec)
```
一种绕过这种限制的方式是按照需要的顺序来定义枚举列。另外也可以在查询中使用FIELD()函数显式地指定排序顺序,但这会导致MySQL无法利用索引消除排序。
```sql
mysql> SELECT e FROM enum_test ORDER BY FIELD(e, 'apple', 'dog','fish');
+-------+
| e |
+-------+
| apple |
| dog |
| fish |
+-------+
3 rows in set (0.07 sec)
```
如果在定义时就是按照字母的顺序,就没有必要这么做了。枚举最不好的地方是,字符串列表是固定的,添加或删除字符串必须使用ALTER TABLE,因此,对于一系列未来可能会改变的字符串,使用枚举不是一个好主意,除非能接受只在列表末尾添加元素,这样在MySQL5.1中就可以不用重建整个表来完成修改。
由于MySQL把每个枚举值保存为整数,并且必须进行查找才能转换为字符串,所以枚举列有一些开销。通常枚举的列表都比较小,所以开销还可以控制,但也不能保证一直如此。在特定情况下,把CHAR/VARCHAR列与枚举列进行关联可能会比直接关联(CHAR/VARCHAR)列更慢。
为了说明这个情况,读一个应用中的一张表进行了基准测试,看看在MySQL中执行上面说的关联的速度如何。该表有一个很大的主键:
```sql
CREATE TABLE webservicecalls(
day date NOT NULL,
account smallint NOT NULL,
service varchar(10) NOT NULL,
method varchar(50) NOT NULL,
calls int NOT NULL,
items int NOT NULL,
time float NOT NULL,
cost decimal(9,5) NOT NULL,
updated datetime,
PRIMARY KEY(day,account, service, method)
) ENGINE=InnoDB;
```
这个表有11万行数据,只有10MB大小,所以可以完全载入内存。service列包含了5个不同的值,平均长度为4个字符,method列包含了71个值,平均产犊为20个字符。
复制一下这个表,但是把service和method字段换成枚举类型,表结构如下:
```sql
CREATE TABLE webservicecalls_enum(
...omitted...
service ENUM(... VALUES omitted ...) NOT NULL,
method ENUM(... VALUES omitted ...) NOT NULL,
...omitted...
) ENGINE=InnoDB;
```
然后我们用主键列关联这两个表,下面是所使用的查询语句:
```sql
mysql> SELECT SQL_NO_CACHE COUNT(*) FROM webservicecalls JOIN webservicecalls USING(day, account,service,method);
```
用VARCHAR和ENUM分别测试了这个语句,结果如表所示
从上面的结果可以看到,当把列都转换成ENUM以后,关联变得很快。但是当VARCHAR列和ENUM列进行关联时则慢很多。在本例中,如果不是必须和VARCHAR列进行关联,那么转换这些列为ENUM就是个好主意。这是一个通用的设计时间,在"查找表"时采用整数主键而避免采用基于字符串的值进行关联。然而,转换列为枚举型还有另外一个好处。根据SHOW TABLE STATUS命令输出结果中Data_length列的值,把这两列转换为ENUM可以让表的大小缩小1/3.在某些情况下,即使可能出现ENUM和VARCHAR进行关联的情况,这也是值得的(这很可能可以节省IO)。同样,转换后主键也只有原来的一半大小了,因为这是InnoDB表,如果表上有其他索引,减小主键大小会使得非主键索引也变得更小。
由于MySQL把每个枚举值保存为整数,并且必须进行查找才能转换为字符串,所以枚举列有一些开销。通常枚举的列表都比较小,所以开销还可以控制,但也不能保证一直如此。在特定情况下,把CHAR/VARCHAR列与枚举列进行关联可能会比直接关联(CHAR/VARCHAR)列更慢。
为了说明这个情况,读一个应用中的一张表进行了基准测试,看看在MySQL中执行上面说的关联的速度如何。该表有一个很大的主键:
```sql
CREATE TABLE webservicecalls(
day date NOT NULL,
account smallint NOT NULL,
service varchar(10) NOT NULL,
method varchar(50) NOT NULL,
calls int NOT NULL,
items int NOT NULL,
time float NOT NULL,
cost decimal(9,5) NOT NULL,
updated datetime,
PRIMARY KEY(day,account, service, method)
) ENGINE=InnoDB;
```
这个表有11万行数据,只有10MB大小,所以可以完全载入内存。service列包含了5个不同的值,平均长度为4个字符,method列包含了71个值,平均产犊为20个字符。
复制一下这个表,但是把service和method字段换成枚举类型,表结构如下:
```sql
CREATE TABLE webservicecalls_enum(
...omitted...
service ENUM(... VALUES omitted ...) NOT NULL,
method ENUM(... VALUES omitted ...) NOT NULL,
...omitted...
) ENGINE=InnoDB;
```
然后我们用主键列关联这两个表,下面是所使用的查询语句:
```sql
mysql> SELECT SQL_NO_CACHE COUNT(*) FROM webservicecalls JOIN webservicecalls USING(day, account,service,method);
```
用VARCHAR和ENUM分别测试了这个语句,结果如表所示
从上面的结果可以看到,当把列都转换成ENUM以后,关联变得很快。但是当VARCHAR列和ENUM列进行关联时则慢很多。在本例中,如果不是必须和VARCHAR列进行关联,那么转换这些列为ENUM就是个好主意。这是一个通用的设计时间,在"查找表"时采用整数主键而避免采用基于字符串的值进行关联。然而,转换列为枚举型还有另外一个好处。根据SHOW TABLE STATUS命令输出结果中Data_length列的值,把这两列转换为ENUM可以让表的大小缩小1/3.在某些情况下,即使可能出现ENUM和VARCHAR进行关联的情况,这也是值得的(这很可能可以节省IO)。同样,转换后主键也只有原来的一半大小了,因为这是InnoDB表,如果表上有其他索引,减小主键大小会使得非主键索引也变得更小。
查看Data_length
日期和时间类型
MySQL可以使用许多类型来保存日期和时间值,例如YEAR和DATE.MySQL能存储的最小时间粒度为秒(MariaDB支持微秒级别的时间类型)。但是MySQL也可以使用微秒级别的粒度进行临时运算,接下来会展示如何绕开这种存储限制。大部分时间类型都没有替代品,因此没有什么事最佳选择的问题。唯一的问题是保存日期和时间的时候需要做什么。MySQL提供两种相似的日期类型:DATETIME和TIMESTAMP。对于很多应用程序,它们都能工作,但是在某些场景,一个比另一个工作得好。
1.DATETIME
这个类型能保存大范围的值,从1001年到9999年,精度为秒。它把日期和时间封装到格式YYYYMMDDHHMMSS的整数中,与时区无关。使用8个字节的存储空间。默认情况下,MySQL以一种可排序的、无歧义的格式显式DATETIME值,例如"2008-01-16 22:37:08"。这是ANSI标准帝国一的日期和时间表示方法。
2.TIMESTAMP
就像它的名字一样,TIMESTAMP类型保存了从1970年1月1日午夜(格林尼治标准时间)以来的秒数,它和UNIX时间戳相同。TIMESTAMP只使用4个字节的存储空间,因此它的范围比DATETIME小得多:只能表示1970年到2038年。MySQL提供了FROM_UNIXTIME()函数把Unix时间戳转换为日期,并提供了UNIX_TIMESTAMP()函数把七日转换为Unix时间戳。MySQL4.1以及更新的版本按照DATETIME的方式格式化TIMESTAMP的值,但是MySQL4.0以及更老的版本不会在各个部分之间显式任何标点符号。这仅仅是显式格式上的区别,TIMESTAMP的存储格式在各个版本都是一样的。
TIMESTAMP显式地值也依赖于时区。MySQL服务器、操作系统,以及客户端连接都有时区设置。因此,存储值为0地TIMESTAMP在美国东部时区显式为"1969-12-31 19:00:00",与格林尼治时间差5个小时。有必要强调一下这个区别:如果在多个时区存储或访问数据,TIMESTAMP和DATETIME的行为将很不一样。前者提供的值与时区有关系,后者则保留文本表示的日期和时间。
TIMESTAMP也有DATETIME没有的特殊属性。默认情况下,如果插入时没有指定第一个TIMESTAMP的值,MySQL则设置这个列的值为当前时间(TIMESTAMP的行为规则比较复杂,并且在不同的MySQL版本里会变动,所以你应该验证数据库的行为是你需要的。一个好的方式是修改完TIMESTAMP列后用SHOW CREATE TABLE命令检查输出)。在插入一行记录时,MySQL默认也会更新第一个TIMESTAMP列的值(除非在UPDATE语句中明确指定了值)。你可以配置任何TIMESTAMP列插入和更新行为。最后,TIMETSAMP列默认为NOT NULL,这也和其他的数据类型不一样。
除了特殊行为之外,通常也应该尽量使用TIMESTAMP,因为它比DATETIME空间效率更高。有时候人们会将Unix时间戳存储为整数值,但这不会带来任何收益。用整数保存时间戳的格式通常不方便处理,所以不推荐这样做。如果需要存储比秒更小粒度的日期和时间值怎么办呢?MySQL目前没有提供合适的数据类型,但是可以使用自己的存储格式:可以使用BIGINT类型存储微秒级别的时间戳,或者使用DOUBLE存储秒之后的小数部分,这两种方式都可以,或者也可以使用MariaDB替代MySQL
MySQL可以使用许多类型来保存日期和时间值,例如YEAR和DATE.MySQL能存储的最小时间粒度为秒(MariaDB支持微秒级别的时间类型)。但是MySQL也可以使用微秒级别的粒度进行临时运算,接下来会展示如何绕开这种存储限制。大部分时间类型都没有替代品,因此没有什么事最佳选择的问题。唯一的问题是保存日期和时间的时候需要做什么。MySQL提供两种相似的日期类型:DATETIME和TIMESTAMP。对于很多应用程序,它们都能工作,但是在某些场景,一个比另一个工作得好。
1.DATETIME
这个类型能保存大范围的值,从1001年到9999年,精度为秒。它把日期和时间封装到格式YYYYMMDDHHMMSS的整数中,与时区无关。使用8个字节的存储空间。默认情况下,MySQL以一种可排序的、无歧义的格式显式DATETIME值,例如"2008-01-16 22:37:08"。这是ANSI标准帝国一的日期和时间表示方法。
2.TIMESTAMP
就像它的名字一样,TIMESTAMP类型保存了从1970年1月1日午夜(格林尼治标准时间)以来的秒数,它和UNIX时间戳相同。TIMESTAMP只使用4个字节的存储空间,因此它的范围比DATETIME小得多:只能表示1970年到2038年。MySQL提供了FROM_UNIXTIME()函数把Unix时间戳转换为日期,并提供了UNIX_TIMESTAMP()函数把七日转换为Unix时间戳。MySQL4.1以及更新的版本按照DATETIME的方式格式化TIMESTAMP的值,但是MySQL4.0以及更老的版本不会在各个部分之间显式任何标点符号。这仅仅是显式格式上的区别,TIMESTAMP的存储格式在各个版本都是一样的。
TIMESTAMP显式地值也依赖于时区。MySQL服务器、操作系统,以及客户端连接都有时区设置。因此,存储值为0地TIMESTAMP在美国东部时区显式为"1969-12-31 19:00:00",与格林尼治时间差5个小时。有必要强调一下这个区别:如果在多个时区存储或访问数据,TIMESTAMP和DATETIME的行为将很不一样。前者提供的值与时区有关系,后者则保留文本表示的日期和时间。
TIMESTAMP也有DATETIME没有的特殊属性。默认情况下,如果插入时没有指定第一个TIMESTAMP的值,MySQL则设置这个列的值为当前时间(TIMESTAMP的行为规则比较复杂,并且在不同的MySQL版本里会变动,所以你应该验证数据库的行为是你需要的。一个好的方式是修改完TIMESTAMP列后用SHOW CREATE TABLE命令检查输出)。在插入一行记录时,MySQL默认也会更新第一个TIMESTAMP列的值(除非在UPDATE语句中明确指定了值)。你可以配置任何TIMESTAMP列插入和更新行为。最后,TIMETSAMP列默认为NOT NULL,这也和其他的数据类型不一样。
除了特殊行为之外,通常也应该尽量使用TIMESTAMP,因为它比DATETIME空间效率更高。有时候人们会将Unix时间戳存储为整数值,但这不会带来任何收益。用整数保存时间戳的格式通常不方便处理,所以不推荐这样做。如果需要存储比秒更小粒度的日期和时间值怎么办呢?MySQL目前没有提供合适的数据类型,但是可以使用自己的存储格式:可以使用BIGINT类型存储微秒级别的时间戳,或者使用DOUBLE存储秒之后的小数部分,这两种方式都可以,或者也可以使用MariaDB替代MySQL
位数据类型
MySQL有少数 集中存储类型使用紧凑的位存储数据。所有这些位类型,不管底层存储格式和处理方式如何,从技术上来说都是字符串类型。
1.BIT
在MySQL5.0之前,BIT是TINYINT的同义词。但是在MySQL5.0以及更新版本,这是一个特性完全不同的数据类型。可以使用BIT列在一列中存储一个或多个true/false值。BIT(1)定义一个包含单个位的字段,BIT(2)存储2个位,依此类推,BIT列的最大长度是64个位。BIT的行为因存储引擎而异。MyISAM会打包存储所有的BIT列,所以17个单独的BIT列只需要17个位存储(假设没有可为NULL的列),这样MyISAM只适用3个字节就能存储17个BIT列。其他存储引擎例如Memory和InnoDB,为每个BIT列使用一个豿存储的最小整数类型来存放,所以不能节省存储空间。
MySQL把BIT当作字符串类型,而不是数字类型。当检索BIT(1)的值时,结果是一个包含二进制0或1值得字符串,而不是ASCII码的"0"或"1".然而,在数字上下文的场景中检索时,结果将时位字符串转换成的数字。如果需要和另外的值比较结果,一定要记得这一点。例如,如果存储一个值b'00111001'(二进制值等于57)到BIT(8)的列并且检索它,得到的内容是字符码为57的字符串。也就是说得到00111001。但是在数字上下文场景中,得到的是数字57:
```sql
mysql> INSERT INTO bittest VALUES(b'00111001');
Query OK, 1 row affected (0.03 sec)
mysql> SELECT CONCAT(a,' ') AS 'a ', a, a+0 FROM bittest;
+----+----------+-----+
| a | a | a+0 |
+----+----------+-----+
| 9 | 00111001 | 57 |
+----+----------+-----+
1 row in set (0.08 sec)
mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 5.7.44 |
+-----------+
1 row in set (0.08 sec)
```
这是相当令人费解的,所以我们认为应当谨慎使用使用BIT类型。对于大部分应用,最好避免使用这种类型。如果想在一个bit的存储空间中个存储一个true/false值,另一个方法是创建一个可以为空的CHAR(0)列。该列可以保存控制(NULL)或者长度为零的字符串(空字符串)
2.SET
如果需要保存很多的true/false值,可以考虑合并这些列到一个SET数据类型,它在MySQL内部是以以一系列打包的位的集合来表示的。这样就有效地利用了存储空间,并且MySQL有像FIND_IN_SET()和FIELD()这样的函数,方便地在查询中使用。它的主要缺点是改变列的定义的代价较高:需要ALTER TABLE,这对达标来说非常昂贵的操作。一半来说,也无法在SET列上通过索引查找。
3.在整数列上进行按位操作
一种替代SET的方式使用一个整数包装一系列的位。例如,可以把8个位包装到一个TINYINT中,并且按位操作来使用。可以在应用中为每个位定义名称常量来简化这个工作。比起SET,这种办法主要的好处在于可以不使用ALTER TABLE改变字段代表的"枚举"值,缺点是查询语句更难写,并且更难理解(当第5个bit位被设置时是什么意思?)一些人非常适应这种方式,也有一些人不适应,所以是否采用这种技术取决于个人的偏好。
MySQL有少数 集中存储类型使用紧凑的位存储数据。所有这些位类型,不管底层存储格式和处理方式如何,从技术上来说都是字符串类型。
1.BIT
在MySQL5.0之前,BIT是TINYINT的同义词。但是在MySQL5.0以及更新版本,这是一个特性完全不同的数据类型。可以使用BIT列在一列中存储一个或多个true/false值。BIT(1)定义一个包含单个位的字段,BIT(2)存储2个位,依此类推,BIT列的最大长度是64个位。BIT的行为因存储引擎而异。MyISAM会打包存储所有的BIT列,所以17个单独的BIT列只需要17个位存储(假设没有可为NULL的列),这样MyISAM只适用3个字节就能存储17个BIT列。其他存储引擎例如Memory和InnoDB,为每个BIT列使用一个豿存储的最小整数类型来存放,所以不能节省存储空间。
MySQL把BIT当作字符串类型,而不是数字类型。当检索BIT(1)的值时,结果是一个包含二进制0或1值得字符串,而不是ASCII码的"0"或"1".然而,在数字上下文的场景中检索时,结果将时位字符串转换成的数字。如果需要和另外的值比较结果,一定要记得这一点。例如,如果存储一个值b'00111001'(二进制值等于57)到BIT(8)的列并且检索它,得到的内容是字符码为57的字符串。也就是说得到00111001。但是在数字上下文场景中,得到的是数字57:
```sql
mysql> INSERT INTO bittest VALUES(b'00111001');
Query OK, 1 row affected (0.03 sec)
mysql> SELECT CONCAT(a,' ') AS 'a ', a, a+0 FROM bittest;
+----+----------+-----+
| a | a | a+0 |
+----+----------+-----+
| 9 | 00111001 | 57 |
+----+----------+-----+
1 row in set (0.08 sec)
mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 5.7.44 |
+-----------+
1 row in set (0.08 sec)
```
这是相当令人费解的,所以我们认为应当谨慎使用使用BIT类型。对于大部分应用,最好避免使用这种类型。如果想在一个bit的存储空间中个存储一个true/false值,另一个方法是创建一个可以为空的CHAR(0)列。该列可以保存控制(NULL)或者长度为零的字符串(空字符串)
2.SET
如果需要保存很多的true/false值,可以考虑合并这些列到一个SET数据类型,它在MySQL内部是以以一系列打包的位的集合来表示的。这样就有效地利用了存储空间,并且MySQL有像FIND_IN_SET()和FIELD()这样的函数,方便地在查询中使用。它的主要缺点是改变列的定义的代价较高:需要ALTER TABLE,这对达标来说非常昂贵的操作。一半来说,也无法在SET列上通过索引查找。
3.在整数列上进行按位操作
一种替代SET的方式使用一个整数包装一系列的位。例如,可以把8个位包装到一个TINYINT中,并且按位操作来使用。可以在应用中为每个位定义名称常量来简化这个工作。比起SET,这种办法主要的好处在于可以不使用ALTER TABLE改变字段代表的"枚举"值,缺点是查询语句更难写,并且更难理解(当第5个bit位被设置时是什么意思?)一些人非常适应这种方式,也有一些人不适应,所以是否采用这种技术取决于个人的偏好。
举个例子。
一个包装位的应用的例子是保存权限的访问控制表(ACL)。每个位或者SET元素代表一个值,例如CAN_READ、CAN_WRITE,或者CAN_DELETE。如果使用SET列,可以让MySQL在列定义里存储位到值得映射关系;入股哦使用整数列,则可以在应用代码里存储这个对应关系。这是使用SET列时的查询:
```sql
mysql> CREATE TABLE acl(perms SET ('CAN_READ', 'CAN_WRITE','CAN_DELETE') NOT NULL);
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO acl(perms) VALUES ('CAN_READ,CAN_DELETE');
Query OK, 1 row affected (0.03 sec)
mysql> SELECT * FROM acl WHERE FIND_IN_SET('CAN_READ', perms);
+---------------------+
| perms |
+---------------------+
| CAN_READ,CAN_DELETE |
+---------------------+
1 row in set (0.07 sec)
```
如果用整数来存储,则可以参考下面的例子:
```sql
mysql> CREATE TABLE ack_number(perms TINYINT UNSIGNED NOT NULL DEFAULT 0);
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO ack_number(perms) VALUES(@CAN_READ + @CAN_DELETE);
Query OK, 1 row affected (0.02 sec)
mysql> SELECT perms FROM ack_number WHERE perms & @CAN_READ;
+-------+
| perms |
+-------+
| 5 |
+-------+
1 row in set (0.07 sec)
```
这里我们使用MySQL变量来定义值
一个包装位的应用的例子是保存权限的访问控制表(ACL)。每个位或者SET元素代表一个值,例如CAN_READ、CAN_WRITE,或者CAN_DELETE。如果使用SET列,可以让MySQL在列定义里存储位到值得映射关系;入股哦使用整数列,则可以在应用代码里存储这个对应关系。这是使用SET列时的查询:
```sql
mysql> CREATE TABLE acl(perms SET ('CAN_READ', 'CAN_WRITE','CAN_DELETE') NOT NULL);
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO acl(perms) VALUES ('CAN_READ,CAN_DELETE');
Query OK, 1 row affected (0.03 sec)
mysql> SELECT * FROM acl WHERE FIND_IN_SET('CAN_READ', perms);
+---------------------+
| perms |
+---------------------+
| CAN_READ,CAN_DELETE |
+---------------------+
1 row in set (0.07 sec)
```
如果用整数来存储,则可以参考下面的例子:
```sql
mysql> CREATE TABLE ack_number(perms TINYINT UNSIGNED NOT NULL DEFAULT 0);
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO ack_number(perms) VALUES(@CAN_READ + @CAN_DELETE);
Query OK, 1 row affected (0.02 sec)
mysql> SELECT perms FROM ack_number WHERE perms & @CAN_READ;
+-------+
| perms |
+-------+
| 5 |
+-------+
1 row in set (0.07 sec)
```
这里我们使用MySQL变量来定义值
选择标识符(identifier)
为表示列(identifier column)选择合适的数据类型非常重要。一般来说更有可能用标识符与其他值进行比较(例如,在关联操作中),或者通过标识列寻找其他列。标识列也可能在另外的表中作为外键使用,所以为标识列选择数据类型时,应当选择跟关联表中的对应列一样的类型(在相关的表中使用相同的数据类型是个好注意,因为这些列很可能在关联中使用)。
当选择标识列的类型时,不仅仅需要考虑存储类型,还需要考虑MySQL对这种类型怎么执行计算和比较。例如,MySQL在内部使用整数存储ENUM和SET类型,然后在做比较操作时转换为字符串。一旦选定了一种类型,要确保在所有关联表种都是用同样的类型。类型之间需要精确匹配,包括像UNSIGNED这样的属性(如果使用的是InnoDB存储引擎,将不能在数据类型不是完全匹配的情况下创建外键,否则会有保存信息:"ERROR 1005(HY000):Can't create table",这个信息可能让人迷惑不解,这个问题在MySQL邮件组也经常有人抱怨(但奇怪的是,在不同长度的VARCHAR列上创建外键又是可以的))混用不同数据类型可能导致性能问题,即使没有性能影响,在比较操作时隐式类型转换也可能导致很难发现的错误。这种错误可能会很久以后才突然出现,那时候可能都已经忘记是在比较不同的数据类型。
在可以满足值得范围的需求,并且预留未来增长空间的前提下,应该选择最小的数据类型/;例如有一个state_id列存储美国各州的名字(这是关联到另一张存储名字的表的ID)就不需要几千或几百万个值,所以不需要使用INT。TINYINT足够存储,而且比INT少了3个字节。如果用这个值作为其他表的外键,3个字节可能导致很大的性能差异。下面是一些小技巧
1.整数类型
整数通常是标识列最好的选择,因为它们很快并且可以使用AUTO_INCREMENT
2.ENUM和SET类型
对于标识列来说,ENUM和SET类型通常是一个糟糕的选择,尽管对某些只包含固定状态或者类型的静态"定义表"来说可能是没有问题的。ENUM和SET列适合存储固定信息,例如有序的状态、产品类型、人的性别。举个例子,如果使用枚举字段来定义产品类型,也许会设计一张以这个枚举字段为主键的查找表(可以在查找表种增加一些列来保存描述性质的文本,这样就能够生成一个术语表,或者为网站的下拉菜单提供有意义的标签)。这时,使用枚举类型作为标识列是可行的,但是大部分情况下都要避免这么做。
3.字符串类型
如果可能,应该避免使用字符串类型作为标识列,因为它们很消耗空间,并且通过常比数字类型慢。尤其是在MyISAM表里使用字符串作为标识列时要特别小心。MyISAM默认对字符串使用压缩索引,这回导致查询慢得多。最多会有6倍的性能下降。
4.对于完全"随机"的字符串也需要多加注意
例如MD5()、SHA1()或者UUID()产生的字符串。这些函数生成的新值会任意分布在很大的空间内,这会导致INSERT以及一些SELECT语句变得很慢(另一方面,对一些有很多写的特别大的表,这总伪随机值实际上可以帮助消除热点)。
3.1 因为插入值会随机地写到索引的不同为止,所以使得INSERT语句更慢。这会导致页分裂、磁盘随机访问,以及对于聚簇存储引擎产生聚簇索引刷碎片。
3.2 SELECT语句会变得很慢,因为逻辑上相邻的行会分布在磁盘和内存的不同地方
3.3 随机值导致缓存对所有类型的查询语句效果都很差,因为会使得 缓存赖以工作的访问局部性原理失效。如果整个数据集都是一样"热",那么缓存任何一部分特定数据到内存都没有好处;如果工作集比内存大,缓存将会有很多刷新和不命中
如果存储UUID,则应该移除"-"符号,或者更好的做法是,用UNHEX()函数转换UUID值伪16字节的数字,并且存储在一个BINARY(16)列种。检索时可以通过HEX()函数来格式化十六进制格式。
UUID()生成的值与加密散列函数例如SHA1()生成的值有不同的特征:UUID虽然分布也不均匀,但还是有一定顺序的。尽管如此,但还是不如递增的整数好用
为表示列(identifier column)选择合适的数据类型非常重要。一般来说更有可能用标识符与其他值进行比较(例如,在关联操作中),或者通过标识列寻找其他列。标识列也可能在另外的表中作为外键使用,所以为标识列选择数据类型时,应当选择跟关联表中的对应列一样的类型(在相关的表中使用相同的数据类型是个好注意,因为这些列很可能在关联中使用)。
当选择标识列的类型时,不仅仅需要考虑存储类型,还需要考虑MySQL对这种类型怎么执行计算和比较。例如,MySQL在内部使用整数存储ENUM和SET类型,然后在做比较操作时转换为字符串。一旦选定了一种类型,要确保在所有关联表种都是用同样的类型。类型之间需要精确匹配,包括像UNSIGNED这样的属性(如果使用的是InnoDB存储引擎,将不能在数据类型不是完全匹配的情况下创建外键,否则会有保存信息:"ERROR 1005(HY000):Can't create table",这个信息可能让人迷惑不解,这个问题在MySQL邮件组也经常有人抱怨(但奇怪的是,在不同长度的VARCHAR列上创建外键又是可以的))混用不同数据类型可能导致性能问题,即使没有性能影响,在比较操作时隐式类型转换也可能导致很难发现的错误。这种错误可能会很久以后才突然出现,那时候可能都已经忘记是在比较不同的数据类型。
在可以满足值得范围的需求,并且预留未来增长空间的前提下,应该选择最小的数据类型/;例如有一个state_id列存储美国各州的名字(这是关联到另一张存储名字的表的ID)就不需要几千或几百万个值,所以不需要使用INT。TINYINT足够存储,而且比INT少了3个字节。如果用这个值作为其他表的外键,3个字节可能导致很大的性能差异。下面是一些小技巧
1.整数类型
整数通常是标识列最好的选择,因为它们很快并且可以使用AUTO_INCREMENT
2.ENUM和SET类型
对于标识列来说,ENUM和SET类型通常是一个糟糕的选择,尽管对某些只包含固定状态或者类型的静态"定义表"来说可能是没有问题的。ENUM和SET列适合存储固定信息,例如有序的状态、产品类型、人的性别。举个例子,如果使用枚举字段来定义产品类型,也许会设计一张以这个枚举字段为主键的查找表(可以在查找表种增加一些列来保存描述性质的文本,这样就能够生成一个术语表,或者为网站的下拉菜单提供有意义的标签)。这时,使用枚举类型作为标识列是可行的,但是大部分情况下都要避免这么做。
3.字符串类型
如果可能,应该避免使用字符串类型作为标识列,因为它们很消耗空间,并且通过常比数字类型慢。尤其是在MyISAM表里使用字符串作为标识列时要特别小心。MyISAM默认对字符串使用压缩索引,这回导致查询慢得多。最多会有6倍的性能下降。
4.对于完全"随机"的字符串也需要多加注意
例如MD5()、SHA1()或者UUID()产生的字符串。这些函数生成的新值会任意分布在很大的空间内,这会导致INSERT以及一些SELECT语句变得很慢(另一方面,对一些有很多写的特别大的表,这总伪随机值实际上可以帮助消除热点)。
3.1 因为插入值会随机地写到索引的不同为止,所以使得INSERT语句更慢。这会导致页分裂、磁盘随机访问,以及对于聚簇存储引擎产生聚簇索引刷碎片。
3.2 SELECT语句会变得很慢,因为逻辑上相邻的行会分布在磁盘和内存的不同地方
3.3 随机值导致缓存对所有类型的查询语句效果都很差,因为会使得 缓存赖以工作的访问局部性原理失效。如果整个数据集都是一样"热",那么缓存任何一部分特定数据到内存都没有好处;如果工作集比内存大,缓存将会有很多刷新和不命中
如果存储UUID,则应该移除"-"符号,或者更好的做法是,用UNHEX()函数转换UUID值伪16字节的数字,并且存储在一个BINARY(16)列种。检索时可以通过HEX()函数来格式化十六进制格式。
UUID()生成的值与加密散列函数例如SHA1()生成的值有不同的特征:UUID虽然分布也不均匀,但还是有一定顺序的。尽管如此,但还是不如递增的整数好用
特殊类型数据。
某些类型的数据并不直接与内置类型一致。低于秒级精度的时间戳就是一个例子。另外一个例子是一个IPv4地址。人们经常使用VARCHAR(15)列存储IP地址。然而,它们实际上是32位无符号整数。不是字符串。用小数点将地址分成四段的表示方法只是为了让人们阅读容易。所以应该用无符号整数存储IP地址。MySQL提供INET_ATON()和INET_NTOA()函数在这两种表示方法之间转换
某些类型的数据并不直接与内置类型一致。低于秒级精度的时间戳就是一个例子。另外一个例子是一个IPv4地址。人们经常使用VARCHAR(15)列存储IP地址。然而,它们实际上是32位无符号整数。不是字符串。用小数点将地址分成四段的表示方法只是为了让人们阅读容易。所以应该用无符号整数存储IP地址。MySQL提供INET_ATON()和INET_NTOA()函数在这两种表示方法之间转换
MySQL schema设计种的陷阱。
虽然有一些普遍的好或坏的设计原则,但也有一些问题是由MySQL的实现机制导致的,这意味着有可能犯一些只在MySQL下发生的特定错误。接下来我们讨论下设计MySQL的schema的问题。这也许会帮助你避免错误,并且选择在MySQL特定实现下工作得更好的替代方案。
1.太多的列。
MySQL的存储引擎API工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列。从行缓冲种将编码过的列转换成行数据结构的操作代价是非常高的。MyISAM的定长行结构实际上与服务器层的行结构正好匹配,所以不需要转换。然而,MyISAM的变长行结构实际和INnoDB的行结构则总是需要转换。转换的代价依赖于列的数量。有一个CPU占用非常高的例子,发现客户使用了非常宽的表(数千个字段),然而只有一小部分列会实际用到,这时转换的代价就非常高。如果计划使用数千个字段,必须意识到服务器的性能运行特征会有一些不同。
2.太多的关联
所谓的"实体 - 属性 - 值"(EAV)设计模式是一个常见的糟糕设计模式,尤其是在MySQL下不能靠谱地工作。MySQL限制了每个关联操作最多只能有61张表,但是EAV数据库需要许多自关联。不少EAV数据库最后超过了这个限制。事实上在许多关联少于61张表地情况下,解析和优化查询地代价也会成为MySQL的问题。一个粗略的经验法则,如果希望查询执行得快速且并发性好,单个查询最好在12个表以内做关联
3.全能的枚举
注意防止过度使用枚举(ENUM).下面是一个例子
```sql
CREATE TABLE ......(country enum('','1','2','3',..........,'31'))
```
这种模式的schema设计非常凌乱。这么使用枚举值类型也许在任何支持枚举类型的数据库都是一个有问题的设计方案,这里应该用整数作为外键关联到字典表或者查找表来查找具体值。但是在MySQL中,当需要在枚举列表中增加一个新的国家时,就要做一次ALTER TABLE操作。在MySQL5.0以及更早的版本中ALTER TABLE是一种阻塞操作;即使在5.1和更新版本中,如果不是在列表的末尾增加值也会一样需要ALTER TABLE
4.变相的枚举
枚举(ENUM)列允许在列中存储一组定义值中的单个值,集合(SET)列则允许在列中存储一组定义值中的一个或多个值。有时候可能比较容易导致混乱。这是一个例子。
```sql
CREATE TABLE ....(is_default set('Y', 'N') NOT NULL default 'N')
```
如果这里真和假两种情况不会同时出现,那么毫无疑问应该使用枚举列代替集合列
5.非此发明(Not Invent Here)的NULL
之前提到避免使用NULL的好处,并且建议尽可能地考虑替代方案。即使需要存储一个事实上的"空值"到列表中时,也不一定非得使用NULL.也许可以使用0、某个特殊值,或者空字符串作为代替。但是遵循这个原则也不要走极端。当确实需要表示未知值时也不要害怕使用NULL.在一些场景中,使用NULL可能回避某个神奇常数更好.从特定类型的值域中选择一个不可能的值。例如用-1代表一个未知的整数,可能导致代码复杂很多,并容易引入bug,还可能会让事情变得一团糟。处理NULL确实不容易,但有时候回避它的替代方案更好:
```sql
CREATE TABLE ... (dt DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00')
```
伪造的全0值可能导致很多问题(可以配置MySQL的SQL_MODE来进制不可能的日期,对于新应用这是个非常好的实践经验,他不会让创建的数据库里充满不可能的值)。值得一提的是,MySQL会在索引中存储NULL值,而Oracle则不会
虽然有一些普遍的好或坏的设计原则,但也有一些问题是由MySQL的实现机制导致的,这意味着有可能犯一些只在MySQL下发生的特定错误。接下来我们讨论下设计MySQL的schema的问题。这也许会帮助你避免错误,并且选择在MySQL特定实现下工作得更好的替代方案。
1.太多的列。
MySQL的存储引擎API工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列。从行缓冲种将编码过的列转换成行数据结构的操作代价是非常高的。MyISAM的定长行结构实际上与服务器层的行结构正好匹配,所以不需要转换。然而,MyISAM的变长行结构实际和INnoDB的行结构则总是需要转换。转换的代价依赖于列的数量。有一个CPU占用非常高的例子,发现客户使用了非常宽的表(数千个字段),然而只有一小部分列会实际用到,这时转换的代价就非常高。如果计划使用数千个字段,必须意识到服务器的性能运行特征会有一些不同。
2.太多的关联
所谓的"实体 - 属性 - 值"(EAV)设计模式是一个常见的糟糕设计模式,尤其是在MySQL下不能靠谱地工作。MySQL限制了每个关联操作最多只能有61张表,但是EAV数据库需要许多自关联。不少EAV数据库最后超过了这个限制。事实上在许多关联少于61张表地情况下,解析和优化查询地代价也会成为MySQL的问题。一个粗略的经验法则,如果希望查询执行得快速且并发性好,单个查询最好在12个表以内做关联
3.全能的枚举
注意防止过度使用枚举(ENUM).下面是一个例子
```sql
CREATE TABLE ......(country enum('','1','2','3',..........,'31'))
```
这种模式的schema设计非常凌乱。这么使用枚举值类型也许在任何支持枚举类型的数据库都是一个有问题的设计方案,这里应该用整数作为外键关联到字典表或者查找表来查找具体值。但是在MySQL中,当需要在枚举列表中增加一个新的国家时,就要做一次ALTER TABLE操作。在MySQL5.0以及更早的版本中ALTER TABLE是一种阻塞操作;即使在5.1和更新版本中,如果不是在列表的末尾增加值也会一样需要ALTER TABLE
4.变相的枚举
枚举(ENUM)列允许在列中存储一组定义值中的单个值,集合(SET)列则允许在列中存储一组定义值中的一个或多个值。有时候可能比较容易导致混乱。这是一个例子。
```sql
CREATE TABLE ....(is_default set('Y', 'N') NOT NULL default 'N')
```
如果这里真和假两种情况不会同时出现,那么毫无疑问应该使用枚举列代替集合列
5.非此发明(Not Invent Here)的NULL
之前提到避免使用NULL的好处,并且建议尽可能地考虑替代方案。即使需要存储一个事实上的"空值"到列表中时,也不一定非得使用NULL.也许可以使用0、某个特殊值,或者空字符串作为代替。但是遵循这个原则也不要走极端。当确实需要表示未知值时也不要害怕使用NULL.在一些场景中,使用NULL可能回避某个神奇常数更好.从特定类型的值域中选择一个不可能的值。例如用-1代表一个未知的整数,可能导致代码复杂很多,并容易引入bug,还可能会让事情变得一团糟。处理NULL确实不容易,但有时候回避它的替代方案更好:
```sql
CREATE TABLE ... (dt DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00')
```
伪造的全0值可能导致很多问题(可以配置MySQL的SQL_MODE来进制不可能的日期,对于新应用这是个非常好的实践经验,他不会让创建的数据库里充满不可能的值)。值得一提的是,MySQL会在索引中存储NULL值,而Oracle则不会
范式和反范式。
对于任何给定的数据通常都有很多种表示方法,从完全的范式化到完全的反范式化,以及两者的折中。在范式化的数据库种,每个事实数据会出现并且只出现一次。相反,在反范式化的数据库种,信息是冗余的,可能会存储在多个地方。下面以经典的"雇员,部门,部门领导"的例子开始:
如图所示,这个schema的问题是修改数据时可能发生不一致。假如Say Brown解人Accounting部门的领导,需要修改多行数据来反应这个变化,这是很痛苦的事并且容易引入错误。如果“Jones”这一行显示部门的领导跟"Brown"这一行的不一样,就没办法直到哪个是对的。这就像有句老话说的:"一个人有两块手标就永远不知道实践"。此外,这个设计在没有雇员信息的情况下就无法表示一个部门——如果我们删除了所有Accounting部门的雇员,我们就失去了关于这个部门本身的所有记录。要避免这个问题,我们需要对这个表进行范式化,方式是拆分雇员和部门项。拆分以后可以用下面两张表来分别存储雇员表:
这样设计的两张表符合第二范式,在很多情况下做到这一步已经足够好了,然而,第二范式只是许多可能的范式种的一种
在这个例子种我们使用姓(Last Name)作为主键,因为这是数据的"自然标识"。从实践来看,无论如何都不应该这么用。这既不能保证唯一性,而且用一个很长的字符串作为主键是很糟糕的主意。
对于任何给定的数据通常都有很多种表示方法,从完全的范式化到完全的反范式化,以及两者的折中。在范式化的数据库种,每个事实数据会出现并且只出现一次。相反,在反范式化的数据库种,信息是冗余的,可能会存储在多个地方。下面以经典的"雇员,部门,部门领导"的例子开始:
如图所示,这个schema的问题是修改数据时可能发生不一致。假如Say Brown解人Accounting部门的领导,需要修改多行数据来反应这个变化,这是很痛苦的事并且容易引入错误。如果“Jones”这一行显示部门的领导跟"Brown"这一行的不一样,就没办法直到哪个是对的。这就像有句老话说的:"一个人有两块手标就永远不知道实践"。此外,这个设计在没有雇员信息的情况下就无法表示一个部门——如果我们删除了所有Accounting部门的雇员,我们就失去了关于这个部门本身的所有记录。要避免这个问题,我们需要对这个表进行范式化,方式是拆分雇员和部门项。拆分以后可以用下面两张表来分别存储雇员表:
这样设计的两张表符合第二范式,在很多情况下做到这一步已经足够好了,然而,第二范式只是许多可能的范式种的一种
在这个例子种我们使用姓(Last Name)作为主键,因为这是数据的"自然标识"。从实践来看,无论如何都不应该这么用。这既不能保证唯一性,而且用一个很长的字符串作为主键是很糟糕的主意。
范式的优点和缺点。
当为性能问题而寻求帮助时,经常会被建议对schema进行范式化设计,尤其是写密集的场景。这通常是个好建议。因为下面这些原因,范式化通常能够带来好处:
1.范式化的更新操作通常比反范式化要快
2.当数据较好地范式化时,就只有很少或者没有重复数据,所以只需要修改更少地数据
3.范式化地表通常更小,可以更好地放在内存里,所以执行操作会更快
4.很少有多余地数据意味着检索列表数据时更少需要DISTINCT或者GROUP BY 语句
还是前面地例子:在非范式化的结构种鄙视使用DISTINCT 或者GROUP BY 才能获得一份唯一的部门列表,如果部门(DEPARTMENT)是一张单独的表,则只需要简单的查询这张表就行了.
范式化设计的schema的缺点是通常需要关联,稍微复杂一些的查询语句在符合范式的schema上都可能需要至少一次的关联,也许更多。这不但代价昂贵,也可能使一些索引策略无效。例如,范式化可能将列放在不同的表种,而这些列如果在一个表种本可以属于同一个索引
当为性能问题而寻求帮助时,经常会被建议对schema进行范式化设计,尤其是写密集的场景。这通常是个好建议。因为下面这些原因,范式化通常能够带来好处:
1.范式化的更新操作通常比反范式化要快
2.当数据较好地范式化时,就只有很少或者没有重复数据,所以只需要修改更少地数据
3.范式化地表通常更小,可以更好地放在内存里,所以执行操作会更快
4.很少有多余地数据意味着检索列表数据时更少需要DISTINCT或者GROUP BY 语句
还是前面地例子:在非范式化的结构种鄙视使用DISTINCT 或者GROUP BY 才能获得一份唯一的部门列表,如果部门(DEPARTMENT)是一张单独的表,则只需要简单的查询这张表就行了.
范式化设计的schema的缺点是通常需要关联,稍微复杂一些的查询语句在符合范式的schema上都可能需要至少一次的关联,也许更多。这不但代价昂贵,也可能使一些索引策略无效。例如,范式化可能将列放在不同的表种,而这些列如果在一个表种本可以属于同一个索引
反范式的优点和缺点。
反范式化的schema因为所有数据都在一张表种,可以很好地避免关联。
如果不需要关联表,则对大部分查询最差地情况——即使表没有使用索引——是全表扫描。当数据比内存大时这可能比关联要快得多,因为这样避免了随机IO.单独得表也能使用更有效得索引策略。假设有一个网站,允许用户发送消息,并且一些用户是付费用户。现在想查看付费用户最近的10条信息。如果是范式化的结构并且索引了发送日期字段published,这个查询也许看起来像这样:
```sql
mysql>SELECT message_text,user_name FROM message INNER JOIN user ON message.user_id =user.id WHERE user.account_type='premiumv' ORDER BY message.publised DESC LIMIT 10;
```
要更有效地执行这个查询,MySQL需要扫描message表的published字段的索引。对于每一行找到的数据,将需要到user表里检查这个用户是不是付费用户。如果只有一小部分用户是付费账户,那么这是效率低下的做法。另一种可能的执行计划是从user表开始,选择所有的付费用户,获得它们所有的信息,并且排序。但这可能更加糟糕。主要问题是关联,使得需要在一个索引中又排序又过滤,如果采用反范式化组织数据,将两张表的字段合并一下,并且增加一个索引(account_type,published),就可以不通过关联写出这个查询。这将非常高效:
```sql
mysql>SELECT message_text,user_name FROM user_messages WHERE account_type='premium' ORDER BY published DESC LIMIT 10;
```
反范式化的schema因为所有数据都在一张表种,可以很好地避免关联。
如果不需要关联表,则对大部分查询最差地情况——即使表没有使用索引——是全表扫描。当数据比内存大时这可能比关联要快得多,因为这样避免了随机IO.单独得表也能使用更有效得索引策略。假设有一个网站,允许用户发送消息,并且一些用户是付费用户。现在想查看付费用户最近的10条信息。如果是范式化的结构并且索引了发送日期字段published,这个查询也许看起来像这样:
```sql
mysql>SELECT message_text,user_name FROM message INNER JOIN user ON message.user_id =user.id WHERE user.account_type='premiumv' ORDER BY message.publised DESC LIMIT 10;
```
要更有效地执行这个查询,MySQL需要扫描message表的published字段的索引。对于每一行找到的数据,将需要到user表里检查这个用户是不是付费用户。如果只有一小部分用户是付费账户,那么这是效率低下的做法。另一种可能的执行计划是从user表开始,选择所有的付费用户,获得它们所有的信息,并且排序。但这可能更加糟糕。主要问题是关联,使得需要在一个索引中又排序又过滤,如果采用反范式化组织数据,将两张表的字段合并一下,并且增加一个索引(account_type,published),就可以不通过关联写出这个查询。这将非常高效:
```sql
mysql>SELECT message_text,user_name FROM user_messages WHERE account_type='premium' ORDER BY published DESC LIMIT 10;
```
混用范式化和反范式化。
范式化和反范式化的schema各有优劣,怎么选择最佳的设计?
事实是,完全的范式化和完全的反范式化schema都是实验室里才有的东西:在真实世界中很少会这么极端地使用。在实际应用中经常需要混用,可能使用部分范式化地schema、缓存表以及其他技巧。最常见地反范式化数据地方法是复制或者缓存,在不同地表中存储相同地特定列。在MySQL5.0和更新版本中,可以使用触发器更新缓存值,这使得实现这样地方案变得更简单。
在网站实例中,可以在user表和message表中都存储account_type字段,而不用完全地范式化。这避免了完全反范式化地插入和删除问题,因为即使没有消息地时候也绝不会丢失用户地信息。这样也不会把user_message表搞得太大,有利于高效地获取数据。但是现在更新用户的账户类型的操作代价就高了,因为需要同时更新两张表。至于这会不会是一个问题,需要考虑更新的频率以及更新的时长,并和执行SELECT查询的频率进行比较。
另一个从附表冗余一些数据到子表的理由是排序的需要,例如,在范式化的schema里通过作者的名字对消息排序的代价将会非常高,但是如果在message表中缓存author_name字段并且建好索引,则可以非常高效地完成排序。
缓存衍生值也是有用的。如果需要显示每个用户发了多少消息(像很多论坛做的),可以没执行一个昂贵的子查询来计算并显示它;也可以在user表中建一个num_messages列,每当用户发新消息时更新这个值。
范式化和反范式化的schema各有优劣,怎么选择最佳的设计?
事实是,完全的范式化和完全的反范式化schema都是实验室里才有的东西:在真实世界中很少会这么极端地使用。在实际应用中经常需要混用,可能使用部分范式化地schema、缓存表以及其他技巧。最常见地反范式化数据地方法是复制或者缓存,在不同地表中存储相同地特定列。在MySQL5.0和更新版本中,可以使用触发器更新缓存值,这使得实现这样地方案变得更简单。
在网站实例中,可以在user表和message表中都存储account_type字段,而不用完全地范式化。这避免了完全反范式化地插入和删除问题,因为即使没有消息地时候也绝不会丢失用户地信息。这样也不会把user_message表搞得太大,有利于高效地获取数据。但是现在更新用户的账户类型的操作代价就高了,因为需要同时更新两张表。至于这会不会是一个问题,需要考虑更新的频率以及更新的时长,并和执行SELECT查询的频率进行比较。
另一个从附表冗余一些数据到子表的理由是排序的需要,例如,在范式化的schema里通过作者的名字对消息排序的代价将会非常高,但是如果在message表中缓存author_name字段并且建好索引,则可以非常高效地完成排序。
缓存衍生值也是有用的。如果需要显示每个用户发了多少消息(像很多论坛做的),可以没执行一个昂贵的子查询来计算并显示它;也可以在user表中建一个num_messages列,每当用户发新消息时更新这个值。
缓存表和汇总表
有时提升性能最好的方法是在同一张表中保存衍生的冗余数据。然而,有时也需要创建一张完全独立的汇总表或缓存表(特别是为满足检索的需求时)。如果能容许少量的脏数据,这是非常好的方法,但是有时确实没有选择的余地(例如,需要避免复杂、昂贵的实时更新操作)。
术语"缓存表"和"汇总表"没有标准的含义。我们用术语"缓存表"来标识存储那些可以比较简单地从schema其他表获取(但是每次获取的速度比较慢)数据的表(例如,逻辑上冗余的数据)。而术语"汇总表"是,则保存的是使用GROUP BY 语句聚合数据的表(例如,数据不是逻辑上冗余的)。也有人使用术语"累积表(Roll-Up Table)"称呼这些表,因为这些数据被"累计"了。
仍然以网站为例,假设需要计算之前24小时内发送的消息数。在一个很繁忙的网站不可能维护一个实时精确的计数器。作为替代方案,可以每小时生成一张汇总表。这样也许一条简单的查询就可以做到,并且比实时维护计数器要高效得多。缺点是计数器并不是100%准确。
如果必须获得过去24小时准确的消息发送数量(没有遗漏),有另外一种选择。以每小时汇总表为基础,把前23个完整小时的统计表全部加起来,最后在加上开始阶段和结束阶段不完整的小时内的计数。假设统计表叫作msg_per_hr并且这样定义:
```sql
CREATE TABLE msg_per_hr (
hr DATETIME NOT NULL,
cnt INT UNSIGNED NOT NULL,
PRIMARY KEY(hr)
);
```
可以通过把下面的三个语句的结果加起来,得到过去24小时发送消息的总数。我们使用LEFT(NOW(), 14)来获得当前的日期和实践最接近的小时:
```sql
mysql> SELECT SUM(cnt) FROM msg_per_hr WHERE hr BETWEEN CONCAT(LEFT(NOW(),14) , '00:00') - INTERVAL 23 HOUR AND CONCAT(LEFT(NOW(), 14), "00:00") - INTERVAL 1 HOUR;
mysql> SELECT COUNT(*) FROM message WHERE posted >= NOW() - INTERVAL 24 HOUR AND posted < CONCAT(LEFT(NOW(), 14), '00:00') - INTERVAL 23 HOUR;
mysql> SELECT COUNT(*) FROM message WHERE posted >= CONCAT(LEFT(NOW(), 14), '00:00');
```
不管是哪种方法——不严格的计数或通过小范围查询填满间隙的严格计数——都比计算message表的所有行要有效得多。这是建立汇总表的最关键原因,实时计算统计值是很昂贵的操作,因为要么需要扫描表中的大部分数据,要么查询语句只能在没某些特定的索引上才能有效运行,而这类特定索引一般会对UPDATE操作有影响,所以一般不希望创建这样的索引。计算最活跃的用户或者最常见的"标签"是折中操作的典型例子。
缓存表则相反,其对优化搜索和检索查询语句很有效。这些查询语句经常需要特殊的表和素银结构,跟普通的OLTP操作用的表有些区别。
例如,可能会需要很多不同的索引组合来加速各种类型的查询。这些矛盾的需求有时需要创建一张只包含主表中部分列的缓存表。一个有用的技巧是对缓存表使用不同的存储引擎。例如,如果主表使用InnoDB,用MyISAM作为缓存表的引擎将会得到更小的索引占用空间,并且可以做全文搜索。有时甚至想把整个表导出MySQL,插入到专门的搜索系统中获得更高的搜索效率,例如Lcene或者Sphinx搜索引擎。
在使用缓存表和汇总表时,必须决定是实时维护数据还是定期重建。哪个更好依赖于应用程序,但是定期重建并不只是节省资源,也可以保持表不会有很多碎片,以及有完全顺序组织的索引(这回更加高效)
当重建汇总表和缓存表时,通常需要保证数据在操作时依然可用。这就需要通过使用"影子表"来实现,"影子表"指的时一张在真实表"背后"创建的表。当完成了建表操作后,可以通过一个原子的重命名操作切换影子表和原表。例如,如果需要重建my_summary,则可以先创建my_summary_new,然后填充好数据,最后和真实表做切换
```sql
mysql> CREATE TABLE my_summary_new LIKE my_summary;
mysql> RENAME TABLE my_summary TO my_summary_old, my_summary_new TO my_summary;
```
如果像上面的例子一样,在将my_summary这个名字分配给新键的表之前将原始的my_summary表重命名为my_summary_old,就可以在下一次重建之前一直保留旧版本的数据,如果新表有问题,则可以很容易地进行快速回滚操作。
有时提升性能最好的方法是在同一张表中保存衍生的冗余数据。然而,有时也需要创建一张完全独立的汇总表或缓存表(特别是为满足检索的需求时)。如果能容许少量的脏数据,这是非常好的方法,但是有时确实没有选择的余地(例如,需要避免复杂、昂贵的实时更新操作)。
术语"缓存表"和"汇总表"没有标准的含义。我们用术语"缓存表"来标识存储那些可以比较简单地从schema其他表获取(但是每次获取的速度比较慢)数据的表(例如,逻辑上冗余的数据)。而术语"汇总表"是,则保存的是使用GROUP BY 语句聚合数据的表(例如,数据不是逻辑上冗余的)。也有人使用术语"累积表(Roll-Up Table)"称呼这些表,因为这些数据被"累计"了。
仍然以网站为例,假设需要计算之前24小时内发送的消息数。在一个很繁忙的网站不可能维护一个实时精确的计数器。作为替代方案,可以每小时生成一张汇总表。这样也许一条简单的查询就可以做到,并且比实时维护计数器要高效得多。缺点是计数器并不是100%准确。
如果必须获得过去24小时准确的消息发送数量(没有遗漏),有另外一种选择。以每小时汇总表为基础,把前23个完整小时的统计表全部加起来,最后在加上开始阶段和结束阶段不完整的小时内的计数。假设统计表叫作msg_per_hr并且这样定义:
```sql
CREATE TABLE msg_per_hr (
hr DATETIME NOT NULL,
cnt INT UNSIGNED NOT NULL,
PRIMARY KEY(hr)
);
```
可以通过把下面的三个语句的结果加起来,得到过去24小时发送消息的总数。我们使用LEFT(NOW(), 14)来获得当前的日期和实践最接近的小时:
```sql
mysql> SELECT SUM(cnt) FROM msg_per_hr WHERE hr BETWEEN CONCAT(LEFT(NOW(),14) , '00:00') - INTERVAL 23 HOUR AND CONCAT(LEFT(NOW(), 14), "00:00") - INTERVAL 1 HOUR;
mysql> SELECT COUNT(*) FROM message WHERE posted >= NOW() - INTERVAL 24 HOUR AND posted < CONCAT(LEFT(NOW(), 14), '00:00') - INTERVAL 23 HOUR;
mysql> SELECT COUNT(*) FROM message WHERE posted >= CONCAT(LEFT(NOW(), 14), '00:00');
```
不管是哪种方法——不严格的计数或通过小范围查询填满间隙的严格计数——都比计算message表的所有行要有效得多。这是建立汇总表的最关键原因,实时计算统计值是很昂贵的操作,因为要么需要扫描表中的大部分数据,要么查询语句只能在没某些特定的索引上才能有效运行,而这类特定索引一般会对UPDATE操作有影响,所以一般不希望创建这样的索引。计算最活跃的用户或者最常见的"标签"是折中操作的典型例子。
缓存表则相反,其对优化搜索和检索查询语句很有效。这些查询语句经常需要特殊的表和素银结构,跟普通的OLTP操作用的表有些区别。
例如,可能会需要很多不同的索引组合来加速各种类型的查询。这些矛盾的需求有时需要创建一张只包含主表中部分列的缓存表。一个有用的技巧是对缓存表使用不同的存储引擎。例如,如果主表使用InnoDB,用MyISAM作为缓存表的引擎将会得到更小的索引占用空间,并且可以做全文搜索。有时甚至想把整个表导出MySQL,插入到专门的搜索系统中获得更高的搜索效率,例如Lcene或者Sphinx搜索引擎。
在使用缓存表和汇总表时,必须决定是实时维护数据还是定期重建。哪个更好依赖于应用程序,但是定期重建并不只是节省资源,也可以保持表不会有很多碎片,以及有完全顺序组织的索引(这回更加高效)
当重建汇总表和缓存表时,通常需要保证数据在操作时依然可用。这就需要通过使用"影子表"来实现,"影子表"指的时一张在真实表"背后"创建的表。当完成了建表操作后,可以通过一个原子的重命名操作切换影子表和原表。例如,如果需要重建my_summary,则可以先创建my_summary_new,然后填充好数据,最后和真实表做切换
```sql
mysql> CREATE TABLE my_summary_new LIKE my_summary;
mysql> RENAME TABLE my_summary TO my_summary_old, my_summary_new TO my_summary;
```
如果像上面的例子一样,在将my_summary这个名字分配给新键的表之前将原始的my_summary表重命名为my_summary_old,就可以在下一次重建之前一直保留旧版本的数据,如果新表有问题,则可以很容易地进行快速回滚操作。
物化视图。
许多数据库管理系统(例如Oracle或者微软SQL Server)都提供了一个被称作物化视图地功能。物化视图实际上是预先计算并且存储在磁盘上的表,可以通过各种各样的策略刷新和更新。MySQL并不原生支持物化视图。然而Justin Swanhart的开源工具Flexviews,也可以自己实现物化视图。Flexviews比完全自己实现的解决方案更精细,并且提供了很多不错的功能使得可以更简单地创建和维护物化视图。它由下面这些部分组成:
1.变更数据抓取(Change Data Capture, CDC)功能,可以读取服务器的二进制日志,并且解析相关行的变更
2.一系列可以帮助创建和管理视图的定义的存储过程
3.一些可以应用变更到数据库中的物化视图的工具
对比传统的维护汇总表和缓存表的方法,Flexviews通过提取对源表的更改,可以增量地重新计算物化视图地内容。这意味着不需要通过查询原始数据来更新视图。例如,如果创建了一张汇总表用于计算每个分组地行数,此后增加了一行数据到源表中,Flexviews简单地给相应地组的行数加一即可。同样的技术对其他的聚合函数也有效,例如SUM()和AVG().这实际上是有好处的,基于行的二进制日志包含行更新前后的镜像,所以Flexviews不仅仅可以获得每行的新值,还可以需要查找源表就能直到每行数据的旧版本。计算增量数据比源表中读取数据的效率要高得多。
例如写出一个SELECT语句描述想从已经存在的数据库中得到的数据。这可能包含关联和聚合(GROUP BY)。Flexviews中有一个辅助工具可以转换SQL语句到Flexviews的API调用。Flexviews会做完所有的脏活、累活:监控数据库的变更并且转换后用于更新存储物化视图的表。现在应用可以简单地查询物化视图来替代查询需要检索的表。
Flexviews有不错的SQL覆盖范围,包括一些棘手的表达式,你可能没有料到一个工具可以在MySQL服务器之外处理这些工作。这一点对创建基于复杂SQL表达式的视图很有用,可以用基于物化视图的简单、快速的查询替换原来复杂的查询
许多数据库管理系统(例如Oracle或者微软SQL Server)都提供了一个被称作物化视图地功能。物化视图实际上是预先计算并且存储在磁盘上的表,可以通过各种各样的策略刷新和更新。MySQL并不原生支持物化视图。然而Justin Swanhart的开源工具Flexviews,也可以自己实现物化视图。Flexviews比完全自己实现的解决方案更精细,并且提供了很多不错的功能使得可以更简单地创建和维护物化视图。它由下面这些部分组成:
1.变更数据抓取(Change Data Capture, CDC)功能,可以读取服务器的二进制日志,并且解析相关行的变更
2.一系列可以帮助创建和管理视图的定义的存储过程
3.一些可以应用变更到数据库中的物化视图的工具
对比传统的维护汇总表和缓存表的方法,Flexviews通过提取对源表的更改,可以增量地重新计算物化视图地内容。这意味着不需要通过查询原始数据来更新视图。例如,如果创建了一张汇总表用于计算每个分组地行数,此后增加了一行数据到源表中,Flexviews简单地给相应地组的行数加一即可。同样的技术对其他的聚合函数也有效,例如SUM()和AVG().这实际上是有好处的,基于行的二进制日志包含行更新前后的镜像,所以Flexviews不仅仅可以获得每行的新值,还可以需要查找源表就能直到每行数据的旧版本。计算增量数据比源表中读取数据的效率要高得多。
例如写出一个SELECT语句描述想从已经存在的数据库中得到的数据。这可能包含关联和聚合(GROUP BY)。Flexviews中有一个辅助工具可以转换SQL语句到Flexviews的API调用。Flexviews会做完所有的脏活、累活:监控数据库的变更并且转换后用于更新存储物化视图的表。现在应用可以简单地查询物化视图来替代查询需要检索的表。
Flexviews有不错的SQL覆盖范围,包括一些棘手的表达式,你可能没有料到一个工具可以在MySQL服务器之外处理这些工作。这一点对创建基于复杂SQL表达式的视图很有用,可以用基于物化视图的简单、快速的查询替换原来复杂的查询
计数器表。
如果应用在表中保存计数器,则在更新计数器时可能碰到并发问题。计数器表在Web应用中很常见。可以用这种表缓存一个用户的朋友数、文件下载次数等。创建一张独立的表存储计数器通常是个好主意,这样可使计数器表小且快。使用独立的表可以帮助避免查询缓存失效,并且可以使用下面展示的一些高级的技巧。应该让事情变得尽可能简单,假设有一个计数器表,只有一行数据,记录网站的点击次数:
```sql
mysql> CREATE TABLE hit_counter (
-> cnt int unsigned not null
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.03 sec)
```
网站的每次点击都会导致对计数器进行更新:
```sql
mysql> UPDATE hit_counter SET cnt = cnt + 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
问题在于,对于任何想要更新这一行的事务来说,这条记录上都有一个全局的互斥锁(mutex)。这会使得这些事务只能串行执行。要获得更高的并发更新性能,也可以将计数器保存在多行中,每次随机选择一行进行更新。这样做需要对计数器表进行如下修改:
```sql
mysql> CREATE TABLE hit_counter_new (
-> slot tinyint unsigned not null primary key,
-> cnt int unsigned not null
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.03 sec)
```
然后预先在这张表增加100行数据。现在选择一个随机的槽(slot)进行更新:
```sql
mysql> UPDATE hit_counter_new SET cnt = cnt + 1 WHERE slot = RAND() * 100;
```
要获得统计结果,需要使用下面这样的聚合查询:
```sql
mysql> SELECT SUM(cnt) FROM hit_counter_new;
```
一个常见的需求是每隔一段时间开始一个新的计数器(例如,每天一个)。如果需要这么做,则可以再简单地修改一下表设计:
```sql
mysql> CREATE TABLE daily_hit_counter (
-> day date not null,
-> slot tinyint unsigned not null,
-> cnt int unsigned not null,
-> primary key(day, slot)
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (21.37 sec)
```
在这个场景中,可以不用像前面的例子那样预先生成行,而用ON DUPLICATE KEY UPDATE代替:
```sql
mysql> INSERT INTO daily_hit_counter(day,slot,cnt)
-> VALUES(CURRENT_DATE, RAND() * 100, 1) ON DUPLICATE KEY UPDATE cnt = cnt +1;
Query OK, 1 row affected (0.02 sec)
```
如果希望减少表的行数,以避免表得太大,可以写一个周期执行的任务,合并所有结果到0号槽,并且删除所有其他的槽:
```sql
mysql> UPDATE daily_hit_counter AS c
-> INNER JOIN (
-> SELECT day,SUM(cnt) AS cnt, MIN(slot) AS mslot
-> FROM daily_hit_counter
-> GROUP BY day
-> ) AS x USING(day)
-> SET c.cnt = IF(c.slot = x.mslot, x.cnt, 0),
-> c.slot =IF(c.slot = x.mslot, 0, c.slot);
Query OK, 1 row affected (0.06 sec)
mysql> DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt = 0;
```
如果应用在表中保存计数器,则在更新计数器时可能碰到并发问题。计数器表在Web应用中很常见。可以用这种表缓存一个用户的朋友数、文件下载次数等。创建一张独立的表存储计数器通常是个好主意,这样可使计数器表小且快。使用独立的表可以帮助避免查询缓存失效,并且可以使用下面展示的一些高级的技巧。应该让事情变得尽可能简单,假设有一个计数器表,只有一行数据,记录网站的点击次数:
```sql
mysql> CREATE TABLE hit_counter (
-> cnt int unsigned not null
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.03 sec)
```
网站的每次点击都会导致对计数器进行更新:
```sql
mysql> UPDATE hit_counter SET cnt = cnt + 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
问题在于,对于任何想要更新这一行的事务来说,这条记录上都有一个全局的互斥锁(mutex)。这会使得这些事务只能串行执行。要获得更高的并发更新性能,也可以将计数器保存在多行中,每次随机选择一行进行更新。这样做需要对计数器表进行如下修改:
```sql
mysql> CREATE TABLE hit_counter_new (
-> slot tinyint unsigned not null primary key,
-> cnt int unsigned not null
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.03 sec)
```
然后预先在这张表增加100行数据。现在选择一个随机的槽(slot)进行更新:
```sql
mysql> UPDATE hit_counter_new SET cnt = cnt + 1 WHERE slot = RAND() * 100;
```
要获得统计结果,需要使用下面这样的聚合查询:
```sql
mysql> SELECT SUM(cnt) FROM hit_counter_new;
```
一个常见的需求是每隔一段时间开始一个新的计数器(例如,每天一个)。如果需要这么做,则可以再简单地修改一下表设计:
```sql
mysql> CREATE TABLE daily_hit_counter (
-> day date not null,
-> slot tinyint unsigned not null,
-> cnt int unsigned not null,
-> primary key(day, slot)
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (21.37 sec)
```
在这个场景中,可以不用像前面的例子那样预先生成行,而用ON DUPLICATE KEY UPDATE代替:
```sql
mysql> INSERT INTO daily_hit_counter(day,slot,cnt)
-> VALUES(CURRENT_DATE, RAND() * 100, 1) ON DUPLICATE KEY UPDATE cnt = cnt +1;
Query OK, 1 row affected (0.02 sec)
```
如果希望减少表的行数,以避免表得太大,可以写一个周期执行的任务,合并所有结果到0号槽,并且删除所有其他的槽:
```sql
mysql> UPDATE daily_hit_counter AS c
-> INNER JOIN (
-> SELECT day,SUM(cnt) AS cnt, MIN(slot) AS mslot
-> FROM daily_hit_counter
-> GROUP BY day
-> ) AS x USING(day)
-> SET c.cnt = IF(c.slot = x.mslot, x.cnt, 0),
-> c.slot =IF(c.slot = x.mslot, 0, c.slot);
Query OK, 1 row affected (0.06 sec)
mysql> DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt = 0;
```
更快地读,更慢地写。
为了提升读查询的速度,经常会需要建一些额外索引,增加冗余列,甚至是创建缓存表和汇总表。这些方法会增加写查询的负担,也需要额外的维护任务,但在设计高性能数据库时,这些都是常见的技巧:虽然写操作变得更慢了,但更显著地提高了读操作的性能
然而,写操作变慢并不是读操作变得更快所付出的唯一代价,还可能同时增加了读操作和写操作的开发难度
为了提升读查询的速度,经常会需要建一些额外索引,增加冗余列,甚至是创建缓存表和汇总表。这些方法会增加写查询的负担,也需要额外的维护任务,但在设计高性能数据库时,这些都是常见的技巧:虽然写操作变得更慢了,但更显著地提高了读操作的性能
然而,写操作变慢并不是读操作变得更快所付出的唯一代价,还可能同时增加了读操作和写操作的开发难度
加快ALTER TABLE操作的速度。
MySQL的ALTER TABLE操作的性能对大表来说是个大问题。MySQL执行大部分修改表结构操作的方法是用新的结构创建一个空表,从旧表中查出所有数据插入新表,然后删除旧表。这样操作可能需要花费很长时间,如果内存不足而表又很大,而且还有很多索引的情况下尤其如此。许多人都有这样的经验,ALTER TABLE操作需要花费数个小时甚至数天才能完成。
MySQL5.1以及更新版本包含一些类型的"在线"操作的支持,这些功能不需要再整个操作过程中锁表。最近版本的InnoDB也支持通过排序来建索引,这使得建索引更快,并且有一个紧凑的索引布局。
一般而言,大部分ALTER TABLE操作将导致MySQL服务中断。我们会展示一些在DDL操作时有用的技巧,但这是针对一些特殊的场景而言的。对常见的场景,能使用的技巧只有两种:一种是现在一台不提供服务的机器上执行ALTER TABLE操作,然后和提供服务的主库进行切换;另外一种是"影子拷贝"。影子拷贝的技巧是用要求的表结构和源表无关的新表,然后通过重命名和删表操作交换两张表。也有一些工具可以帮助完成影子拷贝工作:例如,Facebook数据库运维团队的"online schema change"工具、Shlomi Noach的openark toolkit以及Percona Toolkit 如果使用Flexviews,也可以通过其CDC工具执行无锁的表结构变更。
不是所有的ALTER TABLE操作都会引起表重建。例如,有两种方法可以改变或者删除一个列的默认值(一种方法很快,另外一种则很慢)。假如要修改电影的默认租赁期限,从三天改到五天,下面是很慢的方式:
```sql
mysql> ALTER TABLE film MODIFY COLUMN rental_duration TINYINT(3) NOT NULL DEFAULT 5;
```
SHOW STATUS显示这个语句做了1000次读和1000次插入操作。换句话说,它拷贝了整张表到一张新表,甚至列的类型、大小、和可否为NULL属性都没改变。理论上,MySQL可以跳过创建新表的步骤。列的默认值实际上存在表的.frm文件中,所以可以直接修改这个文件而不需要改动表本身。然而MySQL还没有采用这种优化的方法,所有的MODIFY COLUMN操作都将导致表重建。
另外一种方法是通过ALTER COLUMN操作来该变列的默认值:
```sql
mysql> ALTER TABLE film
-> ALTER COLUMN rental_duration SET DEFAULT 5;
```
这个语句会直接修改.frm文件而不涉及表数据,所以这个操作是非常快的
MySQL的ALTER TABLE操作的性能对大表来说是个大问题。MySQL执行大部分修改表结构操作的方法是用新的结构创建一个空表,从旧表中查出所有数据插入新表,然后删除旧表。这样操作可能需要花费很长时间,如果内存不足而表又很大,而且还有很多索引的情况下尤其如此。许多人都有这样的经验,ALTER TABLE操作需要花费数个小时甚至数天才能完成。
MySQL5.1以及更新版本包含一些类型的"在线"操作的支持,这些功能不需要再整个操作过程中锁表。最近版本的InnoDB也支持通过排序来建索引,这使得建索引更快,并且有一个紧凑的索引布局。
一般而言,大部分ALTER TABLE操作将导致MySQL服务中断。我们会展示一些在DDL操作时有用的技巧,但这是针对一些特殊的场景而言的。对常见的场景,能使用的技巧只有两种:一种是现在一台不提供服务的机器上执行ALTER TABLE操作,然后和提供服务的主库进行切换;另外一种是"影子拷贝"。影子拷贝的技巧是用要求的表结构和源表无关的新表,然后通过重命名和删表操作交换两张表。也有一些工具可以帮助完成影子拷贝工作:例如,Facebook数据库运维团队的"online schema change"工具、Shlomi Noach的openark toolkit以及Percona Toolkit 如果使用Flexviews,也可以通过其CDC工具执行无锁的表结构变更。
不是所有的ALTER TABLE操作都会引起表重建。例如,有两种方法可以改变或者删除一个列的默认值(一种方法很快,另外一种则很慢)。假如要修改电影的默认租赁期限,从三天改到五天,下面是很慢的方式:
```sql
mysql> ALTER TABLE film MODIFY COLUMN rental_duration TINYINT(3) NOT NULL DEFAULT 5;
```
SHOW STATUS显示这个语句做了1000次读和1000次插入操作。换句话说,它拷贝了整张表到一张新表,甚至列的类型、大小、和可否为NULL属性都没改变。理论上,MySQL可以跳过创建新表的步骤。列的默认值实际上存在表的.frm文件中,所以可以直接修改这个文件而不需要改动表本身。然而MySQL还没有采用这种优化的方法,所有的MODIFY COLUMN操作都将导致表重建。
另外一种方法是通过ALTER COLUMN操作来该变列的默认值:
```sql
mysql> ALTER TABLE film
-> ALTER COLUMN rental_duration SET DEFAULT 5;
```
这个语句会直接修改.frm文件而不涉及表数据,所以这个操作是非常快的
只修改.frm文件。
从前面的例子中可以看到修改表的.frm文件是很快的,但MySQL有时候会在没有必要的时候也重建.如果愿意冒一些风险,可以让MySQL做一些其他类型的修改而不用重建表。下面这些操作是有可能不需要重建表的:
1.移除(不是增加)一个列的AUTO_INCREMENT属性
2.增加、移除,或更改ENUM和SET常量。如果移除的是已经有行数据用到其值的变量,查询将会返回一个空字符串
基本的技术是为想要的表结构创建一个新的.frm文件,然后用它替换掉已经存在的那张表的.frm文件,像下面这样:
1.创建一张有相同结构的空表,并进行所需要的修改(例如增加ENUM常量)
2.执行FLUSH TABLES WITH READ LOCK.这将会关闭所有正在使用的表,并且进制任何表被打开
3.交换.frm文件
4.执行UNLOCK TABLES来释放第2步的读锁
下面以给sakila.film表的rating列增加一个常量为例来说明。当前列看起来如下
```sql
mysql> SHOW COLUMNS FROM film LIKE 'rating';
+--------+------------------------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+------------------------------------+------+-----+---------+-------+
| rating | enum('G','PG','PG-13','R','NC-17') | YES | | G | |
+--------+------------------------------------+------+-----+---------+-------+
1 row in set (0.07 sec)
```
假设我们需要为那些对电影更加谨慎的父母们增加一个PG-14的电影分级:
```sql
mysql> CREATE TABLE film_new LIKE film;
Query OK, 0 rows affected (0.03 sec)
mysql> ALTER TABLE film_new
-> MODIFY COLUMN rating ENUM('G','PG','PG-13','R','NC-17','PG-14')
-> DEFAULT 'G';
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> FLUSH TABLES WITH READ LOCK;
Query OK, 0 rows affected (0.02 sec)
```
注意,我们是在常量列表的末尾增加一个新的值。如果把新增的值放在中间,例如PG-13之后,则会导致已经存在的数据的含义被改变:已经存在的R值将变成PG-14,而已经存在的NC-17将变成R,等等。
接下来用操作系统的命令交换.frm文件
```bash
/var/lib/mysql/sakila# mv film.frm film_tmp.film
/var/lib/mysql/sakila# mv film_new.frm film.frm
/var/lib/mysql/sakila# mv film_tmp.frm film_new.frm
```
再回到MySQL 命令行,现在可以解锁表并且看到变更后的效果了:
```sql
mysql> UNLOCK TABLES;
Query OK, 0 rows affected (18.96 sec)
mysql> SHOW COLUMNS FROM film LIKE 'rating';
+--------+--------------------------------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+--------------------------------------------+------+-----+---------+-------+
| rating | enum('G','PG','PG-13','R','NC-17','PG-14') | YES | | G | |
+--------+--------------------------------------------+------+-----+---------+-------+
1 row in set (0.09 sec)
```
最后要做的事删除为完成这个操作而创建的辅助表:
```sql
mysql>DROP TABLE film_new;
```
从前面的例子中可以看到修改表的.frm文件是很快的,但MySQL有时候会在没有必要的时候也重建.如果愿意冒一些风险,可以让MySQL做一些其他类型的修改而不用重建表。下面这些操作是有可能不需要重建表的:
1.移除(不是增加)一个列的AUTO_INCREMENT属性
2.增加、移除,或更改ENUM和SET常量。如果移除的是已经有行数据用到其值的变量,查询将会返回一个空字符串
基本的技术是为想要的表结构创建一个新的.frm文件,然后用它替换掉已经存在的那张表的.frm文件,像下面这样:
1.创建一张有相同结构的空表,并进行所需要的修改(例如增加ENUM常量)
2.执行FLUSH TABLES WITH READ LOCK.这将会关闭所有正在使用的表,并且进制任何表被打开
3.交换.frm文件
4.执行UNLOCK TABLES来释放第2步的读锁
下面以给sakila.film表的rating列增加一个常量为例来说明。当前列看起来如下
```sql
mysql> SHOW COLUMNS FROM film LIKE 'rating';
+--------+------------------------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+------------------------------------+------+-----+---------+-------+
| rating | enum('G','PG','PG-13','R','NC-17') | YES | | G | |
+--------+------------------------------------+------+-----+---------+-------+
1 row in set (0.07 sec)
```
假设我们需要为那些对电影更加谨慎的父母们增加一个PG-14的电影分级:
```sql
mysql> CREATE TABLE film_new LIKE film;
Query OK, 0 rows affected (0.03 sec)
mysql> ALTER TABLE film_new
-> MODIFY COLUMN rating ENUM('G','PG','PG-13','R','NC-17','PG-14')
-> DEFAULT 'G';
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> FLUSH TABLES WITH READ LOCK;
Query OK, 0 rows affected (0.02 sec)
```
注意,我们是在常量列表的末尾增加一个新的值。如果把新增的值放在中间,例如PG-13之后,则会导致已经存在的数据的含义被改变:已经存在的R值将变成PG-14,而已经存在的NC-17将变成R,等等。
接下来用操作系统的命令交换.frm文件
```bash
/var/lib/mysql/sakila# mv film.frm film_tmp.film
/var/lib/mysql/sakila# mv film_new.frm film.frm
/var/lib/mysql/sakila# mv film_tmp.frm film_new.frm
```
再回到MySQL 命令行,现在可以解锁表并且看到变更后的效果了:
```sql
mysql> UNLOCK TABLES;
Query OK, 0 rows affected (18.96 sec)
mysql> SHOW COLUMNS FROM film LIKE 'rating';
+--------+--------------------------------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+--------------------------------------------+------+-----+---------+-------+
| rating | enum('G','PG','PG-13','R','NC-17','PG-14') | YES | | G | |
+--------+--------------------------------------------+------+-----+---------+-------+
1 row in set (0.09 sec)
```
最后要做的事删除为完成这个操作而创建的辅助表:
```sql
mysql>DROP TABLE film_new;
```
快速创建MyISAM索引.
为了高效地载入数据到MyISAM表中,有一个常用的技巧是先禁用索引、载入数据,然后重新启用索引:
```sql
mysql>ALTER TABLE test.load_data DISABLE KEYS;
mysql>ALTER TABLE test.load_data ENABLE KEYS;
```
这个技巧能够发挥作用,是因为构建索引的工作被延迟到数据完全载入以后,这个时候已经可以通过排序来构建索引了,这样做会快很多,并且使得索引树的碎片更少、更紧凑。
不幸的是,这个办法对唯一索引无效,因为DISABLE KEYS只对非唯一索引有效。MyISAM会在内存中构造唯一索引,并且为载入的每一行检查唯一性。一旦索引的大小超过了有效内存大小,载入操作就会变得越来越慢。
在现代版本的InnoDB版本中,有一个类似的技巧,这依赖于InnoDB的快速在线索引创建功能。这个技巧是,先删除所有的非唯一索引,然后增加新的列,最后重新创建删除掉的索引。Percona Server可以自动完成这些操作步骤。也可以使用像前面说的ALTER TABLE的骇客方法来加速这个操作,但需要多做一些工作并且承担一定的风险。这对备份中载入数据是很有用的,例如,当已经直到所有的数据都是有效的并且没有必要做唯一性检查就可以这么来操作。
下面是操作步骤:
1.用需要的表结构创建一张表,但是不包括索引。
2.载入数据到表以后构建.MYD文件
3.按照需要的结构创建另外一张空表,这次要包含索引。这回创建需要的.frm和.MYI文件
4.获取读锁并刷新表
5.重命名第二张表的.frm和MYI文件,让MySQL认为是第一张表的文件
6.释放读锁
7.使用REPAIR TABLE来重建表的索引。该操作会通过排序来构建所有索引,包括唯一索引
为了高效地载入数据到MyISAM表中,有一个常用的技巧是先禁用索引、载入数据,然后重新启用索引:
```sql
mysql>ALTER TABLE test.load_data DISABLE KEYS;
mysql>ALTER TABLE test.load_data ENABLE KEYS;
```
这个技巧能够发挥作用,是因为构建索引的工作被延迟到数据完全载入以后,这个时候已经可以通过排序来构建索引了,这样做会快很多,并且使得索引树的碎片更少、更紧凑。
不幸的是,这个办法对唯一索引无效,因为DISABLE KEYS只对非唯一索引有效。MyISAM会在内存中构造唯一索引,并且为载入的每一行检查唯一性。一旦索引的大小超过了有效内存大小,载入操作就会变得越来越慢。
在现代版本的InnoDB版本中,有一个类似的技巧,这依赖于InnoDB的快速在线索引创建功能。这个技巧是,先删除所有的非唯一索引,然后增加新的列,最后重新创建删除掉的索引。Percona Server可以自动完成这些操作步骤。也可以使用像前面说的ALTER TABLE的骇客方法来加速这个操作,但需要多做一些工作并且承担一定的风险。这对备份中载入数据是很有用的,例如,当已经直到所有的数据都是有效的并且没有必要做唯一性检查就可以这么来操作。
下面是操作步骤:
1.用需要的表结构创建一张表,但是不包括索引。
2.载入数据到表以后构建.MYD文件
3.按照需要的结构创建另外一张空表,这次要包含索引。这回创建需要的.frm和.MYI文件
4.获取读锁并刷新表
5.重命名第二张表的.frm和MYI文件,让MySQL认为是第一张表的文件
6.释放读锁
7.使用REPAIR TABLE来重建表的索引。该操作会通过排序来构建所有索引,包括唯一索引
创建高性能的索引
概述。
索引(在MySQL中也叫做"键(key)")是存储引擎用于快速找到记录的一种数据结构。这是索引的基本功能。
索引对于良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响语法重要。在数据量较小时负载较低时,不恰当的索引对性能的影响可能还不明显,但当数据量逐渐增大时,性能则会急剧下降(除非特别说明,假设都是硬盘驱动器。固态硬盘驱动器有着完全不同的性能特性)。不过,索引却经常被忽略,有时候甚至被误解,所以在实际案例中经常会遇到由糟糕索引导致的问题。索引优化应该是对查询性能优化是最有效的手段,索引能够轻易将查询性能提高几个数量级,"最优"的索引有时比一个"好的"索引性能要好两个数量级。创建一个真正“最有”的索引经常需要重写查询
索引(在MySQL中也叫做"键(key)")是存储引擎用于快速找到记录的一种数据结构。这是索引的基本功能。
索引对于良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响语法重要。在数据量较小时负载较低时,不恰当的索引对性能的影响可能还不明显,但当数据量逐渐增大时,性能则会急剧下降(除非特别说明,假设都是硬盘驱动器。固态硬盘驱动器有着完全不同的性能特性)。不过,索引却经常被忽略,有时候甚至被误解,所以在实际案例中经常会遇到由糟糕索引导致的问题。索引优化应该是对查询性能优化是最有效的手段,索引能够轻易将查询性能提高几个数量级,"最优"的索引有时比一个"好的"索引性能要好两个数量级。创建一个真正“最有”的索引经常需要重写查询
索引基础。
在MySQL中,存储引擎用类似的方法使用索引,其先在索引中找到对应值,然后根据匹配的索引记录找到对应的数据行。假如要运行下面的查询:
```sql
mysql> SELECT first_name FROM actor WHERE actor_id = 5;
```
如果在actor_id列上建有索引,则MySQL将使用该索引找到actor_id为5的行,也就是说,MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行。索引可以包含一个或者多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能高效地使用索引的最左前缀列,创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的。
在MySQL中,存储引擎用类似的方法使用索引,其先在索引中找到对应值,然后根据匹配的索引记录找到对应的数据行。假如要运行下面的查询:
```sql
mysql> SELECT first_name FROM actor WHERE actor_id = 5;
```
如果在actor_id列上建有索引,则MySQL将使用该索引找到actor_id为5的行,也就是说,MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行。索引可以包含一个或者多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能高效地使用索引的最左前缀列,创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的。
如果使用的是ORM,是否还需要关心索引?
简而言之:是的,仍然需要理解索引,即使是适用对象关系映射(ORM)工具。
ORM工具能够生产符合逻辑的、合法的查询(多数时候),除非只是生成非常基本的查询(例如仅是根据主键查询),否则它很难生成适合索引的查询。无论是多个复杂的ORM工具,在精妙和复杂的索引面前都是"浮云"。很多时候,即使是查询优化技术专家也很难兼顾到各种情况,更别说ORM了
简而言之:是的,仍然需要理解索引,即使是适用对象关系映射(ORM)工具。
ORM工具能够生产符合逻辑的、合法的查询(多数时候),除非只是生成非常基本的查询(例如仅是根据主键查询),否则它很难生成适合索引的查询。无论是多个复杂的ORM工具,在精妙和复杂的索引面前都是"浮云"。很多时候,即使是查询优化技术专家也很难兼顾到各种情况,更别说ORM了
索引的类型
索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一类型的索引,其底层的实现也可能不同。下面我们先来看看MySQL支持的索引类型,以及它们的优点和缺点:
索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一类型的索引,其底层的实现也可能不同。下面我们先来看看MySQL支持的索引类型,以及它们的优点和缺点:
B-Tree索引
当人们讨论索引的时候,如果没有特别指明类型,那多半说的是B-Tree索引,它使用B-Tree数据结构来存储数据。(实际上很多存储引擎使用的是B+Tree,既每一个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点的范围遍历),大多数MySQL引擎都支持折中索引。Archive引擎是一个例外:5.1之前Archive不支持任何索引,直到5.1才开始支持单个自增列(AUTO_INCREMENT)的索引。使用术语"B-Tree",是因为MySQL在CREATE TABLE和其他语句中也使用该关键字,不过,底层的存储引擎也可能使用不同的存储结构,例如,NDB集群存储引擎内部实际上使用了T-Tree结构存储这种索引,即使其名字是BTREE;InnoDB则使用的是B+Tree;
存储引擎以不同的方式使用B-Tree索引,性能也各有不同,各有优劣。例如,MyISAM使用前缀压缩技术使得索引更小,但InnoDB则按照原数据格式进行存储。再如MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行。
B-Tree通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。如图所示,该图展示了B-Tree索引的抽象表示,大致反映了InnoDB索引是如何工作的。MyISAM使用的结构有所不同,但基本思想是类似的。
B-Tree索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点(未画出)开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值得上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。
叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页(不同引擎的"指针类型不同")。如图所示,该图回直了一个节点和其对应的叶子节点,其实在根节点和叶子节点之间可能有很多层节点页。树的深度和表的大小直接相关。
B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据。例如,在一个基于文本域的索引树上,按字母顺序传递连续的值进行查找是非常合适的,所以像"找出所有以I到K开头的名字"这样的查询效率会非常高.假设有如下数据表:
```sql
mysql> CREATE TABLE Peoople (
-> last_name varchar(50) not null,
-> first_name varchar(50) not null,
-> dob date not null,
-> gender enum('m','f') not null,
-> key(last_name, first_name, dob)
-> );
```
对于表中的每一行数据,索引中包含了last_name、first_name和dob列的值,上图展示了该索引是如何组织数据的存储的。请注意,索引对多个值进行排序的依据是CREATE TBLE语句中定义索引时列的顺序,看一下最后两个条目,两个人的姓和名都一样,则根据它们的出生日期来排列顺序。
当人们讨论索引的时候,如果没有特别指明类型,那多半说的是B-Tree索引,它使用B-Tree数据结构来存储数据。(实际上很多存储引擎使用的是B+Tree,既每一个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点的范围遍历),大多数MySQL引擎都支持折中索引。Archive引擎是一个例外:5.1之前Archive不支持任何索引,直到5.1才开始支持单个自增列(AUTO_INCREMENT)的索引。使用术语"B-Tree",是因为MySQL在CREATE TABLE和其他语句中也使用该关键字,不过,底层的存储引擎也可能使用不同的存储结构,例如,NDB集群存储引擎内部实际上使用了T-Tree结构存储这种索引,即使其名字是BTREE;InnoDB则使用的是B+Tree;
存储引擎以不同的方式使用B-Tree索引,性能也各有不同,各有优劣。例如,MyISAM使用前缀压缩技术使得索引更小,但InnoDB则按照原数据格式进行存储。再如MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行。
B-Tree通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。如图所示,该图展示了B-Tree索引的抽象表示,大致反映了InnoDB索引是如何工作的。MyISAM使用的结构有所不同,但基本思想是类似的。
B-Tree索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点(未画出)开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值得上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。
叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页(不同引擎的"指针类型不同")。如图所示,该图回直了一个节点和其对应的叶子节点,其实在根节点和叶子节点之间可能有很多层节点页。树的深度和表的大小直接相关。
B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据。例如,在一个基于文本域的索引树上,按字母顺序传递连续的值进行查找是非常合适的,所以像"找出所有以I到K开头的名字"这样的查询效率会非常高.假设有如下数据表:
```sql
mysql> CREATE TABLE Peoople (
-> last_name varchar(50) not null,
-> first_name varchar(50) not null,
-> dob date not null,
-> gender enum('m','f') not null,
-> key(last_name, first_name, dob)
-> );
```
对于表中的每一行数据,索引中包含了last_name、first_name和dob列的值,上图展示了该索引是如何组织数据的存储的。请注意,索引对多个值进行排序的依据是CREATE TBLE语句中定义索引时列的顺序,看一下最后两个条目,两个人的姓和名都一样,则根据它们的出生日期来排列顺序。
B-Tree(从技术上来说是B+Tree)索引树中的部分条目示例
可以使用B-Tree索引的查询类型。B-Tree索引使用于全键值、键值范围或键前缀查找。其中键前缀查找只适用于根据最左前缀的查找。(这是MySQL相关的特性,甚至和具体的版本也相关。其他有些数据库也可以使用索引的非前缀部分,虽然使用完全的前缀的效率更好。MySQL未来也可能会提供这个特性).前面所述的索引对如下类型的查询有效:
1.全值匹配:全值匹配指的是和索引中的所有列进行匹配,例如前面提到的索引可用于查找行命为Cuba Allen、出生于1960-01-01的人
2.匹配最左前缀:前面提到的索引可用于查找所有姓为Allen的人,即只使用索引的第一列
3.匹配列前缀:也可以只匹配某一列的值的开头部分。例如前面提到的索引可用于查找所有以J开头的姓的人。这里页只使用了索引的第一列
4.匹配范围值:例如前面提到的索引可用于查找姓在Allen和Barrymore之间的人。这里也只使用了索引的第一列
5.精确匹配某一列范围并匹配另外一列:前面提到的索引也可用于查找所有姓Allen,并且名字是字母K开头(比如Kim、Karl等)。即第一列last_name全匹配,第二列first_name范围匹配。
6.只访问索引的查询:B-Tree通常可以支持"只访问索引的查询",即查询只需要访问索引,而无须访问数据行。
因为索引树中的节点是有序的,所以除了按值查找之外,索引还可以用于查询中的ORDER BY操作(按顺序查找)。一般来说,如果B-Tree可以按照某种方式查找到值,那么也可以按照这种方式用于排序。所有,如果ORDER BY 子句满足前面列出的几种查询类型,则这个索引也可以满足对应的排序需求。
1.全值匹配:全值匹配指的是和索引中的所有列进行匹配,例如前面提到的索引可用于查找行命为Cuba Allen、出生于1960-01-01的人
2.匹配最左前缀:前面提到的索引可用于查找所有姓为Allen的人,即只使用索引的第一列
3.匹配列前缀:也可以只匹配某一列的值的开头部分。例如前面提到的索引可用于查找所有以J开头的姓的人。这里页只使用了索引的第一列
4.匹配范围值:例如前面提到的索引可用于查找姓在Allen和Barrymore之间的人。这里也只使用了索引的第一列
5.精确匹配某一列范围并匹配另外一列:前面提到的索引也可用于查找所有姓Allen,并且名字是字母K开头(比如Kim、Karl等)。即第一列last_name全匹配,第二列first_name范围匹配。
6.只访问索引的查询:B-Tree通常可以支持"只访问索引的查询",即查询只需要访问索引,而无须访问数据行。
因为索引树中的节点是有序的,所以除了按值查找之外,索引还可以用于查询中的ORDER BY操作(按顺序查找)。一般来说,如果B-Tree可以按照某种方式查找到值,那么也可以按照这种方式用于排序。所有,如果ORDER BY 子句满足前面列出的几种查询类型,则这个索引也可以满足对应的排序需求。
下面是一些关于B-Tree索引的限制:
1.如果不是按照索引的最左列开始查找,则无法使用索引。例如上面例子中的索引无法用于查找名字为Bill的人,也无法查找某个特定生日的人,因为这两列都不是最左数据列。类似地,也无法查找姓氏以某个字母结尾的人
2.不能跳过索引中的列。也就是说,前面所述的索引无法用于查找姓为Smith并且在某个特定日期出生的人。如果不指定名(first_name),则MySQL只能使用索引的第一列
3.如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。例如有查询WHERE last_name ='Simith' AND first_name LIKE 'J%' AND dob = '1976-12-23',这个查询只能使用索引的前两列,因为这里LIKE是一个范围条件(但是服务器可以把其余列用于其他目的)。如果范围查询列值的数量优先,那么可以通过使用多个等于条件来代替范围条件
到这里大家应该可以明白,前面提到的索引列的顺序是多么的重要:这些限制都和索引列的顺序有关。在优化性能的时候,可能需要使用相同的列但顺序不同的索引满足不同类型的查询需求。
也有些限制并不是B-Tree本身导致的,而是MySQL优化器和存储引擎使用索引的方式导致的。这部分限制在未来的版本中可能就不再是限制了
1.如果不是按照索引的最左列开始查找,则无法使用索引。例如上面例子中的索引无法用于查找名字为Bill的人,也无法查找某个特定生日的人,因为这两列都不是最左数据列。类似地,也无法查找姓氏以某个字母结尾的人
2.不能跳过索引中的列。也就是说,前面所述的索引无法用于查找姓为Smith并且在某个特定日期出生的人。如果不指定名(first_name),则MySQL只能使用索引的第一列
3.如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。例如有查询WHERE last_name ='Simith' AND first_name LIKE 'J%' AND dob = '1976-12-23',这个查询只能使用索引的前两列,因为这里LIKE是一个范围条件(但是服务器可以把其余列用于其他目的)。如果范围查询列值的数量优先,那么可以通过使用多个等于条件来代替范围条件
到这里大家应该可以明白,前面提到的索引列的顺序是多么的重要:这些限制都和索引列的顺序有关。在优化性能的时候,可能需要使用相同的列但顺序不同的索引满足不同类型的查询需求。
也有些限制并不是B-Tree本身导致的,而是MySQL优化器和存储引擎使用索引的方式导致的。这部分限制在未来的版本中可能就不再是限制了
哈希索引
哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有的列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code).哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时哈希表中保存指向每个数据行的指针。在MySQL中,只有Memory引擎显式支持哈希索引。这也是Memory引擎表的默认索引类型,Memory引擎同时也支持B-Tree索引。值得一提的是,Memory引擎是支持非唯一哈希索引的,这在数据库世界里面是比较与众不同的。如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中.下面来看一个例子
```sql
mysql> CREATE TABLE testhash(
-> fname VARCHAR(50) NOT NULL,
-> lname VARCHAR(50) NOT NULL,
-> KEY USING HASH(fname)
-> ) ENGINE=MEMORY;
```
表种包含如下数据:
```sql
mysql> SELECT * FROM testhash;
+-------+-----------+
| fname | lname |
+-------+-----------+
| Arjen | Lentz |
| Baron | Schwartz |
| Peter | Zaitsev |
| Vadim | Tkachenko |
+-------+-----------+
```
假设索引使用假想的哈希函数f(),它返回下面的值(都是示例数据,非真实数据):
```c
f('Arjen')=2323
f('Baron')=7437
f('Peter')=8784
f('Vadim')=2458
```
则哈希索引的数据结构如下:
```c
槽(Slot) 值(Value)
2323 指向第1行的指针
2458 指向第4行的指针
7437 指向第2行的指针
8784 指向第3行的指针
```
注意每隔槽的编号是顺序的,但是数据行不是。先在来看如下查询
```sql
mysql> SELECT lname FROM testhash WHERE fname ='Peter';
```
MySQL先计算'Peter'的哈希值,并使用该值寻找对应的记录指针。因为f('Peter')=8784,所以MySQL在索引中查找8784,可以找到指向第3行的指针,最后异步是比较第三行的值是否为'Peter',以确保就是要查找的行。因为索引自身只需要存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。然而,哈希索引也有它的限制:
1.哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显
2.哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序
3.哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如,在数据列(A,B)上建立哈希索引,如果查询只有数据列A,则无法使用该索引
4.哈希索引只支持等值比较查询,包括=、IN()、<=>(注意<>和<=>是不同的操作)。也不支持任何范围查询,例如WHERE price >100
5.访问哈希索引的数据非常快,除非有很多哈希冲突(不同的索引列值却有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,主键进行比较,直到找出所有符合条件的行
6.如果哈希冲突很多的化,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。因为这些限制,哈希索引只适用于某些特定的场合。而一旦适合哈希索引,则它带来的的性能提升将非常显著。举个例子,在数据仓库应用中有一种经典的"星型"schema。需要关联很多表,哈希索引就非常适合查找表的需求。除了Memory引擎外,NDB集群引擎也支持唯一哈希索引,且在NDB集群引擎中作用非常特殊。
InnoDB引擎有一个特殊的功能叫作"自适应哈希索引(Adaptive Hash Index)"。当InnoDB注意到某些索引值被使用得非常频繁时,它会在内存中基于B-Tree之上再创建一个哈希索引,这样就让B-Tree索引也具有哈希索引的一些优点,比如快速的哈希查找。这是一个完全自动的、内部的行为,用户无法控制或者配置,不过有必要,完全可以关闭该功能。
哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有的列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code).哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时哈希表中保存指向每个数据行的指针。在MySQL中,只有Memory引擎显式支持哈希索引。这也是Memory引擎表的默认索引类型,Memory引擎同时也支持B-Tree索引。值得一提的是,Memory引擎是支持非唯一哈希索引的,这在数据库世界里面是比较与众不同的。如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中.下面来看一个例子
```sql
mysql> CREATE TABLE testhash(
-> fname VARCHAR(50) NOT NULL,
-> lname VARCHAR(50) NOT NULL,
-> KEY USING HASH(fname)
-> ) ENGINE=MEMORY;
```
表种包含如下数据:
```sql
mysql> SELECT * FROM testhash;
+-------+-----------+
| fname | lname |
+-------+-----------+
| Arjen | Lentz |
| Baron | Schwartz |
| Peter | Zaitsev |
| Vadim | Tkachenko |
+-------+-----------+
```
假设索引使用假想的哈希函数f(),它返回下面的值(都是示例数据,非真实数据):
```c
f('Arjen')=2323
f('Baron')=7437
f('Peter')=8784
f('Vadim')=2458
```
则哈希索引的数据结构如下:
```c
槽(Slot) 值(Value)
2323 指向第1行的指针
2458 指向第4行的指针
7437 指向第2行的指针
8784 指向第3行的指针
```
注意每隔槽的编号是顺序的,但是数据行不是。先在来看如下查询
```sql
mysql> SELECT lname FROM testhash WHERE fname ='Peter';
```
MySQL先计算'Peter'的哈希值,并使用该值寻找对应的记录指针。因为f('Peter')=8784,所以MySQL在索引中查找8784,可以找到指向第3行的指针,最后异步是比较第三行的值是否为'Peter',以确保就是要查找的行。因为索引自身只需要存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。然而,哈希索引也有它的限制:
1.哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显
2.哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序
3.哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如,在数据列(A,B)上建立哈希索引,如果查询只有数据列A,则无法使用该索引
4.哈希索引只支持等值比较查询,包括=、IN()、<=>(注意<>和<=>是不同的操作)。也不支持任何范围查询,例如WHERE price >100
5.访问哈希索引的数据非常快,除非有很多哈希冲突(不同的索引列值却有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,主键进行比较,直到找出所有符合条件的行
6.如果哈希冲突很多的化,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。因为这些限制,哈希索引只适用于某些特定的场合。而一旦适合哈希索引,则它带来的的性能提升将非常显著。举个例子,在数据仓库应用中有一种经典的"星型"schema。需要关联很多表,哈希索引就非常适合查找表的需求。除了Memory引擎外,NDB集群引擎也支持唯一哈希索引,且在NDB集群引擎中作用非常特殊。
InnoDB引擎有一个特殊的功能叫作"自适应哈希索引(Adaptive Hash Index)"。当InnoDB注意到某些索引值被使用得非常频繁时,它会在内存中基于B-Tree之上再创建一个哈希索引,这样就让B-Tree索引也具有哈希索引的一些优点,比如快速的哈希查找。这是一个完全自动的、内部的行为,用户无法控制或者配置,不过有必要,完全可以关闭该功能。
创建自定义哈希索引
如果存储引擎不支持哈希索引,则可以模拟像InnoDB一样创建哈希索引,这可以享受一些哈希索引的便利,例如只需要很小的索引就可以为超长的键创建索引。思路很简单:在B-Tree基础上创建一个伪哈希索引。这和真正的哈希索引不是一回事,因为还是适用B-Tree进行查找,但是它适用哈希值而不是键本身进行索引查找。你需要做的事就是在查询的WHERE子句中手动指定适用哈希函数。下面是一个示例,例如需要存储大量的URL,并需要根据URL进行搜索查找。如果适用B-Tree来存储URL,存储的内容就会很大,因为URL本身都很长。正常情况下会有如下查询:
```sql
mysql> SELECT id FROM url WHERE url = "http://www.mysql.com";
```
若删除原来URL列上的索引,而新增一个被索引的url_crc列,适用CRC32做哈希,就可以适用下面的方式查询:
```sql
mysql>SELECT id FROM url WHERE url="http://www.mysql.com" AND url_crc=CRC32("http://www.mysql.com");
```
这样做的性能会非常高,因为MySQL优化器会适用这个选择性很高而提及很小的基于url_crc列的索引来完成查找(在上面的案例中,索引值伪1560514994)即使有多个记录相同的索引值,查找仍然很快,只需要根据哈希值做快速的整数比较就能找到索引条目,然后一一比较返回对应的行。另外一种方式就是对完整的URL字符串做索引,那样会非常慢。
```sql
mysql> SELECT CRC32("http://www.mysql.com");
+-------------------------------+
| CRC32("http://www.mysql.com") |
+-------------------------------+
| 1560514994 |
+-------------------------------+
1 row in set (0.10 sec)
```
这样实现的缺陷事需要维护哈希值。可以手动维护,也可以适用触发器实现。下面的案例演示了触发器如何在插入和更新时维护url_crc列,首先创建如下表:
```sql
CREATE TABLE pseudohash (
id INT UNSIGNED NOT NULL auto_increment,
url VARCHAR ( 255 ) NOT NULL,
url_crc INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY ( id ));
```
然后创建触发器。先临时修改一下语句分隔符,这样就可以在触发器定义中适用分毫:
```sql
DELIMITER //
CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
END;
//
CREATE TRIGGER pseudohash_crc_upd BEFORE UPDATE ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
END;
//
DELIMITER;
```
剩下工作就是验证一下触发器如何维护哈希索引:
```sql
mysql>INSERT INTO pseudohash (url) VALUES ('http://www.mysql.com');
mysql> SELECT * FROM pseudohash;
+----+----------------------+------------+
| id | url | url_crc |
+----+----------------------+------------+
| 1 | http://www.mysql.com | 1560514994 |
+----+----------------------+------------+
1 row in set (0.08 sec)
mysql> UPDATE pseudohash SET url = 'http://www.mysql.com/' WHERE id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT * FROM pseudohash;
+----+-----------------------+------------+
| id | url | url_crc |
+----+-----------------------+------------+
| 1 | http://www.mysql.com/ | 1558250469 |
+----+-----------------------+------------+
1 row in set (0.07 sec)
```
如果存储引擎不支持哈希索引,则可以模拟像InnoDB一样创建哈希索引,这可以享受一些哈希索引的便利,例如只需要很小的索引就可以为超长的键创建索引。思路很简单:在B-Tree基础上创建一个伪哈希索引。这和真正的哈希索引不是一回事,因为还是适用B-Tree进行查找,但是它适用哈希值而不是键本身进行索引查找。你需要做的事就是在查询的WHERE子句中手动指定适用哈希函数。下面是一个示例,例如需要存储大量的URL,并需要根据URL进行搜索查找。如果适用B-Tree来存储URL,存储的内容就会很大,因为URL本身都很长。正常情况下会有如下查询:
```sql
mysql> SELECT id FROM url WHERE url = "http://www.mysql.com";
```
若删除原来URL列上的索引,而新增一个被索引的url_crc列,适用CRC32做哈希,就可以适用下面的方式查询:
```sql
mysql>SELECT id FROM url WHERE url="http://www.mysql.com" AND url_crc=CRC32("http://www.mysql.com");
```
这样做的性能会非常高,因为MySQL优化器会适用这个选择性很高而提及很小的基于url_crc列的索引来完成查找(在上面的案例中,索引值伪1560514994)即使有多个记录相同的索引值,查找仍然很快,只需要根据哈希值做快速的整数比较就能找到索引条目,然后一一比较返回对应的行。另外一种方式就是对完整的URL字符串做索引,那样会非常慢。
```sql
mysql> SELECT CRC32("http://www.mysql.com");
+-------------------------------+
| CRC32("http://www.mysql.com") |
+-------------------------------+
| 1560514994 |
+-------------------------------+
1 row in set (0.10 sec)
```
这样实现的缺陷事需要维护哈希值。可以手动维护,也可以适用触发器实现。下面的案例演示了触发器如何在插入和更新时维护url_crc列,首先创建如下表:
```sql
CREATE TABLE pseudohash (
id INT UNSIGNED NOT NULL auto_increment,
url VARCHAR ( 255 ) NOT NULL,
url_crc INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY ( id ));
```
然后创建触发器。先临时修改一下语句分隔符,这样就可以在触发器定义中适用分毫:
```sql
DELIMITER //
CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
END;
//
CREATE TRIGGER pseudohash_crc_upd BEFORE UPDATE ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
END;
//
DELIMITER;
```
剩下工作就是验证一下触发器如何维护哈希索引:
```sql
mysql>INSERT INTO pseudohash (url) VALUES ('http://www.mysql.com');
mysql> SELECT * FROM pseudohash;
+----+----------------------+------------+
| id | url | url_crc |
+----+----------------------+------------+
| 1 | http://www.mysql.com | 1560514994 |
+----+----------------------+------------+
1 row in set (0.08 sec)
mysql> UPDATE pseudohash SET url = 'http://www.mysql.com/' WHERE id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT * FROM pseudohash;
+----+-----------------------+------------+
| id | url | url_crc |
+----+-----------------------+------------+
| 1 | http://www.mysql.com/ | 1558250469 |
+----+-----------------------+------------+
1 row in set (0.07 sec)
```
如果采用这种方式,记住不要适用SHA1()和MD5(0作为哈希函数。因为这两个函数计算出来的哈希值是非常长的字符串,会浪费大量空间,比较时也会更慢。SHA1()和MD5()是强加密函数,设计目标是最大限度消除冲突,但这里并需要这样高的要求。简单哈希函数的冲突在一个可以接受的范围,同时又能够提供好的性能。如果数据表非常大,CRC32()会出现大量的哈希冲突,则可以考虑自己实现一个简单的64位哈希函数。这个自定义函数要返回整数,而不是字符串。一个简单的办法可以适用MD5()函数返回值的一部分来作为自定义哈希函数。这可能比自己写一个哈希算法的性能要差,不过这样实现最简单:
```sql
mysql> SELECT CONV(RIGHT(MD5('http://www.mysql.com/'), 16), 16, 10) AS HASH64;
+---------------------+
| HASH64 |
+---------------------+
| 9761173720318281581 |
+---------------------+
```
处理哈希冲突,当适用哈希索引进行查询的时候,必须在WHERE子句中包含常量值:
```sql
mysql> SELECT id FROM url WHERE url_crc=CRC32('http://mysql.com') AND url='http://www.mysql.com';
```
一旦出现哈希冲突,另一个字符串的哈希值也恰是1560514994,则下面的查询时无法正确工作的。
```sql
mysql> SELECT id FROM url WHERE url_crc=CRC32('http://www.mysql.com');
```
因为所谓的"生日悖论",出现哈希冲突的概率的增长速度可能比想象的要快得多。CRC32()返回的是32位的整数,当索引有93 000条时出现的冲突的概率是1%。例如将/usr/share/dict/words中的词导入数据表并进行CRC32()计算,最后会有98 569行,这就已经出现一次哈希冲突了,冲突让下面的插叙年返回了多条记录
```sql
mysql>SELECT word,crc FROM words WHERE crc=CRC32('gnu');
```
正确的刑法应该如下:
```sql
mysql>SELECT word, crc FROM words WHERE crc=CRC32('gnu') AND word = 'gnu';
```
要避免冲突问题,必须在WHERE条件中带入哈希值和对应列值。如果不是想查询具体值,例如只是统计记录数(不精确的),则可以不带如列值,直接适用CRC32()的哈希值查询即可。还可以适用如FNV64()函数作为哈希函数,这是移植自Percona Server的函数,可以以插件的方式在任何MySQL版本中适用,哈希值64位,速度快,且冲突比CRC32()要少很多
```sql
mysql> SELECT CONV(RIGHT(MD5('http://www.mysql.com/'), 16), 16, 10) AS HASH64;
+---------------------+
| HASH64 |
+---------------------+
| 9761173720318281581 |
+---------------------+
```
处理哈希冲突,当适用哈希索引进行查询的时候,必须在WHERE子句中包含常量值:
```sql
mysql> SELECT id FROM url WHERE url_crc=CRC32('http://mysql.com') AND url='http://www.mysql.com';
```
一旦出现哈希冲突,另一个字符串的哈希值也恰是1560514994,则下面的查询时无法正确工作的。
```sql
mysql> SELECT id FROM url WHERE url_crc=CRC32('http://www.mysql.com');
```
因为所谓的"生日悖论",出现哈希冲突的概率的增长速度可能比想象的要快得多。CRC32()返回的是32位的整数,当索引有93 000条时出现的冲突的概率是1%。例如将/usr/share/dict/words中的词导入数据表并进行CRC32()计算,最后会有98 569行,这就已经出现一次哈希冲突了,冲突让下面的插叙年返回了多条记录
```sql
mysql>SELECT word,crc FROM words WHERE crc=CRC32('gnu');
```
正确的刑法应该如下:
```sql
mysql>SELECT word, crc FROM words WHERE crc=CRC32('gnu') AND word = 'gnu';
```
要避免冲突问题,必须在WHERE条件中带入哈希值和对应列值。如果不是想查询具体值,例如只是统计记录数(不精确的),则可以不带如列值,直接适用CRC32()的哈希值查询即可。还可以适用如FNV64()函数作为哈希函数,这是移植自Percona Server的函数,可以以插件的方式在任何MySQL版本中适用,哈希值64位,速度快,且冲突比CRC32()要少很多
空间数据索引(R-Tree)
MyISAM表支持空间索引,可以用作地理数据存储。和B-Tree索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。查询时,可以有效地使用任意维度来组合查询。必须使用MySQL的GIS相关函数如MBRCONTAINS()等来维护数据。MySQL的GIS支持并不完善,所以大部分人都不会使用这个特性。开源关系数据库系统中对GIS的解决方案做得比较好的是PostgreSQL的PostGIS.
MyISAM表支持空间索引,可以用作地理数据存储。和B-Tree索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。查询时,可以有效地使用任意维度来组合查询。必须使用MySQL的GIS相关函数如MBRCONTAINS()等来维护数据。MySQL的GIS支持并不完善,所以大部分人都不会使用这个特性。开源关系数据库系统中对GIS的解决方案做得比较好的是PostgreSQL的PostGIS.
全文索引
全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。全文搜索和其他几类索引的匹配方式完全不一样。它有许多需要注意的细节,如停用词、词干和复数、布尔搜索等。全文索引更类似于搜索引擎作得事情,而不是简单的WHERE条件匹配。
在相同的列上同时创建全文索引和基于值得B-Tree索引不会有冲突,全文索引适用于MATCH AGAINST操作,而不是普通得WHERE条件操作
全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。全文搜索和其他几类索引的匹配方式完全不一样。它有许多需要注意的细节,如停用词、词干和复数、布尔搜索等。全文索引更类似于搜索引擎作得事情,而不是简单的WHERE条件匹配。
在相同的列上同时创建全文索引和基于值得B-Tree索引不会有冲突,全文索引适用于MATCH AGAINST操作,而不是普通得WHERE条件操作
索引的优点。
索引可以让服务器快速地定位到表的指定位置。但是这并不是索引的唯一作用,到目前为止可以看到,根据创建索引的数据结构不同,索引也有一些其他的附加作用。最常见的B-Tree索引,按照顺序存储数据,所有MySQL可以用来做ORDER BY 和GROUP BY操作。因为数据是有序的,所以B-Tree也就会将相关的列值存储在一起。最后,因为索引中存储了实际的列值,所以某些查询只使用索引就能够完成全部查询。据此特性,总结下来索引有如下三个优点:
1.索引大大减少了服务器需要扫描的数据量
2.索引可以帮助服务器避免排序和临时表
3.索引可以将随机IO变为顺序IO
索引这个主题完全值得单独写一本书,如果想深入理解这部分内容,强烈建议阅读由Tapio Lahdenmaki和Mike Leach编写的Relational Database Index Design and the Optimizers(Wiley出版社)一书,该书详细介绍了如何计算索引的成本和作用、如何评估查询速度、如何分析索引维护的代价和其带来的好处等.Lahdenmaki和Leach在书中介绍了如何评价一个索引是否适合某个查询的"三星系统"(three-star system):索引将相关的记录放到一起则获得一星;如果索引中的数据顺序和查找的排列顺序一致则获得二星;如果索引中的列包含了查询中需要的全部列则获得三星
索引可以让服务器快速地定位到表的指定位置。但是这并不是索引的唯一作用,到目前为止可以看到,根据创建索引的数据结构不同,索引也有一些其他的附加作用。最常见的B-Tree索引,按照顺序存储数据,所有MySQL可以用来做ORDER BY 和GROUP BY操作。因为数据是有序的,所以B-Tree也就会将相关的列值存储在一起。最后,因为索引中存储了实际的列值,所以某些查询只使用索引就能够完成全部查询。据此特性,总结下来索引有如下三个优点:
1.索引大大减少了服务器需要扫描的数据量
2.索引可以帮助服务器避免排序和临时表
3.索引可以将随机IO变为顺序IO
索引这个主题完全值得单独写一本书,如果想深入理解这部分内容,强烈建议阅读由Tapio Lahdenmaki和Mike Leach编写的Relational Database Index Design and the Optimizers(Wiley出版社)一书,该书详细介绍了如何计算索引的成本和作用、如何评估查询速度、如何分析索引维护的代价和其带来的好处等.Lahdenmaki和Leach在书中介绍了如何评价一个索引是否适合某个查询的"三星系统"(three-star system):索引将相关的记录放到一起则获得一星;如果索引中的数据顺序和查找的排列顺序一致则获得二星;如果索引中的列包含了查询中需要的全部列则获得三星
索引是最好的解决方案吗?
索引并不总是最好的工具。总的来说,只有当索引帮助存储引擎快速查找到记录带来的好处大于其带来的额外工作时,索引才是有效地。对于非常小的表,大部分情况下简单的全表扫描更高效。对于中到大型的表,索引就非常有效。但对于大型的表,建立和使用索引的代价将随之增长。这种情况下,则需要一种技术可以直接区分出查询需要的一组数据,而不是一条记录一条记录地匹配。例如可以使用分区技术。如果表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录"哪个用户的信息存储在哪个表中"的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于大型系统,这是一个常用的技巧,事实上,Infobright就是使用类似的实现。对于TB级别的数据,定位单条记录的意义不大,所以经常会使用块级别元数据技术来替代索引
索引并不总是最好的工具。总的来说,只有当索引帮助存储引擎快速查找到记录带来的好处大于其带来的额外工作时,索引才是有效地。对于非常小的表,大部分情况下简单的全表扫描更高效。对于中到大型的表,索引就非常有效。但对于大型的表,建立和使用索引的代价将随之增长。这种情况下,则需要一种技术可以直接区分出查询需要的一组数据,而不是一条记录一条记录地匹配。例如可以使用分区技术。如果表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录"哪个用户的信息存储在哪个表中"的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于大型系统,这是一个常用的技巧,事实上,Infobright就是使用类似的实现。对于TB级别的数据,定位单条记录的意义不大,所以经常会使用块级别元数据技术来替代索引
高性能的索引策略
正确地创建和使用索引是实现高性能查询的基础。前面已经介绍了各种类型的索引及其对应的优缺点。现在一起来看看如何真正地发挥这些索引的优势。
高效地选择和使用索引有很多种方式,其中有些是针对特殊案例的优化方法,有些则是针对特定行为的优化。使用哪个索引,以及如何评估选择不同索引的性能影响的技巧,则需要持续不断地学习。
正确地创建和使用索引是实现高性能查询的基础。前面已经介绍了各种类型的索引及其对应的优缺点。现在一起来看看如何真正地发挥这些索引的优势。
高效地选择和使用索引有很多种方式,其中有些是针对特殊案例的优化方法,有些则是针对特定行为的优化。使用哪个索引,以及如何评估选择不同索引的性能影响的技巧,则需要持续不断地学习。
独立的列
我们通常会看到一些查询不当地使用索引,或者使得MySQL无法使用已有的索引。如果查询中的列不是独立的,则MySQL就不会使用索引。"独立的列"是指索引不能是表达式的一部分,也不能是函数的参数。例如,下面这个查询无法使用actor_id列的索引:
```sql
mysql> SELECT actor_id FROM actor WHERE actor_id + 1 =5;
```
凭肉眼很容易看出WHERE中的表达式其实等价于actor_id=4,但是MySQL无法自动解析这个方程式。这完全是用户行为。我们应该养成简化WHERE条件的习惯,始终将索引列单独放在比较符号的一侧。下面是另一个常见的错误:
```sql
mysql> SELECT .... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;
```
我们通常会看到一些查询不当地使用索引,或者使得MySQL无法使用已有的索引。如果查询中的列不是独立的,则MySQL就不会使用索引。"独立的列"是指索引不能是表达式的一部分,也不能是函数的参数。例如,下面这个查询无法使用actor_id列的索引:
```sql
mysql> SELECT actor_id FROM actor WHERE actor_id + 1 =5;
```
凭肉眼很容易看出WHERE中的表达式其实等价于actor_id=4,但是MySQL无法自动解析这个方程式。这完全是用户行为。我们应该养成简化WHERE条件的习惯,始终将索引列单独放在比较符号的一侧。下面是另一个常见的错误:
```sql
mysql> SELECT .... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;
```
前缀索引和索引选择性
有时候需要索引很长的字符列,这会让索引变得大且慢。一个策略是前面提到过的模拟哈希索引。但有时候这样做还不够,还可以做些什么呢?
通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样也会降低索引的选择姓。索引的选择性是指,不重复的所引致(也成为技术,cardinality)和数据表的记录总数(#T)的比值。范围从1/#T到1之间。索引的选择性越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于BLOB、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换句话说,前缀的"技术"应该接近于完整列的"基数"。为了决定前缀的合适长度,需要找到最常见的值得列表,然后和最常见得前缀列表进行比较。(MySQL Sakila官方链接http://downloads.mysql.com/docs/sakila-db.zip)大家可以下载下来导入进去。这里从表city中生成一个示例表,这样就有足够得数据进行演示:
```sql
use sakila;
CREATE TABLE city_demo(city VARCHAR(50) NOT NULL);
INSERT INTO city_demo(city) SELECT city FROM city;
INSERT INTO city_demo(city) SELECT city FROM city_demo;# 执行五次
UPDATE city_demo SET city = (SELECT city FROM city ORDER BY RAND() LIMIT 1);
```
现在我们有了示例数据集。数据分布当然不是真实得分布;因为我们使用了RAND().所以你的结果会与此不同,但对这个例子说这并不重要。首先,我们找到最常见的城市列表:
```sql
mysql> SELECT COUNT(*) AS cnt,city FROM city_demo GROUP BY city ORDER BY cnt DESC LIMIT 10;
+-----+-------------+
| cnt | city |
+-----+-------------+
| 64 | London |
| 50 | Bagé |
| 49 | Brockton |
| 48 | Soshanguve |
| 47 | Bucuresti |
| 47 | El Alto |
| 47 | Balašiha |
| 46 | Osmaniye |
| 46 | Arecibo |
| 45 | Santo André |
+-----+-------------+
10 rows in set (0.09 sec)
```
注意到,上面每隔值都出现了45~64次。现在查找结果最频繁出现的城市前缀,先从前3个前缀字母开始:
有时候需要索引很长的字符列,这会让索引变得大且慢。一个策略是前面提到过的模拟哈希索引。但有时候这样做还不够,还可以做些什么呢?
通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样也会降低索引的选择姓。索引的选择性是指,不重复的所引致(也成为技术,cardinality)和数据表的记录总数(#T)的比值。范围从1/#T到1之间。索引的选择性越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于BLOB、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换句话说,前缀的"技术"应该接近于完整列的"基数"。为了决定前缀的合适长度,需要找到最常见的值得列表,然后和最常见得前缀列表进行比较。(MySQL Sakila官方链接http://downloads.mysql.com/docs/sakila-db.zip)大家可以下载下来导入进去。这里从表city中生成一个示例表,这样就有足够得数据进行演示:
```sql
use sakila;
CREATE TABLE city_demo(city VARCHAR(50) NOT NULL);
INSERT INTO city_demo(city) SELECT city FROM city;
INSERT INTO city_demo(city) SELECT city FROM city_demo;# 执行五次
UPDATE city_demo SET city = (SELECT city FROM city ORDER BY RAND() LIMIT 1);
```
现在我们有了示例数据集。数据分布当然不是真实得分布;因为我们使用了RAND().所以你的结果会与此不同,但对这个例子说这并不重要。首先,我们找到最常见的城市列表:
```sql
mysql> SELECT COUNT(*) AS cnt,city FROM city_demo GROUP BY city ORDER BY cnt DESC LIMIT 10;
+-----+-------------+
| cnt | city |
+-----+-------------+
| 64 | London |
| 50 | Bagé |
| 49 | Brockton |
| 48 | Soshanguve |
| 47 | Bucuresti |
| 47 | El Alto |
| 47 | Balašiha |
| 46 | Osmaniye |
| 46 | Arecibo |
| 45 | Santo André |
+-----+-------------+
10 rows in set (0.09 sec)
```
注意到,上面每隔值都出现了45~64次。现在查找结果最频繁出现的城市前缀,先从前3个前缀字母开始:
```sql
mysql> SELECT COUNT(*) AS cnt, LEFT(city, 3) AS pref FROM city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
+-----+------+
| cnt | pref |
+-----+------+
| 456 | San |
| 175 | Cha |
| 174 | Tan |
| 172 | Sou |
| 165 | Sal |
| 144 | al- |
| 128 | Man |
| 127 | Hal |
| 124 | Bal |
| 122 | Shi |
+-----+------+
10 rows in set (0.08 sec)
```
每个前缀都比原来的城市出现的次数更多,因此唯一前缀比唯一城市要少得多。然后我们增加前缀长度,直到这个前缀的选择性接近完整列的选择性。经过实验后发现前缀为7时比较合适:
```sql
mysql> SELECT COUNT(*) AS cnt, LEFT(city, 7) AS pref FROM city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
+-----+---------+
| cnt | pref |
+-----+---------+
| 64 | London |
| 62 | San Fel |
| 60 | Santiag |
| 58 | Valle d |
| 50 | Bagé |
| 49 | Brockto |
| 48 | Soshang |
| 47 | El Alto |
| 47 | Bucures |
| 47 | Balaših |
+-----+---------+
10 rows in set (0.08 sec)
```
mysql> SELECT COUNT(*) AS cnt, LEFT(city, 3) AS pref FROM city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
+-----+------+
| cnt | pref |
+-----+------+
| 456 | San |
| 175 | Cha |
| 174 | Tan |
| 172 | Sou |
| 165 | Sal |
| 144 | al- |
| 128 | Man |
| 127 | Hal |
| 124 | Bal |
| 122 | Shi |
+-----+------+
10 rows in set (0.08 sec)
```
每个前缀都比原来的城市出现的次数更多,因此唯一前缀比唯一城市要少得多。然后我们增加前缀长度,直到这个前缀的选择性接近完整列的选择性。经过实验后发现前缀为7时比较合适:
```sql
mysql> SELECT COUNT(*) AS cnt, LEFT(city, 7) AS pref FROM city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
+-----+---------+
| cnt | pref |
+-----+---------+
| 64 | London |
| 62 | San Fel |
| 60 | Santiag |
| 58 | Valle d |
| 50 | Bagé |
| 49 | Brockto |
| 48 | Soshang |
| 47 | El Alto |
| 47 | Bucures |
| 47 | Balaših |
+-----+---------+
10 rows in set (0.08 sec)
```
计算合适的前缀长度的另外一个办法就是计算完整列的选择性,并使前缀的选择性接近于完整列的选择性。下面显式如何计算完整列的选择性:
```sql
mysql> SELECT COUNT(DISTINCT city)/COUNT(*) FROM city_demo;
+-------------------------------+
| COUNT(DISTINCT city)/COUNT(*) |
+-------------------------------+
| 0.0312 |
+-------------------------------+
1 row in set (0.08 sec)
```
通常来说(尽管也有例外情况)。这个例子中如何前缀的选择性能够接近0.031,基本上就可以可用了。可以在一个查询中针对不同前缀长度进行计算,这对于大表非常有用,下面给出了如何在同一个查询中计算不同前缀长度的选择性:
```sql
mysql> SELECT
COUNT(DISTINCT LEFT(city,3))/COUNT(*) AS sel3,
COUNT(DISTINCT LEFT(city,4))/COUNT(*) AS sel4,
COUNT(DISTINCT LEFT(city,5))/COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(city,6))/COUNT(*) AS sel6,
COUNT(DISTINCT LEFT(city,7))/COUNT(*) AS sel7
FROM city_demo;
+--------+--------+--------+--------+--------+
| sel3 | sel4 | sel5 | sel6 | sel7 |
+--------+--------+--------+--------+--------+
| 0.0237 | 0.0293 | 0.0305 | 0.0309 | 0.0310 |
+--------+--------+--------+--------+--------+
```
查询显式当前缀长度达到7的时候,再增加前缀长度,选择性能提升的复度已经很小了,只看平均选择性使不够的,也有例外的情况,需要考虑最坏情况下的选择性。平均选择性会让你认为前缀长度4或者5的索引已经足够了,但如果数据分布很不均匀,可能就会有陷阱。如果观察前缀为4的最常出现城市的次数,可以看到明显不均匀:
```sql
mysql> SELECT COUNT(*) AS cnt, LEFT(city,4) AS pref FROM city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 5;
+-----+------+
| cnt | pref |
+-----+------+
| 203 | San |
| 190 | Sant |
| 141 | Sout |
| 98 | Chan |
| 85 | Toul |
+-----+------+
```
如果前缀事4个字节,则最常出现的前缀的出现次数比最常出现的城市的出现次数要大很多。即使这些值的选择性比平均选择性要地。如果有比这个随机生成的示例更真实地数据,就更有可能看到这种现象。例如在真实的城市名上建一个长度为4地前缀索引,对于以"San"和"New"开头地城市地选择性就会非常糟糕,因为很多城市都以这两个词开头。
```sql
mysql> SELECT COUNT(DISTINCT city)/COUNT(*) FROM city_demo;
+-------------------------------+
| COUNT(DISTINCT city)/COUNT(*) |
+-------------------------------+
| 0.0312 |
+-------------------------------+
1 row in set (0.08 sec)
```
通常来说(尽管也有例外情况)。这个例子中如何前缀的选择性能够接近0.031,基本上就可以可用了。可以在一个查询中针对不同前缀长度进行计算,这对于大表非常有用,下面给出了如何在同一个查询中计算不同前缀长度的选择性:
```sql
mysql> SELECT
COUNT(DISTINCT LEFT(city,3))/COUNT(*) AS sel3,
COUNT(DISTINCT LEFT(city,4))/COUNT(*) AS sel4,
COUNT(DISTINCT LEFT(city,5))/COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(city,6))/COUNT(*) AS sel6,
COUNT(DISTINCT LEFT(city,7))/COUNT(*) AS sel7
FROM city_demo;
+--------+--------+--------+--------+--------+
| sel3 | sel4 | sel5 | sel6 | sel7 |
+--------+--------+--------+--------+--------+
| 0.0237 | 0.0293 | 0.0305 | 0.0309 | 0.0310 |
+--------+--------+--------+--------+--------+
```
查询显式当前缀长度达到7的时候,再增加前缀长度,选择性能提升的复度已经很小了,只看平均选择性使不够的,也有例外的情况,需要考虑最坏情况下的选择性。平均选择性会让你认为前缀长度4或者5的索引已经足够了,但如果数据分布很不均匀,可能就会有陷阱。如果观察前缀为4的最常出现城市的次数,可以看到明显不均匀:
```sql
mysql> SELECT COUNT(*) AS cnt, LEFT(city,4) AS pref FROM city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 5;
+-----+------+
| cnt | pref |
+-----+------+
| 203 | San |
| 190 | Sant |
| 141 | Sout |
| 98 | Chan |
| 85 | Toul |
+-----+------+
```
如果前缀事4个字节,则最常出现的前缀的出现次数比最常出现的城市的出现次数要大很多。即使这些值的选择性比平均选择性要地。如果有比这个随机生成的示例更真实地数据,就更有可能看到这种现象。例如在真实的城市名上建一个长度为4地前缀索引,对于以"San"和"New"开头地城市地选择性就会非常糟糕,因为很多城市都以这两个词开头。
在上面地示例中,已经找到了合适的前缀长度,下面演示如何创建前缀素银:
```sql
mysql> ALTER TABLE city_demo ADD KEY(city(7));
```
前缀索引是一种使索引更小、更快的有效方法,但另一方面也有其缺点:MySQL无法使用前缀索引做ORDER BY 和GROUP BY,也无法使用前缀索引做覆盖扫描。一个常见的场景使针对很长的十六进制唯一ID使用前缀索引。
有时候后缀索引(suffix index)也有用途(例如,找到某个域名的所有电子邮件地址)。MySQL原生不支持反向索引,但是可以把字符串反转后存储,并基于词建立前缀索引
```sql
mysql> ALTER TABLE city_demo ADD KEY(city(7));
```
前缀索引是一种使索引更小、更快的有效方法,但另一方面也有其缺点:MySQL无法使用前缀索引做ORDER BY 和GROUP BY,也无法使用前缀索引做覆盖扫描。一个常见的场景使针对很长的十六进制唯一ID使用前缀索引。
有时候后缀索引(suffix index)也有用途(例如,找到某个域名的所有电子邮件地址)。MySQL原生不支持反向索引,但是可以把字符串反转后存储,并基于词建立前缀索引
多列索引
很多人对多列索引的理解都不够。一个常见的错误就是,为每个列创建独立的索引,或者按照错误的顺序创建多列索引。先来看第一个问题,为每个列创建独立的索引,从SHOW CREATE TABLE总很容易看到这种情况:
```sql
CREATE TABLE t (
c1 INT,
c2 INT ,
c3 INT,
KEY(c1),
KEY(c2),
KEY(c3)
);
```
这种索引策略,一般是由于人们听到一些专家诸如"把WHERE条件里面的列都建上索引"这样模糊的建议导致的.实际上这个建议是非常错误的。这样一来最好的情况也只能是一星索引,其性能必去真正最优的索引可能差几个数量级。有时如果无法设计一个三星索引,那么不如忽略掉WHERE子句,集中精力优化索引列的顺序,或者创建一个全覆盖索引。在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。MySQL5.0和更新版本引入了一种"索引合并"(index merge)的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。更早版本的MySQL只能使用其中某一个单列索引,然而这种情况没有哪一个独立的单列索引是非常有效的。例如,表fime_actor在字典fim_id和actor_id上各有一个单列索引。但对于下面的这个查询WHERE条件,这两个单列索引都不是好的选择:
```sql
CREATE TABLE t (
c1 INT,
c2 INT ,
c3 INT,
KEY(c1),
KEY(c2),
KEY(c3)
);
```
在老的MySQL版本中,MySQL对这个查询会使用全表扫描。除非改写成如下的两个查询UNION的方式:
```sql
mysql> SELECT film_id,actor_id FROM film_actor WHERE actor_id = 1
UNION ALL
SELECT film_id,actor_id FROM film_actor WHERE film_id =1 AND actor_id <>1;
```
但在MySQL5.0和更新的版本中,查询能够同时使用这两个单列索引进行扫描,并将结果进行合并。这种算法有三个变种:OR条件的联合(union),AND条件的相交(intersection),组合前两种情况的联合及相交。下面的查询就是使用了两个索引扫描的联合,通过EXPLAIN中的Extra列可以看到这点:
```sql
mysql> EXPLAIN SELECT film_id,actor_id FROM film_actor WHERE actor_id = 1 OR film_id =1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: index_merge
possible_keys: PRIMARY,idx_fk_film_id
key: PRIMARY,idx_fk_film_id
key_len: 2,2
ref: NULL
rows: 29
filtered: 100.00
Extra: Using union(PRIMARY,idx_fk_film_id); Using where
1 row in set, 1 warning (0.00 sec)
```
MySQL会使用这类基数优化复杂查询,所以在某些语句的Extra列中还可以看到嵌套操作。
很多人对多列索引的理解都不够。一个常见的错误就是,为每个列创建独立的索引,或者按照错误的顺序创建多列索引。先来看第一个问题,为每个列创建独立的索引,从SHOW CREATE TABLE总很容易看到这种情况:
```sql
CREATE TABLE t (
c1 INT,
c2 INT ,
c3 INT,
KEY(c1),
KEY(c2),
KEY(c3)
);
```
这种索引策略,一般是由于人们听到一些专家诸如"把WHERE条件里面的列都建上索引"这样模糊的建议导致的.实际上这个建议是非常错误的。这样一来最好的情况也只能是一星索引,其性能必去真正最优的索引可能差几个数量级。有时如果无法设计一个三星索引,那么不如忽略掉WHERE子句,集中精力优化索引列的顺序,或者创建一个全覆盖索引。在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。MySQL5.0和更新版本引入了一种"索引合并"(index merge)的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。更早版本的MySQL只能使用其中某一个单列索引,然而这种情况没有哪一个独立的单列索引是非常有效的。例如,表fime_actor在字典fim_id和actor_id上各有一个单列索引。但对于下面的这个查询WHERE条件,这两个单列索引都不是好的选择:
```sql
CREATE TABLE t (
c1 INT,
c2 INT ,
c3 INT,
KEY(c1),
KEY(c2),
KEY(c3)
);
```
在老的MySQL版本中,MySQL对这个查询会使用全表扫描。除非改写成如下的两个查询UNION的方式:
```sql
mysql> SELECT film_id,actor_id FROM film_actor WHERE actor_id = 1
UNION ALL
SELECT film_id,actor_id FROM film_actor WHERE film_id =1 AND actor_id <>1;
```
但在MySQL5.0和更新的版本中,查询能够同时使用这两个单列索引进行扫描,并将结果进行合并。这种算法有三个变种:OR条件的联合(union),AND条件的相交(intersection),组合前两种情况的联合及相交。下面的查询就是使用了两个索引扫描的联合,通过EXPLAIN中的Extra列可以看到这点:
```sql
mysql> EXPLAIN SELECT film_id,actor_id FROM film_actor WHERE actor_id = 1 OR film_id =1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: index_merge
possible_keys: PRIMARY,idx_fk_film_id
key: PRIMARY,idx_fk_film_id
key_len: 2,2
ref: NULL
rows: 29
filtered: 100.00
Extra: Using union(PRIMARY,idx_fk_film_id); Using where
1 row in set, 1 warning (0.00 sec)
```
MySQL会使用这类基数优化复杂查询,所以在某些语句的Extra列中还可以看到嵌套操作。
索引合并策略有时候是一种优化的结果,但实际上更多时候说明了表上的索引建的很糟糕:
1.当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引
2.当服务器需要对多个索引做联合操作时(通常有多个OR条件)通常需要耗费大量CPU和内存资源在算法的缓存、排序和合并操作上。特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候
3.更重要的是,优化器不会把这些计算到"查询成本"(cost)中,优化器只关心随机页面读取。这会使得查询的成本被"低估",导致该执行计划还不如直接走全表扫描。这样做不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性,但如果是单独运行这样的查询则往往会忽略对并发性的影响。通常来说,还不如像在MySQL4.1或者更早的时代一样,将查询改写成UNION的方式往往更好
如果在中看到有索引合并,应该好好检查一下查询和表的结构,看是不是已经是最优的。可以通过参数optimizer_switch来关闭索引合并功能,也可以使用IGNORE INDEX提示让优化器忽略掉某些索引
1.当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引
2.当服务器需要对多个索引做联合操作时(通常有多个OR条件)通常需要耗费大量CPU和内存资源在算法的缓存、排序和合并操作上。特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候
3.更重要的是,优化器不会把这些计算到"查询成本"(cost)中,优化器只关心随机页面读取。这会使得查询的成本被"低估",导致该执行计划还不如直接走全表扫描。这样做不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性,但如果是单独运行这样的查询则往往会忽略对并发性的影响。通常来说,还不如像在MySQL4.1或者更早的时代一样,将查询改写成UNION的方式往往更好
如果在中看到有索引合并,应该好好检查一下查询和表的结构,看是不是已经是最优的。可以通过参数optimizer_switch来关闭索引合并功能,也可以使用IGNORE INDEX提示让优化器忽略掉某些索引
选择合适的索引列顺序
我们遇到最容易引起困惑的问题就是索引列的顺序。正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要(以B-Tree索引为例)。在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY 、GROUP BY和DISTINCT等子句的查询需求。所以多列索引的列顺序至关重要。在Lahdenmaki和Leach的"三星索引"系统中,列顺序也决定了一个索引是否能够成为一个真正的"三星索引".对于如何选择索引的列顺序有一个经验法则:将选择性最高的列放到索引最前列。这个建议有用吗?在某些场景可能有帮助,但通常不如避免随机IO和排序那么重要,考虑问题需要更全面(场景不同则选择不同,没有一个放之四海而皆准的法则。这里只是说明,这个经验法则可能没有你想象的重要)。当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的,这时候索引的作用只是用于优化WHERE条件的查找。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在WHERE子句中只适用了索引部分前缀列的查询来说选择性也更高。然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关,可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。以下面的查询为例:
```sql
mysql> SELECT * FROM payment WHERE staff_id = 2 AND customer_id =584;
```
是应该创建一个(staff_id,customer_id)索引还是应该颠倒一下顺序?可以跑一些查询来确定在这个表中值的分布情况,并确定哪个列的选择性更高。先用下面的查询预测一下,看看各个WHERE条件的分支对应的数据基数有多大:
```sql
*************************** 1. row ***************************
SUM(staff_id=2): 7990
SUM(customer_id =584): 30
```
根据前面的经验法则,应该将索引列customer_id放到前面,因为对应条件值的customer_id数量更小。我们再来看看对于这个customer_id的条件值,对应的staff_id列的选择性如何:
```sql
mysql> SELECT SUM(staff_id=2) FROM payment WHERE customer_id=584\G
*************************** 1. row ***************************
SUM(staff_id=2): 17
```
这样做有一个地方需要注意,查询的结果非常依赖于选定的值。如果按照上述办法优化,可能对其他一些条件值的查询不公平,服务器的整体性能可能变得更糟,或者其他某些查询的运行变得不如预期。如果是诸如pt-query-digest这样的工具的报告中提取"最差"查询,那么再按上述办法选定的索引顺序往往是非常高效的。如果没有类似的具体查询来运行,那么最好还是按照经验法则来做,因为经验法则考虑的是全局基数和选择性,而不是某个具体查询:
```sql
mysql> SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
-> COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
-> COUNT(*) FROM payment\G
*************************** 1. row ***************************
staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
COUNT(*): 16044
```
customer_id的选择性更高,所以答案是将其作为索引列的第一列:
```sql
mysql> ALTER TABLE payment ADD KEY(customer_id,staff_id);
```
我们遇到最容易引起困惑的问题就是索引列的顺序。正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要(以B-Tree索引为例)。在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY 、GROUP BY和DISTINCT等子句的查询需求。所以多列索引的列顺序至关重要。在Lahdenmaki和Leach的"三星索引"系统中,列顺序也决定了一个索引是否能够成为一个真正的"三星索引".对于如何选择索引的列顺序有一个经验法则:将选择性最高的列放到索引最前列。这个建议有用吗?在某些场景可能有帮助,但通常不如避免随机IO和排序那么重要,考虑问题需要更全面(场景不同则选择不同,没有一个放之四海而皆准的法则。这里只是说明,这个经验法则可能没有你想象的重要)。当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的,这时候索引的作用只是用于优化WHERE条件的查找。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在WHERE子句中只适用了索引部分前缀列的查询来说选择性也更高。然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关,可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。以下面的查询为例:
```sql
mysql> SELECT * FROM payment WHERE staff_id = 2 AND customer_id =584;
```
是应该创建一个(staff_id,customer_id)索引还是应该颠倒一下顺序?可以跑一些查询来确定在这个表中值的分布情况,并确定哪个列的选择性更高。先用下面的查询预测一下,看看各个WHERE条件的分支对应的数据基数有多大:
```sql
*************************** 1. row ***************************
SUM(staff_id=2): 7990
SUM(customer_id =584): 30
```
根据前面的经验法则,应该将索引列customer_id放到前面,因为对应条件值的customer_id数量更小。我们再来看看对于这个customer_id的条件值,对应的staff_id列的选择性如何:
```sql
mysql> SELECT SUM(staff_id=2) FROM payment WHERE customer_id=584\G
*************************** 1. row ***************************
SUM(staff_id=2): 17
```
这样做有一个地方需要注意,查询的结果非常依赖于选定的值。如果按照上述办法优化,可能对其他一些条件值的查询不公平,服务器的整体性能可能变得更糟,或者其他某些查询的运行变得不如预期。如果是诸如pt-query-digest这样的工具的报告中提取"最差"查询,那么再按上述办法选定的索引顺序往往是非常高效的。如果没有类似的具体查询来运行,那么最好还是按照经验法则来做,因为经验法则考虑的是全局基数和选择性,而不是某个具体查询:
```sql
mysql> SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
-> COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
-> COUNT(*) FROM payment\G
*************************** 1. row ***************************
staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
COUNT(*): 16044
```
customer_id的选择性更高,所以答案是将其作为索引列的第一列:
```sql
mysql> ALTER TABLE payment ADD KEY(customer_id,staff_id);
```
当使用前缀索引的时候,在某些条件值的基数比正常值高的时候,问题就来了。例如,在某些应用程序中,对于没有登录的用户,都将其用户名记录为"guest",在记录用户行为的会话(session)表和其他记录用户活动的表中"guest"就成为了一个特殊用户ID.一旦查询涉及这个用户,那么和对于正常用户的查询就大不同了,因为通常由很多会话都是没有登录的。系统账号也会导致类似的问题。一个应用通常都有一个特殊的管理员账号,和普通账号不同,它并不是一个具体的用户,系统中所有的其他用户都是这个用户的好友,所以系统往往通过它向网站的所有用户发送状态通知和其他消息。这个账号的巨大的好友列表很容易导致网站初夏你服务器性能问题。这实际上是一个非常典型的问题。任何的异常用户,不仅仅是那些用于管理应用的设计糟糕的账号会有同样的问题;那些拥有大量好友、图片、状态、收藏的用户,也会有前面提到的系统账号同样的问题。下面s是一个真实案例,在一个用户分享购买商品和购买经验的论坛上,这个特殊表上的查询运行得非常慢:
```sql
mysql> SELECT COUNT(DISTINCT threadId) AS COUNT_VALUE FROM Message
-> WHERE (groupId = 10137) AND (userId = 1288826) AND (anonymous = 0)
-> ORDER BY priority DESC, modifiedDate DESC
-> ;
```
这个查询看似没有建立合适的索引,所以客户咨询是否可以优化。EXPLAIN的结果如下:
```sql
id:1
select_type:SIMPLE
table:Message
type:ref
key:idx_groupId_userId
key_len:18
ref:const,const
rows:1251162
Extra:Using where
```
MySQL为这个查询选择了索引(groupId, userId),如果不考虑列的基数,这看起来是一个非常合理的选择。但如果考虑一下userID和groupID条件匹配的行数,可能就会有不同的想法了:
```sql
mysql> SELECT COUNT(*) , SUM(groupId=10137), SUM(userId=1288826),SUM(anonymous = 0)
-> FROM Message\G
*************************** 1. row ***************************
COUNT(*):4142217
SUM(groupId=10137):4092654
SUM(userId=1288826):1288496
SUM(anonymous=0):4141934
```
从上面的结果来看符合组(groupId)条件几乎满足表中的所有行,符合用户(userId)条件的有130弯条记录——也就是说索引基本上没什么用。因为这些数据是从其他应用中迁移过来的,迁移的时候把所有的消息都赋予了管理员组的用户。这个案例的解决办法是修改应用程序代码。去分这类特殊用户和组,禁止针对这类用户和组执行这个查询。从这个小案例可以看到经验法则和推论在多数情况下是有用的,但要注意不要假设平均情况下的性能也能代表特殊情况下的性能,特殊情况可能会摧毁整个应用的性能。最后,尽管关于选择性和基数的经验法则值得去研究和分析,但一定要记住别忘了WHERE子句中的排序、分组和范围条件等其他因素,这些因素可能对查询的性能造成非常大的影响。
```sql
mysql> SELECT COUNT(DISTINCT threadId) AS COUNT_VALUE FROM Message
-> WHERE (groupId = 10137) AND (userId = 1288826) AND (anonymous = 0)
-> ORDER BY priority DESC, modifiedDate DESC
-> ;
```
这个查询看似没有建立合适的索引,所以客户咨询是否可以优化。EXPLAIN的结果如下:
```sql
id:1
select_type:SIMPLE
table:Message
type:ref
key:idx_groupId_userId
key_len:18
ref:const,const
rows:1251162
Extra:Using where
```
MySQL为这个查询选择了索引(groupId, userId),如果不考虑列的基数,这看起来是一个非常合理的选择。但如果考虑一下userID和groupID条件匹配的行数,可能就会有不同的想法了:
```sql
mysql> SELECT COUNT(*) , SUM(groupId=10137), SUM(userId=1288826),SUM(anonymous = 0)
-> FROM Message\G
*************************** 1. row ***************************
COUNT(*):4142217
SUM(groupId=10137):4092654
SUM(userId=1288826):1288496
SUM(anonymous=0):4141934
```
从上面的结果来看符合组(groupId)条件几乎满足表中的所有行,符合用户(userId)条件的有130弯条记录——也就是说索引基本上没什么用。因为这些数据是从其他应用中迁移过来的,迁移的时候把所有的消息都赋予了管理员组的用户。这个案例的解决办法是修改应用程序代码。去分这类特殊用户和组,禁止针对这类用户和组执行这个查询。从这个小案例可以看到经验法则和推论在多数情况下是有用的,但要注意不要假设平均情况下的性能也能代表特殊情况下的性能,特殊情况可能会摧毁整个应用的性能。最后,尽管关于选择性和基数的经验法则值得去研究和分析,但一定要记住别忘了WHERE子句中的排序、分组和范围条件等其他因素,这些因素可能对查询的性能造成非常大的影响。
聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式但InnoDB得聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。当表有聚簇索引时,它的数据行实际上存放在索引的叶子页(leaf page)中。术语"聚簇"表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引(不过,覆盖索引可以模拟多个聚簇索引的情况)。因为是存储引擎负责实现索引,因此不是所有的存储引擎都支持聚簇索引。主要关注InnoDB.如图展示了聚簇索引中的记录是如何存放的。注意到,叶子页包含了行的全部数据,但是节点页只包含了索引列。该图中,索引包含的是整数值。
一些数据库服务器允许选择哪个索引作为聚簇索引,但是目前市场上,还没有任何一个MySQL内建的存储引擎支持这一点。InnoDB将通过主键聚集数据,这也就是说上图中的"被索引的列"就是主键列。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引,InnoDB只聚集在同一个页面中的记录。包含相邻键值的页面可能会相距甚远。
聚簇索引可能对性能有帮助,但也可能导致严重的性能问题。所以需要仔细地考虑聚簇索引,尤其是将表的存储引擎从InnoDB改成其他引擎的时候(反过来也一样)。聚集的数据有一些重要的优点:
1.可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户ID来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件。如果没有使用聚簇索引,则每封邮件都可能导致一次磁盘IO
2.数据访问更快。聚簇索引将索引和数据保存在同一个B-Tree中,因此聚簇索引中获取数据通常比在非聚簇索引中查找要快。
3.使用覆盖索引扫描的查询可以直接使用叶节点中的主键值
如果在设计表和查询时能充分利用上面的优点,那就能极大地提升性能。同时,聚簇索引也有一些缺点:
1.聚簇数据最大限度地提高了IO密集型应用的性能,但如果数据全部都存放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势了
2.插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表
3.更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置
4.基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临"页分裂(page split)"的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。也分裂会导致表占用更多的磁盘空间
5.聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候
6.二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列
7.二级索引访问需要两次索引查找,而不是一次
最后一点可能让人有些疑惑,为什么二级索引需要两次索引查找?答案在于二级索引中保存的"行指针"的实质。要记住,二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。这意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键值,然后根据这个值去聚簇索引中查找到对应的行。这里做了重复的工作:两次B-Tree查找而不是一次(顺便提一下,并不是所有的非聚簇索引都能做到一次索引查询就找到行。当行更新的时候可能无法存储在原来的位置,这会导致表中出现行的碎片花或者移动行并在原位置保存"向前指针"。这两种情况都会导致查找行时需要更多的工作),对于InnoDB,自适应哈希索引能够减少这样的重复工作
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式但InnoDB得聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。当表有聚簇索引时,它的数据行实际上存放在索引的叶子页(leaf page)中。术语"聚簇"表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引(不过,覆盖索引可以模拟多个聚簇索引的情况)。因为是存储引擎负责实现索引,因此不是所有的存储引擎都支持聚簇索引。主要关注InnoDB.如图展示了聚簇索引中的记录是如何存放的。注意到,叶子页包含了行的全部数据,但是节点页只包含了索引列。该图中,索引包含的是整数值。
一些数据库服务器允许选择哪个索引作为聚簇索引,但是目前市场上,还没有任何一个MySQL内建的存储引擎支持这一点。InnoDB将通过主键聚集数据,这也就是说上图中的"被索引的列"就是主键列。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引,InnoDB只聚集在同一个页面中的记录。包含相邻键值的页面可能会相距甚远。
聚簇索引可能对性能有帮助,但也可能导致严重的性能问题。所以需要仔细地考虑聚簇索引,尤其是将表的存储引擎从InnoDB改成其他引擎的时候(反过来也一样)。聚集的数据有一些重要的优点:
1.可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户ID来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件。如果没有使用聚簇索引,则每封邮件都可能导致一次磁盘IO
2.数据访问更快。聚簇索引将索引和数据保存在同一个B-Tree中,因此聚簇索引中获取数据通常比在非聚簇索引中查找要快。
3.使用覆盖索引扫描的查询可以直接使用叶节点中的主键值
如果在设计表和查询时能充分利用上面的优点,那就能极大地提升性能。同时,聚簇索引也有一些缺点:
1.聚簇数据最大限度地提高了IO密集型应用的性能,但如果数据全部都存放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势了
2.插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表
3.更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置
4.基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临"页分裂(page split)"的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。也分裂会导致表占用更多的磁盘空间
5.聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候
6.二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列
7.二级索引访问需要两次索引查找,而不是一次
最后一点可能让人有些疑惑,为什么二级索引需要两次索引查找?答案在于二级索引中保存的"行指针"的实质。要记住,二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。这意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键值,然后根据这个值去聚簇索引中查找到对应的行。这里做了重复的工作:两次B-Tree查找而不是一次(顺便提一下,并不是所有的非聚簇索引都能做到一次索引查询就找到行。当行更新的时候可能无法存储在原来的位置,这会导致表中出现行的碎片花或者移动行并在原位置保存"向前指针"。这两种情况都会导致查找行时需要更多的工作),对于InnoDB,自适应哈希索引能够减少这样的重复工作
InnoDB和MyISAM的数据分布对比
聚簇索引和非聚簇索引的数据分布有区别,以及对应的主键索引和二级索引的数据分布也有区别,通常会让人感到困扰和意外。来看看InnoDB和MyISAM时如何存储下面这个表的:
```sql
CREATE TABLE layout_test (
col1 INT NOT NULL,
col2 INT NOT NULL,
PRIMARY KEY ( col1 ),
KEY ( col2 )
);
```
假设该表的主键取值为1~10 00,按照随机顺序插入并使用OPTIMIZE TABLE命令做了优化。换句话说,数据在磁盘上的存储方式已经是最优,但行的顺序是随机的。列col2的值是从1~100之间随机复制,所以有很多重复的值。
聚簇索引和非聚簇索引的数据分布有区别,以及对应的主键索引和二级索引的数据分布也有区别,通常会让人感到困扰和意外。来看看InnoDB和MyISAM时如何存储下面这个表的:
```sql
CREATE TABLE layout_test (
col1 INT NOT NULL,
col2 INT NOT NULL,
PRIMARY KEY ( col1 ),
KEY ( col2 )
);
```
假设该表的主键取值为1~10 00,按照随机顺序插入并使用OPTIMIZE TABLE命令做了优化。换句话说,数据在磁盘上的存储方式已经是最优,但行的顺序是随机的。列col2的值是从1~100之间随机复制,所以有很多重复的值。
MyISAM的数据分布。
MyISAM的数据分布非常简单。MyISAM按照数据插入的顺序存储在磁盘上,如图所示。在行的旁边显式了行号,从0开始递增。因为行是定长的,所以MyISAM可以从表的开头跳过所需的字节找到需要的行(MyISAM并不总是使用上图中的"行号",而是根据定长还是变长的行使用不同策略)。这种分布方式很容易创建索引。下面显式的一系列图,隐藏了页的物理细节,只显示索引中的"节点",索引中的每个叶子节点包含"行号"。如图所示显式了表的主键。这里忽略了一些细节,例如前一个B-Tree节点有多少个内部节点,不过这并不影响对非聚簇存储引擎的基本数据分布的理解,
那么col2l列上的索引又会如何呢?有什么特殊的吗?回答是否定的:它和其他的所以没有什么区别,如图所示显示了col2列上的索引.事实上,MyISAM中主键索引和其他索引在结构上没有什么不同,主键索引就是一个名为PRIMARY的唯一非空索引。
MyISAM的数据分布非常简单。MyISAM按照数据插入的顺序存储在磁盘上,如图所示。在行的旁边显式了行号,从0开始递增。因为行是定长的,所以MyISAM可以从表的开头跳过所需的字节找到需要的行(MyISAM并不总是使用上图中的"行号",而是根据定长还是变长的行使用不同策略)。这种分布方式很容易创建索引。下面显式的一系列图,隐藏了页的物理细节,只显示索引中的"节点",索引中的每个叶子节点包含"行号"。如图所示显式了表的主键。这里忽略了一些细节,例如前一个B-Tree节点有多少个内部节点,不过这并不影响对非聚簇存储引擎的基本数据分布的理解,
那么col2l列上的索引又会如何呢?有什么特殊的吗?回答是否定的:它和其他的所以没有什么区别,如图所示显示了col2列上的索引.事实上,MyISAM中主键索引和其他索引在结构上没有什么不同,主键索引就是一个名为PRIMARY的唯一非空索引。
MyISAM表layout_test的主键分布
MyISAM表layout_test的col2列索引的分布
InnoDB的数据分布。
因为InnoDB支持聚簇索引,所以使用非常不同的方式存储同样的数据。InnoDB以如图所示的方式存储数据。
第一眼看上去,感觉该图和前面的图没有什么区别,但再仔细看细节,会注意道该图显示了整个表,而不是只有索引。因为在InnoDB中,聚簇索引"就是"表,所以不像MyISAM那样需要独立的行存储。聚簇索引的每一个叶子节点都包含了主键值、事务ID、用于事务和MVCC的回滚指针以及所有的剩余列(在这个例子中是col2).如果主键是一个列前缀索引,InnoDB也会包含完整的主键列和剩下的其他列。
还有一点和MyISAM的不同是,InnoDB的二级索引和聚簇索引很不相同。InnoDB二级索引的叶子节点中存储的不是"行指针",而是主键值,并以此作为指向行的"指针"。这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。使用主键值当作指针会让二级索引占用更多的空间,换来的好处是,InnoDB在移动时无须更新二级索引中的这个"指针".
如图所示,显示了示例表的col2索引。每一个叶子节点都包含了索引列(这里是col2),紧接着是主键值(col1).该图展示了B-Tree的叶子节点结构,但我们故意省略了非叶子节点这样的细节,InnoDB的非叶子节点包含了索引列和一个指向下级节点的指针(下一级节点可以是非叶子节点,也可以是叶子节点)。这对聚簇索引和二级索引都适用。
下图是描述InnoDB和MyISAM如何存放表的抽象图。从图中可以很容易看出InnoDB和MyISAM保存数据和索引的区别
因为InnoDB支持聚簇索引,所以使用非常不同的方式存储同样的数据。InnoDB以如图所示的方式存储数据。
第一眼看上去,感觉该图和前面的图没有什么区别,但再仔细看细节,会注意道该图显示了整个表,而不是只有索引。因为在InnoDB中,聚簇索引"就是"表,所以不像MyISAM那样需要独立的行存储。聚簇索引的每一个叶子节点都包含了主键值、事务ID、用于事务和MVCC的回滚指针以及所有的剩余列(在这个例子中是col2).如果主键是一个列前缀索引,InnoDB也会包含完整的主键列和剩下的其他列。
还有一点和MyISAM的不同是,InnoDB的二级索引和聚簇索引很不相同。InnoDB二级索引的叶子节点中存储的不是"行指针",而是主键值,并以此作为指向行的"指针"。这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。使用主键值当作指针会让二级索引占用更多的空间,换来的好处是,InnoDB在移动时无须更新二级索引中的这个"指针".
如图所示,显示了示例表的col2索引。每一个叶子节点都包含了索引列(这里是col2),紧接着是主键值(col1).该图展示了B-Tree的叶子节点结构,但我们故意省略了非叶子节点这样的细节,InnoDB的非叶子节点包含了索引列和一个指向下级节点的指针(下一级节点可以是非叶子节点,也可以是叶子节点)。这对聚簇索引和二级索引都适用。
下图是描述InnoDB和MyISAM如何存放表的抽象图。从图中可以很容易看出InnoDB和MyISAM保存数据和索引的区别
InnoDB表layout_test的主键分布
InnoDB表layout_test的二级索引分布
在InnoDB表中按主键顺序插入行。
如果正在适用InnoDB并且没有什么数据需要聚集,那么可以定义一个代理键(surroagte key)作为主键,这种主键的数据应该和应用无关,最简单的方法是适用AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入,对于根据主键作关联操作的性能也会更好。最好避免随机的(不连续且值得分布范围非常大)聚簇索引,特别是对于IO密集型得应用。例如,如果从性能得考虑,适用UUID来作为聚簇索引则会很操作:它使得聚簇索引得插入变得完全随机,这是最坏得情况,使得数据没有任何聚集特性
```sql
CREATE TABLE userinfo (
id int unsigned NOT NULL AUTO_INCREMENT,
name varchar(64) NOT NULL DEFAULT '',
email varchar(64) NOT NULL DEFAULT '',
password varchar(64) NOT NULL DEFAULT '',
dob date DEFAULT NULL,
address varchar(255) NOT NULL DEFAULT '',
city varchar(64) NOT NULL DEFAULT '',
state_id tinyint unsigned NOT NULL DEFAULT '0',
zip varchar(8) NOT NULL DEFAULT '',
country_id smallint unsigned NOT NULL DEFAULT '0',
account_type varchar(32) NOT NULL DEFAULT '',
verfied tinyint NOT NULL DEFAULT '0',
allow_email tinyint unsigned NOT NULL DEFAULT '0',
parrent_account int unsigned NOT NULL DEFAULT '0',
closest_airport varchar(3) NOT NULL DEFAULT '',
PRIMARY KEY(id),
UNIQUE KEY email(email),
KEY country_id (country_id),
KEY state_id(state_id),
KEY state_id_2(state_id,city,address)
) ENGINE=InnoDB;
```
注意到使用了自增的整数ID作为主键。
第二个例子是user_info表,除了主键改为UUID,其余和上面的userinfo表完全相同。
```sql
CREATE TABLE userinfo_uuid(
uuid varchar(36) NOTNULL,
....
```
首先,要在一个有足够内存容纳索引的服务器上向这两个表各插入100万条记录,然后向这两个表继续插入300万条记录,使索引的大小超过服务器的内存容量。测试结果如图.注意到向UUID主键插入行不仅花费的时间更长,而且索引占用的空间也更大。这一方面是由于主键字段更长;另一方面毫无疑问是由于页分裂和碎片导致的。为了明白为什么会这样,来看看往第一个表种插入数据时,索引发生了什么变化。如图所示了插满一个页面后继续插入相邻的下一个页面的场景。如图所示,因为主键的值是顺序的,所以InnoDB把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出部分空间用于以后修改),下一条记录就会写入新的页种。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果(然而,二级索引页可能是不一样的)。
对比一下向第二个使用了UUID聚簇索引的表插入数据,看看有什么不同,如图所示。因为新行的主键值不一定比之前插入的大,所以InnoDB无法简单地总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置——通常是已有数据的中间位置——并且分配空间。这会增加很多的额外工作,并导致数据分布不够优化。下面是总结的一些缺点:
1.写入的目标页可能已经刷到磁盘上并从缓存种移除,或者是还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存种。这将导致大量的随机IO
2.因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新地行分配空间。页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页
3.由于频繁的页分裂,页会变得稀疏并被不规则地填充,所以最终数据会有碎片。
把这些随机值载入到聚簇索引以后,也需要做一次OPTIMIZE TABLE来重建表并优化页的填充。从这个案例可以看出,使用InnoDB时应该尽可能地按逐渐顺序插入数据,并且尽可能地使用单调递增的聚簇键的值来插入新行
如果正在适用InnoDB并且没有什么数据需要聚集,那么可以定义一个代理键(surroagte key)作为主键,这种主键的数据应该和应用无关,最简单的方法是适用AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入,对于根据主键作关联操作的性能也会更好。最好避免随机的(不连续且值得分布范围非常大)聚簇索引,特别是对于IO密集型得应用。例如,如果从性能得考虑,适用UUID来作为聚簇索引则会很操作:它使得聚簇索引得插入变得完全随机,这是最坏得情况,使得数据没有任何聚集特性
```sql
CREATE TABLE userinfo (
id int unsigned NOT NULL AUTO_INCREMENT,
name varchar(64) NOT NULL DEFAULT '',
email varchar(64) NOT NULL DEFAULT '',
password varchar(64) NOT NULL DEFAULT '',
dob date DEFAULT NULL,
address varchar(255) NOT NULL DEFAULT '',
city varchar(64) NOT NULL DEFAULT '',
state_id tinyint unsigned NOT NULL DEFAULT '0',
zip varchar(8) NOT NULL DEFAULT '',
country_id smallint unsigned NOT NULL DEFAULT '0',
account_type varchar(32) NOT NULL DEFAULT '',
verfied tinyint NOT NULL DEFAULT '0',
allow_email tinyint unsigned NOT NULL DEFAULT '0',
parrent_account int unsigned NOT NULL DEFAULT '0',
closest_airport varchar(3) NOT NULL DEFAULT '',
PRIMARY KEY(id),
UNIQUE KEY email(email),
KEY country_id (country_id),
KEY state_id(state_id),
KEY state_id_2(state_id,city,address)
) ENGINE=InnoDB;
```
注意到使用了自增的整数ID作为主键。
第二个例子是user_info表,除了主键改为UUID,其余和上面的userinfo表完全相同。
```sql
CREATE TABLE userinfo_uuid(
uuid varchar(36) NOTNULL,
....
```
首先,要在一个有足够内存容纳索引的服务器上向这两个表各插入100万条记录,然后向这两个表继续插入300万条记录,使索引的大小超过服务器的内存容量。测试结果如图.注意到向UUID主键插入行不仅花费的时间更长,而且索引占用的空间也更大。这一方面是由于主键字段更长;另一方面毫无疑问是由于页分裂和碎片导致的。为了明白为什么会这样,来看看往第一个表种插入数据时,索引发生了什么变化。如图所示了插满一个页面后继续插入相邻的下一个页面的场景。如图所示,因为主键的值是顺序的,所以InnoDB把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出部分空间用于以后修改),下一条记录就会写入新的页种。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果(然而,二级索引页可能是不一样的)。
对比一下向第二个使用了UUID聚簇索引的表插入数据,看看有什么不同,如图所示。因为新行的主键值不一定比之前插入的大,所以InnoDB无法简单地总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置——通常是已有数据的中间位置——并且分配空间。这会增加很多的额外工作,并导致数据分布不够优化。下面是总结的一些缺点:
1.写入的目标页可能已经刷到磁盘上并从缓存种移除,或者是还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存种。这将导致大量的随机IO
2.因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新地行分配空间。页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页
3.由于频繁的页分裂,页会变得稀疏并被不规则地填充,所以最终数据会有碎片。
把这些随机值载入到聚簇索引以后,也需要做一次OPTIMIZE TABLE来重建表并优化页的填充。从这个案例可以看出,使用InnoDB时应该尽可能地按逐渐顺序插入数据,并且尽可能地使用单调递增的聚簇键的值来插入新行
向聚簇索引插入顺序的索引值
向聚簇索引种插入无序的值
顺序的主键什么时候会造成更坏的结果?
对于高并发工作负载,在InnoDB中按主键顺序插入可能会造成明显的争用。主键的上界会成为"热点"。因为所有的插入都发生在这里,所以并发插入可能导致间隙锁竞争。另一个热点可能是AUTO_INCREMENT锁机制;如果遇到这个问题,则可能需要考虑重新设计表或者应用,或者更改innodb_autoinc_lock_mode配置。如果你的服务器版本还不支持innodb_autoinc_lock_mode参数,可以升级到新版本的InnoDB,可能对这种场景工作得更好
对于高并发工作负载,在InnoDB中按主键顺序插入可能会造成明显的争用。主键的上界会成为"热点"。因为所有的插入都发生在这里,所以并发插入可能导致间隙锁竞争。另一个热点可能是AUTO_INCREMENT锁机制;如果遇到这个问题,则可能需要考虑重新设计表或者应用,或者更改innodb_autoinc_lock_mode配置。如果你的服务器版本还不支持innodb_autoinc_lock_mode参数,可以升级到新版本的InnoDB,可能对这种场景工作得更好
覆盖索引
通常大家都会根据查询的WHERE条件来创建合适的索引,不过这只是索引优化的一个方面。设计优秀的索引应该考虑到整个查询,而不单单是WHERE条件部分。索引确实是一种查找数据的高效方式,但是MySQL也可以使用索引来直接获取列的数据,这样就不再需要读取数据行。如果索引的叶子几点钟已经包含要查询的数据,那么还有什么必要再回表查询呢?如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为"覆盖索引"。
覆盖索引是非常有用的工具,能够极大地提高性能。考虑一下如果查询只需要扫描索引而无须回表,会带来多少好处:
1.索引条目通常远小于数据行大小,所以如果只需要读取索引,那MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下相应时间大部分花费在数据拷贝上。覆盖索引对于IO密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中(这对于MyISAM尤其正确,因为MyISAM能压缩索引以变得更小)
2.因为索引是按照列值顺序存储的(至少在单个页内是如此),所以对于IO密集型的范围查询会比随机从磁盘读取每一行数据的IO要少得多。对于某些存储引擎,例如MyISAM和Percona XtraDB,甚至可以通过OPTIMIZE命令使得索引完全顺序排列,这让简单的范围查询能使用完全的顺序的索引访问
3.一些存储引擎如MyISAM在内存中只缓存索引,数据则依赖于操作系统缓存,因此要访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统调用占了数据访问中的最大开销的场景
4.由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询
在所有这些场景中,在索引中满足查询的成本一般要比查询行要小得多。不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree索引做覆盖索引。另外,不同的存储引擎实现覆盖索引的方式也不同,而且不是所有的引擎都支持覆盖索引(如Memory存储引擎就不支持覆盖索引),当发起一个被索引覆盖的查询(也叫做索引覆盖查询时),在EXPLAIN的Extra列可以看到"Using index"的信息(很容易把Extra列的"Using index"和type列的"index"搞混淆。其实这两者完全不同,type列和覆盖索引毫无关系:它只是表示这个查询访问数据的方式,或者说是MySQL查找行的方式,MySQL手册中称之为连接方式(join type))。例如,表sakila.inventory有一个多列索引(store_id,film_id).MySQL如果只需访问这两列,就可以使用这个索引做索引覆盖,如下所示
```sql
mysql> EXPLAIN SELECT store_id,film_id FROM sakila.inventory\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: inventory
partitions: NULL
type: index
possible_keys: NULL
key: idx_store_id_film_id
key_len: 3
ref: NULL
rows: 4581
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
```
索引覆盖查询还有很多陷阱可能会导致无法实现优化。MySQL查询优化器会在执行查询前判断是否有一个索引能进行覆盖。假设索引覆盖了WHERE条件中的字段,但是不是整个查询涉及的字段。如果条件为假(false),MySQL5.5和更早的版本也总是会回表获取数据行,尽管并不需要这一行且最终会被过滤掉。
来看看为什么会发生这样的情况,以及如何重写查询以解决该问题。从下面的查询开始
通常大家都会根据查询的WHERE条件来创建合适的索引,不过这只是索引优化的一个方面。设计优秀的索引应该考虑到整个查询,而不单单是WHERE条件部分。索引确实是一种查找数据的高效方式,但是MySQL也可以使用索引来直接获取列的数据,这样就不再需要读取数据行。如果索引的叶子几点钟已经包含要查询的数据,那么还有什么必要再回表查询呢?如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为"覆盖索引"。
覆盖索引是非常有用的工具,能够极大地提高性能。考虑一下如果查询只需要扫描索引而无须回表,会带来多少好处:
1.索引条目通常远小于数据行大小,所以如果只需要读取索引,那MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下相应时间大部分花费在数据拷贝上。覆盖索引对于IO密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中(这对于MyISAM尤其正确,因为MyISAM能压缩索引以变得更小)
2.因为索引是按照列值顺序存储的(至少在单个页内是如此),所以对于IO密集型的范围查询会比随机从磁盘读取每一行数据的IO要少得多。对于某些存储引擎,例如MyISAM和Percona XtraDB,甚至可以通过OPTIMIZE命令使得索引完全顺序排列,这让简单的范围查询能使用完全的顺序的索引访问
3.一些存储引擎如MyISAM在内存中只缓存索引,数据则依赖于操作系统缓存,因此要访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统调用占了数据访问中的最大开销的场景
4.由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询
在所有这些场景中,在索引中满足查询的成本一般要比查询行要小得多。不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree索引做覆盖索引。另外,不同的存储引擎实现覆盖索引的方式也不同,而且不是所有的引擎都支持覆盖索引(如Memory存储引擎就不支持覆盖索引),当发起一个被索引覆盖的查询(也叫做索引覆盖查询时),在EXPLAIN的Extra列可以看到"Using index"的信息(很容易把Extra列的"Using index"和type列的"index"搞混淆。其实这两者完全不同,type列和覆盖索引毫无关系:它只是表示这个查询访问数据的方式,或者说是MySQL查找行的方式,MySQL手册中称之为连接方式(join type))。例如,表sakila.inventory有一个多列索引(store_id,film_id).MySQL如果只需访问这两列,就可以使用这个索引做索引覆盖,如下所示
```sql
mysql> EXPLAIN SELECT store_id,film_id FROM sakila.inventory\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: inventory
partitions: NULL
type: index
possible_keys: NULL
key: idx_store_id_film_id
key_len: 3
ref: NULL
rows: 4581
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
```
索引覆盖查询还有很多陷阱可能会导致无法实现优化。MySQL查询优化器会在执行查询前判断是否有一个索引能进行覆盖。假设索引覆盖了WHERE条件中的字段,但是不是整个查询涉及的字段。如果条件为假(false),MySQL5.5和更早的版本也总是会回表获取数据行,尽管并不需要这一行且最终会被过滤掉。
来看看为什么会发生这样的情况,以及如何重写查询以解决该问题。从下面的查询开始
```sql
mysql> EXPLAIN SELECT * FROM products WHERE actor='SEAN CARREY' AND title LIKE '%APOLLO%'\G
*************************** 1. row ***************************
id:1
select_type:SIMPLE
table:products
type:ref
possible_keys:ACTOR,IDX_PROD_ACTOR
key:ACTOR
key_len:52
ref:const
rows:10
Extra:Using where
```
这里索引无法覆盖该查询,有两个原因:
1.没有任何索引能覆盖这个查询。因为查询从表中选择了所有的列,而没有任何索引覆盖了所有的列。不过理论上MySQL还有一个捷径可以利用:WHERE条件中的列是有索引可以覆盖的,因此MySQL可以使用该索引找到对应的actor并检查title是否匹配,过滤之后再读取需要的数据行
2.MySQL不能再索引中执行LIKE操作。这是底层存储引擎API的限制,MySQL5.5和更早的版本中只允许在索引中做简单比较操作(例如等于、不等于以及大于)。MySQL能在索引中做最左前缀匹配的LIKE比较,因为该操作可以转换为简单的比较操作,但是如果是通配符开头的LIKE查询,存储引擎就无法做比较匹配。这种情况下,MySQL服务器只能提取数据行的值而不是索引值来做比较
也有办法可以解决上面说的两个问题,需要重写查询并巧妙地涉及索引。先将索引扩展至覆盖三个数据列(artist,title,prod_id),然后按如下方式重写查询
```sql
mysql>EXPLAIN SELECT
*
FROM
products
JOIN ( SELECT prod_id FROM products WHERE actor = 'SEAN CARREY' AND title LIKE '%APOLLO%' ) AS t1 ON (
t1.prod_id = products.prod_id)
*************************** 1. row ***************************
id:1
select_type:PRIMARY
table:<derived2>
....omitted...
*************************** 2. row ***************************
id:1
select_type:PRIMARY
table:products
...omitted...
*************************** 3. row ***************************
id:2
select_type:DERIVED
table:products
type:ref
possable_keys:ACTOR,ACTOR_2,IDX_PROD_ACTOR
key:ACTOR_2
key_len:52
ref:
rows:11
Extra:Using where, Using index
```
mysql> EXPLAIN SELECT * FROM products WHERE actor='SEAN CARREY' AND title LIKE '%APOLLO%'\G
*************************** 1. row ***************************
id:1
select_type:SIMPLE
table:products
type:ref
possible_keys:ACTOR,IDX_PROD_ACTOR
key:ACTOR
key_len:52
ref:const
rows:10
Extra:Using where
```
这里索引无法覆盖该查询,有两个原因:
1.没有任何索引能覆盖这个查询。因为查询从表中选择了所有的列,而没有任何索引覆盖了所有的列。不过理论上MySQL还有一个捷径可以利用:WHERE条件中的列是有索引可以覆盖的,因此MySQL可以使用该索引找到对应的actor并检查title是否匹配,过滤之后再读取需要的数据行
2.MySQL不能再索引中执行LIKE操作。这是底层存储引擎API的限制,MySQL5.5和更早的版本中只允许在索引中做简单比较操作(例如等于、不等于以及大于)。MySQL能在索引中做最左前缀匹配的LIKE比较,因为该操作可以转换为简单的比较操作,但是如果是通配符开头的LIKE查询,存储引擎就无法做比较匹配。这种情况下,MySQL服务器只能提取数据行的值而不是索引值来做比较
也有办法可以解决上面说的两个问题,需要重写查询并巧妙地涉及索引。先将索引扩展至覆盖三个数据列(artist,title,prod_id),然后按如下方式重写查询
```sql
mysql>EXPLAIN SELECT
*
FROM
products
JOIN ( SELECT prod_id FROM products WHERE actor = 'SEAN CARREY' AND title LIKE '%APOLLO%' ) AS t1 ON (
t1.prod_id = products.prod_id)
*************************** 1. row ***************************
id:1
select_type:PRIMARY
table:<derived2>
....omitted...
*************************** 2. row ***************************
id:1
select_type:PRIMARY
table:products
...omitted...
*************************** 3. row ***************************
id:2
select_type:DERIVED
table:products
type:ref
possable_keys:ACTOR,ACTOR_2,IDX_PROD_ACTOR
key:ACTOR_2
key_len:52
ref:
rows:11
Extra:Using where, Using index
```
我们把这种方式叫作延迟关联(deferred join),因为延迟了对列的访问。在查询的第一阶段MySQL使用覆盖索引,在FROM子句中的子查询中找到匹配的prod_id,然后根据这些prod_id值在外层查询匹配获取需要的所有列值。虽然无法使用索引覆盖整个查询,但总算比完全无法利用索引覆盖的好。
这样优化的效果取决于WHERE条件匹配返回的行数。假设这个products表有100万行,我们来看一下上面两个初选在三个不同的数据集上的表现,每个数据集都包含100万行:
1.第一个数据集,Sean Carrey出演了30 000部作品,其中有20 000部的标题中包含了Apollo
2.第二个数据集,Sean Carrey出演了30 000部作品,其中40 部的标题中包含了Apollo
3.第三个数据集,Sean Carrey出演了50部作品,其中10部的标题中包含了Apollo
使用上面的三种数据集来测试两种不同的查询,得到的结果如表所示:下面是对结果的分析:
1.在示例1中,查询返回了一个很大的结果集,因此看不到优化的效果。大部分时间都花在读取和发送数据上了
2.在示例2中,经过索引过滤,尤其是第二个条件过滤后只返回了很少的结果集,优化的效果非常明显:在这个数据集上性能提高了5倍,优化后的查询的效率主要得益于只需要读取40行完整数据行,而不是原查询中需要的30 000行
3.在示例3中,显示了子查询效率反而下降的情况。因为索引过滤时符合第一个条件的结果集已经很小,所以子查询带来的成本反而比从表中直接提取完整行要高。
在大多数存储引擎中,覆盖索引只能覆盖那些只访问索引中部分列的查询。不过,可以更进一步优化InnoDB。回想一下,InnoDB的二级索引的叶子节点都包含了主键的只,这意味着InnoDB的二级索引可以有效地利用这些"额外"的主键列来覆盖查询。例如,actor使用InnoDB存储引擎,并在last_name字段有二级索引,虽然该索引的列不包括主键actor_id,但也能够用于对actor_id做覆盖查询:
```sql
mysql> EXPLAIN SELECT actor_id,last_name FROM sakila.actor WHERE last_name= 'HOPPER'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: ref
possible_keys: idx_actor_last_name
key: idx_actor_last_name
key_len: 182
ref: const
rows: 2
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
```
这样优化的效果取决于WHERE条件匹配返回的行数。假设这个products表有100万行,我们来看一下上面两个初选在三个不同的数据集上的表现,每个数据集都包含100万行:
1.第一个数据集,Sean Carrey出演了30 000部作品,其中有20 000部的标题中包含了Apollo
2.第二个数据集,Sean Carrey出演了30 000部作品,其中40 部的标题中包含了Apollo
3.第三个数据集,Sean Carrey出演了50部作品,其中10部的标题中包含了Apollo
使用上面的三种数据集来测试两种不同的查询,得到的结果如表所示:下面是对结果的分析:
1.在示例1中,查询返回了一个很大的结果集,因此看不到优化的效果。大部分时间都花在读取和发送数据上了
2.在示例2中,经过索引过滤,尤其是第二个条件过滤后只返回了很少的结果集,优化的效果非常明显:在这个数据集上性能提高了5倍,优化后的查询的效率主要得益于只需要读取40行完整数据行,而不是原查询中需要的30 000行
3.在示例3中,显示了子查询效率反而下降的情况。因为索引过滤时符合第一个条件的结果集已经很小,所以子查询带来的成本反而比从表中直接提取完整行要高。
在大多数存储引擎中,覆盖索引只能覆盖那些只访问索引中部分列的查询。不过,可以更进一步优化InnoDB。回想一下,InnoDB的二级索引的叶子节点都包含了主键的只,这意味着InnoDB的二级索引可以有效地利用这些"额外"的主键列来覆盖查询。例如,actor使用InnoDB存储引擎,并在last_name字段有二级索引,虽然该索引的列不包括主键actor_id,但也能够用于对actor_id做覆盖查询:
```sql
mysql> EXPLAIN SELECT actor_id,last_name FROM sakila.actor WHERE last_name= 'HOPPER'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: ref
possible_keys: idx_actor_last_name
key: idx_actor_last_name
key_len: 182
ref: const
rows: 2
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
```
未来MySQL版本的改进(索引下推)
上面提到的很多限制都是由于存储引擎API涉及所导致的,目前的API设计不允许MySQL将过滤条件传到存储引擎层。如果MySQL在后续版本能够做到这一点,则可以把查询发送到数据上,而不是像现在这样只能把数据从存储引擎拉到服务器层,再根据查询条件过滤。MySQL将这个功能称之为索引下推(Index Condition Pushdown)
上面提到的很多限制都是由于存储引擎API涉及所导致的,目前的API设计不允许MySQL将过滤条件传到存储引擎层。如果MySQL在后续版本能够做到这一点,则可以把查询发送到数据上,而不是像现在这样只能把数据从存储引擎拉到服务器层,再根据查询条件过滤。MySQL将这个功能称之为索引下推(Index Condition Pushdown)
什么是索引下推?
索引下推(Index Condition Pushdown,简称ICP),是MySQL5.6版本的新特性,它能减少回表查询次数,提高查询效率。
索引下推(Index Condition Pushdown,简称ICP),是MySQL5.6版本的新特性,它能减少回表查询次数,提高查询效率。
索引下推优化的原理。
我们先简单了解一下MySQL大概的架构:如图所示。
MySQL服务层负责SQL语法解析、生成执行计划等,并调用存储引擎层去执行数据的存储和检索。索引下推的下推其实就是将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理。我们来具体看一下,在没有使用ICP的情况下,MySQL的查询:
1.存储引擎读取索引记录
2.根据索引中的主键值,定位并读取完整的行记录
3.存储引擎把记录交给Server层去检测该记录是否满足WHERE条件
使用ICP的情况下,查询过程:
1.存储引擎读取索引记录(不是完整的行记录);
2.判断WHERE条件部分能否用索引中的列来做检查,条件不满足,则处理下一行索引记录
3.条件满足,使用索引中的主键去定位并读取完整的行记录(就是所谓的回表)
4.存储引擎把记录交给Server层,Server层检测该记录是否满足WHERE条件的其余部分
我们先简单了解一下MySQL大概的架构:如图所示。
MySQL服务层负责SQL语法解析、生成执行计划等,并调用存储引擎层去执行数据的存储和检索。索引下推的下推其实就是将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理。我们来具体看一下,在没有使用ICP的情况下,MySQL的查询:
1.存储引擎读取索引记录
2.根据索引中的主键值,定位并读取完整的行记录
3.存储引擎把记录交给Server层去检测该记录是否满足WHERE条件
使用ICP的情况下,查询过程:
1.存储引擎读取索引记录(不是完整的行记录);
2.判断WHERE条件部分能否用索引中的列来做检查,条件不满足,则处理下一行索引记录
3.条件满足,使用索引中的主键去定位并读取完整的行记录(就是所谓的回表)
4.存储引擎把记录交给Server层,Server层检测该记录是否满足WHERE条件的其余部分
索引下推的具体实践
理论比较抽象,我们来上一个实践。使用一张用户表tuser,表里创建联合索引(name,age)
```sql
CREATE TABLE `tuser` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`phone` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
`address` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name_age` (`name`,`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `tuser` (`id`, `name`, `age`, `phone`, `address`) VALUES (1, '张三', 10, '12222', '蛮逗村');
INSERT INTO `tuser` (`id`, `name`, `age`, `phone`, `address`) VALUES (2, '李四', 12, '1333', '葫芦镇');
INSERT INTO `tuser` (`id`, `name`, `age`, `phone`, `address`) VALUES (3, '王五', 12, '1555', '公平城');
INSERT INTO `tuser` (`id`, `name`, `age`, `phone`, `address`) VALUES (4, '张猛', 16, '1666', '深坑洞');
```
如果现在有一个需求:检索出表中,名字第一个是张,而且年龄是10岁的所有用户,那么,SQL语句是这么些:
```sql
SELECT * FROM tuser WHERE name LIKE '张%' AND age = 10;
```
假如你了解索引最左匹配原则,那么就直到这个语句在搜索索引树的时候,只能用张,找到的第一个满足条件的记录id为1.
如图所示。那么接下来的步骤是什么呢?
理论比较抽象,我们来上一个实践。使用一张用户表tuser,表里创建联合索引(name,age)
```sql
CREATE TABLE `tuser` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`phone` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
`address` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name_age` (`name`,`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `tuser` (`id`, `name`, `age`, `phone`, `address`) VALUES (1, '张三', 10, '12222', '蛮逗村');
INSERT INTO `tuser` (`id`, `name`, `age`, `phone`, `address`) VALUES (2, '李四', 12, '1333', '葫芦镇');
INSERT INTO `tuser` (`id`, `name`, `age`, `phone`, `address`) VALUES (3, '王五', 12, '1555', '公平城');
INSERT INTO `tuser` (`id`, `name`, `age`, `phone`, `address`) VALUES (4, '张猛', 16, '1666', '深坑洞');
```
如果现在有一个需求:检索出表中,名字第一个是张,而且年龄是10岁的所有用户,那么,SQL语句是这么些:
```sql
SELECT * FROM tuser WHERE name LIKE '张%' AND age = 10;
```
假如你了解索引最左匹配原则,那么就直到这个语句在搜索索引树的时候,只能用张,找到的第一个满足条件的记录id为1.
如图所示。那么接下来的步骤是什么呢?
没有使用ICP.
在MySQL5.6之前,存储引擎根据通过联合索引找到name like '张%'的主键id(1、4),逐一进行回表扫描,去聚簇索引找到完整的行记录,server层在对数据根据age=10进行筛选。如图所示,可以看到需要回表两次,把我们联合索引的另一个字段age浪费了
在MySQL5.6之前,存储引擎根据通过联合索引找到name like '张%'的主键id(1、4),逐一进行回表扫描,去聚簇索引找到完整的行记录,server层在对数据根据age=10进行筛选。如图所示,可以看到需要回表两次,把我们联合索引的另一个字段age浪费了
使用ICP
而MySQL5.6以后,存储引擎根据(name,age)联合索引,找到name like '张%',由于联合索引中包含age列,所以存储引擎直接再联合索引里按照age=10过滤。按照过滤后的数据再一一进行回表扫描,可以看到只回表了一次。除此之外我们还可以看一下执行计划,看到Extra一列里Using index condition,这就是使用了索引下推
```sql
mysql> EXPLAIN SELECT * FROM tuser WHERE name LIKE '张%' AND age = 10;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | tuser | NULL | range | idx_name_age | idx_name_age | 1028 | NULL | 2 | 25.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
1 row in set (21.99 sec)
```
而MySQL5.6以后,存储引擎根据(name,age)联合索引,找到name like '张%',由于联合索引中包含age列,所以存储引擎直接再联合索引里按照age=10过滤。按照过滤后的数据再一一进行回表扫描,可以看到只回表了一次。除此之外我们还可以看一下执行计划,看到Extra一列里Using index condition,这就是使用了索引下推
```sql
mysql> EXPLAIN SELECT * FROM tuser WHERE name LIKE '张%' AND age = 10;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | tuser | NULL | range | idx_name_age | idx_name_age | 1028 | NULL | 2 | 25.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
1 row in set (21.99 sec)
```
索引下推使用条件。
1.只能用于range、ref、eq_ref、ref_or_null访问方法
2.只能用于InnoDB和MyISAM存储及其分区表
3.对InnoDB存储引擎来说,索引下推只适用于二级索引(也叫辅助索引),索引下推的目的为了减少回表次数,也就是要减少IO操作。对于InnoDB的聚簇索引来说,数据和索引是在一起的,不存在回表这一说
4.引用了子查询的条件不能下推
5.引用了存储函数的条件不能下推,因为存储引擎无法调用存储函数
1.只能用于range、ref、eq_ref、ref_or_null访问方法
2.只能用于InnoDB和MyISAM存储及其分区表
3.对InnoDB存储引擎来说,索引下推只适用于二级索引(也叫辅助索引),索引下推的目的为了减少回表次数,也就是要减少IO操作。对于InnoDB的聚簇索引来说,数据和索引是在一起的,不存在回表这一说
4.引用了子查询的条件不能下推
5.引用了存储函数的条件不能下推,因为存储引擎无法调用存储函数
相关系统参数。
索引条件下推默认是开启的,可以使用系统参数optimizer_switch来控制是否开启:
查看默认状态:
```sql
mysql> SELECT @@optimizer_switch\G
*************************** 1. row ***************************
@@optimizer_switch: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,
index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on,prefer_ordering_index=on
1 row in set (0.00 sec)
```
切换状态:
```sql
mysql> set optimizer_switch='index_condition_pushdown=off';
Query OK, 0 rows affected (0.00 sec)
mysql> set optimizer_switch='index_condition_pushdown=on';
Query OK, 0 rows affected (0.00 sec)
```
索引条件下推默认是开启的,可以使用系统参数optimizer_switch来控制是否开启:
查看默认状态:
```sql
mysql> SELECT @@optimizer_switch\G
*************************** 1. row ***************************
@@optimizer_switch: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,
index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on,prefer_ordering_index=on
1 row in set (0.00 sec)
```
切换状态:
```sql
mysql> set optimizer_switch='index_condition_pushdown=off';
Query OK, 0 rows affected (0.00 sec)
mysql> set optimizer_switch='index_condition_pushdown=on';
Query OK, 0 rows affected (0.00 sec)
```
使用索引扫描来做排序。
MySQL有两种方式可以生成有序的结果:通过排序操作;或者按索引顺序扫描(MySQL有两种排序算法);如果EXPLAIN出来的type列的值为"index",则说明使用了索引扫描来做排序(不要和Extra列的"Using index")搞混淆了。扫描索引本身是很快的。因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机IO,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在IO密集型地工作负载时。MySQL可以使用同一个索引即满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好地。只有当索引的列顺序和ORDER BY子句的顺序完全一致,并i企鹅所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求,否则MySQL都需要执行排序操作,而无法利用索引排序。有一种情况ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果WHERE子句或者JOIN子句中对这些列指定了常量,就可以"弥补"索引的不足。
例如,Sakila示例数据库的表rental在列(rental_date,inventory_id,customer_id)上有名为rental_date的索引.(rental_date,inventory_id,customer_id):
```sql
CREATE TABLE `rental` (
....
PRIMARY KEY (`rental_id`),
UNIQUE KEY `rental_date` (`rental_date`,`inventory_id`,`customer_id`),
KEY `idx_fk_inventory_id` (`inventory_id`),
KEY `idx_fk_customer_id` (`customer_id`),
KEY `idx_fk_staff_id` (`staff_id`),
....
);
```
MySQL可以使用rental_date索引为下面的查询做排序,从EXPLAIN张可以看到没有出现文件排序(filesort)操作(MySQL这里称其为文件排序(filesort),其实并不一定使用磁盘文件):
```sql
mysql> EXPLAIN SELECT rental_id, staff_id FROM sakila.rental WHERE rental_date = '2005-05-25' ORDER BY inventory_id,customer_id\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: rental
partitions: NULL
type: ref
possible_keys: rental_date
key: rental_date
key_len: 5
ref: const
rows: 1
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
```
即使ORDER BY 子句不满足索引的最左前缀的要求,也可以用于查询排序,这是因为索引的第一列被指定为一个常数。还有更多可以使用索引做排序的查询示例。下面这个查询可以利用索引排序,是因为查询为索引的第一列提供了常量条件,而使用第二列进行排序,将两列组合在一起,就形成了索引的最左前缀:
```sql
.... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;
```
下面这个查询也没问题,因为ORDER BY 使用的两列就是索引的最左前缀:
```sql
... WHERE rental_date > '2005-05-25' ORDER BY rental_date,inventory_id;
```
MySQL有两种方式可以生成有序的结果:通过排序操作;或者按索引顺序扫描(MySQL有两种排序算法);如果EXPLAIN出来的type列的值为"index",则说明使用了索引扫描来做排序(不要和Extra列的"Using index")搞混淆了。扫描索引本身是很快的。因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机IO,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在IO密集型地工作负载时。MySQL可以使用同一个索引即满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好地。只有当索引的列顺序和ORDER BY子句的顺序完全一致,并i企鹅所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求,否则MySQL都需要执行排序操作,而无法利用索引排序。有一种情况ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果WHERE子句或者JOIN子句中对这些列指定了常量,就可以"弥补"索引的不足。
例如,Sakila示例数据库的表rental在列(rental_date,inventory_id,customer_id)上有名为rental_date的索引.(rental_date,inventory_id,customer_id):
```sql
CREATE TABLE `rental` (
....
PRIMARY KEY (`rental_id`),
UNIQUE KEY `rental_date` (`rental_date`,`inventory_id`,`customer_id`),
KEY `idx_fk_inventory_id` (`inventory_id`),
KEY `idx_fk_customer_id` (`customer_id`),
KEY `idx_fk_staff_id` (`staff_id`),
....
);
```
MySQL可以使用rental_date索引为下面的查询做排序,从EXPLAIN张可以看到没有出现文件排序(filesort)操作(MySQL这里称其为文件排序(filesort),其实并不一定使用磁盘文件):
```sql
mysql> EXPLAIN SELECT rental_id, staff_id FROM sakila.rental WHERE rental_date = '2005-05-25' ORDER BY inventory_id,customer_id\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: rental
partitions: NULL
type: ref
possible_keys: rental_date
key: rental_date
key_len: 5
ref: const
rows: 1
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
```
即使ORDER BY 子句不满足索引的最左前缀的要求,也可以用于查询排序,这是因为索引的第一列被指定为一个常数。还有更多可以使用索引做排序的查询示例。下面这个查询可以利用索引排序,是因为查询为索引的第一列提供了常量条件,而使用第二列进行排序,将两列组合在一起,就形成了索引的最左前缀:
```sql
.... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;
```
下面这个查询也没问题,因为ORDER BY 使用的两列就是索引的最左前缀:
```sql
... WHERE rental_date > '2005-05-25' ORDER BY rental_date,inventory_id;
```
下面是一些不能使用索引做排序的查询:
1.下面这个查询使用了两种不同的排序方向,但是索引列都是正序排序的
```sql
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC,customer_id ASC;
```
2.下面这个查询的ORDER BY 子句中引用了一个不在索引中的列:
```sql
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id,staff_id;
```
3.下面这个查询的WHERE和ORDER BY中的列无法组合成索引的最左前缀:
```sql
... WHERE rental_date = '2025-05-25' ORDER BY customer_id;
```
4.下面这个查询在索引列的第一个列上是范围查询,所以MySQL无法使用索引的其余列:
```sql
... WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id;
```
5.这个查询在inventory_id列上有多个等于条件,对于排序来说,这也是一种范围查询:
```sql
... WHERE rental_date='2005-05-25' AND inventory_id IN (1,2) ORDER BY customer_id;
```
下面这个例子理论上是可以使用索引进行关联排序的,但由于优化器在优化时将film_acotr表当作关联的第二张表,所以实际上无法使用索引:
```sql
mysql> EXPLAIN SELECT actor_id,title FROM sakila.film_actor INNER JOIN sakila.film USING(film_id) ORDER BY actor_id\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: index
possible_keys: PRIMARY
key: idx_title
key_len: 514
ref: NULL
rows: 1000
filtered: 100.00
Extra: Using index; Using temporary; Using filesort
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 5
filtered: 100.00
Extra: Using index
2 rows in set, 1 warning (0.00 sec)
```
使用索引做排序的一个最重要的用法是查询同时有ORDER BY 和LIMIT子句的时候
1.下面这个查询使用了两种不同的排序方向,但是索引列都是正序排序的
```sql
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC,customer_id ASC;
```
2.下面这个查询的ORDER BY 子句中引用了一个不在索引中的列:
```sql
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id,staff_id;
```
3.下面这个查询的WHERE和ORDER BY中的列无法组合成索引的最左前缀:
```sql
... WHERE rental_date = '2025-05-25' ORDER BY customer_id;
```
4.下面这个查询在索引列的第一个列上是范围查询,所以MySQL无法使用索引的其余列:
```sql
... WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id;
```
5.这个查询在inventory_id列上有多个等于条件,对于排序来说,这也是一种范围查询:
```sql
... WHERE rental_date='2005-05-25' AND inventory_id IN (1,2) ORDER BY customer_id;
```
下面这个例子理论上是可以使用索引进行关联排序的,但由于优化器在优化时将film_acotr表当作关联的第二张表,所以实际上无法使用索引:
```sql
mysql> EXPLAIN SELECT actor_id,title FROM sakila.film_actor INNER JOIN sakila.film USING(film_id) ORDER BY actor_id\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: index
possible_keys: PRIMARY
key: idx_title
key_len: 514
ref: NULL
rows: 1000
filtered: 100.00
Extra: Using index; Using temporary; Using filesort
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 5
filtered: 100.00
Extra: Using index
2 rows in set, 1 warning (0.00 sec)
```
使用索引做排序的一个最重要的用法是查询同时有ORDER BY 和LIMIT子句的时候
压缩(前缀压缩)索引
MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大地提高性能。默认只压缩字符串,但通过参数设置也可以对整数做压缩。MyISAM压缩每个索引块的方法是,先完全保存索引块中的第一个值,然后将其他值和第一个值进行比较得到相同前缀的字节数和剩余不同后缀部分,把这部分存储起来即可。例如,索引块中的第一个值是"perform",第二个值是"performance",那么第二个值得前缀压缩后存储得是类似"7,ance"这样的形式。MyISAM对行指针也采用类似的压缩方式。
压缩块使用更少的空间,代价是某些操作可能更慢,因为每个值得压缩前缀都依赖前面的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。正序的扫描速度还不错,但是如果时倒序扫描——例如ORDER BY DESC——就不是很好了,所有在块中查找某一行的操作平均都需要扫描半个索引块。
测试表明,对于CPU密集型应用,因为扫描需要随机查找,压缩索引使得MyISAM在索引查找上要慢好几倍。压缩索引的倒序扫描就更慢了。压缩索引需要在CPU内存资源与磁盘之间做权衡。压缩索引可能只需要十分之一大小的磁盘空间,如果时IO密集型应用,对某些查询带来的好处会比成本多很多。
MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大地提高性能。默认只压缩字符串,但通过参数设置也可以对整数做压缩。MyISAM压缩每个索引块的方法是,先完全保存索引块中的第一个值,然后将其他值和第一个值进行比较得到相同前缀的字节数和剩余不同后缀部分,把这部分存储起来即可。例如,索引块中的第一个值是"perform",第二个值是"performance",那么第二个值得前缀压缩后存储得是类似"7,ance"这样的形式。MyISAM对行指针也采用类似的压缩方式。
压缩块使用更少的空间,代价是某些操作可能更慢,因为每个值得压缩前缀都依赖前面的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。正序的扫描速度还不错,但是如果时倒序扫描——例如ORDER BY DESC——就不是很好了,所有在块中查找某一行的操作平均都需要扫描半个索引块。
测试表明,对于CPU密集型应用,因为扫描需要随机查找,压缩索引使得MyISAM在索引查找上要慢好几倍。压缩索引的倒序扫描就更慢了。压缩索引需要在CPU内存资源与磁盘之间做权衡。压缩索引可能只需要十分之一大小的磁盘空间,如果时IO密集型应用,对某些查询带来的好处会比成本多很多。
冗余和重复索引
MySQL允许在相同列上创建多个索引,无论时有意的还是无意的。MySQL需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能。重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引,应该避免这样创建重复索引,发现以后也应该立即移除。有时会在不经意间创建了重复索引,例如下面的代码:
```sql
CREATE TABLE test (
ID INT NOT NULL PRIMARY KEY,
A INT NOT NULL,
B INT NOT NULL,
UNIQUE(ID),
INDEX(ID)
) ENGINE=InnoDB;
```
一个经验不足的用户可能是想创建一个主键,先加上唯一限制,然后再加上索引以供查询使用。事实上,MySQL的唯一限制和主键限制都是通过索引实现的,因此,上面的写发实际上在相同的列上创建了三个重复索引。通常并没有理由这样做,除非是在同一列上创建不同类型的索引来满足不同的查询需求(如果索引类型不同,并不算是重复索引。例如经常有很好的理由创建KEY(col)和FULLTEXTKEY(col)两种索引)。冗余索引和重复索引有一些不同。如果创建了索引(A,B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引。因此索引(A,B)也可以当作索引(A)来使用(这种冗余索引只是对B-Tree索引来说的)。但是如果再创建索引(B,A)则不是冗余索引,索引(B)也不是,因为B不是索引(A,B)的最左前缀列。另外,其他不同类型的索引(例如哈希索引或者全文索引)也不会是B-Tree索引的冗余,而无论覆盖的索引列是什么。冗余索引通常发生在为表添加新索引的时候。例如,有人可能会增加一个新的索引(A,B)而不是扩展已有的索引(A)。还有一种情况是将一个索引扩展为(A,ID),其中ID是主键,对于InnoDB来说主键列已经包含在二级索引中了,所以这也是冗余的。大多数情况下都不需要冗余索引,应该尽量扩展已有的索引而不是创建新索引。但有时候处于性能方面的考虑需要冗余索引,因为扩展已有的索引会导致其变得太大,从而影响其他使用该索引的查询的性能。
例如,如果在整数列上有一个索引,现在需要额外增加一个很长的VARCHAR列来扩展该索引,那性能可能会急剧下降。特别是有查询把这个索引当作覆盖索引,或者这是MyISAM表并且有很多范围查询(由于MyISAM的前缀压缩)的时候。例如前面提到的userinfo表,这个表有1 000 000行,对每个state_id值大概有20 000条记录。在state_id列有一个索引对下面的查询有用,假设查询名为Q1:
```sql
mysql> SELECT COUNT(*) FROM user_info WHERE state_id=5;
```
一个简单的测试表明该查询的执行速度大概是每秒115次(QPS).还有一个相关查询需要检索几个列的值,而不是只统计行数,假设名为Q2:
```sql
mysql> SELECT state_id,city,address FROM userinfo WHERE state_id =5;
```
对于这个查询,测试结果QPS小于10(这里使用了全内存的案例,如果表逐渐变大,导致工作负载变成IO密集型时,性能测试结果差距会更大。对于COUNT()查询。覆盖索引性能提升100倍也是很有可能的)。提升该查询性能的最简单办法就是扩展索引为(state_id,city,address),让索引能覆盖查询:
```sql
mysql> ALTER TABLE userinfo DROP KEY state_id, ADD KEY state_id2 (state_id,city,address);
```
索引扩展后,Q2运行得更快了,但是Q1却变慢了。如果我们想让两个查询都变快,就需要两个索引,尽管这样一来原来的单列索引是冗余的了。如表所示,显示这两个查询在不同的索引策略下的详细结果,分别使用MyISAM和InnoDB存储引擎。注意到只有state_id_2索引时,InnoDB引擎上的查询Q1的性能下降并不明显,这是因为InnoDB没有使用索引压缩。有两个索引的缺点时索引成本更高。如表所示显示了向表中插入100万行数据所需要的时间
MySQL允许在相同列上创建多个索引,无论时有意的还是无意的。MySQL需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能。重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引,应该避免这样创建重复索引,发现以后也应该立即移除。有时会在不经意间创建了重复索引,例如下面的代码:
```sql
CREATE TABLE test (
ID INT NOT NULL PRIMARY KEY,
A INT NOT NULL,
B INT NOT NULL,
UNIQUE(ID),
INDEX(ID)
) ENGINE=InnoDB;
```
一个经验不足的用户可能是想创建一个主键,先加上唯一限制,然后再加上索引以供查询使用。事实上,MySQL的唯一限制和主键限制都是通过索引实现的,因此,上面的写发实际上在相同的列上创建了三个重复索引。通常并没有理由这样做,除非是在同一列上创建不同类型的索引来满足不同的查询需求(如果索引类型不同,并不算是重复索引。例如经常有很好的理由创建KEY(col)和FULLTEXTKEY(col)两种索引)。冗余索引和重复索引有一些不同。如果创建了索引(A,B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引。因此索引(A,B)也可以当作索引(A)来使用(这种冗余索引只是对B-Tree索引来说的)。但是如果再创建索引(B,A)则不是冗余索引,索引(B)也不是,因为B不是索引(A,B)的最左前缀列。另外,其他不同类型的索引(例如哈希索引或者全文索引)也不会是B-Tree索引的冗余,而无论覆盖的索引列是什么。冗余索引通常发生在为表添加新索引的时候。例如,有人可能会增加一个新的索引(A,B)而不是扩展已有的索引(A)。还有一种情况是将一个索引扩展为(A,ID),其中ID是主键,对于InnoDB来说主键列已经包含在二级索引中了,所以这也是冗余的。大多数情况下都不需要冗余索引,应该尽量扩展已有的索引而不是创建新索引。但有时候处于性能方面的考虑需要冗余索引,因为扩展已有的索引会导致其变得太大,从而影响其他使用该索引的查询的性能。
例如,如果在整数列上有一个索引,现在需要额外增加一个很长的VARCHAR列来扩展该索引,那性能可能会急剧下降。特别是有查询把这个索引当作覆盖索引,或者这是MyISAM表并且有很多范围查询(由于MyISAM的前缀压缩)的时候。例如前面提到的userinfo表,这个表有1 000 000行,对每个state_id值大概有20 000条记录。在state_id列有一个索引对下面的查询有用,假设查询名为Q1:
```sql
mysql> SELECT COUNT(*) FROM user_info WHERE state_id=5;
```
一个简单的测试表明该查询的执行速度大概是每秒115次(QPS).还有一个相关查询需要检索几个列的值,而不是只统计行数,假设名为Q2:
```sql
mysql> SELECT state_id,city,address FROM userinfo WHERE state_id =5;
```
对于这个查询,测试结果QPS小于10(这里使用了全内存的案例,如果表逐渐变大,导致工作负载变成IO密集型时,性能测试结果差距会更大。对于COUNT()查询。覆盖索引性能提升100倍也是很有可能的)。提升该查询性能的最简单办法就是扩展索引为(state_id,city,address),让索引能覆盖查询:
```sql
mysql> ALTER TABLE userinfo DROP KEY state_id, ADD KEY state_id2 (state_id,city,address);
```
索引扩展后,Q2运行得更快了,但是Q1却变慢了。如果我们想让两个查询都变快,就需要两个索引,尽管这样一来原来的单列索引是冗余的了。如表所示,显示这两个查询在不同的索引策略下的详细结果,分别使用MyISAM和InnoDB存储引擎。注意到只有state_id_2索引时,InnoDB引擎上的查询Q1的性能下降并不明显,这是因为InnoDB没有使用索引压缩。有两个索引的缺点时索引成本更高。如表所示显示了向表中插入100万行数据所需要的时间
可以看到,表中的索引越多插入速度会越慢。一般来说,增加新索引将会导致INSERT、UPDATE、DELETE等操作的速度变慢,特别时当心增加索引后导致达到了内存瓶颈的时候。解决冗余索引和重复索引的方法很简单,删除这些索引就可以,但首先要做的事是找出这样的索引。可以通过写一些复杂的访问INFOMATION_SCHEMA表的查询来找,不过还有两个更简单的方法。可以使用Shlomi Noach的common_schema中的一些视图来定位,common_schema是一系列可以安装到服务器上的常用的存储和视图。这比自己编写查询要快而且简单。另外也可以使用Percona Toolkit中的pt-duplicate-key-checker,该工具通过分析表结构来找出冗余和重复的索引。对于大型服务器来说,使用外部的工具可能更合适些;如果服务器上有大量的数据或者大量的表,查询INFORMATION_SCHEMA表可能会导致性能问题。
在决定哪些索引可以被删除的时候要非常小心。回忆一下,在前面的InnoDB的示例表中,因为二级索引的叶子节点包含了主键值,所以在列(A)上的索引就相当于在(A,ID)上的索引。如果有像WHERE A = 5 ORDER BY ID这样的查询,这个索引会很有作用。但如果将索引扩展为(A,B),则实际上就变成了(A,B,ID),那么上面查询的ORDER BY 子句就无法使用该索引做排序,而只能用文件排序了。所以,建议使用Percona工具箱中的pt-upgrade工具来仔细检查计划中的索引变更
在决定哪些索引可以被删除的时候要非常小心。回忆一下,在前面的InnoDB的示例表中,因为二级索引的叶子节点包含了主键值,所以在列(A)上的索引就相当于在(A,ID)上的索引。如果有像WHERE A = 5 ORDER BY ID这样的查询,这个索引会很有作用。但如果将索引扩展为(A,B),则实际上就变成了(A,B,ID),那么上面查询的ORDER BY 子句就无法使用该索引做排序,而只能用文件排序了。所以,建议使用Percona工具箱中的pt-upgrade工具来仔细检查计划中的索引变更
未使用的索引。
除了冗余索引和重复素银,可能还会有一些服务器永远不同的索引。这样的索引完全是累赘,建议考虑删除(有些索引的功能相当于唯一约束,虽然该索引一致没有被查询使用,却可能是用于避免产生重复数据的)有两个工具可以帮助定位未使用的索引。最简单有效的办法是在Percona Server或者MariaDB中线打开userstates服务器变量(默认是关闭的),然后让服务器正常运行一段时间,再通过查询INFORMATION_SCHEMA.INDEX_STATISTICS就能查到每个索引的使用频率。另外,还可以使用Percona Toolkit中的pt-index-usage,该工具可以读取查询日志,并对日志中的每条查询进行EXPLAIN操作,然后打印出关于索引和查询的报告。这个工具不仅可以找出哪些索引是未使用的,还可以了解查询的执行计划——例如在某些情况有些类似的查询的执行方式不一样,这可以帮助定位到哪些偶尔服务质量差的查询,优化它们以得到一致的性能表现。该工具也可以将结果写入到MySQL的表中,方便查询结果
除了冗余索引和重复素银,可能还会有一些服务器永远不同的索引。这样的索引完全是累赘,建议考虑删除(有些索引的功能相当于唯一约束,虽然该索引一致没有被查询使用,却可能是用于避免产生重复数据的)有两个工具可以帮助定位未使用的索引。最简单有效的办法是在Percona Server或者MariaDB中线打开userstates服务器变量(默认是关闭的),然后让服务器正常运行一段时间,再通过查询INFORMATION_SCHEMA.INDEX_STATISTICS就能查到每个索引的使用频率。另外,还可以使用Percona Toolkit中的pt-index-usage,该工具可以读取查询日志,并对日志中的每条查询进行EXPLAIN操作,然后打印出关于索引和查询的报告。这个工具不仅可以找出哪些索引是未使用的,还可以了解查询的执行计划——例如在某些情况有些类似的查询的执行方式不一样,这可以帮助定位到哪些偶尔服务质量差的查询,优化它们以得到一致的性能表现。该工具也可以将结果写入到MySQL的表中,方便查询结果
索引和锁。
索引可以让查询锁定更少的行。如果你的查询从不访问那些不需要的行,那么就会锁定更少的行,从两个方面来看这对性能都有好处。首先,虽然InnoDB的行锁效率很高,内存使用也很少,但是锁定行的时候仍然会带来额外开销,其次,锁定超过需要的行会增加锁争用并减少并发性。InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从而减少锁的数量。但这只有当InnoDB在存储引擎层能够过滤掉所有不需要的行时才有效。如果索引无法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层以后,MySQL服务器才能应用WHERE子句。这是已经无法避免锁定行了;InnoDB已经锁住了这些行,到适当的时候才会释放。在MySQL5.1和更新的版本中,InnoDB可以在服务器器端过滤掉行后就释放锁,但是在早期的MySQL版本中,InnoDB只有在事务提交后才能释放锁。通过下面的例子再次使用数据库Sakila很好地解释了这些情况:
```sql
mysql> SET autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT actor_id FROM sakila.actor WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE;
+----------+
| actor_id |
+----------+
| 2 |
| 3 |
| 4 |
+----------+
```
这条查询仅仅会返回2~4之间的行,但是实际上获取了1~4之间的行的排他锁。InnoDB会锁住第1行,这是因为MySQL为该查询选择的执行计划是索引范围扫描:
```sql
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | actor | NULL | range | PRIMARY | PRIMARY | 2 | NULL | 4 | 100.00 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)
```
换句话说,底层存储引擎的操作是"从索引的开头开始获取满足条件acotr_id < 5"的记录,服务器并没有告诉InnoDB可以过滤第1行的WHERE条件。注意到EXPLAIN的Extra列出现了"Using where",这表示MySQL服务器将存储以前宁返回行以后再应用WHERE过滤条件。
下面的第二个查询就能证明第1行确实已经被锁定,尽管第一个查询的结果中并没有这个第1行。保持第一个链接打开,然后开启第二个连接并执行如下查询:
```sql
mysql> SET autocommit=0;
Query OK, 0 rows affected (18.96 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.02 sec)
mysql> SELECT actor_id FROM sakila.actor WHERE actor_id =1 FOR UPDATE;
1205 - Lock wait timeout exceeded; try restarting transaction
```
这个查询将会挂起,直到第一个事务释放第一行的锁。这个行为对于基于语句的复制的正常运行来说是必要的。就像这个例子显示的,即使使用了索引,InnoDB也可能锁住一些不需要的数据。如果不能使用索引查找和锁定行的问题可能会更糟糕,MySQL会做全表扫描并锁住所有的行,而不管是不是需要。关于InnoDB、索引和锁有一些很少有人直到的细节:InnoDB在二级索引上使用共享(读)锁,但访问主键索引需要排他(写)锁。这消除了使用覆盖索引的可能性,并且使得SELECT FOR UPDATE比LOCK IN SHARE MODE或非锁定查询要慢得多
索引可以让查询锁定更少的行。如果你的查询从不访问那些不需要的行,那么就会锁定更少的行,从两个方面来看这对性能都有好处。首先,虽然InnoDB的行锁效率很高,内存使用也很少,但是锁定行的时候仍然会带来额外开销,其次,锁定超过需要的行会增加锁争用并减少并发性。InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从而减少锁的数量。但这只有当InnoDB在存储引擎层能够过滤掉所有不需要的行时才有效。如果索引无法过滤掉无效的行,那么在InnoDB检索到数据并返回给服务器层以后,MySQL服务器才能应用WHERE子句。这是已经无法避免锁定行了;InnoDB已经锁住了这些行,到适当的时候才会释放。在MySQL5.1和更新的版本中,InnoDB可以在服务器器端过滤掉行后就释放锁,但是在早期的MySQL版本中,InnoDB只有在事务提交后才能释放锁。通过下面的例子再次使用数据库Sakila很好地解释了这些情况:
```sql
mysql> SET autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT actor_id FROM sakila.actor WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE;
+----------+
| actor_id |
+----------+
| 2 |
| 3 |
| 4 |
+----------+
```
这条查询仅仅会返回2~4之间的行,但是实际上获取了1~4之间的行的排他锁。InnoDB会锁住第1行,这是因为MySQL为该查询选择的执行计划是索引范围扫描:
```sql
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | actor | NULL | range | PRIMARY | PRIMARY | 2 | NULL | 4 | 100.00 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)
```
换句话说,底层存储引擎的操作是"从索引的开头开始获取满足条件acotr_id < 5"的记录,服务器并没有告诉InnoDB可以过滤第1行的WHERE条件。注意到EXPLAIN的Extra列出现了"Using where",这表示MySQL服务器将存储以前宁返回行以后再应用WHERE过滤条件。
下面的第二个查询就能证明第1行确实已经被锁定,尽管第一个查询的结果中并没有这个第1行。保持第一个链接打开,然后开启第二个连接并执行如下查询:
```sql
mysql> SET autocommit=0;
Query OK, 0 rows affected (18.96 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.02 sec)
mysql> SELECT actor_id FROM sakila.actor WHERE actor_id =1 FOR UPDATE;
1205 - Lock wait timeout exceeded; try restarting transaction
```
这个查询将会挂起,直到第一个事务释放第一行的锁。这个行为对于基于语句的复制的正常运行来说是必要的。就像这个例子显示的,即使使用了索引,InnoDB也可能锁住一些不需要的数据。如果不能使用索引查找和锁定行的问题可能会更糟糕,MySQL会做全表扫描并锁住所有的行,而不管是不是需要。关于InnoDB、索引和锁有一些很少有人直到的细节:InnoDB在二级索引上使用共享(读)锁,但访问主键索引需要排他(写)锁。这消除了使用覆盖索引的可能性,并且使得SELECT FOR UPDATE比LOCK IN SHARE MODE或非锁定查询要慢得多
索引案例学习
理解索引最好的办法是结合示例,假设要设计一个在线约会网站,用户信息表有很多列,包括国家、地区、城市、性别、眼睛颜色,等等。网站必须支持上面这些特征的各种组合来搜索用户,还必须允许根据用户的最后在线时间、其他会员对用户的评分等对用户进行排序并对结果进行限制。如何设计索引满足上面的复杂需求呢?
出人意料的是第一件需要考虑的事情是需要使用索引来排序,还是先检索数据再排序。使用索引排序会严格限制索引和查询的设计。例如,如果希望使用索引做根据其他会员对用户的评分的排序,则WHER条件中的age BETWEEN 18 AND 25就无法使用索引。如果MySQL使用某个索引进行范围查询,也就无法再使用另一个索引(或者是该索引的后续字段)进行排序了。如果这是很常见的WHERE条件,那么我们当然就会认为很多查询需要做排序操作(例如文件排序filesort)
理解索引最好的办法是结合示例,假设要设计一个在线约会网站,用户信息表有很多列,包括国家、地区、城市、性别、眼睛颜色,等等。网站必须支持上面这些特征的各种组合来搜索用户,还必须允许根据用户的最后在线时间、其他会员对用户的评分等对用户进行排序并对结果进行限制。如何设计索引满足上面的复杂需求呢?
出人意料的是第一件需要考虑的事情是需要使用索引来排序,还是先检索数据再排序。使用索引排序会严格限制索引和查询的设计。例如,如果希望使用索引做根据其他会员对用户的评分的排序,则WHER条件中的age BETWEEN 18 AND 25就无法使用索引。如果MySQL使用某个索引进行范围查询,也就无法再使用另一个索引(或者是该索引的后续字段)进行排序了。如果这是很常见的WHERE条件,那么我们当然就会认为很多查询需要做排序操作(例如文件排序filesort)
支持多种过滤条件
现在需要看看那些列拥有不同的取值,那些列再WHERE子句中出现得最频繁。再有更多不同值得列上创建索引的选择性会更好。一般来说这样做都是对的,因为可以让MySQL更有效地过滤掉不需要的行。country列的选择性通常不高,但可能很多查询都会用到。sex列的选择性肯定很低,但也会再很多查询中用到。所以考虑到使用的频率,还是建议在创建不同的组合索引的时候将(sex,country)列作为前缀。根据传统的经验不是说不应该在选择性低的列上创建索引的吗?那为什么这里将两个选择性都很低的字段作为所以你的前缀列?我们的脑子坏了?
我们的脑子当然没坏。这么做有两个理由:第一点,如前所述几乎所有的查询都会用到sex列。前面曾提到,几乎每一个查询都会用到sex列,甚至会把网站设计成每次都只能按某一种性别搜索用户。更重要的是,索引中加上这一列也没有坏处,即使查询没用使用sex列也可以通过一些"诀窍"绕过。
这个"诀窍"就是:如果某个查询不限制性别,那么可以通过在查询条件中新增AND SEX IN('m','f')来让MySQL选择该索引。这样写并不会过滤任何行,和没有这个条件时返回的结果相同。但是必须加上这个列的条件,MySQL才能够匹配索引的最左前缀。这个"诀窍"在这类场景中非常有效,但如果列有太多不同的值,就会让IN()列表太长,这样做就不行了。这个案例显示了一个基本原则:考虑表上所有的选项。当设计索引时,不要只为现有的查询考虑需要哪些索引,还需要考虑对查询进行优化。如果发现某些查询需要创建新索引,但是这个索引又会降低另一些查询的效率,那么应该想一下是否能优化原来的查询。应该同时优化查询和索引以找到最佳的平衡,而不是闭门造车去设计最完美的索引。
接下来,需要考虑其他场景WHERE条件的组合,并需要了解哪些组合在没有合适索引的情况下会很慢。(sex,country,age)上的索引就是一个明显的选择,另外很有可能还需要(sex,country,region,age)和(sex,country,region,city,age)这样的组合个索引。这样就会需要大量的索引。如果想尽可能地重用索引而不是建立大量的组合索引,可以使用前面提到的IN()的技巧来避免同时需要(sex,country,age)和(sex,country,region,age)的索引。如果没有指定这个字段搜索,就需要定义一个全部国家列表,或者国家的全部地区列表,来确保索引前缀有同样的约束(组合所有国家、地区、性别将会是一个非常大的条件)
现在需要看看那些列拥有不同的取值,那些列再WHERE子句中出现得最频繁。再有更多不同值得列上创建索引的选择性会更好。一般来说这样做都是对的,因为可以让MySQL更有效地过滤掉不需要的行。country列的选择性通常不高,但可能很多查询都会用到。sex列的选择性肯定很低,但也会再很多查询中用到。所以考虑到使用的频率,还是建议在创建不同的组合索引的时候将(sex,country)列作为前缀。根据传统的经验不是说不应该在选择性低的列上创建索引的吗?那为什么这里将两个选择性都很低的字段作为所以你的前缀列?我们的脑子坏了?
我们的脑子当然没坏。这么做有两个理由:第一点,如前所述几乎所有的查询都会用到sex列。前面曾提到,几乎每一个查询都会用到sex列,甚至会把网站设计成每次都只能按某一种性别搜索用户。更重要的是,索引中加上这一列也没有坏处,即使查询没用使用sex列也可以通过一些"诀窍"绕过。
这个"诀窍"就是:如果某个查询不限制性别,那么可以通过在查询条件中新增AND SEX IN('m','f')来让MySQL选择该索引。这样写并不会过滤任何行,和没有这个条件时返回的结果相同。但是必须加上这个列的条件,MySQL才能够匹配索引的最左前缀。这个"诀窍"在这类场景中非常有效,但如果列有太多不同的值,就会让IN()列表太长,这样做就不行了。这个案例显示了一个基本原则:考虑表上所有的选项。当设计索引时,不要只为现有的查询考虑需要哪些索引,还需要考虑对查询进行优化。如果发现某些查询需要创建新索引,但是这个索引又会降低另一些查询的效率,那么应该想一下是否能优化原来的查询。应该同时优化查询和索引以找到最佳的平衡,而不是闭门造车去设计最完美的索引。
接下来,需要考虑其他场景WHERE条件的组合,并需要了解哪些组合在没有合适索引的情况下会很慢。(sex,country,age)上的索引就是一个明显的选择,另外很有可能还需要(sex,country,region,age)和(sex,country,region,city,age)这样的组合个索引。这样就会需要大量的索引。如果想尽可能地重用索引而不是建立大量的组合索引,可以使用前面提到的IN()的技巧来避免同时需要(sex,country,age)和(sex,country,region,age)的索引。如果没有指定这个字段搜索,就需要定义一个全部国家列表,或者国家的全部地区列表,来确保索引前缀有同样的约束(组合所有国家、地区、性别将会是一个非常大的条件)
这些索引将满足大部分最常见的搜索查询,但是如何为一些生僻的搜索条件(比如has_pictures、eye_color、hair_colr和education)来设计索引呢?这些列的选择性搞,使用也不频繁,可以选择忽略它们,让MySQL多扫描一些额外的行即可。另一个可选的方法是在age列的前面加上这些列,在查询时使用前面提到过的IN()基数来处理搜索时没有指定这些列的场景。你可能已经注意到了,我们一直将age列放在索引的最后面。age列有什么特殊的地方吗?为什么要放在索引的最后?我们总是尽可能让MySQL使用更多的索引列,因为查询只能使用索引的最左前缀,直到遇到第一个范围条件列。前面提到的列在WHERE子句中都是等于条件,但age列则多半是范围查询(例如查找年龄在18~25岁之间的人)。当然,也可以使用IN()来代替范围查询,例如年龄条件改写为IN(18,19,20,21,22,23,24,25)但不是所有的范围查询都可以转换。这里描述的基本原则是,尽可能将需要做范围查询的列放到索引的后面,以便优化器能使用尽可能的索引列。前面提到可以在索引中假如更多的列,并通过IN()方式覆盖那些不在WHERE子句中的列。但这种技巧也不能滥用,否则可能会带来麻烦。因为每额外增加一个IN()条件,优化器需要做的组合都将以指数形式增加,最终可能会极大地降低查询性能。考虑下面地子句:
```sql
WHERE eye_color IN ('brown', 'blue','hazel') AND hair_color IN ('black','red','blonde','brown') AND sex IN('M','F')
```
优化器则会转换成4x3x2=24种组合,执行计划需要检查WHERE子句中所有地24种组合。对于MySQL来说,24种组合并不是很夸张,但如果组合数达到上千个则需要特别小心。老版本的MySQL在IN()组合条件过多的时候会有很多问题。查询优化可能需要花很多时间,并消耗大量的内存。新版本的MySQL在组合数超过一定数量后就不再进行执行计划评估了,这可能会导致MySQL不能很好地利用索引。
```sql
WHERE eye_color IN ('brown', 'blue','hazel') AND hair_color IN ('black','red','blonde','brown') AND sex IN('M','F')
```
优化器则会转换成4x3x2=24种组合,执行计划需要检查WHERE子句中所有地24种组合。对于MySQL来说,24种组合并不是很夸张,但如果组合数达到上千个则需要特别小心。老版本的MySQL在IN()组合条件过多的时候会有很多问题。查询优化可能需要花很多时间,并消耗大量的内存。新版本的MySQL在组合数超过一定数量后就不再进行执行计划评估了,这可能会导致MySQL不能很好地利用索引。
避免多个范围条件。
假设我们有一个last_online列并系统通过下面的查询显示在过去几周上线过的用户:
```sql
WHERE eye_color IN ('brown','blue','hazel')
AND hair_color IN('blackj','red','blonde','brown')
AND sex IN ('M','F')
AND last_online > DATE_SUB(NOW, INTERVAL 7 DAY)
AND age BETWEEN 18 AND 25
```
什么是范围条件?
从EXPLAIN的输出很难区分MySQL是要查询范围值还是查询列表值.EXPLAIN使用同样的词"range"来描述这两种情况.例如,从type列来看,MySQL会把下下面这种查询当作是"range"类型:
```sql
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id > 45\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: NULL
rows: 155
filtered: 100.00
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
```
但是下面这条查询呢?
```sql
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id IN(1,4,99)\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: NULL
rows: 3
filtered: 100.00
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
```
从EXPLAIN的结果是无法区分这两者的,但可以从值的范围和多个等于条件来得出不同。在我们看来,第二个查询就是多个等值条件查询的。我们不是挑剔:这两种访问效率是不同的。对于范围条件查询,MySQL无法再使用范围列后面的其他索引列了,但是对于"多个等值条件查询"则没有这个限制
假设我们有一个last_online列并系统通过下面的查询显示在过去几周上线过的用户:
```sql
WHERE eye_color IN ('brown','blue','hazel')
AND hair_color IN('blackj','red','blonde','brown')
AND sex IN ('M','F')
AND last_online > DATE_SUB(NOW, INTERVAL 7 DAY)
AND age BETWEEN 18 AND 25
```
什么是范围条件?
从EXPLAIN的输出很难区分MySQL是要查询范围值还是查询列表值.EXPLAIN使用同样的词"range"来描述这两种情况.例如,从type列来看,MySQL会把下下面这种查询当作是"range"类型:
```sql
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id > 45\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: NULL
rows: 155
filtered: 100.00
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
```
但是下面这条查询呢?
```sql
mysql> EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id IN(1,4,99)\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: NULL
rows: 3
filtered: 100.00
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
```
从EXPLAIN的结果是无法区分这两者的,但可以从值的范围和多个等于条件来得出不同。在我们看来,第二个查询就是多个等值条件查询的。我们不是挑剔:这两种访问效率是不同的。对于范围条件查询,MySQL无法再使用范围列后面的其他索引列了,但是对于"多个等值条件查询"则没有这个限制
这个查询有一个问题:它有两个范围条件,last_online列和agelie,MySQL可以使用last_online列索引或者age列索引,但无法同时使用它们。如果条件中只有last_online而没有age,那么我们可能考虑再索引的后面加上last_online列。这里考虑如果我们无法把age字段转换为一个IN()的列表,并且仍要求对于同时有last_online和age这两个维度的范围查询的速度很快,那该怎么办?答案是,很遗憾没有一个直接的办法能够解决这个问题。但是我们能够将其中的一个范围查询转换为一个简单的等值比较。为了实现这一点,我们需要事先计算好一个active列,这个字段由定时任务来维护。当用户每次登录时,将杜英值设置为1,并且将过去连续七天未曾登录的用户的值设置为0.
这个方法可以让MySQL使用(active,sex,country,age)索引。active列并不是完全精确的,但是对于这类查询来说,对精度的要求也没有那么高,如果需要精确数据,可以把last_online列放到WHERE子句,但不加入到索引中。这和起那么通过计算URL哈希值来实现URL的快速查找类似。所以这个查询条件没法使用任何索引,但因为这个条件的过滤性不高,即使在索引中加入该列也没有太大的帮助,换个角度来说,缺乏合适的索引对该查询的影响也不明显。
到目前为止,我们可以看到:如果用户希望同时看到活跃和不活跃的用户,可以在查询中使用IN()列表。我们已经加入了很多这样的列表,但另外一个可选的方案就只能是为不同的组合创建单独的索引。至少需要建立如下的索引:(active,sex.country,age),(active,country,age),(sex,country,age)和(country,age)。这些索引对某个具体的查询来说可能都是更优化的了,但是考虑到索引的维护和额外的空间占用的代价,这个可选方案就不是一个好策略了。
在这个案例中,优化器的特性是影响索引策略的一个很重要的因素。如果未来版本的MySQL能够实现松散索引扫描,就能在一个索引上使用多个范围条件,那也就不需要为上面考虑的这类查询使用IN()列表了
这个方法可以让MySQL使用(active,sex,country,age)索引。active列并不是完全精确的,但是对于这类查询来说,对精度的要求也没有那么高,如果需要精确数据,可以把last_online列放到WHERE子句,但不加入到索引中。这和起那么通过计算URL哈希值来实现URL的快速查找类似。所以这个查询条件没法使用任何索引,但因为这个条件的过滤性不高,即使在索引中加入该列也没有太大的帮助,换个角度来说,缺乏合适的索引对该查询的影响也不明显。
到目前为止,我们可以看到:如果用户希望同时看到活跃和不活跃的用户,可以在查询中使用IN()列表。我们已经加入了很多这样的列表,但另外一个可选的方案就只能是为不同的组合创建单独的索引。至少需要建立如下的索引:(active,sex.country,age),(active,country,age),(sex,country,age)和(country,age)。这些索引对某个具体的查询来说可能都是更优化的了,但是考虑到索引的维护和额外的空间占用的代价,这个可选方案就不是一个好策略了。
在这个案例中,优化器的特性是影响索引策略的一个很重要的因素。如果未来版本的MySQL能够实现松散索引扫描,就能在一个索引上使用多个范围条件,那也就不需要为上面考虑的这类查询使用IN()列表了
优化排序.
最后要介绍的是排序。使用文件排序对小数据集是很快的,但如果一个查询匹配的结果有上百万行的话会怎样?例如如果WHERE子句只有sex列,如何排序?对于那些选择性非常低的列,可以增加一些特殊的索引来做排序。例如,可以创建(sex,rating)索引用于下面的查询:
```sql
mysql SELECT <cols> FROM profiles WHERE sex= 'M' ORDER BY rating LIMIT 10;
```
这个查询同时使用了ORDER BY 和LIMIT,如果没有索引的话会很慢。即使有索引,如果用户界面山需要翻页,并且翻页翻到比较靠后时查询也可能非常慢。下面这个查询就通过ORDER BY 和LIMIT偏移量的组合翻页到很后面的时候:
```sql
mysql> SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 1000000,10;
```
无论如何创建索引,这种查询都是个严重的问题,因为随者偏移量的增加,MySQL需要花费大量的时间来扫描需要丢弃的数据。反范式化、预先计算和缓存可能是解决这类查询的仅有策略。一个更好的办法是限制yoghurt能够翻页的数量,实际上这对用户体验的影响不大,因为用户很少会真正在乎搜索结果的第10 000页。优化这类索引的另一个比较好的策略是使用延迟关联,通过使用覆盖索引查询返回需要的主键,再根据这些主键关联源表获得需要的行。这可以减少MySQL扫描那些需要丢弃的行数。下面这个查询显示了如何高效地使用(sex,rating)索引进行排序和分页:
```sql
mysql> SELECT <cols> FROM profiles INNER JOIN(SELECT <primary key cols> FROM profiles WHERE x.sex ='M' ORDER BY rating LIMIT 1000000,10) AS x USING(<primary key cols>);
```
最后要介绍的是排序。使用文件排序对小数据集是很快的,但如果一个查询匹配的结果有上百万行的话会怎样?例如如果WHERE子句只有sex列,如何排序?对于那些选择性非常低的列,可以增加一些特殊的索引来做排序。例如,可以创建(sex,rating)索引用于下面的查询:
```sql
mysql SELECT <cols> FROM profiles WHERE sex= 'M' ORDER BY rating LIMIT 10;
```
这个查询同时使用了ORDER BY 和LIMIT,如果没有索引的话会很慢。即使有索引,如果用户界面山需要翻页,并且翻页翻到比较靠后时查询也可能非常慢。下面这个查询就通过ORDER BY 和LIMIT偏移量的组合翻页到很后面的时候:
```sql
mysql> SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 1000000,10;
```
无论如何创建索引,这种查询都是个严重的问题,因为随者偏移量的增加,MySQL需要花费大量的时间来扫描需要丢弃的数据。反范式化、预先计算和缓存可能是解决这类查询的仅有策略。一个更好的办法是限制yoghurt能够翻页的数量,实际上这对用户体验的影响不大,因为用户很少会真正在乎搜索结果的第10 000页。优化这类索引的另一个比较好的策略是使用延迟关联,通过使用覆盖索引查询返回需要的主键,再根据这些主键关联源表获得需要的行。这可以减少MySQL扫描那些需要丢弃的行数。下面这个查询显示了如何高效地使用(sex,rating)索引进行排序和分页:
```sql
mysql> SELECT <cols> FROM profiles INNER JOIN(SELECT <primary key cols> FROM profiles WHERE x.sex ='M' ORDER BY rating LIMIT 1000000,10) AS x USING(<primary key cols>);
```
维护索引和表。
即使使用正确的类型创建了表并加上了合适的索引,工作也没有结束:还需要维护表和索引来确保它们都正常工作。维护表有三个主要的目的:找到并修复损坏的表,维护准确的索引统计信息,减少碎片。
即使使用正确的类型创建了表并加上了合适的索引,工作也没有结束:还需要维护表和索引来确保它们都正常工作。维护表有三个主要的目的:找到并修复损坏的表,维护准确的索引统计信息,减少碎片。
找到并修复损坏的表
表损坏(corruption)是很糟糕的事情。对于MyISAM存储引擎,表损坏通常是系统崩溃导致的。其他的引擎也会由于硬件问题、MySQL本身的缺陷或者操作系统的问题导致索引损坏。损坏的索引会导致查询返回错误的结果或者莫须有的主键冲突等问题,严重时甚至还会导致数据库的崩溃。如果你遇到了古怪的问题——例如一些不应该发生的错误——可以尝试运行CHECK TABLE来检查是否发生了表损坏(注意有些存储引擎不支持该命令;二有些引擎则支持以不同的选项来控制完全检查表的方式)。CHECK TABLE通常能够找出大多数的表和索引的错误。可以使用REPAIR TABLE命令来修复损坏的表,但同样不是所有的存储引擎都支持该命令。如果存储疫情不支持,也可通过一个不做任何操作(no-op)的ALTER操作来重建表,例如修改表的存储引擎为当前欸度引擎。下面是一个针对InnoDB表的例子:
```sql
mysql> ALTER TABLE actor ENGINE=INNODB;
```
此外,也可以使用一些存储引擎相关的离线工具,例如myisamchk;或者将数据导出一份,然后再冲洗内倒入。不过,如果损坏的是系统区域,或者是表的"行数据"区域,而不是索引,那么上面的办法就没有用了。在这种情况下,可以从备份中恢复表,或者尝试从损坏的数据文件中尽可能地恢复数据。如果InnoDB引擎的表出现了损坏,那么一定是发生了严重的错误,需要立刻调查一下原因。InnoDB一般不会出现损坏。InnoDB的设计保证了它并不容易被损坏。如果发生损坏,一般要么是数据库的硬件问题例如内存或者磁盘问题(有可能),抑或是InnoDB本身的缺陷(不太可能)。常见的类似错误通常是由于尝试使用rsync备份InnoDB导致的。不存在什么查询能够让InnoDB表损坏,也不用担心暗处有"陷阱"。如果某条查询导致InnoDB数据的损坏,那一定是遇到了bug,而不是查询的问题。如果遇到数据损坏,最重要的是找出是什么导致了损坏,而不只是简单地修复,否则很有可能还会不断地损坏。可以通过设置innodb_force_recovery参数进入InnoDB地强制恢复模式来修复数据。另外,还可以使用开源地InnoDB数据恢复工具箱(InnoDB Data Recovery Toolkit)直接从InnoDB文件恢复出数据
表损坏(corruption)是很糟糕的事情。对于MyISAM存储引擎,表损坏通常是系统崩溃导致的。其他的引擎也会由于硬件问题、MySQL本身的缺陷或者操作系统的问题导致索引损坏。损坏的索引会导致查询返回错误的结果或者莫须有的主键冲突等问题,严重时甚至还会导致数据库的崩溃。如果你遇到了古怪的问题——例如一些不应该发生的错误——可以尝试运行CHECK TABLE来检查是否发生了表损坏(注意有些存储引擎不支持该命令;二有些引擎则支持以不同的选项来控制完全检查表的方式)。CHECK TABLE通常能够找出大多数的表和索引的错误。可以使用REPAIR TABLE命令来修复损坏的表,但同样不是所有的存储引擎都支持该命令。如果存储疫情不支持,也可通过一个不做任何操作(no-op)的ALTER操作来重建表,例如修改表的存储引擎为当前欸度引擎。下面是一个针对InnoDB表的例子:
```sql
mysql> ALTER TABLE actor ENGINE=INNODB;
```
此外,也可以使用一些存储引擎相关的离线工具,例如myisamchk;或者将数据导出一份,然后再冲洗内倒入。不过,如果损坏的是系统区域,或者是表的"行数据"区域,而不是索引,那么上面的办法就没有用了。在这种情况下,可以从备份中恢复表,或者尝试从损坏的数据文件中尽可能地恢复数据。如果InnoDB引擎的表出现了损坏,那么一定是发生了严重的错误,需要立刻调查一下原因。InnoDB一般不会出现损坏。InnoDB的设计保证了它并不容易被损坏。如果发生损坏,一般要么是数据库的硬件问题例如内存或者磁盘问题(有可能),抑或是InnoDB本身的缺陷(不太可能)。常见的类似错误通常是由于尝试使用rsync备份InnoDB导致的。不存在什么查询能够让InnoDB表损坏,也不用担心暗处有"陷阱"。如果某条查询导致InnoDB数据的损坏,那一定是遇到了bug,而不是查询的问题。如果遇到数据损坏,最重要的是找出是什么导致了损坏,而不只是简单地修复,否则很有可能还会不断地损坏。可以通过设置innodb_force_recovery参数进入InnoDB地强制恢复模式来修复数据。另外,还可以使用开源地InnoDB数据恢复工具箱(InnoDB Data Recovery Toolkit)直接从InnoDB文件恢复出数据
更新索引统计信息
MySQL的查询优化器会通过两个API来了解存储引擎的索引值的分布信息,以决定如何使用索引。第一个API是recors_in_rage(),通过向存储引擎传入两个边界值获取在这个范围大概有多少条记录。对于某些存储引擎,该接口返回精确值,例如MyISAM;但对于另一些存储引擎则是一个估算值。例如InnoDB.第二个API是info(),该接口返回各种类型的数据,包括索引的基数(每个键值有多少条记录).如果存储引擎向优化器提供的扫描行数信息是不准确的数据,或者执行计划本身太复杂以致无法准确地获取各个阶段匹配地行数,那么优化器会使用索引统计信息来估算扫描行数。MySQL优化器使用地是基于成本的模型,而衡量成本的主要指标就是一个查询需要扫描多少行。如果表没有统计信息,或者统计信息不准确,优化器就很有可能做出错误的决定。可以通过运行ANALYZE TABLE来重新生成统计信息解决这个问题。每种存储引擎实现索引统计信息的方式不同,所以需要进行ANALYZE TABLE的频率也因不同的引擎而不同,每次运行的成本也不同:
1.Memory引擎根本存储索引统计信息
2.MyISAM将索引统计信息存储在磁盘中,ANALYZE TABLE需要进行一次全索引扫描来计算索引基数。在整个过程需要锁表
3.直到MySQL5.5版本,InnoDB也不在磁盘存储索引统计信息,而是通过随机的索引访问进行评估并将其存储在内存中
可以使用SHOW INDEX FROM命令来查看索引的基数(Cardinality)。例如:
```sql
mysql> SHOW INDEX FROM actor\G
*************************** 1. row ***************************
Table: actor
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: actor_id
Collation: A
Cardinality: 200
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 2. row ***************************
Table: actor
Non_unique: 1
Key_name: idx_actor_last_name
Seq_in_index: 1
Column_name: last_name
Collation: A
Cardinality: 121
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
2 rows in set (0.00 sec)
```
这个命令输出了很多关于索引的信息,在MySQL手册中对上面每个字段的含义都有详细的解释。这里需要特别提及的是索引列的基数(Cardinality),其显示了存储引擎估算索引列有多少个不同的取值(离散程度)。在MySQL5.0和更新的版本中,还可以通过INFORMATION_SCHEMA.STATISTICS表很方便地查询到这些信息。例如基于INFORMATION_SCHEMA的表,可以编写一个查询给出当前选择性比较低的索引。需要注意的是,如果服务器上的库表非常多,则从这里获取元数据的速度可能会非常慢,而且会给MySQL带来额外的压力
MySQL的查询优化器会通过两个API来了解存储引擎的索引值的分布信息,以决定如何使用索引。第一个API是recors_in_rage(),通过向存储引擎传入两个边界值获取在这个范围大概有多少条记录。对于某些存储引擎,该接口返回精确值,例如MyISAM;但对于另一些存储引擎则是一个估算值。例如InnoDB.第二个API是info(),该接口返回各种类型的数据,包括索引的基数(每个键值有多少条记录).如果存储引擎向优化器提供的扫描行数信息是不准确的数据,或者执行计划本身太复杂以致无法准确地获取各个阶段匹配地行数,那么优化器会使用索引统计信息来估算扫描行数。MySQL优化器使用地是基于成本的模型,而衡量成本的主要指标就是一个查询需要扫描多少行。如果表没有统计信息,或者统计信息不准确,优化器就很有可能做出错误的决定。可以通过运行ANALYZE TABLE来重新生成统计信息解决这个问题。每种存储引擎实现索引统计信息的方式不同,所以需要进行ANALYZE TABLE的频率也因不同的引擎而不同,每次运行的成本也不同:
1.Memory引擎根本存储索引统计信息
2.MyISAM将索引统计信息存储在磁盘中,ANALYZE TABLE需要进行一次全索引扫描来计算索引基数。在整个过程需要锁表
3.直到MySQL5.5版本,InnoDB也不在磁盘存储索引统计信息,而是通过随机的索引访问进行评估并将其存储在内存中
可以使用SHOW INDEX FROM命令来查看索引的基数(Cardinality)。例如:
```sql
mysql> SHOW INDEX FROM actor\G
*************************** 1. row ***************************
Table: actor
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: actor_id
Collation: A
Cardinality: 200
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 2. row ***************************
Table: actor
Non_unique: 1
Key_name: idx_actor_last_name
Seq_in_index: 1
Column_name: last_name
Collation: A
Cardinality: 121
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
2 rows in set (0.00 sec)
```
这个命令输出了很多关于索引的信息,在MySQL手册中对上面每个字段的含义都有详细的解释。这里需要特别提及的是索引列的基数(Cardinality),其显示了存储引擎估算索引列有多少个不同的取值(离散程度)。在MySQL5.0和更新的版本中,还可以通过INFORMATION_SCHEMA.STATISTICS表很方便地查询到这些信息。例如基于INFORMATION_SCHEMA的表,可以编写一个查询给出当前选择性比较低的索引。需要注意的是,如果服务器上的库表非常多,则从这里获取元数据的速度可能会非常慢,而且会给MySQL带来额外的压力
InnoDB的统计信息值得深入研究。InnoDB引擎通过抽象的方式来计算统计信息,首先随机地读取少量的索引页面,然后以此为样本计算索引的统计信息。在老的InnoDB版本中,样本页面数是8,新版本的InnoDB可以铜鼓哦参数innodb_stats_sample_pages来设置样本页的数量.设置更大的值,理论上来说可以帮助生成更准确的索引信息,特别是对于某些超大的数据表来说,但具体设置多大合适依赖于具体的环境
```sql
mysql> SHOW VARIABLES LIKE '%innodb_stats_sample_pages%';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_stats_sample_pages | 8 |
+---------------------------+-------+
1 row in set (0.01 sec)
```
InnoDB会在表首次打开,或者执行ANALYZE TABLE,抑或表的大小发生非常大的变化大小变化超过十六分之一或者新插入了20亿行都会触发的时候计算索引的统计信息。InnoDB在打开某些INFORMATION_SCHEMA表,或者使用SHOW TABLE STATUS和SHOW INDEX,抑或在MySQL客户端开启自动补全功能的时候都会触发索引统计信息的更新。如果服务器上有大量的数据,这可能就是个很严重的问题,尤其是当IO比较慢的时候,客户端或者监控程序触发索引信息采样更新时可能会导致大量的锁,并给服务器带来很多的额外压力,这会让用户因为启动时间漫长而沮丧。只要SHOW INDEX查看索引统计信息,就一定会触发统计信息的更新。可以关闭innodb_stats_on_metadata参数来避免上面提到的问题
```sql
mysql> SHOW VARIABLES LIKE '%innodb_stats_on_metadata%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_stats_on_metadata | OFF |
+--------------------------+-------+
1 row in set (0.00 sec)
```
如果使用Percona版本,使用的就是XtraDB引擎而不是原生的InnoDB引擎,那么可以通过innodb_auto_update参数来禁止通过自动采样的方式更新索引统计信息,这时需要手动执行ANALYZE TABLE命令来更新统计信息。如果某些查询执行计划很不稳定的话,可以用该办法固话查询计划。如果想要更稳定的执行计划,并在系统重启后更快地生成这些统计i信息,那么可以使用系统表来持久化这些索引统计信息。甚至还可以在不同的机器间迁移索引统计信息,这样新环境启动时就无须再收集这些数据。在Percona5.1版本和官方的5.6版本都已经加入这个特性。一旦关闭索引统计信息的自动更新,那么久需要周期性地使用ANALYZE TABLEE来手动更新。否则,索引统计信息就会永远不变。如果数据分布发生大地变化,可能会出现一些很糟糕的执行计划。
```sql
mysql> SHOW VARIABLES LIKE '%innodb_stats_sample_pages%';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_stats_sample_pages | 8 |
+---------------------------+-------+
1 row in set (0.01 sec)
```
InnoDB会在表首次打开,或者执行ANALYZE TABLE,抑或表的大小发生非常大的变化大小变化超过十六分之一或者新插入了20亿行都会触发的时候计算索引的统计信息。InnoDB在打开某些INFORMATION_SCHEMA表,或者使用SHOW TABLE STATUS和SHOW INDEX,抑或在MySQL客户端开启自动补全功能的时候都会触发索引统计信息的更新。如果服务器上有大量的数据,这可能就是个很严重的问题,尤其是当IO比较慢的时候,客户端或者监控程序触发索引信息采样更新时可能会导致大量的锁,并给服务器带来很多的额外压力,这会让用户因为启动时间漫长而沮丧。只要SHOW INDEX查看索引统计信息,就一定会触发统计信息的更新。可以关闭innodb_stats_on_metadata参数来避免上面提到的问题
```sql
mysql> SHOW VARIABLES LIKE '%innodb_stats_on_metadata%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_stats_on_metadata | OFF |
+--------------------------+-------+
1 row in set (0.00 sec)
```
如果使用Percona版本,使用的就是XtraDB引擎而不是原生的InnoDB引擎,那么可以通过innodb_auto_update参数来禁止通过自动采样的方式更新索引统计信息,这时需要手动执行ANALYZE TABLE命令来更新统计信息。如果某些查询执行计划很不稳定的话,可以用该办法固话查询计划。如果想要更稳定的执行计划,并在系统重启后更快地生成这些统计i信息,那么可以使用系统表来持久化这些索引统计信息。甚至还可以在不同的机器间迁移索引统计信息,这样新环境启动时就无须再收集这些数据。在Percona5.1版本和官方的5.6版本都已经加入这个特性。一旦关闭索引统计信息的自动更新,那么久需要周期性地使用ANALYZE TABLEE来手动更新。否则,索引统计信息就会永远不变。如果数据分布发生大地变化,可能会出现一些很糟糕的执行计划。
减少索引和数据的碎片。
B-Tree索引可能会碎片化,这会降低查询的效率。碎片化的索引可能会以很差或者无序的方式存储在磁盘上。根据设计,B-Tree需要随机磁盘访问才能定位到叶子页,所以随机访问是不可避免地。然而,如果叶子页在物理分布上是顺序且紧密地,那么查询的性能就会更好。否则,对于范围查询、索引覆盖扫描等操作来说,速度可能会降低很多倍;对于索引覆盖扫描这一点更加明显。表的数据存储也可能碎片化。
然而,数据存储的碎片化比索引更加复杂。有三种类型的数据碎片:
1.行碎片(Row fragmentation)
这种碎片指的是数据行被存储在多个地方的片段中,即使查询只从索引中访问一行记录,行碎片也会导致性能下降
2.行间碎片(Intra-row fragmentation)
行间碎片是指逻辑上顺序的页,或者行在磁盘上不是顺序存储的。行间碎片对诸如全表扫描和聚簇索引扫描之类的操作有很大的影响,因为这些操作原本能从磁盘上顺序存储的数据中获益
3.剩余空间碎片(Free space fragmentation)
剩余空间碎片是指数据页中有大量的空余空间。这会导致服务器读取大量不需要的数据,从而造成浪费。
对于MyISAM表,这三类碎片化都可能发生。但InnoDB不会出现短小的行碎片;InnoDB会移动短小的行并重写到另一个片段中。可以通过执行OPTIMIZE TABLE或者导出再导入的方式来重新整理数据。这对多数存储引擎都是有效地。对于一些存储引擎如MyISAM,可以通过排序算法重建索引的方式来消除碎片。老版本的InnoDB没有什么消除碎片化的方法。不过最新版本InnoDB新增了"在线"添加和删除索引的功能,可以通过先删除,然后再重新创建索引的方式来消除索引的碎片化。对于那些不支持OPTIMIZE TABLE的存储引擎,可以通过一个不做任何操作(no-op)的ALTER TABLE操作来重建表。只需要将表的存储引擎修改为当前的存储引擎即可:
```sql
mysql>ALTER TABLE <table> ENGINE=<engine>;
```
对于开启了expand_fast_index_creation参数的Percona Server,按这种方式重建表,则会同时消除表和索引的碎片化。但对于标准版本的MySQL则只会消除表(实际上是聚簇索引)的碎片化。可用先删除所有索引,然后重建表,最后重新创建索引的方式模拟Percona Server的这个功能。应该通过一些实际测量而不是随意假设来确定是否需要消除索引和表的碎片化。Percona的XtraBackup有个--stats参数以非备份的方式运行,而只是打印索引和表的统计情况。包括页中的数据量和空余空间。这可以用来确定数据的碎片化程度。另外也要考虑数据是否已经达到稳定状态,如果你进行碎片整理将数据压缩到一起,可能反而会导致后续的更新操作触发一系列的页分裂和重组,这会对性能造成不良的影响(直到数据再次达到新的稳定状态)
B-Tree索引可能会碎片化,这会降低查询的效率。碎片化的索引可能会以很差或者无序的方式存储在磁盘上。根据设计,B-Tree需要随机磁盘访问才能定位到叶子页,所以随机访问是不可避免地。然而,如果叶子页在物理分布上是顺序且紧密地,那么查询的性能就会更好。否则,对于范围查询、索引覆盖扫描等操作来说,速度可能会降低很多倍;对于索引覆盖扫描这一点更加明显。表的数据存储也可能碎片化。
然而,数据存储的碎片化比索引更加复杂。有三种类型的数据碎片:
1.行碎片(Row fragmentation)
这种碎片指的是数据行被存储在多个地方的片段中,即使查询只从索引中访问一行记录,行碎片也会导致性能下降
2.行间碎片(Intra-row fragmentation)
行间碎片是指逻辑上顺序的页,或者行在磁盘上不是顺序存储的。行间碎片对诸如全表扫描和聚簇索引扫描之类的操作有很大的影响,因为这些操作原本能从磁盘上顺序存储的数据中获益
3.剩余空间碎片(Free space fragmentation)
剩余空间碎片是指数据页中有大量的空余空间。这会导致服务器读取大量不需要的数据,从而造成浪费。
对于MyISAM表,这三类碎片化都可能发生。但InnoDB不会出现短小的行碎片;InnoDB会移动短小的行并重写到另一个片段中。可以通过执行OPTIMIZE TABLE或者导出再导入的方式来重新整理数据。这对多数存储引擎都是有效地。对于一些存储引擎如MyISAM,可以通过排序算法重建索引的方式来消除碎片。老版本的InnoDB没有什么消除碎片化的方法。不过最新版本InnoDB新增了"在线"添加和删除索引的功能,可以通过先删除,然后再重新创建索引的方式来消除索引的碎片化。对于那些不支持OPTIMIZE TABLE的存储引擎,可以通过一个不做任何操作(no-op)的ALTER TABLE操作来重建表。只需要将表的存储引擎修改为当前的存储引擎即可:
```sql
mysql>ALTER TABLE <table> ENGINE=<engine>;
```
对于开启了expand_fast_index_creation参数的Percona Server,按这种方式重建表,则会同时消除表和索引的碎片化。但对于标准版本的MySQL则只会消除表(实际上是聚簇索引)的碎片化。可用先删除所有索引,然后重建表,最后重新创建索引的方式模拟Percona Server的这个功能。应该通过一些实际测量而不是随意假设来确定是否需要消除索引和表的碎片化。Percona的XtraBackup有个--stats参数以非备份的方式运行,而只是打印索引和表的统计情况。包括页中的数据量和空余空间。这可以用来确定数据的碎片化程度。另外也要考虑数据是否已经达到稳定状态,如果你进行碎片整理将数据压缩到一起,可能反而会导致后续的更新操作触发一系列的页分裂和重组,这会对性能造成不良的影响(直到数据再次达到新的稳定状态)
总结。
索引是一个非常复杂的话题!MySQL和存储引擎访问数据的方式,加上索引的特性,使得索引成为一个影响数据访问的有力而灵活的工作(无论数据是在磁盘中还是在内存中)。在MySQL中,大多数情况下都会使用B-Tree索引。其他类型的索引大多只适用于特殊的目的。如果在合适的场景中使用索引,将大大提高查询的响应时间。在选择索引和编写利用这些索引的查询时,有如下三个原则始终需要记住:
1.单行访问是很慢的。特别是在机械硬盘存储中(SSD的随机IO要快很多,不过这一点仍然成立)。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么就浪费了很多工作。最好读取的块中能包含尽可能多需要的行。使用索引可以创建位置引用以提升效率
2.按顺序访问范围数据是很快的,这有两个原因。第一,顺序IO不需要多次磁盘寻道,所以比随机IO要快很多(特别是对机械硬盘)。第二,如果服务器能够按需要顺序读取数据,那么久不再需要额外的排序操作,并且GROUP BY 查询也无序再做排序和将行按组进行聚合计算了。
3.索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎久不需要再回表查找行。这避免了大量的单行访问。
总的来说,编写查询语句时应该尽可能选择合适的索引以避免单行查找、尽可能地使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。这也与Lahdenmaki和Leach的书中的"三星"评价系统是一致的。如果表上的每一个查询都有一个完美的索引来满足当然是最好的。但不幸的是,要这么做有时可能需要创建大量的索引。还有一些时候对某些查询是不可能创建一个达到"三星"的索引(例如查询要按照两个列排序,其中一个列是正需,另一个列倒序)。这时必须有所取舍以创建最合适的索引,或者寻求替代策略(例如反范式化,或者提前计算汇总表等)
理解索引是如何工作的非常重要,应该根据这些理解来创建最合适的索引,而不是根据一些诸如"在多列索引中将选择性最高的列放在第一列(例如约会网站按性别查询)"或"应该为WHERE子句中出现的所有列创建索引"之类的经验法则及其推论。那么如何判断一个系统创建的索引是合理的呢?一般来说,建议按照响应时间来对查询进行分析。找出那些消耗最长时间的查询或者那些给服务器带来最大压力的查询,然后检查这些查询的schema、SQL和索引结构,判断是否有查询扫描了太多的行,是否做了很多额外的排序或者使用了临时表,是否适用了随机IO访问数据,或者是有太多回表查询那些不再索引中的列的操作。如果一个查询无法从所有可能的索引中获益,则应该看看是否可以创建一个更合适的索引来提升性能。如果不行,也可以看看是否可以重写该查询,将其转化成一个能够高效利用现有索引或者新创建索引的查询。
如果基于响应时间的分析不能找出有问题的查询呢?是否可能我们没有注意道"很糟糕"的查询,需要一个更好地索引来获取更高的性能?一般来说,不可能。对于诊断时抓不到的查询,那就不是问题。但是这个查询未来有可能会成为问题,因为应用程序、数据和负载都在变化。如果仍然想找到那些索引不是很合适的查询,并在它们成为问题前进行优化,则可以适用pt-query-digest的查询审查"review"功能,分析其EXPLAIN出来的执行计划
索引是一个非常复杂的话题!MySQL和存储引擎访问数据的方式,加上索引的特性,使得索引成为一个影响数据访问的有力而灵活的工作(无论数据是在磁盘中还是在内存中)。在MySQL中,大多数情况下都会使用B-Tree索引。其他类型的索引大多只适用于特殊的目的。如果在合适的场景中使用索引,将大大提高查询的响应时间。在选择索引和编写利用这些索引的查询时,有如下三个原则始终需要记住:
1.单行访问是很慢的。特别是在机械硬盘存储中(SSD的随机IO要快很多,不过这一点仍然成立)。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么就浪费了很多工作。最好读取的块中能包含尽可能多需要的行。使用索引可以创建位置引用以提升效率
2.按顺序访问范围数据是很快的,这有两个原因。第一,顺序IO不需要多次磁盘寻道,所以比随机IO要快很多(特别是对机械硬盘)。第二,如果服务器能够按需要顺序读取数据,那么久不再需要额外的排序操作,并且GROUP BY 查询也无序再做排序和将行按组进行聚合计算了。
3.索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎久不需要再回表查找行。这避免了大量的单行访问。
总的来说,编写查询语句时应该尽可能选择合适的索引以避免单行查找、尽可能地使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。这也与Lahdenmaki和Leach的书中的"三星"评价系统是一致的。如果表上的每一个查询都有一个完美的索引来满足当然是最好的。但不幸的是,要这么做有时可能需要创建大量的索引。还有一些时候对某些查询是不可能创建一个达到"三星"的索引(例如查询要按照两个列排序,其中一个列是正需,另一个列倒序)。这时必须有所取舍以创建最合适的索引,或者寻求替代策略(例如反范式化,或者提前计算汇总表等)
理解索引是如何工作的非常重要,应该根据这些理解来创建最合适的索引,而不是根据一些诸如"在多列索引中将选择性最高的列放在第一列(例如约会网站按性别查询)"或"应该为WHERE子句中出现的所有列创建索引"之类的经验法则及其推论。那么如何判断一个系统创建的索引是合理的呢?一般来说,建议按照响应时间来对查询进行分析。找出那些消耗最长时间的查询或者那些给服务器带来最大压力的查询,然后检查这些查询的schema、SQL和索引结构,判断是否有查询扫描了太多的行,是否做了很多额外的排序或者使用了临时表,是否适用了随机IO访问数据,或者是有太多回表查询那些不再索引中的列的操作。如果一个查询无法从所有可能的索引中获益,则应该看看是否可以创建一个更合适的索引来提升性能。如果不行,也可以看看是否可以重写该查询,将其转化成一个能够高效利用现有索引或者新创建索引的查询。
如果基于响应时间的分析不能找出有问题的查询呢?是否可能我们没有注意道"很糟糕"的查询,需要一个更好地索引来获取更高的性能?一般来说,不可能。对于诊断时抓不到的查询,那就不是问题。但是这个查询未来有可能会成为问题,因为应用程序、数据和负载都在变化。如果仍然想找到那些索引不是很合适的查询,并在它们成为问题前进行优化,则可以适用pt-query-digest的查询审查"review"功能,分析其EXPLAIN出来的执行计划
查询性能优化
概述。
前面介绍了如何设计最优的库表结构、如何建立最好的索引,这些对于高性能来说是必不可少的。但这些还不够——还需要合理的设计查询。如果查询写的很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能。查询优化、索引优化、库表结构优化需要齐头并进,一个不落。在获得编写MySQL查询的经验的同时,也将学习到如何为高效的查询设计表和索引。同样的,也可以学习到在优化库表结构时会影响到那些类型的查询。这个过程需要时间。接下来激昂从查询设计的一些基本原则开始——这也是在发现查询效率不高的时候首先需要考虑的因素。然后会介绍一些更深的查询优化技巧,并会介绍一些MySQL优化器内部的机制。将展示MySQL时如何执行查询的,如何改变一个查询的执行计划
前面介绍了如何设计最优的库表结构、如何建立最好的索引,这些对于高性能来说是必不可少的。但这些还不够——还需要合理的设计查询。如果查询写的很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能。查询优化、索引优化、库表结构优化需要齐头并进,一个不落。在获得编写MySQL查询的经验的同时,也将学习到如何为高效的查询设计表和索引。同样的,也可以学习到在优化库表结构时会影响到那些类型的查询。这个过程需要时间。接下来激昂从查询设计的一些基本原则开始——这也是在发现查询效率不高的时候首先需要考虑的因素。然后会介绍一些更深的查询优化技巧,并会介绍一些MySQL优化器内部的机制。将展示MySQL时如何执行查询的,如何改变一个查询的执行计划
为什么查询速度会慢。
在尝试编写快速的查询之前,需要清楚一点,真正重要的是响应时间。如果把查询看作是一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行得更快(有时候你可能还需要修改一些查询,减少这些查询对系统中运行的其他查询的影响,这种情况下,你是在减少一个查询的资源消耗)。MySQL在执行查询的时候有那些子任务,哪些子任务运行的速度很慢?这里很难给出完整的列表。通常来说,查询的生命周期大致可以按照顺序来看:从客户端,到服务器,然后再服务器上进行解析,生成执行计划,执行,并返回结果给客户端。其中"执行"可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎以及调用后的数据处理,包括排序、分组等。在完成这些任务的时候,查询需要在不同的地方花费时间,包括网络、CPU计算,生成统计信息和执行计划、锁等待(互斥等待)等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作和内存不足时导致的IO操作上消耗时间,根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。在每一个消耗大量时间的查询案例中,都能看到一些不必要的额外操作、某些操作被额外地重复了很多次、某些操作执行得太慢等。优化查询的目的就是减少和消除这些操作上所花费的时间.对于一个查询的全部生命周期,上面描述的并不完整。这里只是向说明:了解查询的生命周期、清楚查询的时间消耗情况对于优化查询有很大的意义
在尝试编写快速的查询之前,需要清楚一点,真正重要的是响应时间。如果把查询看作是一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行得更快(有时候你可能还需要修改一些查询,减少这些查询对系统中运行的其他查询的影响,这种情况下,你是在减少一个查询的资源消耗)。MySQL在执行查询的时候有那些子任务,哪些子任务运行的速度很慢?这里很难给出完整的列表。通常来说,查询的生命周期大致可以按照顺序来看:从客户端,到服务器,然后再服务器上进行解析,生成执行计划,执行,并返回结果给客户端。其中"执行"可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎以及调用后的数据处理,包括排序、分组等。在完成这些任务的时候,查询需要在不同的地方花费时间,包括网络、CPU计算,生成统计信息和执行计划、锁等待(互斥等待)等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作和内存不足时导致的IO操作上消耗时间,根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。在每一个消耗大量时间的查询案例中,都能看到一些不必要的额外操作、某些操作被额外地重复了很多次、某些操作执行得太慢等。优化查询的目的就是减少和消除这些操作上所花费的时间.对于一个查询的全部生命周期,上面描述的并不完整。这里只是向说明:了解查询的生命周期、清楚查询的时间消耗情况对于优化查询有很大的意义
慢查询基础:优化数据访问
查询性能低下最基本的原因是访问的数据太多。某些查询可能不可避免地需要筛选大量数据,但这并不场景。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询,我们发现通过下面两个步骤来分析总是很有效:
1.确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列
2.确认MySQL服务器是否在分析大量超过需要的数据行
查询性能低下最基本的原因是访问的数据太多。某些查询可能不可避免地需要筛选大量数据,但这并不场景。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询,我们发现通过下面两个步骤来分析总是很有效:
1.确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列
2.确认MySQL服务器是否在分析大量超过需要的数据行
是否向数据库请求了不需要的数据。
有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这回给MySQL服务器带来额外的负担,并增加网络开销(如果应用服务器和数据库不在同一台主机上,网络开销就显得很明显了。即使在同一台服务器上仍然会有数据传输的开销)。另外也会消耗应用服务器的CPU和内存资源。下面是一些典型案例:
1.查询不需要的记录:一个常见的错误是常常误以为MySQL会只返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。我们经常会看到一些了解其他数据库系统的人会设计出这类应用程序。这些开发者习惯适用这样的使用,先使用SELECT语句查询大量的结果,然后获取前面的N行后关闭结果集(例如在新闻网站中取出100条记录,但是只是在页面上显示前面10条)。它们认为MySQL会执行查询,并只返回它们需要的10条数据,然后停止查询。实际情况是MySQL会查询出全部的结果集,客户端的应用程序会接收全部的结果集数据,然后抛弃其中大部分数据。最简单有效的解决方法就是在这样的查询后面加上LIMIT
2.多表关联时返回全部列:如果你想查询所有在电影Academy Dinosaur中出现的演员,千万不要按下面的写法编写查询:
```sql
mysql> SELECT * FROM actor
-> INNER JOIN film_actor USING(actor_id)
-> INNER JOIN film USING(film_id)
-> WHERE film.title='Academy Dinosaur';
```
这将返回这三个表的全部数据列。正确的方式应该时像下面这样只取需要的列:
```sql
mysql>SELECT actor.* FROM actor .....
```
3.总是取出全部列:每次看到SELECT * 的时候都需要用怀疑的眼光审视,是不是真的需要返回全部的列?很可能不是必需的。取出全部列,会让优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的IO、内存和CPU的消耗。因此,一些DBA是严格禁止SELECT * 的写法的,这样做有时候还能避免某些列被修改带来的问题。当然,查询返回超过需要的数据也不总是坏事。在许多案例中,人们会说这种有点浪费数据库资源的方式可以简化开发,因为能提高相同代码片段的复用性,如果清除这样做的性能影响,那么这种做法也是值得考虑的。如果应用程序使用了某种缓存机制,或者有其他考虑,获取超过需要的数据也可能有其他好处,但不要忘记这样做的代价是什么。获取并缓存所有的列的查询相比多个独立的只获取部分列的查询可能就更有好处。
4.重复查询相同的数据:如果你不太小心,很容易出现这样的错误——不断地重复执行相同的查询,然后每次都返回完全相同的数据。例如,在用户评论的地方需要查询用户头像的URL,那么用户多次评论的时候,可能就会反复查询这个数据。比较好的方案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样性能显然会更好。
有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这回给MySQL服务器带来额外的负担,并增加网络开销(如果应用服务器和数据库不在同一台主机上,网络开销就显得很明显了。即使在同一台服务器上仍然会有数据传输的开销)。另外也会消耗应用服务器的CPU和内存资源。下面是一些典型案例:
1.查询不需要的记录:一个常见的错误是常常误以为MySQL会只返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。我们经常会看到一些了解其他数据库系统的人会设计出这类应用程序。这些开发者习惯适用这样的使用,先使用SELECT语句查询大量的结果,然后获取前面的N行后关闭结果集(例如在新闻网站中取出100条记录,但是只是在页面上显示前面10条)。它们认为MySQL会执行查询,并只返回它们需要的10条数据,然后停止查询。实际情况是MySQL会查询出全部的结果集,客户端的应用程序会接收全部的结果集数据,然后抛弃其中大部分数据。最简单有效的解决方法就是在这样的查询后面加上LIMIT
2.多表关联时返回全部列:如果你想查询所有在电影Academy Dinosaur中出现的演员,千万不要按下面的写法编写查询:
```sql
mysql> SELECT * FROM actor
-> INNER JOIN film_actor USING(actor_id)
-> INNER JOIN film USING(film_id)
-> WHERE film.title='Academy Dinosaur';
```
这将返回这三个表的全部数据列。正确的方式应该时像下面这样只取需要的列:
```sql
mysql>SELECT actor.* FROM actor .....
```
3.总是取出全部列:每次看到SELECT * 的时候都需要用怀疑的眼光审视,是不是真的需要返回全部的列?很可能不是必需的。取出全部列,会让优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的IO、内存和CPU的消耗。因此,一些DBA是严格禁止SELECT * 的写法的,这样做有时候还能避免某些列被修改带来的问题。当然,查询返回超过需要的数据也不总是坏事。在许多案例中,人们会说这种有点浪费数据库资源的方式可以简化开发,因为能提高相同代码片段的复用性,如果清除这样做的性能影响,那么这种做法也是值得考虑的。如果应用程序使用了某种缓存机制,或者有其他考虑,获取超过需要的数据也可能有其他好处,但不要忘记这样做的代价是什么。获取并缓存所有的列的查询相比多个独立的只获取部分列的查询可能就更有好处。
4.重复查询相同的数据:如果你不太小心,很容易出现这样的错误——不断地重复执行相同的查询,然后每次都返回完全相同的数据。例如,在用户评论的地方需要查询用户头像的URL,那么用户多次评论的时候,可能就会反复查询这个数据。比较好的方案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样性能显然会更好。
MySQL是否扫描额外的记录。
在确定查询只返回需要的数据以后,接下来应该看看查询为了返回结果是否扫描了许多的数据。对于MySQL,最简单的衡量查询开销的三个指标如下:
1.响应时间
2.扫描的行数
3.返回的行数
没有哪个指标能够完美地衡量查询的开销,但它们大致反映了MySQL在内部执行查询时需要访问多少数据,并可以大概推算出查询运行的时间。这三个指标都会记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描行数过多的查询的好办法
在确定查询只返回需要的数据以后,接下来应该看看查询为了返回结果是否扫描了许多的数据。对于MySQL,最简单的衡量查询开销的三个指标如下:
1.响应时间
2.扫描的行数
3.返回的行数
没有哪个指标能够完美地衡量查询的开销,但它们大致反映了MySQL在内部执行查询时需要访问多少数据,并可以大概推算出查询运行的时间。这三个指标都会记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描行数过多的查询的好办法
响应时间。
要记住, 响应时间只是一个表面的值。这样说可能看起来和前面关于响应时间的说法有矛盾?其实并不矛盾,响应时间仍然是最重要的指标,这有一点复杂,后面细细道来。响应时间是两个部分之和:服务时间和排队时间。服务时间是指数据处理这个查询真正花了多长时间。排队时间是指服务器因为等待某些资源而没有真正执行查询的时间——可能是等待IO操作完成,也可能是等待行锁,等等。遗憾的是,我们无法把响应时间细分到上面这些部分,除非有什么办法能够逐个测量上面这些消耗,不过很难做到,一般最常见和重要的等待是IO和锁等待,但实际情况更加复杂。所以在不同类型的应用压力下,响应时间并没有什么一致的规律或者共识。诸如存储引擎的锁(表锁、行锁)、高并发资源竞争、硬件响应等诸多因素都会影响到响应时间。所以,响应时间既可能是一个问题的结果也可能是一个问题的原因,不同案例情况不同,除非我们能深入测量出每个环节。
当你看到一个查询的响应时间的时候,首先需要问问自己,这个响应时间是否是一个合理的值。实际上可以使用"快速上限估计"法来估算查询的响应时间,这是由Lahdenmaki和Mike Leach编写的Relational Database Index Design and the Optimizers一书中提到的技术。概括地说,了解这个查询需要哪些索引以及它的执行计划是什么,然后计算大概需要多少个顺序和随机IO,再用其乘以在具体硬件条件下一次IO的消耗。最后把这些消耗都加起来,就可以获得一个大概参考值来判断当前响应时间是不是一个合理得值
要记住, 响应时间只是一个表面的值。这样说可能看起来和前面关于响应时间的说法有矛盾?其实并不矛盾,响应时间仍然是最重要的指标,这有一点复杂,后面细细道来。响应时间是两个部分之和:服务时间和排队时间。服务时间是指数据处理这个查询真正花了多长时间。排队时间是指服务器因为等待某些资源而没有真正执行查询的时间——可能是等待IO操作完成,也可能是等待行锁,等等。遗憾的是,我们无法把响应时间细分到上面这些部分,除非有什么办法能够逐个测量上面这些消耗,不过很难做到,一般最常见和重要的等待是IO和锁等待,但实际情况更加复杂。所以在不同类型的应用压力下,响应时间并没有什么一致的规律或者共识。诸如存储引擎的锁(表锁、行锁)、高并发资源竞争、硬件响应等诸多因素都会影响到响应时间。所以,响应时间既可能是一个问题的结果也可能是一个问题的原因,不同案例情况不同,除非我们能深入测量出每个环节。
当你看到一个查询的响应时间的时候,首先需要问问自己,这个响应时间是否是一个合理的值。实际上可以使用"快速上限估计"法来估算查询的响应时间,这是由Lahdenmaki和Mike Leach编写的Relational Database Index Design and the Optimizers一书中提到的技术。概括地说,了解这个查询需要哪些索引以及它的执行计划是什么,然后计算大概需要多少个顺序和随机IO,再用其乘以在具体硬件条件下一次IO的消耗。最后把这些消耗都加起来,就可以获得一个大概参考值来判断当前响应时间是不是一个合理得值
扫描的行数和返回的行数。
分析查询时,查看该查询扫描的行数时非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。对于找出哪些"糟糕"的查询,这个指标可能还不够完美,因为并不是所有的行的访问代价都是相同的。较短的行的访问速度更快,内存中的行也比磁盘中的行访问速度要快得多。理想情况下扫描得行数和返回的行数应该是相同的。但实际情况中这种"美事"并不多。例如在做一个管来奶查询时,服务器必须要扫描多行才能生成结果集中的一行。扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间,不过有时候这个值也可能非常非常大
分析查询时,查看该查询扫描的行数时非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。对于找出哪些"糟糕"的查询,这个指标可能还不够完美,因为并不是所有的行的访问代价都是相同的。较短的行的访问速度更快,内存中的行也比磁盘中的行访问速度要快得多。理想情况下扫描得行数和返回的行数应该是相同的。但实际情况中这种"美事"并不多。例如在做一个管来奶查询时,服务器必须要扫描多行才能生成结果集中的一行。扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间,不过有时候这个值也可能非常非常大
扫描的行数和访问类型。
在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。在EXPLAIN语句中的type列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里列的这些,速度时从慢到快,扫描的行数也是从多到少。你不需要记住这些访问类型,但需要明白扫描表、扫描索引、范围访问和单值访问的概念.如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引。现在应该明白为什么索引对于查询优化如此重要了。索引让MySQL以最高效、扫描行数最少的方式找到需要的记录。例如,我们看看示例库Sakila中的一个查询案例:
```sql
mysql> SELECT * FROM film_actor WHERE film_id =1;
+----------+---------+---------------------+
| actor_id | film_id | last_update |
+----------+---------+---------------------+
| 1 | 1 | 2006-02-15 05:05:03 |
| 10 | 1 | 2006-02-15 05:05:03 |
| 20 | 1 | 2006-02-15 05:05:03 |
| 30 | 1 | 2006-02-15 05:05:03 |
| 40 | 1 | 2006-02-15 05:05:03 |
| 53 | 1 | 2006-02-15 05:05:03 |
| 108 | 1 | 2006-02-15 05:05:03 |
| 162 | 1 | 2006-02-15 05:05:03 |
| 188 | 1 | 2006-02-15 05:05:03 |
| 198 | 1 | 2006-02-15 05:05:03 |
+----------+---------+---------------------+
```
这个查询将返回10行数据,从EXPLAIN的结果可以看到,MySQL在索引idx_fk_film_id上使用了ref访问类型来执行查询:
```sql
mysql> EXPLAIN SELECT * FROM film_actor WHERE film_id=1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: const
rows: 10
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
```
在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。在EXPLAIN语句中的type列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里列的这些,速度时从慢到快,扫描的行数也是从多到少。你不需要记住这些访问类型,但需要明白扫描表、扫描索引、范围访问和单值访问的概念.如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引。现在应该明白为什么索引对于查询优化如此重要了。索引让MySQL以最高效、扫描行数最少的方式找到需要的记录。例如,我们看看示例库Sakila中的一个查询案例:
```sql
mysql> SELECT * FROM film_actor WHERE film_id =1;
+----------+---------+---------------------+
| actor_id | film_id | last_update |
+----------+---------+---------------------+
| 1 | 1 | 2006-02-15 05:05:03 |
| 10 | 1 | 2006-02-15 05:05:03 |
| 20 | 1 | 2006-02-15 05:05:03 |
| 30 | 1 | 2006-02-15 05:05:03 |
| 40 | 1 | 2006-02-15 05:05:03 |
| 53 | 1 | 2006-02-15 05:05:03 |
| 108 | 1 | 2006-02-15 05:05:03 |
| 162 | 1 | 2006-02-15 05:05:03 |
| 188 | 1 | 2006-02-15 05:05:03 |
| 198 | 1 | 2006-02-15 05:05:03 |
+----------+---------+---------------------+
```
这个查询将返回10行数据,从EXPLAIN的结果可以看到,MySQL在索引idx_fk_film_id上使用了ref访问类型来执行查询:
```sql
mysql> EXPLAIN SELECT * FROM film_actor WHERE film_id=1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: const
rows: 10
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
```
EXPLAIN的结果也显示MySQL预估需要访问10行数据。换句话说,查询优化器认为这种访问类型可以高效地完成查询。如果没有合适地索引会怎样呢?MySQL就不得不使用一种更糟糕地访问类型,下面我们来看看如果我们删除对应的索引再来运行这个查询:
```sql
mysql> ALTER TABLE film_actor DROP FOREIGN KEY fk_film_actor_film;
Query OK, 0 rows affected (18.97 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE film_actor DROP KEY idx_fk_film_id;
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> EXPLAIN SELECT * FROM film_actor WHERE film_id=1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5462
filtered: 10.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
```
正如我们预测的,访问类型变成了一个全表扫描(ALL),现在MySQL预估需要扫描5462条记录来完成这个查询。这里的"Using Where"表示MySQL将通过WHERE条件来筛选存储引擎返回的记录。一般MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为:
1.在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的
2.使用索引覆盖扫描(在Extra列出现了Using index)来返回记录,直接从素银中过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,但无须再回表查询记录
3.从数据表中返回数据,然后过滤不满足条件的记录(在Extra列中出现Using WHere)。这在MySQL服务器层完成,MySQL需要从数据表独处记录后过滤。
上面这个例子说明了好的索引多么重要。好的索引可以让查询使用合适的访问了悉尼港,尽可能地只扫描需要的数据行。但也不是说增加索引就能让扫描的行数等于返回的行数。例如下面使用聚合函数COUNT()的查询:
```sql
mysql> SELECT * FROM film_actor WHERE film_id =1;
```
这个查询需要读取几千行数据,但是仅返回200行结果。没有什么索引能够让这样的查询减少需要扫描的行数。不幸的是,MySQL不会告诉我们生成结果实际上需要扫描多少行数据(例如关联查询结果返回的一条记录通常是由多条记录组成的)而只会告诉我们生成结果时一共扫描了多少行数据。扫描的行数中的大部分都很可能是被WHERE条件过滤掉的,对最终结果集并没有贡献。在上面的例子中,删除索引后,看到MySQL需要扫描所有记录然后根据WHERE条件过滤,最终只返回10行结果。理解一个查询需要扫描多少行和实际需要使用的行数需要先去理解这个查询背后的逻辑和思想。
如果发现查询需要扫描大量的数据但只返回少数的行,那么通常可以尝试下面的技巧去优化它:
1.使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无须回表获取对应行就可以返回结果;额
2.改变库表结构。例如使用单独的汇总表
3.重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询
```sql
mysql> ALTER TABLE film_actor DROP FOREIGN KEY fk_film_actor_film;
Query OK, 0 rows affected (18.97 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE film_actor DROP KEY idx_fk_film_id;
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> EXPLAIN SELECT * FROM film_actor WHERE film_id=1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5462
filtered: 10.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
```
正如我们预测的,访问类型变成了一个全表扫描(ALL),现在MySQL预估需要扫描5462条记录来完成这个查询。这里的"Using Where"表示MySQL将通过WHERE条件来筛选存储引擎返回的记录。一般MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为:
1.在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的
2.使用索引覆盖扫描(在Extra列出现了Using index)来返回记录,直接从素银中过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,但无须再回表查询记录
3.从数据表中返回数据,然后过滤不满足条件的记录(在Extra列中出现Using WHere)。这在MySQL服务器层完成,MySQL需要从数据表独处记录后过滤。
上面这个例子说明了好的索引多么重要。好的索引可以让查询使用合适的访问了悉尼港,尽可能地只扫描需要的数据行。但也不是说增加索引就能让扫描的行数等于返回的行数。例如下面使用聚合函数COUNT()的查询:
```sql
mysql> SELECT * FROM film_actor WHERE film_id =1;
```
这个查询需要读取几千行数据,但是仅返回200行结果。没有什么索引能够让这样的查询减少需要扫描的行数。不幸的是,MySQL不会告诉我们生成结果实际上需要扫描多少行数据(例如关联查询结果返回的一条记录通常是由多条记录组成的)而只会告诉我们生成结果时一共扫描了多少行数据。扫描的行数中的大部分都很可能是被WHERE条件过滤掉的,对最终结果集并没有贡献。在上面的例子中,删除索引后,看到MySQL需要扫描所有记录然后根据WHERE条件过滤,最终只返回10行结果。理解一个查询需要扫描多少行和实际需要使用的行数需要先去理解这个查询背后的逻辑和思想。
如果发现查询需要扫描大量的数据但只返回少数的行,那么通常可以尝试下面的技巧去优化它:
1.使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无须回表获取对应行就可以返回结果;额
2.改变库表结构。例如使用单独的汇总表
3.重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询
重构查询的方式。
在优化有问题的查询时,目标应该是找到一个更优的方法获得实际需要的记过——而不是一定总是需要从MySQL获取一模一样的结果集。有时候,可以将查询转换一种写法让其返回一样的结果,但是性能更好。但也可以通过修改应用代码,用另一种方式完成查询,最终达到一样的目的。
在优化有问题的查询时,目标应该是找到一个更优的方法获得实际需要的记过——而不是一定总是需要从MySQL获取一模一样的结果集。有时候,可以将查询转换一种写法让其返回一样的结果,但是性能更好。但也可以通过修改应用代码,用另一种方式完成查询,最终达到一样的目的。
一个复杂查询还是多个简单查询。
设计查询的时候一个需要考虑的重要问题是,是否需要将一个复杂的查询分成多个简单的查询,在传统实现中,总是强调需要数据库层完成尽可能多的工作,这样做的逻辑在于以前总是认为网络通信、查询解析和优化是一件代价很高的事情。但是这样的想法对于MySQL并不适用,MySQL从设计上让连接和断开连接都很轻量级,在返回一个小的查询结果方面很高效。现代的网络速度比以前要快很多,无论是贷款还是延迟。在某些版本的MySQL上,即使在一个通用服务器上,也能够运行超过10万的查询,即使是一个千兆网卡(1000Mbps / 8 bit = 125M/s)也能轻松满足每秒超过2000次的查询。所以运行多个小查询现在已经不是大问题了。MySQL内部每秒能够扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端就慢得多了。在其他条件都相同的时候,适用尽可能少的查询当然是更好地。但是有时候,将一个大查询分解为多个小查询是很有必要的。别害怕这样做,好好衡量一下这样做是不是会减少工作量。
不过,在应用设计的时候,如果一个查询能够胜任时还写成多个独立查询是不明智的。例如,有些应用对一个数据表做10次独立的查询来返回10行数据,每个查询返回一条结果,查询10次
设计查询的时候一个需要考虑的重要问题是,是否需要将一个复杂的查询分成多个简单的查询,在传统实现中,总是强调需要数据库层完成尽可能多的工作,这样做的逻辑在于以前总是认为网络通信、查询解析和优化是一件代价很高的事情。但是这样的想法对于MySQL并不适用,MySQL从设计上让连接和断开连接都很轻量级,在返回一个小的查询结果方面很高效。现代的网络速度比以前要快很多,无论是贷款还是延迟。在某些版本的MySQL上,即使在一个通用服务器上,也能够运行超过10万的查询,即使是一个千兆网卡(1000Mbps / 8 bit = 125M/s)也能轻松满足每秒超过2000次的查询。所以运行多个小查询现在已经不是大问题了。MySQL内部每秒能够扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端就慢得多了。在其他条件都相同的时候,适用尽可能少的查询当然是更好地。但是有时候,将一个大查询分解为多个小查询是很有必要的。别害怕这样做,好好衡量一下这样做是不是会减少工作量。
不过,在应用设计的时候,如果一个查询能够胜任时还写成多个独立查询是不明智的。例如,有些应用对一个数据表做10次独立的查询来返回10行数据,每个查询返回一条结果,查询10次
切分查询。
有时候对于一个大查询我们需要"分而治之",将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果。删除旧的数据就是一个很好的例子。定期地清除大量数据时,如果用一个大的语句一次性完成的话,则可能需要依次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但很重要的查询。将一个大的DELETE语句切分成多个较小的查询可以尽可能小地影响MySQL性能,同时还可以减少MySQL复制地延迟。例如,我们需要每个月运行一次下面的查询:
```sql
mysql> DELETE FROM message WHERE created < DATE_SUB(NOW(), INTERVAL 3 MONT
```
那么可以用类似下面的办法来完成同样的工作:
```sql
rows_affected=0
do {
rows_affected = do_query(
"DELETE FROM message WHERE created < DATE_SUB(NOW(), INTERVAL 3 MONTH)
LIMIT 10000"
)
} while rows_affected > 0
```
一次性删除一万行数据一般来说是一个比较高效而且对服务器影响也是最小的做法(如果是事务型引擎,很多时候小事务能够更高效),同时,需要注意的是,如果每次删除数据后,都暂停一会儿再做下一次删除,这样也可以将服务器上原本一次性的压力分散到一个很长的时间段中,就可以大大降低对服务器的影响,还可以大大减少删除时锁的持有时间
有时候对于一个大查询我们需要"分而治之",将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果。删除旧的数据就是一个很好的例子。定期地清除大量数据时,如果用一个大的语句一次性完成的话,则可能需要依次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但很重要的查询。将一个大的DELETE语句切分成多个较小的查询可以尽可能小地影响MySQL性能,同时还可以减少MySQL复制地延迟。例如,我们需要每个月运行一次下面的查询:
```sql
mysql> DELETE FROM message WHERE created < DATE_SUB(NOW(), INTERVAL 3 MONT
```
那么可以用类似下面的办法来完成同样的工作:
```sql
rows_affected=0
do {
rows_affected = do_query(
"DELETE FROM message WHERE created < DATE_SUB(NOW(), INTERVAL 3 MONTH)
LIMIT 10000"
)
} while rows_affected > 0
```
一次性删除一万行数据一般来说是一个比较高效而且对服务器影响也是最小的做法(如果是事务型引擎,很多时候小事务能够更高效),同时,需要注意的是,如果每次删除数据后,都暂停一会儿再做下一次删除,这样也可以将服务器上原本一次性的压力分散到一个很长的时间段中,就可以大大降低对服务器的影响,还可以大大减少删除时锁的持有时间
分阶关联查询。
很多高性能的应用都会对关联查询进行分解。简单地,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。例如,下面这个查询:
```sql
mysql> SELECT * FROM tag
-> JOIN tag_post ON tag_post.tag_id=tag.id
-> JOIN post ON tag_post.post_id=post.id
-> WHERE tag.tag = 'mysql';
```
可以分解成下面这些查询来代替:
```sql
mysql> SELECT * FROM tag WHERE tag = 'mysql';
mysql> SELECT * FROM tag_post WHERE tag_id = 1234;
mysql> SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
```
到底为什么要这样做呢?乍一看,这样做并没有什么好处,原本一条查询,这里却变成多条查询,返回的结果又是一模一样的。事实上,用分解关联查询的方式重构查询有如下的优势:
1.让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。例如,上面查询中的tag已经被缓存了,那么应用就可以跳过第一个查询。再例如,应用中已经缓存了ID为123、567、9098的内容,那么第三个查询中的IN()中就可以少几个ID.另外,对MySQL的查询缓存来说(Query Cache),如果关联中的某个表发生了变化,那么就无法适用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了
2.将查询分解后,执行单个查询可以减少锁的竞争
3.再在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展
4.查询本身效率也可能会有所提升。这个例子中,使用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机关联要更高效。
5.可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消耗
6.更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高很多。
在很多场景下,通过重构查询将关联放到应用程序中将会更加高效,这样的场景有很多。比如,当应用能够方便地缓存单个查询的结果的时候、当可以将数据分不到不同的MySQL服务器上的时候、当能够使用IN()的方式代替关联查询的时候、当查询中使用同一个数据表的时候
很多高性能的应用都会对关联查询进行分解。简单地,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。例如,下面这个查询:
```sql
mysql> SELECT * FROM tag
-> JOIN tag_post ON tag_post.tag_id=tag.id
-> JOIN post ON tag_post.post_id=post.id
-> WHERE tag.tag = 'mysql';
```
可以分解成下面这些查询来代替:
```sql
mysql> SELECT * FROM tag WHERE tag = 'mysql';
mysql> SELECT * FROM tag_post WHERE tag_id = 1234;
mysql> SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
```
到底为什么要这样做呢?乍一看,这样做并没有什么好处,原本一条查询,这里却变成多条查询,返回的结果又是一模一样的。事实上,用分解关联查询的方式重构查询有如下的优势:
1.让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。例如,上面查询中的tag已经被缓存了,那么应用就可以跳过第一个查询。再例如,应用中已经缓存了ID为123、567、9098的内容,那么第三个查询中的IN()中就可以少几个ID.另外,对MySQL的查询缓存来说(Query Cache),如果关联中的某个表发生了变化,那么就无法适用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了
2.将查询分解后,执行单个查询可以减少锁的竞争
3.再在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展
4.查询本身效率也可能会有所提升。这个例子中,使用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机关联要更高效。
5.可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消耗
6.更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高很多。
在很多场景下,通过重构查询将关联放到应用程序中将会更加高效,这样的场景有很多。比如,当应用能够方便地缓存单个查询的结果的时候、当可以将数据分不到不同的MySQL服务器上的时候、当能够使用IN()的方式代替关联查询的时候、当查询中使用同一个数据表的时候
查询执行的基础。
当希望MySQL能够以更高效的性能运行查询时,最好的办法就是弄清楚MySQL是如何优化和执行查询的。一旦理解这一点,很多查询优化工作实际上就是遵循一些原则让优化器能够按照预想的合理的方式运行。MySQL执行一个查询的过程。根据如图所示,我们可以看到当向MySQL发送一个请求的时候,MySQL到底做了些什么。
1.客户端发送一条查询给服务器
2.服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段
3.服务器端进入SQL解析、预处理,再由优化器生成对应的执行计划
4.MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询
5.将结果返回给客户端
上面的每一步都比想象的复杂,接下来我们会看到在每一个阶段查询处于何种状态。查询优化器是其中特别复杂也特别难以理解的部分。还有很多的例外情况,例如,当查询使用绑定变量后,执行路径会有所不同
当希望MySQL能够以更高效的性能运行查询时,最好的办法就是弄清楚MySQL是如何优化和执行查询的。一旦理解这一点,很多查询优化工作实际上就是遵循一些原则让优化器能够按照预想的合理的方式运行。MySQL执行一个查询的过程。根据如图所示,我们可以看到当向MySQL发送一个请求的时候,MySQL到底做了些什么。
1.客户端发送一条查询给服务器
2.服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段
3.服务器端进入SQL解析、预处理,再由优化器生成对应的执行计划
4.MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询
5.将结果返回给客户端
上面的每一步都比想象的复杂,接下来我们会看到在每一个阶段查询处于何种状态。查询优化器是其中特别复杂也特别难以理解的部分。还有很多的例外情况,例如,当查询使用绑定变量后,执行路径会有所不同
MySQL客户端/服务器通信协议。
一般来说,不需要去理解MySQL通信协议的内部实现细节,只需要大致理解通信协议是如何工作的。MySQL客户端和服务器之间的通信协议是"半双工"的,这意味着,在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能同时发生。所以,我们也无法也无须将一个消息切成小块独立来发送。这种协议让MySQL通信简单快速,但是也从很多地方限制了MySQL.一个明显的限制是,这意味着没法进行流量控制。一旦一端开始发送消息,另一端要接收完整个消息才能响应它。这就像来回抛球的游戏:在任何时刻,只有一个人能控制球,而且只有控制球的人才能将球抛回去(发送消息)。一旦客户端发送了请求,它能做的事情就只是等待结果了。相反的,一般服务器响应给用户的数据通常很多,由多个数据包组成。当服务器开始响应客户端请求时,客户端必须完整接收整个返回结果,而不能简单地只取前面几条结果,然后让服务器停止发送数据。这种情况下,客户端若接收完整的结果,然后取前面几条需要的结果,或者接收完几条结果后就"粗暴"地断开连接,都不是好注意。这也是在必要的时候一定要在查询中加上LIMIT限制的原因。换一种方式解释这种行为:当客户端从服务器取数据时,看起来是一个拉数据的过程,但实际上是MySQL在向客户端推送数据的过程。客户端不断地接收从服务器推送的数据,客户端也没法让服务器停下来。客户端像是"从消防管喝水"(这是一个术语)。多数连接MySQL的库函数都可以获得全部结果集并缓存到内存力,还可以逐行获取需要的数据。默认一般是获得全部结果集并缓存到内存中。MySQL通常需要等所有的数据都已经发生给客户端才能释放这条查询所占用的资源,所以接收全部结果并缓存通常可以减少服务器的压力,让查询能够早点结束、早点释放相应的资源。
当使用多数连接MySQL的库函数从MySQL获取数据时,其结果看起来都像时从MySQL服务器获取数据,而实际上都是从这个库函数的缓存获取数据。多数情况下这没什么问题,但是如果需要返回一个很大的结果集的时候,这样做并不好,因为库函数会花很多时间和内存来存储所有的结果集。如果能够尽早开始处理这些结果集,就能大大减少内存的消耗,这种情况下可以不适用缓存来记录结果而是直接处理。这样做的缺点是,对于服务器来说,需要查询完成后才能释放资源,所以在和客户端交互的整个过程中,服务器的资源都是被这个查询搜占用的(你可以使用SQL_BUFFER_RESULT)
我们看啊可能当使用PHP的时候是什么情况。首先,下面是我们连接MySQL的通常写法:
```php
<?php
$link = mysql_connect('localhost','user','p4ssword');
$result = mysql_query('SELECT * FROM HUGE_TABLE', $link);
$while($row = mysql_fetch_array($result)) {
// Do something with result
}
?>
```
这段代码看起来像是只有当你需要的时候,才通过循环从服务器取出数据。而实际上,在上面的代码中,在调用mysql_query()的时候,PHP就已经将整个结果缓存到内存中。下面的while循环只是从这个缓存中逐行取出数据,相反如果使用下面的查询,用mysql_unbuffered_query()代替mysql_query(),PHP则不会缓存结果:
```php
<?php
$link = mysql_connect('localhost', 'user', 'p4ssword');
$result = mysql_unbuffered_query('SELECT * FROM HUGE_TABLE', $link);
while($row = mysql_fetch_array($result)) {
//Do something with result
}
?>
```
不同的编程语言处理缓存的方式不同。例如,在Perl的DBD:mysql驱动中需要指定C连接库的mysql_use_result属性(默认是mysql_buffer_result)。下面是一个例子:
```perl
#!usr/bin/perl
use DBI;
my $dbh = DBI->connect('DBI:mysql:;host=localhost', 'user', 'p4ssword');
my $sth = $dbh->prepare('SELECT * FROM HUGE_TABLE', {mysql_use_result => 1});
$sth -> execute();
while (my $row = $sth->fetchrow_array()) {
# Do something with result
}
```
注意到上面的prepare()调用指定了mysql_use_result属性为1,所以应用将直接"使用"返回的结果集而不会将其缓存。也可以在连接MySQL的时候指定这个属性,这会让整个连接都使用不缓存的方式处理结果集:
```perl
my $dbh = DBI->connect('DBI:mysql:;mysql_use_result=1', 'user','p4ssword');
```
一般来说,不需要去理解MySQL通信协议的内部实现细节,只需要大致理解通信协议是如何工作的。MySQL客户端和服务器之间的通信协议是"半双工"的,这意味着,在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能同时发生。所以,我们也无法也无须将一个消息切成小块独立来发送。这种协议让MySQL通信简单快速,但是也从很多地方限制了MySQL.一个明显的限制是,这意味着没法进行流量控制。一旦一端开始发送消息,另一端要接收完整个消息才能响应它。这就像来回抛球的游戏:在任何时刻,只有一个人能控制球,而且只有控制球的人才能将球抛回去(发送消息)。一旦客户端发送了请求,它能做的事情就只是等待结果了。相反的,一般服务器响应给用户的数据通常很多,由多个数据包组成。当服务器开始响应客户端请求时,客户端必须完整接收整个返回结果,而不能简单地只取前面几条结果,然后让服务器停止发送数据。这种情况下,客户端若接收完整的结果,然后取前面几条需要的结果,或者接收完几条结果后就"粗暴"地断开连接,都不是好注意。这也是在必要的时候一定要在查询中加上LIMIT限制的原因。换一种方式解释这种行为:当客户端从服务器取数据时,看起来是一个拉数据的过程,但实际上是MySQL在向客户端推送数据的过程。客户端不断地接收从服务器推送的数据,客户端也没法让服务器停下来。客户端像是"从消防管喝水"(这是一个术语)。多数连接MySQL的库函数都可以获得全部结果集并缓存到内存力,还可以逐行获取需要的数据。默认一般是获得全部结果集并缓存到内存中。MySQL通常需要等所有的数据都已经发生给客户端才能释放这条查询所占用的资源,所以接收全部结果并缓存通常可以减少服务器的压力,让查询能够早点结束、早点释放相应的资源。
当使用多数连接MySQL的库函数从MySQL获取数据时,其结果看起来都像时从MySQL服务器获取数据,而实际上都是从这个库函数的缓存获取数据。多数情况下这没什么问题,但是如果需要返回一个很大的结果集的时候,这样做并不好,因为库函数会花很多时间和内存来存储所有的结果集。如果能够尽早开始处理这些结果集,就能大大减少内存的消耗,这种情况下可以不适用缓存来记录结果而是直接处理。这样做的缺点是,对于服务器来说,需要查询完成后才能释放资源,所以在和客户端交互的整个过程中,服务器的资源都是被这个查询搜占用的(你可以使用SQL_BUFFER_RESULT)
我们看啊可能当使用PHP的时候是什么情况。首先,下面是我们连接MySQL的通常写法:
```php
<?php
$link = mysql_connect('localhost','user','p4ssword');
$result = mysql_query('SELECT * FROM HUGE_TABLE', $link);
$while($row = mysql_fetch_array($result)) {
// Do something with result
}
?>
```
这段代码看起来像是只有当你需要的时候,才通过循环从服务器取出数据。而实际上,在上面的代码中,在调用mysql_query()的时候,PHP就已经将整个结果缓存到内存中。下面的while循环只是从这个缓存中逐行取出数据,相反如果使用下面的查询,用mysql_unbuffered_query()代替mysql_query(),PHP则不会缓存结果:
```php
<?php
$link = mysql_connect('localhost', 'user', 'p4ssword');
$result = mysql_unbuffered_query('SELECT * FROM HUGE_TABLE', $link);
while($row = mysql_fetch_array($result)) {
//Do something with result
}
?>
```
不同的编程语言处理缓存的方式不同。例如,在Perl的DBD:mysql驱动中需要指定C连接库的mysql_use_result属性(默认是mysql_buffer_result)。下面是一个例子:
```perl
#!usr/bin/perl
use DBI;
my $dbh = DBI->connect('DBI:mysql:;host=localhost', 'user', 'p4ssword');
my $sth = $dbh->prepare('SELECT * FROM HUGE_TABLE', {mysql_use_result => 1});
$sth -> execute();
while (my $row = $sth->fetchrow_array()) {
# Do something with result
}
```
注意到上面的prepare()调用指定了mysql_use_result属性为1,所以应用将直接"使用"返回的结果集而不会将其缓存。也可以在连接MySQL的时候指定这个属性,这会让整个连接都使用不缓存的方式处理结果集:
```perl
my $dbh = DBI->connect('DBI:mysql:;mysql_use_result=1', 'user','p4ssword');
```
查询状态
对于一个MySQL连接,或者说一个线程,任何时刻都有一个状态,该状态标识了MySQL当前正在做什么。有很多种方式能查看当前的状态,最简单的是使用SHOW FULL PROCESSLIST命令(该命令返回结果中的Command列就表示当前的状态)。在一个查询的生命周期中,状态会变化很多次。MySQL官方手册中对这些状态值的含义有权威的解释,下面将这些状态列出来,并做一个简单的解释.
1.Sleep
线程正在等待客户端发送新的请求
2. Query
线程正在执行查询或者正在将结果发送给客户端
3.Locked
在MySQL服务器层,该线程正在等待表锁。存储引擎级别实现的锁,例如InnoDB的行锁,并不会体现在线程状态中。对于MyISAM来说这是一个比较典型的状态,但在其他没有行锁的一你请中也会经常出现
4.Analyzing and statistics
线程正在收集存储引擎的统计洗脑洗,并生成查询的执行计划
5.Copying to tmp table [on disk]
线程正在执行查询,并且将结果集都复制到一个临时表中,这种状态一般要么是在做GROUP BY操作,要么是文件排序操作,或者UNION操作。如果这个状态后面还有"on disk"标记,那表示MySQL正在将一个内存临时表放到磁盘上
6.Sorting result
线程正在对结果集进行排序
7.Sending data
这表示多种情况:线程可能在多个状态之间传送数据,或者在生成结果集,或者在向客户端返回数据。
了解这些状态的基本含义非常有用,这可以让我们很快地了解当前"谁正在持球"。在一个繁忙的服务器上,可能会看到大量的不正常的状态。例如statstics正占用大量的时间。这通常表示,某个地方有异常了
对于一个MySQL连接,或者说一个线程,任何时刻都有一个状态,该状态标识了MySQL当前正在做什么。有很多种方式能查看当前的状态,最简单的是使用SHOW FULL PROCESSLIST命令(该命令返回结果中的Command列就表示当前的状态)。在一个查询的生命周期中,状态会变化很多次。MySQL官方手册中对这些状态值的含义有权威的解释,下面将这些状态列出来,并做一个简单的解释.
1.Sleep
线程正在等待客户端发送新的请求
2. Query
线程正在执行查询或者正在将结果发送给客户端
3.Locked
在MySQL服务器层,该线程正在等待表锁。存储引擎级别实现的锁,例如InnoDB的行锁,并不会体现在线程状态中。对于MyISAM来说这是一个比较典型的状态,但在其他没有行锁的一你请中也会经常出现
4.Analyzing and statistics
线程正在收集存储引擎的统计洗脑洗,并生成查询的执行计划
5.Copying to tmp table [on disk]
线程正在执行查询,并且将结果集都复制到一个临时表中,这种状态一般要么是在做GROUP BY操作,要么是文件排序操作,或者UNION操作。如果这个状态后面还有"on disk"标记,那表示MySQL正在将一个内存临时表放到磁盘上
6.Sorting result
线程正在对结果集进行排序
7.Sending data
这表示多种情况:线程可能在多个状态之间传送数据,或者在生成结果集,或者在向客户端返回数据。
了解这些状态的基本含义非常有用,这可以让我们很快地了解当前"谁正在持球"。在一个繁忙的服务器上,可能会看到大量的不正常的状态。例如statstics正占用大量的时间。这通常表示,某个地方有异常了
查询缓存(这里是指Query Cache).
在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找实现的。查询和缓存中的查询即使只有一个字节不同,那也不会匹配缓存结果(Percona版本的MySQL中提供了一个新的特性,可以在计算查询语句哈希值时,先将注释移除再计算哈希值,这对于不同注释的相同查询可以命中相同的查询缓存结果)。这种情况下查询就会进入下一个阶段的处理。如果当前的查询恰好命中了查询缓存,那么再返回查询结果之前MySQL会检查一次用户权限。这仍然时无须解析查询SQL语句的,因为在查询缓存中已经存放了当前查询需要访问的表信息。如果权限没有问题,MySQL会跳过所有其他阶段,直接从缓存中拿到结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行
在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找实现的。查询和缓存中的查询即使只有一个字节不同,那也不会匹配缓存结果(Percona版本的MySQL中提供了一个新的特性,可以在计算查询语句哈希值时,先将注释移除再计算哈希值,这对于不同注释的相同查询可以命中相同的查询缓存结果)。这种情况下查询就会进入下一个阶段的处理。如果当前的查询恰好命中了查询缓存,那么再返回查询结果之前MySQL会检查一次用户权限。这仍然时无须解析查询SQL语句的,因为在查询缓存中已经存放了当前查询需要访问的表信息。如果权限没有问题,MySQL会跳过所有其他阶段,直接从缓存中拿到结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行
查询优化处理
查询的生命周期的下一步时将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。这个过程中任何错误(例如语法错误)都可能终止查询。接下来选择性地介绍其中几个独立的部分,在实际执行中,这几部分可能一起执行也可能单独执行。目的是帮助大家理解MySQL是如何执行查询,以便写出更优秀的查询。
查询的生命周期的下一步时将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。这个过程中任何错误(例如语法错误)都可能终止查询。接下来选择性地介绍其中几个独立的部分,在实际执行中,这几部分可能一起执行也可能单独执行。目的是帮助大家理解MySQL是如何执行查询,以便写出更优秀的查询。
语法解析器和预处理
首先,MySQL通过关键字将SQL语句进行解析,并生成一棵对应的"解析树",MySQL解析器将使用MySQL语法规则验证和解析查询,例如,它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后匹配。预处理器则根据一些MySQL规则进一步检查解析树是否合法,例如,这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义,下一步预处理器会验证权限。这通常很快,除非服务器上有很多的权限配置。
首先,MySQL通过关键字将SQL语句进行解析,并生成一棵对应的"解析树",MySQL解析器将使用MySQL语法规则验证和解析查询,例如,它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后匹配。预处理器则根据一些MySQL规则进一步检查解析树是否合法,例如,这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义,下一步预处理器会验证权限。这通常很快,除非服务器上有很多的权限配置。
查询优化器
现在语法书被认为是合法的了,并且由优化器将其转化成执行计划。一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。最初,成本的最小单位是随机读取一个4K数据页的成本,后来(成本计算公式)变得更加复杂,并且引入了一些"因子"来估算某些操作的代价,如当执行一次WHERE条件比较的成本.可以通过查询当前会话的Last_query_cost的值来得知MySQL计算的当前查询的成本;
```sql
mysql> SELECT SQL_NO_CACHE COUNT(*) FROM film_actor;
+----------+
| COUNT(*) |
+----------+
| 5462 |
+----------+
1 row in set (0.07 sec)
mysql> SHOW STATUS LIKE 'Last_query_cost';
+-----------------+-------------+
| Variable_name | Value |
+-----------------+-------------+
| Last_query_cost | 1104.399000 |
+-----------------+-------------+
1 row in set (0.07 sec)
```
这个结果表示MySQL的优化器认为大概需要做1040个数据页的随机查找才能完成上面的查询。这是根据一系列的统计信息计算得来的:每个表或者索引的页面个数、索引的基数(索引中不同值得数量)、索引和数据行得长度、索引分布情况。优化器再评估成本的时候并不考虑任何层面的缓存,它假设读取任何数据都需要一次磁盘IO.有很多种原因会导致MySQL优化器选择错误的执行计划,如下所示:
1.统计信息不准确。MySQL依赖存储引擎提供的统计信息来评估成本,但是有的存储引擎提供的信息是准确的,有的偏差可能非常大。例如,InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息
2.执行计划种的成本估算不等同于实际执行的成本,所以即使统计信息准确,优化器给出的执行计划也可能不是最优的。例如有时候某个执行计划虽然需要读取更多的页面,但是它的成本却更小。因为如果这些页面都是顺序读或者这些页面已经在内存种的花,那么它的访问成本将很小。MySQL层面并不知道哪些页面在内存中、哪些在磁盘上,所以查询实际执行过程中到底需要多少次物理IO是无法得知的。
3.MySQL的最优可能和你想的最优不一样。你可能希望执行时间尽可能的短,但是MySQL只是基于其成本模型选择最优的执行计划,而有些时候这并不是最快的执行方式。所以,这里我们看到的根据执行成本来选择执行计划并不是完美的模型
4.MySQL从不考虑其他并发执行的查询,这可能会影响到当前查询的速度
5.MySQL也并不是任何时候都是基于成本的优化。有时也会给予一些固定的规则,例如,如果存在全文搜索的MATCH()子句,则在存在全文索引的时候就使用全文索引。即使有时候使用别的索引和WHERE条件可以远比这种方式要快,MySQL也仍然会使用对应的全文索引。
6.MySQL不会考虑不受其控制的操作的成本,例如执行存储过程或者用户自定义函数的成本
7.后面我们还会看到,优化器有时候无法去估算所有可能的执行计划,所以它可能错过实际上最优的执行计划
MySQL的查询优化器是一个非常复杂的部件,它使用了很多优化策略来生成一个最优的执行计划。优化策略可以简单地分为两种,一种是静态优化,一种是动态优化。静态优化可以直接对解析树进行分析,并完成优化。例如,优化器可以通过一些简单的代数变换将WHERE条件转换成另一种等价形式。静态优化不依赖于特别的数值,如WHERE条件中带入的一些常数等。静态优化在第一次完成后就一直有效,即使使用不同的参数重复执行查询也不会发生变化。可以认为这是一种"编译优化"。
相反,动态优化则和查询的上下文有关,也可能和很多其他因素有关,例如WHERE条件中的取值、索引中条目对应的数据行数等。这需要在每次查询的时候都重新评估,可以认为这是"运行时优化"
现在语法书被认为是合法的了,并且由优化器将其转化成执行计划。一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。最初,成本的最小单位是随机读取一个4K数据页的成本,后来(成本计算公式)变得更加复杂,并且引入了一些"因子"来估算某些操作的代价,如当执行一次WHERE条件比较的成本.可以通过查询当前会话的Last_query_cost的值来得知MySQL计算的当前查询的成本;
```sql
mysql> SELECT SQL_NO_CACHE COUNT(*) FROM film_actor;
+----------+
| COUNT(*) |
+----------+
| 5462 |
+----------+
1 row in set (0.07 sec)
mysql> SHOW STATUS LIKE 'Last_query_cost';
+-----------------+-------------+
| Variable_name | Value |
+-----------------+-------------+
| Last_query_cost | 1104.399000 |
+-----------------+-------------+
1 row in set (0.07 sec)
```
这个结果表示MySQL的优化器认为大概需要做1040个数据页的随机查找才能完成上面的查询。这是根据一系列的统计信息计算得来的:每个表或者索引的页面个数、索引的基数(索引中不同值得数量)、索引和数据行得长度、索引分布情况。优化器再评估成本的时候并不考虑任何层面的缓存,它假设读取任何数据都需要一次磁盘IO.有很多种原因会导致MySQL优化器选择错误的执行计划,如下所示:
1.统计信息不准确。MySQL依赖存储引擎提供的统计信息来评估成本,但是有的存储引擎提供的信息是准确的,有的偏差可能非常大。例如,InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息
2.执行计划种的成本估算不等同于实际执行的成本,所以即使统计信息准确,优化器给出的执行计划也可能不是最优的。例如有时候某个执行计划虽然需要读取更多的页面,但是它的成本却更小。因为如果这些页面都是顺序读或者这些页面已经在内存种的花,那么它的访问成本将很小。MySQL层面并不知道哪些页面在内存中、哪些在磁盘上,所以查询实际执行过程中到底需要多少次物理IO是无法得知的。
3.MySQL的最优可能和你想的最优不一样。你可能希望执行时间尽可能的短,但是MySQL只是基于其成本模型选择最优的执行计划,而有些时候这并不是最快的执行方式。所以,这里我们看到的根据执行成本来选择执行计划并不是完美的模型
4.MySQL从不考虑其他并发执行的查询,这可能会影响到当前查询的速度
5.MySQL也并不是任何时候都是基于成本的优化。有时也会给予一些固定的规则,例如,如果存在全文搜索的MATCH()子句,则在存在全文索引的时候就使用全文索引。即使有时候使用别的索引和WHERE条件可以远比这种方式要快,MySQL也仍然会使用对应的全文索引。
6.MySQL不会考虑不受其控制的操作的成本,例如执行存储过程或者用户自定义函数的成本
7.后面我们还会看到,优化器有时候无法去估算所有可能的执行计划,所以它可能错过实际上最优的执行计划
MySQL的查询优化器是一个非常复杂的部件,它使用了很多优化策略来生成一个最优的执行计划。优化策略可以简单地分为两种,一种是静态优化,一种是动态优化。静态优化可以直接对解析树进行分析,并完成优化。例如,优化器可以通过一些简单的代数变换将WHERE条件转换成另一种等价形式。静态优化不依赖于特别的数值,如WHERE条件中带入的一些常数等。静态优化在第一次完成后就一直有效,即使使用不同的参数重复执行查询也不会发生变化。可以认为这是一种"编译优化"。
相反,动态优化则和查询的上下文有关,也可能和很多其他因素有关,例如WHERE条件中的取值、索引中条目对应的数据行数等。这需要在每次查询的时候都重新评估,可以认为这是"运行时优化"
下面是一些MySQL能够处理的优化类型:
1.重新定义关联表的顺序
数据表的关联并不总是按照在查询中指定的顺序进行。决定关联的顺序是优化器很重要的一部分功能
2.将外连接转化为内连接(外连接:左、右连接)
并不是所有的OUTER JOIN语句都必须以外连接的方式执行。诸多因素,例如WHERE条件、库表结构都可能会让外连接等价于一个内连接。MySQL能够识别这点并重写查询,让其可以调整关联顺序
3.使用等价变化规则
MySQL可以使用一些等价变化来简化并规范表达式。它可以合并和减少一些比较,还可以移除一些恒成立和一些恒不成立的判断。例如(5=5 AND a > 5) 将被改写为a>5。类似的,如果有(a<b AND b = c)AND a =5则会改写为 b>5 AND b =c AND a =5.这些规则对于我们编写条件语句很有用
4.优化COUNT()、MIN()和MAX()
索引和列是否可为空通常可以帮助MySQL优化这类表达式。例如,要找到某一列的最小值,只需要查询对应B-Tree索引最左端的记录,MySQL可以直接获取索引的第一行记录。在优化器生成执行计划的时候可以利用这一点,在B-Tree索引中,优化器会将这个表达式作为一个常数对待。类似的,如果要查找一个最大值,也只需读取B-Tree索引的最后一条记录。如果MySQL使用了这种类型的优化,那么在EXPLAIN中就可以看到"Select tables optimized away".从字面意思可以看出,它表示优化器已经从执行计划中移除了该表,并以一个常数取而代之。类似的,没有任何WHERE条件的COUNT(*)查询通常也可以使用存储引擎提供的一些优化(例如,MyISAM维护了一个变量来存放数据表的行数)
5.预估并转化为常数表达式
当MySQL检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行优化处理。例如,一个用户自定义变量在查询中没有发生变化时就可以转换为一个常数。数学表达式则是另外一种典型的例子。让人惊讶的是,在优化阶段,有时候甚至一个查询也能够转化为一个常数。一个例子是在索引列上执行MIN()函数。甚至是主键或者唯一键查找语句也可以转换为常数表达式。如果WHEREE子句中使用了该类索引的常数条件,MySQL可以在查询开始阶段就先查找这些值,这样优化器就能够直到并转换为常数表达式,下面是一个例子:
```sql
mysql> EXPLAIN SELECT film.film_id, film_actor.actor_id
FROM film
INNER JOIN film_actor USING(film_id)
WHERE film_id=1;
+----+-------------+------------+------------+-------+----------------+----------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+----------------+----------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | film | NULL | const | PRIMARY | PRIMARY | 2 | const | 1 | 100.00 | Using index |
| 1 | SIMPLE | film_actor | NULL | ref | idx_fk_film_id | idx_fk_film_id | 2 | const | 10 | 100.00 | Using index |
+----+-------------+------------+------------+-------+----------------+----------------+---------+-------+------+----------+-------------+
2 rows in set (0.04 sec)
```
MySQL分成两步来执行这个查询,也就是上面执行计划的两行输出。第一步先从film表找到需要的行。因为在film_id字段上有主键索引,所以MySQL优化器直到这只会返回一行数据,优化器在生成执行计划的时候,就已经通过索引信息直到将返回多少行数据。因为优化器已经明确直到有多少个值(WHERE条件中的值)需要做索引查询,所以这里的表访问类型是const.
在执行计划的第二步,MySQL将第一步返回的film_id列当作一个已知取值的列来处理。因为优化器清除在第一步执行完成后,该值就会是明确的了。注意到正如第一步中一样,使用film_actor字段对表的访问类型也是const.另一种会看到常数条件的情况是通过等式将常数值从一个表传到另一个表,这可以通过WHERE、USING或者ON语句来限制某列取值为常数。在上面的例子中,因为使用了USING子句,优化器直到这也限制了film_id在整个查询中都始终是一个常量——因为它必须等于WHERE子句中的那个值
1.重新定义关联表的顺序
数据表的关联并不总是按照在查询中指定的顺序进行。决定关联的顺序是优化器很重要的一部分功能
2.将外连接转化为内连接(外连接:左、右连接)
并不是所有的OUTER JOIN语句都必须以外连接的方式执行。诸多因素,例如WHERE条件、库表结构都可能会让外连接等价于一个内连接。MySQL能够识别这点并重写查询,让其可以调整关联顺序
3.使用等价变化规则
MySQL可以使用一些等价变化来简化并规范表达式。它可以合并和减少一些比较,还可以移除一些恒成立和一些恒不成立的判断。例如(5=5 AND a > 5) 将被改写为a>5。类似的,如果有(a<b AND b = c)AND a =5则会改写为 b>5 AND b =c AND a =5.这些规则对于我们编写条件语句很有用
4.优化COUNT()、MIN()和MAX()
索引和列是否可为空通常可以帮助MySQL优化这类表达式。例如,要找到某一列的最小值,只需要查询对应B-Tree索引最左端的记录,MySQL可以直接获取索引的第一行记录。在优化器生成执行计划的时候可以利用这一点,在B-Tree索引中,优化器会将这个表达式作为一个常数对待。类似的,如果要查找一个最大值,也只需读取B-Tree索引的最后一条记录。如果MySQL使用了这种类型的优化,那么在EXPLAIN中就可以看到"Select tables optimized away".从字面意思可以看出,它表示优化器已经从执行计划中移除了该表,并以一个常数取而代之。类似的,没有任何WHERE条件的COUNT(*)查询通常也可以使用存储引擎提供的一些优化(例如,MyISAM维护了一个变量来存放数据表的行数)
5.预估并转化为常数表达式
当MySQL检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行优化处理。例如,一个用户自定义变量在查询中没有发生变化时就可以转换为一个常数。数学表达式则是另外一种典型的例子。让人惊讶的是,在优化阶段,有时候甚至一个查询也能够转化为一个常数。一个例子是在索引列上执行MIN()函数。甚至是主键或者唯一键查找语句也可以转换为常数表达式。如果WHEREE子句中使用了该类索引的常数条件,MySQL可以在查询开始阶段就先查找这些值,这样优化器就能够直到并转换为常数表达式,下面是一个例子:
```sql
mysql> EXPLAIN SELECT film.film_id, film_actor.actor_id
FROM film
INNER JOIN film_actor USING(film_id)
WHERE film_id=1;
+----+-------------+------------+------------+-------+----------------+----------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+----------------+----------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | film | NULL | const | PRIMARY | PRIMARY | 2 | const | 1 | 100.00 | Using index |
| 1 | SIMPLE | film_actor | NULL | ref | idx_fk_film_id | idx_fk_film_id | 2 | const | 10 | 100.00 | Using index |
+----+-------------+------------+------------+-------+----------------+----------------+---------+-------+------+----------+-------------+
2 rows in set (0.04 sec)
```
MySQL分成两步来执行这个查询,也就是上面执行计划的两行输出。第一步先从film表找到需要的行。因为在film_id字段上有主键索引,所以MySQL优化器直到这只会返回一行数据,优化器在生成执行计划的时候,就已经通过索引信息直到将返回多少行数据。因为优化器已经明确直到有多少个值(WHERE条件中的值)需要做索引查询,所以这里的表访问类型是const.
在执行计划的第二步,MySQL将第一步返回的film_id列当作一个已知取值的列来处理。因为优化器清除在第一步执行完成后,该值就会是明确的了。注意到正如第一步中一样,使用film_actor字段对表的访问类型也是const.另一种会看到常数条件的情况是通过等式将常数值从一个表传到另一个表,这可以通过WHERE、USING或者ON语句来限制某列取值为常数。在上面的例子中,因为使用了USING子句,优化器直到这也限制了film_id在整个查询中都始终是一个常量——因为它必须等于WHERE子句中的那个值
6.覆盖索引扫描
当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据,而无须查询对应的数据行
7.子查询优化
MySQL在某些情况下可以将子查询转换一种效率更高的形式,从而减少多个查询多次对数据进行访问
8.提前终止查询
在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。一个典型的例子就是当使用了LIMIT子句的时候,除此之外,MySQL还有几类情况也会提前终止查询,例如发现了一个不成立的条件,这时MySQL可以立刻返回一个空结果。从下面的例子可以看到这一点:
```sql
mysql> EXPLAIN SELECT film.film_id FROM sakila.film WHERE film_id = -1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | no matching row in const table |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
1 row in set (0.05 sec)
```
从这个例子看到查询在优化阶段就已经终止。除此之外,MySQL在执行过程中,如果发现某些特殊的条件,则会提前终止查询。当存储引擎需要检索"不同取值"或者判断存在性的时候,MySQL都可以使用这类优化。例如,我们现在需要找到没有演员的所有电影
```sql
mysql> SELECT film.film_id
-> FROM sakila.film
-> LEFT OUTER JOIN sakila.film_actor USING(film_id)
-> WHERE film_actor.film_id IS NULL;
+---------+
| film_id |
+---------+
| 257 |
| 323 |
| 803 |
+---------+
3 rows in set (0.05 sec)
```
这个查询将会过滤掉所有有演员的电影。每一部电影可能会有很多的演员,但是上面的查询一旦找到任何一个,就会停止并立刻判断下一部电影,因为只要有一名演员,那么WHERE条件则会过滤掉这类电影。类似这种"不同值/不存在"的优化一般可用于DISTINCT、NOT EXIST()或者LEFT JOIN类型的查询
当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据,而无须查询对应的数据行
7.子查询优化
MySQL在某些情况下可以将子查询转换一种效率更高的形式,从而减少多个查询多次对数据进行访问
8.提前终止查询
在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。一个典型的例子就是当使用了LIMIT子句的时候,除此之外,MySQL还有几类情况也会提前终止查询,例如发现了一个不成立的条件,这时MySQL可以立刻返回一个空结果。从下面的例子可以看到这一点:
```sql
mysql> EXPLAIN SELECT film.film_id FROM sakila.film WHERE film_id = -1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | no matching row in const table |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
1 row in set (0.05 sec)
```
从这个例子看到查询在优化阶段就已经终止。除此之外,MySQL在执行过程中,如果发现某些特殊的条件,则会提前终止查询。当存储引擎需要检索"不同取值"或者判断存在性的时候,MySQL都可以使用这类优化。例如,我们现在需要找到没有演员的所有电影
```sql
mysql> SELECT film.film_id
-> FROM sakila.film
-> LEFT OUTER JOIN sakila.film_actor USING(film_id)
-> WHERE film_actor.film_id IS NULL;
+---------+
| film_id |
+---------+
| 257 |
| 323 |
| 803 |
+---------+
3 rows in set (0.05 sec)
```
这个查询将会过滤掉所有有演员的电影。每一部电影可能会有很多的演员,但是上面的查询一旦找到任何一个,就会停止并立刻判断下一部电影,因为只要有一名演员,那么WHERE条件则会过滤掉这类电影。类似这种"不同值/不存在"的优化一般可用于DISTINCT、NOT EXIST()或者LEFT JOIN类型的查询
9.等值传播
如果两个列的值通过等式关联,那么MySQL能够把其中一个列的WHERE条件传递到另一列上。例如,我们看下面的查询:
```sql
mysql> SELECT film.film_id FROM film
-> INNER JOIN film_actor USING(film_id)
-> WHERE film_id > 500;
```
因为这里使用了film_id字段进行等值关联,MySQL知道这里的WHERE子句不仅适用于film表,而且对于film_actor表同样适用。如果适用的是其他的数据库管理系统,可能还需要手动通过一些条件来告知优化器这个WHERE条件适用于两个表,那么写法就会如下:
```sql
... WHERE film.film_id > 500 AND film_actor.film_id > 500
```
在MySQL中这是不必要的,这样写反而会让查询更难维护。
10.列表IN()的比较
在很多数据库系统中,IN()完全等同于多个OR条件的子句,因为这两者是完全等价的。在MySQL中这点是不成立的,MySQL将IN()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个O(logn)复杂度的操作,等价地转换成OR查询的复杂度为O(n),对于IN()列表中有大量取值的时候,MySQL的处理速度将会更快。
上面列举的远不是MySQL优化器的全部,MySQL还会做大量其他的优化。上面的例子已经足以说明优化器的复杂性和智能性了。如果说从上面的讨论中应该学到什么,那就是"不要自以为比优化器更聪明"。最终你可能会占点便宜,但是更有可能会使查询变得更加复杂而难以维护,而最终的收益却未零。让优化器按照它的方式工作就可以了。当然,虽然优化器已经很智能了,但是有时候也无法给出最优的结果。有时候你可能比优化器更了解数据,例如,由于应用逻辑使得某些条件总是成立;还有时,优化器缺少某种功能特性,如哈希索引;再如前面提到的,从优化器的执行成本角度评估出来的最优执行计划,实际运行中可能比其他的执行计划更慢。如果能够确认优化器给出的不是最佳选择,并且清楚背后的原理,那么也可以帮助优化器做进一步的优化。例如,可以在查询中添加hint提示,也可以重写查询,或者重新设计更优的库表结构,或者添加更合适的索引
如果两个列的值通过等式关联,那么MySQL能够把其中一个列的WHERE条件传递到另一列上。例如,我们看下面的查询:
```sql
mysql> SELECT film.film_id FROM film
-> INNER JOIN film_actor USING(film_id)
-> WHERE film_id > 500;
```
因为这里使用了film_id字段进行等值关联,MySQL知道这里的WHERE子句不仅适用于film表,而且对于film_actor表同样适用。如果适用的是其他的数据库管理系统,可能还需要手动通过一些条件来告知优化器这个WHERE条件适用于两个表,那么写法就会如下:
```sql
... WHERE film.film_id > 500 AND film_actor.film_id > 500
```
在MySQL中这是不必要的,这样写反而会让查询更难维护。
10.列表IN()的比较
在很多数据库系统中,IN()完全等同于多个OR条件的子句,因为这两者是完全等价的。在MySQL中这点是不成立的,MySQL将IN()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个O(logn)复杂度的操作,等价地转换成OR查询的复杂度为O(n),对于IN()列表中有大量取值的时候,MySQL的处理速度将会更快。
上面列举的远不是MySQL优化器的全部,MySQL还会做大量其他的优化。上面的例子已经足以说明优化器的复杂性和智能性了。如果说从上面的讨论中应该学到什么,那就是"不要自以为比优化器更聪明"。最终你可能会占点便宜,但是更有可能会使查询变得更加复杂而难以维护,而最终的收益却未零。让优化器按照它的方式工作就可以了。当然,虽然优化器已经很智能了,但是有时候也无法给出最优的结果。有时候你可能比优化器更了解数据,例如,由于应用逻辑使得某些条件总是成立;还有时,优化器缺少某种功能特性,如哈希索引;再如前面提到的,从优化器的执行成本角度评估出来的最优执行计划,实际运行中可能比其他的执行计划更慢。如果能够确认优化器给出的不是最佳选择,并且清楚背后的原理,那么也可以帮助优化器做进一步的优化。例如,可以在查询中添加hint提示,也可以重写查询,或者重新设计更优的库表结构,或者添加更合适的索引
数据和索引的统计信息。
重新回忆一下MySQL的架构,MySQL架构由多个层次组成。在服务器层有查询优化器,却没有保存数据和索引的统计信息。统计信息由存储引擎实现,不同的存储引擎可能会存储不同的统计信息(也可以按照不同的格式存储统计信息)。某些引擎,例如Archive引擎,则根本没有存储任何统计信息。因为服务器层没有任务统计信息,所以MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息,包括:每个表或者索引有多少个页面、每个表的每个索引的基数是多少、数据行和索引长度、索引的分布信息等。优化器根据这些信息来选择一个最优的执行计划。
重新回忆一下MySQL的架构,MySQL架构由多个层次组成。在服务器层有查询优化器,却没有保存数据和索引的统计信息。统计信息由存储引擎实现,不同的存储引擎可能会存储不同的统计信息(也可以按照不同的格式存储统计信息)。某些引擎,例如Archive引擎,则根本没有存储任何统计信息。因为服务器层没有任务统计信息,所以MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息,包括:每个表或者索引有多少个页面、每个表的每个索引的基数是多少、数据行和索引长度、索引的分布信息等。优化器根据这些信息来选择一个最优的执行计划。
MySQL如何执行关联查询。
MySQL中"关联"(join)一次所包含的意义比一般意义上理解的要更广泛。总的来说,MySQL认为任何一个查询都是一次"关联"——并不仅仅是一个查询需要到两个表匹配才叫关联,所以在MySQL中,每一个查询,每一个片段(包括子查询,甚至基于单表的SELECT)都可能是关联。所以,理解MySQL如何执行关联查询至关重要。我们先来看一个UNION查询的例子,对于UNION查询,MySQL先将一系列的单个查询结果放到一个临时表中,然后再重新读出临时表数据来完成UNION查询。在MySQL的概念中,每个查询都是一次关联,所以读取结果临时表也是一次关联。当前MySQL关联执行的策略很简单:MySQL对任何关联都执行嵌套循环关联操作,即MySQL先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,知道找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。MySQL会尝试在最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行以后,MySQL返回上一层次关联表,看是否能够找到更多的匹配记录,依此类推迭代执行。按照这样的方式查找第一个表记录,再嵌套查询下一个关联表,然后回溯到上一个表,在MySQL中是通过嵌套循环的方式实现——正如其名"嵌套循环关联"。请看下面的例子中的简单查询:
```sql
SELECT
tbl1.col1,
tbl2.col2
FROM
tbl1
INNER JOIN tbl2 USING ( col3 )
WHERE
tbl1.col1 IN (5,6)
```
假设MySQL按照查询中的表顺序进行关联,我们则可以用下面的伪代码表示MySQL将如何完成这个查询:
```sql
outer_iter = iterator over tbl1 where col1 IN (5,6)
outer_row = outer_iter.next
while outer_row
inner_iter = iterator over tbl2 where col3 = outer_row.col3
inner_row = inner_iter.next
while inner_now
output [outer_row.col1, inner_row.col2]
inner_row = inner_iter.next
end
outer_row = outer_iter.next
end
```
MySQL中"关联"(join)一次所包含的意义比一般意义上理解的要更广泛。总的来说,MySQL认为任何一个查询都是一次"关联"——并不仅仅是一个查询需要到两个表匹配才叫关联,所以在MySQL中,每一个查询,每一个片段(包括子查询,甚至基于单表的SELECT)都可能是关联。所以,理解MySQL如何执行关联查询至关重要。我们先来看一个UNION查询的例子,对于UNION查询,MySQL先将一系列的单个查询结果放到一个临时表中,然后再重新读出临时表数据来完成UNION查询。在MySQL的概念中,每个查询都是一次关联,所以读取结果临时表也是一次关联。当前MySQL关联执行的策略很简单:MySQL对任何关联都执行嵌套循环关联操作,即MySQL先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,知道找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。MySQL会尝试在最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行以后,MySQL返回上一层次关联表,看是否能够找到更多的匹配记录,依此类推迭代执行。按照这样的方式查找第一个表记录,再嵌套查询下一个关联表,然后回溯到上一个表,在MySQL中是通过嵌套循环的方式实现——正如其名"嵌套循环关联"。请看下面的例子中的简单查询:
```sql
SELECT
tbl1.col1,
tbl2.col2
FROM
tbl1
INNER JOIN tbl2 USING ( col3 )
WHERE
tbl1.col1 IN (5,6)
```
假设MySQL按照查询中的表顺序进行关联,我们则可以用下面的伪代码表示MySQL将如何完成这个查询:
```sql
outer_iter = iterator over tbl1 where col1 IN (5,6)
outer_row = outer_iter.next
while outer_row
inner_iter = iterator over tbl2 where col3 = outer_row.col3
inner_row = inner_iter.next
while inner_now
output [outer_row.col1, inner_row.col2]
inner_row = inner_iter.next
end
outer_row = outer_iter.next
end
```
上面的执行计划对于单表查询和多表关联查询都适用,如果是一个单表查询,那么只需完成上面外层的基本操作。对于外连接上面的执行过程仍然适用。例如,我们将上面查询修改如下:
```sql
SELECT
tbl1.col1,
tbl2.col2
FROM
tbl1
LEFT OUTER JOIN tbl2 USING ( col3 )
WHERE
tbl1.col1 IN (5,6)
```
对应的伪代码如下,
```sql
outer_iter = iterator over tbl1 where col1 IN (5,6)
outer_row = outer_iter.next
while outer_row
inner_iter = iterator over tbl2 where col3 = outer_row.col3
inner_row = inner_iter.next
if inner_row
while inner_row
output [outer_row.col1, inner_row.col2]
inner_row = inner_iter.next
end
else
output [outer_row.col1, NULL]
end
outer_row = outer_iter.next
end
```
```sql
SELECT
tbl1.col1,
tbl2.col2
FROM
tbl1
LEFT OUTER JOIN tbl2 USING ( col3 )
WHERE
tbl1.col1 IN (5,6)
```
对应的伪代码如下,
```sql
outer_iter = iterator over tbl1 where col1 IN (5,6)
outer_row = outer_iter.next
while outer_row
inner_iter = iterator over tbl2 where col3 = outer_row.col3
inner_row = inner_iter.next
if inner_row
while inner_row
output [outer_row.col1, inner_row.col2]
inner_row = inner_iter.next
end
else
output [outer_row.col1, NULL]
end
outer_row = outer_iter.next
end
```
另一种可视化查询执行计划的方法是根据优化器执行的路径绘制出对应的"泳道图"。如图所示,绘制了前面示例中内连接的泳道图。如图所示,从本质上说,MySQL对所有的类型的查询都以同样的方式运行。例如,MySQL在FROM子句中遇到子查询时,先执行子查询并将其结果放到一个临时表中(MySQL的临时表是没有任何索引的,在编写复杂的子查询和关联查询的时候需要注意这一点。这一点对UNION查询也一样)。然后将这个临时表当作一个普通表对待(正如其名"派生表")。MySQL在执行UNION查询时也使用类似的临时表,在遇到右外连接的时候,MySQL将其改写成等价的左外连接。简而言之,当前版本的MySQL会将所有的查询类型都转换成类似的执行计划。不过,不是所有的查询都可以转换成上面的形式。例如,全外连接就无法通过嵌套玄幻和回溯的方式完成,这是当发现关联表中没有找到任何匹配行的时候,则可能是因为关联是恰好从一个没有任何匹配的表开始。这大概也是MySQL并不支持全外连接的原因,还有些场景,虽然可以转换成嵌套循环的方式,但是效率却非常差。
执行计划。
和很多其他关系数据库不同,MySQL并不会生成查询字节码执行查询。MySQL生成查询的一棵指令书,然后通过存储引擎执行完成这棵指令树并返回结果。最终的执行计划包含了重构查询的全部信息。如果对某个查询执行EXPLAIN EXTENDED后,再执行SHOW WARNINGS,就可以看到重构出的查询。(MySQL根据执行计划生成输出。这和原查询有完全相同的语义,但是查询语句可能并不完全相同)。任何多表查询都可以使用一棵树表示,例如,可以按照如图所示执行一个四表的关联操作.在计算机科学中,这被成为一棵平衡树。但是,这并不是MySQL执行查询的方式。正如前面提到的,MySQL总是会从一个表开始一直嵌套循环、回溯完成所有表关联。所以,MySQL的执行计划总是如图所示,是一棵左侧深度优先的树
和很多其他关系数据库不同,MySQL并不会生成查询字节码执行查询。MySQL生成查询的一棵指令书,然后通过存储引擎执行完成这棵指令树并返回结果。最终的执行计划包含了重构查询的全部信息。如果对某个查询执行EXPLAIN EXTENDED后,再执行SHOW WARNINGS,就可以看到重构出的查询。(MySQL根据执行计划生成输出。这和原查询有完全相同的语义,但是查询语句可能并不完全相同)。任何多表查询都可以使用一棵树表示,例如,可以按照如图所示执行一个四表的关联操作.在计算机科学中,这被成为一棵平衡树。但是,这并不是MySQL执行查询的方式。正如前面提到的,MySQL总是会从一个表开始一直嵌套循环、回溯完成所有表关联。所以,MySQL的执行计划总是如图所示,是一棵左侧深度优先的树
关联查询优化器。
MySQL有优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序。通常多表关联的时候,可以有多种不同的关联顺序来获得相同的执行结果。关联查询优化器则通过评估不同顺序时的成本来选择一个代价最小的关联顺序。下面的查询可以通过不同顺序的关联最后都获得相同的结果:
```sql
SELECT
film.film_id,
film.title,
film.release_year,
actor.actor_id,
actor.first_name,
actor.last_name
FROM
sakila.film
INNER JOIN sakila.film_actor USING ( film_id )
INNER JOIN sakila.actor USING ( actor_id );
```
容易看出,可以通过一些不同的执行计划来完成上面的查询。例如,MySQL可以从film表开始,使用film_actor表的索引film_id来查找对应的actor_id值,然后再根据actor表的主键找到对应的记录。Oracle用户会用下面的术语描述:"film表作为驱动表先查找film_actor表,然后以此结果为驱动表再查找actor表"。这样做效率应该会不错,我们再使用EXPLAIN 看看MySQL将如何执行这个查询:
```sql
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 200
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: PRIMARY,idx_fk_film_id
key: PRIMARY
key_len: 2
ref: sakila.actor.actor_id
rows: 27
filtered: 100.00
Extra: Using index
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.film_actor.film_id
rows: 1
filtered: 100.00
Extra: NULL
3 rows in set, 1 warning (0.00 sec)
```
MySQL有优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序。通常多表关联的时候,可以有多种不同的关联顺序来获得相同的执行结果。关联查询优化器则通过评估不同顺序时的成本来选择一个代价最小的关联顺序。下面的查询可以通过不同顺序的关联最后都获得相同的结果:
```sql
SELECT
film.film_id,
film.title,
film.release_year,
actor.actor_id,
actor.first_name,
actor.last_name
FROM
sakila.film
INNER JOIN sakila.film_actor USING ( film_id )
INNER JOIN sakila.actor USING ( actor_id );
```
容易看出,可以通过一些不同的执行计划来完成上面的查询。例如,MySQL可以从film表开始,使用film_actor表的索引film_id来查找对应的actor_id值,然后再根据actor表的主键找到对应的记录。Oracle用户会用下面的术语描述:"film表作为驱动表先查找film_actor表,然后以此结果为驱动表再查找actor表"。这样做效率应该会不错,我们再使用EXPLAIN 看看MySQL将如何执行这个查询:
```sql
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 200
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: PRIMARY,idx_fk_film_id
key: PRIMARY
key_len: 2
ref: sakila.actor.actor_id
rows: 27
filtered: 100.00
Extra: Using index
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.film_actor.film_id
rows: 1
filtered: 100.00
Extra: NULL
3 rows in set, 1 warning (0.00 sec)
```
这和我们前面给出的执行计划完全不同。MySQL从actor表开始(我们从上面的EXPLAIN 结果的第一行输出可以看出这点)。然后与我们前面的计划按照相反的顺序进行关联。这样是否效率更高呢?我们来看看,我们先使用STRIGHT_JOIN关键字,按照我们之前的顺序执行,这里是对应的EXPLAIN输出结果:
```sql
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 1000
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: PRIMARY,idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 5
filtered: 100.00
Extra: Using index
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.film_actor.actor_id
rows: 1
filtered: 100.00
Extra: NULL
3 rows in set, 1 warning (0.00 sec)
```
```sql
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 1000
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: PRIMARY,idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 5
filtered: 100.00
Extra: Using index
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.film_actor.actor_id
rows: 1
filtered: 100.00
Extra: NULL
3 rows in set, 1 warning (0.00 sec)
```
我们来分析一下为什么MySQL会将关联顺序倒转过来:可以看到,关联顺序倒转后的第一个关联表只需要扫描很少的行数(严格来说,MySQL并不根据读取的记录来选择最优的执行计划。实际上MySQL通过预估需要读取的数据页来选择,读取的数据页越少越好。不过读取的记录数通常能够很好地反应一个查询的成本)。在两种关联顺序下,第二个和第三个关联表都是根据索引查询,速度都很快,不同地是需要扫描的索引项的数量是不同的:
1.将film表作为第一个关联表时,会找到1000条记录,然后对film_actor和actor表进行嵌套循环查询
2.如果MySQL选择首先扫描actor表,只会返回200条记录进行后面的嵌套循环查询。
换句话说,倒转的关联顺序会让查询进行跟梢的嵌套循环和回溯操作。为了验证优化器的选择是否正确,我们单独执行两个查询,并且看看对应的Last_query_cost状态值。我们看到倒转的关联顺序的预估成本为241,而原来的查询的预估成本为1154.
这个简单的例子主要想说明MySQL是如何选择合适的关联顺序来让查询执行的成本尽可能的低。重新定义关联的顺序是优化器非常重要的一部分功能。不过有的时候,优化器给出的并不是最优的关联顺序。这时可以使用STRAIGHT_JOIN关键字重写查询,让优化器按照你认为的最优的关联顺序执行——不过老实说,人的判断很难那么精准。绝大多数时候,优化器做出的选择都比普通人的判断要更准确。关联优化器会尝试在所有的关联顺序中选择一个成本最小的来生成执行计划树。如果可能,优化器会遍历每一个表然后逐个做嵌套循环计算每一棵可能的执行计划树的成本,最后返回一个最优的执行计划。
不过,糟糕的是,如果有超过N个表的关联,那么需要检查N的阶乘种关联顺序。我们称之为所有可能的执行计划的"搜索空间",搜索空间的增长速度非常快——例如,若是10个表的关联,那么共有3 628 800种不同的关联顺序!当搜索空间非常大的时候,优化器不可能注意评估每一种关联顺序的成本。这时,优化器选择使用"贪婪"搜索的方式查找"最优"的关联顺序。实际上,当需要关联的表超过optmizer_search_depth的限制的时候,就会选择"贪婪"搜索模式了。在MySQL这些年的发展过程重,优化器积累了很多"启发式"的优化策略来加速执行计划的生成。绝大多数情况下,这都是有效地,但因为不会去计算每一种关联顺序的成本,所以偶尔也会选择一个不是最优的执行计划。有时,各个查询的顺序并不能随意安排,这时关联优化器可以根据这些规则大大减少搜索空间,例如,左连接、相关子查询(后面将讨论子查询)。这是因为后面的表的查询需要依赖于前面表的查询结果。这种依赖关系通常可以帮助优化器大大减少需要扫描的的执行计划数量
1.将film表作为第一个关联表时,会找到1000条记录,然后对film_actor和actor表进行嵌套循环查询
2.如果MySQL选择首先扫描actor表,只会返回200条记录进行后面的嵌套循环查询。
换句话说,倒转的关联顺序会让查询进行跟梢的嵌套循环和回溯操作。为了验证优化器的选择是否正确,我们单独执行两个查询,并且看看对应的Last_query_cost状态值。我们看到倒转的关联顺序的预估成本为241,而原来的查询的预估成本为1154.
这个简单的例子主要想说明MySQL是如何选择合适的关联顺序来让查询执行的成本尽可能的低。重新定义关联的顺序是优化器非常重要的一部分功能。不过有的时候,优化器给出的并不是最优的关联顺序。这时可以使用STRAIGHT_JOIN关键字重写查询,让优化器按照你认为的最优的关联顺序执行——不过老实说,人的判断很难那么精准。绝大多数时候,优化器做出的选择都比普通人的判断要更准确。关联优化器会尝试在所有的关联顺序中选择一个成本最小的来生成执行计划树。如果可能,优化器会遍历每一个表然后逐个做嵌套循环计算每一棵可能的执行计划树的成本,最后返回一个最优的执行计划。
不过,糟糕的是,如果有超过N个表的关联,那么需要检查N的阶乘种关联顺序。我们称之为所有可能的执行计划的"搜索空间",搜索空间的增长速度非常快——例如,若是10个表的关联,那么共有3 628 800种不同的关联顺序!当搜索空间非常大的时候,优化器不可能注意评估每一种关联顺序的成本。这时,优化器选择使用"贪婪"搜索的方式查找"最优"的关联顺序。实际上,当需要关联的表超过optmizer_search_depth的限制的时候,就会选择"贪婪"搜索模式了。在MySQL这些年的发展过程重,优化器积累了很多"启发式"的优化策略来加速执行计划的生成。绝大多数情况下,这都是有效地,但因为不会去计算每一种关联顺序的成本,所以偶尔也会选择一个不是最优的执行计划。有时,各个查询的顺序并不能随意安排,这时关联优化器可以根据这些规则大大减少搜索空间,例如,左连接、相关子查询(后面将讨论子查询)。这是因为后面的表的查询需要依赖于前面表的查询结果。这种依赖关系通常可以帮助优化器大大减少需要扫描的的执行计划数量
排序优化。
无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。前面已经提到了,当不能使用索引生成排序结果的时候,MySQL需要自己进行排序,如果数据量小则在内存种进行,如果数据量大则需要使用磁盘,不过MySQL将这个过程统一称为文件排序(filesort),即使完全是内存排序不需要任何磁盘文件时也是如此。如果需要排序的数据量小于"排序缓冲区",MySQL使用内存进行"快速排序"操作。如果内存不够排序,那么MySQL会先将数据分块,对每个独立的块使用"快速排序"进行排序,并将各个块的排序结果存放在磁盘上,然后将各个排好序的快进行合并(merge),最后返回排序结果。MySQL有如下两种排序算法:
1.两次传输排序(旧版本使用)(也称双路排序)
读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。这需要进行两次数据传输,即需要从数据表中读取两次数据,第二次读取数据的时候,因为是读取排序列进行排序后的所有记录,这回产生大量的随机IO,所以两次数据传输的成本非常高。当使用的是MyISAM表的时候,成本可能会更高,因为MyISAM使用系统调用进行数据的读取(MyISAM非常依赖操作系统对数据的缓存)。不过这样做的优点是,在排序的时候存储尽可能少的数据,这就让"排序缓冲区"(内存)中可能容纳尽可能多的行数进行排序
2.单次传输排序(新版本使用)
先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。这个算法只在MySQL4.1和后续更新的版本中引入。因为不再需要从数据表中读取两次数据,对于IO密集型的应用,这样做的效率高了很多。另外,相比两次传输排序,这个算法只需要一次顺序IO读取所有的数据,而无须任何的随机IO.缺点是,如果需要返回的列非常多、非常大、会额外占用大量的空间,而这些列对排序操作本身来说是没有任何作用的。因为单条排序记录很大,所以可能会有更多的排序块需要合并.
很难说哪个算法效率更高,两种算法都有各自最好和最糟的场景。当查询需要所有列的总长度不超过参数max_length_for_sort_data时,MySQL使用"单次传输排序",,可以通过调整这个参数来影响MySQL排序算法的选择
MySQL在进行文件排序的时候需要使用的临时存储空间可能会比想象的要大得多。原因在于MySQL在排序时,对每一个排序记录都会分配一足够长的定长空间来存放。这个定长空间必须足够长以容纳其中最长的字符串,例如,如果是VARCHAR列则需要分配其完整长度;如果使用UTF-8字符集,那么MySQL将会为每个字符预留两个字节。曾经在一个库表结构设计不合理的案例中看到,排序消耗的临时空间比磁盘上的原表要大很多倍。
在关联查询的时候如果需要排序,MySQL会分两种情况来处理这样的文件排序。如果ORDER BY 子句中的所有列都来自关联的第一个表,那么MysQL在关联处理第一个表的时候就进行文件排序。如果是这样,那么在MySQL的EXPLAIN结果中可以看到Extra字段会有"Using filesort".除此之外的所有情况,MySQL都会先将关联的结果存放到一个临时表中,然后在所有的关联都结束后,再进行文件排序。这种情况下,在MySQL的EXPLAIN结果的Extra字段可以看到"Using temporary;Using filesort",如果查询中有LIMIT的话,LIMIT也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然会非常大。MySQL5.6在这里做了很多重要的改进。当只需要返回部分排序结果的时候,例如使用了LIMIT子句,MySQL不再对所有的结果进行排序,而是根据实际情况,选择抛弃不满足条件的结果,然后再进行排序
无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。前面已经提到了,当不能使用索引生成排序结果的时候,MySQL需要自己进行排序,如果数据量小则在内存种进行,如果数据量大则需要使用磁盘,不过MySQL将这个过程统一称为文件排序(filesort),即使完全是内存排序不需要任何磁盘文件时也是如此。如果需要排序的数据量小于"排序缓冲区",MySQL使用内存进行"快速排序"操作。如果内存不够排序,那么MySQL会先将数据分块,对每个独立的块使用"快速排序"进行排序,并将各个块的排序结果存放在磁盘上,然后将各个排好序的快进行合并(merge),最后返回排序结果。MySQL有如下两种排序算法:
1.两次传输排序(旧版本使用)(也称双路排序)
读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。这需要进行两次数据传输,即需要从数据表中读取两次数据,第二次读取数据的时候,因为是读取排序列进行排序后的所有记录,这回产生大量的随机IO,所以两次数据传输的成本非常高。当使用的是MyISAM表的时候,成本可能会更高,因为MyISAM使用系统调用进行数据的读取(MyISAM非常依赖操作系统对数据的缓存)。不过这样做的优点是,在排序的时候存储尽可能少的数据,这就让"排序缓冲区"(内存)中可能容纳尽可能多的行数进行排序
2.单次传输排序(新版本使用)
先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。这个算法只在MySQL4.1和后续更新的版本中引入。因为不再需要从数据表中读取两次数据,对于IO密集型的应用,这样做的效率高了很多。另外,相比两次传输排序,这个算法只需要一次顺序IO读取所有的数据,而无须任何的随机IO.缺点是,如果需要返回的列非常多、非常大、会额外占用大量的空间,而这些列对排序操作本身来说是没有任何作用的。因为单条排序记录很大,所以可能会有更多的排序块需要合并.
很难说哪个算法效率更高,两种算法都有各自最好和最糟的场景。当查询需要所有列的总长度不超过参数max_length_for_sort_data时,MySQL使用"单次传输排序",,可以通过调整这个参数来影响MySQL排序算法的选择
MySQL在进行文件排序的时候需要使用的临时存储空间可能会比想象的要大得多。原因在于MySQL在排序时,对每一个排序记录都会分配一足够长的定长空间来存放。这个定长空间必须足够长以容纳其中最长的字符串,例如,如果是VARCHAR列则需要分配其完整长度;如果使用UTF-8字符集,那么MySQL将会为每个字符预留两个字节。曾经在一个库表结构设计不合理的案例中看到,排序消耗的临时空间比磁盘上的原表要大很多倍。
在关联查询的时候如果需要排序,MySQL会分两种情况来处理这样的文件排序。如果ORDER BY 子句中的所有列都来自关联的第一个表,那么MysQL在关联处理第一个表的时候就进行文件排序。如果是这样,那么在MySQL的EXPLAIN结果中可以看到Extra字段会有"Using filesort".除此之外的所有情况,MySQL都会先将关联的结果存放到一个临时表中,然后在所有的关联都结束后,再进行文件排序。这种情况下,在MySQL的EXPLAIN结果的Extra字段可以看到"Using temporary;Using filesort",如果查询中有LIMIT的话,LIMIT也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然会非常大。MySQL5.6在这里做了很多重要的改进。当只需要返回部分排序结果的时候,例如使用了LIMIT子句,MySQL不再对所有的结果进行排序,而是根据实际情况,选择抛弃不满足条件的结果,然后再进行排序
查询执行引擎
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎根据这个执行计划来完成整个查询。这里执行计划是一个数据结构,而不是和其他的关系型数据库那样生成对应的字节码。相对于查询优化阶段,查询执行阶段不是那么复杂:MySQL只是简单地根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口来完成,这些接口也就是我们成为"handler API"的接口。查询中的每一个表由一个handler的实例表示。实际上,MySQL在优化阶段就为每个表创建了一个handler实例,优化器根据这些实例的接口可以获取表的相关信息,包括表的所有列名、索引统计信息,等等。
存储引擎接口有着非常丰富的功能,但是底层接口却只有几十个,这些接口像"搭积木"一样能够完成查询的大部分操作。例如,有一个查询某个索引的第一行的解耦,再有一个查询某个索引条目的下一个条目的功能,有了这两个功能我们就可以完成全索引扫描的操作了。这种简单的接口模式,让MySQL的存储引擎插件式架构成为可能,但是正如前面的讨论,也给优化器带来了一定的限制。
并不是所有的操作都有handler完成。例如,当MySQL需要进行表锁的时候,handler可能会实现自己的级别的、更细粒度的锁,如InnoDB就实现了自己的行基本锁,但这并不能代替服务器层的表锁。如果是所有存储引擎共有的特性则由服务器层实现,比如时间和日期函数、视图、触发器等等。
为了执行查询,MySQL只需要重复执行计划中的各个操作,知道完成所有的数据查询。
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎根据这个执行计划来完成整个查询。这里执行计划是一个数据结构,而不是和其他的关系型数据库那样生成对应的字节码。相对于查询优化阶段,查询执行阶段不是那么复杂:MySQL只是简单地根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口来完成,这些接口也就是我们成为"handler API"的接口。查询中的每一个表由一个handler的实例表示。实际上,MySQL在优化阶段就为每个表创建了一个handler实例,优化器根据这些实例的接口可以获取表的相关信息,包括表的所有列名、索引统计信息,等等。
存储引擎接口有着非常丰富的功能,但是底层接口却只有几十个,这些接口像"搭积木"一样能够完成查询的大部分操作。例如,有一个查询某个索引的第一行的解耦,再有一个查询某个索引条目的下一个条目的功能,有了这两个功能我们就可以完成全索引扫描的操作了。这种简单的接口模式,让MySQL的存储引擎插件式架构成为可能,但是正如前面的讨论,也给优化器带来了一定的限制。
并不是所有的操作都有handler完成。例如,当MySQL需要进行表锁的时候,handler可能会实现自己的级别的、更细粒度的锁,如InnoDB就实现了自己的行基本锁,但这并不能代替服务器层的表锁。如果是所有存储引擎共有的特性则由服务器层实现,比如时间和日期函数、视图、触发器等等。
为了执行查询,MySQL只需要重复执行计划中的各个操作,知道完成所有的数据查询。
返回结果给客户端。
查询执行的最后一个阶段是将结果返回给客户端。即使查询不需要返回结果集给客户端,MySQL仍然会返回这个查询的一些信息,如该查询影响到的行数。如果查询可以被缓存,那么MySQL在这个阶段也会将结果存放到查询缓存中。MySQL将结果集返回客户端是一个增量、逐步返回的过程。例如,我们回头看看前面的关联操作,一旦服务器处理完成最后一个关联表,开始生成第一条结果时,MySQL就可以开始向客户端逐步返回结果集了。这样处理有两个好处:服务器端无须存储太多的结果,也就不会因为要返回太多结果而消耗太多内存。另外,这样的处理也让MySQL客户端第一时间获得返回的结果。结果集中的每一行会以一个满足MySQL客户端/服务器通信协议的封包发送,再通过TCP协议进行传输,在TCP传输的过程中,可能对MySQL的风暴进行缓存然后批量传输。
查询执行的最后一个阶段是将结果返回给客户端。即使查询不需要返回结果集给客户端,MySQL仍然会返回这个查询的一些信息,如该查询影响到的行数。如果查询可以被缓存,那么MySQL在这个阶段也会将结果存放到查询缓存中。MySQL将结果集返回客户端是一个增量、逐步返回的过程。例如,我们回头看看前面的关联操作,一旦服务器处理完成最后一个关联表,开始生成第一条结果时,MySQL就可以开始向客户端逐步返回结果集了。这样处理有两个好处:服务器端无须存储太多的结果,也就不会因为要返回太多结果而消耗太多内存。另外,这样的处理也让MySQL客户端第一时间获得返回的结果。结果集中的每一行会以一个满足MySQL客户端/服务器通信协议的封包发送,再通过TCP协议进行传输,在TCP传输的过程中,可能对MySQL的风暴进行缓存然后批量传输。
MySQL查询优化器的局限性。
MySQL的万能"嵌套循环"并不是对每种查询都是最优的。不过还好,MySQL查询优化器只对少部分查询不适用,而且我们往往可以通过改写查询让MySQL高效地完成工作。还有一个好消息,MySQL5.6版本正式发布后,会消除很多MySQL原本的限制,让更多的查询能够以尽可能高的效率完成。
MySQL的万能"嵌套循环"并不是对每种查询都是最优的。不过还好,MySQL查询优化器只对少部分查询不适用,而且我们往往可以通过改写查询让MySQL高效地完成工作。还有一个好消息,MySQL5.6版本正式发布后,会消除很多MySQL原本的限制,让更多的查询能够以尽可能高的效率完成。
关联子查询。
MySQL的子查询实现得非常糟糕。最糟糕的一类查询是WHERE条件中包含IN()的子查询语句。例如,我们希望找到Sakila数据库中,演员Penelope Guinness(他的actor_id为1)参演过的所有影片信息。很自然的,我们会按照下面的方式用子查询实现:
```sql
mysql> SELECT * FROM sakila.film WHERE film_id IN(SELECT film_id FROM sakila.film_actor WHERE actor_id =1);
```
因为MySQL对IN()列表的选项有专门的优化策略,一般会认为MySQL会先执行子查询返回所有包含actor_id为1的film_id。一般来说,IN()列表查询速度很快,所以我们会认为上面的查询会这样执行:
```sql
-- SELECT GROUP_CONCAT(film_id) FROM sakila.film_actor WHERE actor_id=1;
-- Result :1,23,25,106,140,166,277,361,438,499,506,509,605,635,749,832,939,970,980
SELECT * FROM sakila.film WHERE film_id IN(1,23.....................,980);
```
很不幸,MySQL不是这样做的。MySQL会讲相关的外层表压到子查询中,它认为这样可以更高效率地查找到数据行。也就是说,MySQL会将查询改写成下面的样子:
```sql
SELECT * FROM sakila.film WHERE EXISTS (SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id)
```
这时,子查询需要根据film_id来关联外部表film,因为需要film_id字段,所以MySQL认为无法先执行这个查询。通过EXPLAIN可以看到子查询是一个相关子查询(DEPENDENT SUBQUERY)(可以使用EXPLAIN EXTENDED来查看这个查询被改写成了什么样子)
```sql
mysql> EXPLAIN SELECT * FROM sakila.film WHERE EXISTS (SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id)
-> ;
+----+--------------------+------------+------------+--------+------------------------+---------+---------+---------------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+------------+------------+--------+------------------------+---------+---------+---------------------------+------+----------+-------------+
| 1 | PRIMARY | film | NULL | ALL | NULL | NULL | NULL | NULL | 1000 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | film_actor | NULL | eq_ref | PRIMARY,idx_fk_film_id | PRIMARY | 4 | const,sakila.film.film_id | 1 | 100.00 | Using index |
+----+--------------------+------------+------------+--------+------------------------+---------+---------+---------------------------+------+----------+-------------+
2 rows in set (0.10 sec)
```
根据EXPLAIN的输出我们可以看到,MySQL先选择对flim表进行全表扫描,然后根据返回的film_id逐个进行子查询。如果是一个很小的表,这个查询的糟糕的性能可能还不会引起注意,但是如果外层的表是一个非常大的表,那么这个查询的性能会非常糟糕。当然我们很容易用下面的办法来重写这个查询:
```sql
mysql>SELECT film.* FROM sakila.film INNER JOIN sakila.film_actor USING(film_id) WHERE actor_id = 1;
```
另一个优化的办法是使用函数GROUP_CONCAT()在IN()中构造一个由逗号分割的列表,有时这比上面的使用关联改写更快。因为使用IN()加子查询,性能经常会非常糟,所以通常建议使用EXISTS()等效的改写查询来获取更好的效率。下面是另一种改写IN()加子查询的办法:
```sql
mysql>SELECT * FROM sakila.film WHERE EXISTS (SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id)
```
MySQL的子查询实现得非常糟糕。最糟糕的一类查询是WHERE条件中包含IN()的子查询语句。例如,我们希望找到Sakila数据库中,演员Penelope Guinness(他的actor_id为1)参演过的所有影片信息。很自然的,我们会按照下面的方式用子查询实现:
```sql
mysql> SELECT * FROM sakila.film WHERE film_id IN(SELECT film_id FROM sakila.film_actor WHERE actor_id =1);
```
因为MySQL对IN()列表的选项有专门的优化策略,一般会认为MySQL会先执行子查询返回所有包含actor_id为1的film_id。一般来说,IN()列表查询速度很快,所以我们会认为上面的查询会这样执行:
```sql
-- SELECT GROUP_CONCAT(film_id) FROM sakila.film_actor WHERE actor_id=1;
-- Result :1,23,25,106,140,166,277,361,438,499,506,509,605,635,749,832,939,970,980
SELECT * FROM sakila.film WHERE film_id IN(1,23.....................,980);
```
很不幸,MySQL不是这样做的。MySQL会讲相关的外层表压到子查询中,它认为这样可以更高效率地查找到数据行。也就是说,MySQL会将查询改写成下面的样子:
```sql
SELECT * FROM sakila.film WHERE EXISTS (SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id)
```
这时,子查询需要根据film_id来关联外部表film,因为需要film_id字段,所以MySQL认为无法先执行这个查询。通过EXPLAIN可以看到子查询是一个相关子查询(DEPENDENT SUBQUERY)(可以使用EXPLAIN EXTENDED来查看这个查询被改写成了什么样子)
```sql
mysql> EXPLAIN SELECT * FROM sakila.film WHERE EXISTS (SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id)
-> ;
+----+--------------------+------------+------------+--------+------------------------+---------+---------+---------------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+------------+------------+--------+------------------------+---------+---------+---------------------------+------+----------+-------------+
| 1 | PRIMARY | film | NULL | ALL | NULL | NULL | NULL | NULL | 1000 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | film_actor | NULL | eq_ref | PRIMARY,idx_fk_film_id | PRIMARY | 4 | const,sakila.film.film_id | 1 | 100.00 | Using index |
+----+--------------------+------------+------------+--------+------------------------+---------+---------+---------------------------+------+----------+-------------+
2 rows in set (0.10 sec)
```
根据EXPLAIN的输出我们可以看到,MySQL先选择对flim表进行全表扫描,然后根据返回的film_id逐个进行子查询。如果是一个很小的表,这个查询的糟糕的性能可能还不会引起注意,但是如果外层的表是一个非常大的表,那么这个查询的性能会非常糟糕。当然我们很容易用下面的办法来重写这个查询:
```sql
mysql>SELECT film.* FROM sakila.film INNER JOIN sakila.film_actor USING(film_id) WHERE actor_id = 1;
```
另一个优化的办法是使用函数GROUP_CONCAT()在IN()中构造一个由逗号分割的列表,有时这比上面的使用关联改写更快。因为使用IN()加子查询,性能经常会非常糟,所以通常建议使用EXISTS()等效的改写查询来获取更好的效率。下面是另一种改写IN()加子查询的办法:
```sql
mysql>SELECT * FROM sakila.film WHERE EXISTS (SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id)
```
如何用好关联子查询。
并不是所有关联子查询的性能都回很差。如果有人跟你说:"别用关联子查询",那么不要理他。先测试,然后做出自己的判断。很多时候关联子查询是一种非常合理、自然,甚至是性能最好的写法,看看下面的例子:
```sql
mysql> EXPLAIN SELECT film_id,language_id FROM sakila.film
-> WHERE NOT EXISTS(SELECT * FROM sakila.film_actor WHERE film_actor.film_id=film.film_id)\G
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: film
partitions: NULL
type: index
possible_keys: NULL
key: idx_fk_language_id
key_len: 1
ref: NULL
rows: 1000
filtered: 100.00
Extra: Using where; Using index
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: film_actor
partitions: NULL
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 5
filtered: 100.00
Extra: Using index
2 rows in set, 2 warnings (0.00 sec)
```
一般回建议使用左外连接(LEFT OUTER JOIN)重写该查询,以代替子查询。理论上,改写后MySQL的执行计划完全不会改变。我们来看这个例子
并不是所有关联子查询的性能都回很差。如果有人跟你说:"别用关联子查询",那么不要理他。先测试,然后做出自己的判断。很多时候关联子查询是一种非常合理、自然,甚至是性能最好的写法,看看下面的例子:
```sql
mysql> EXPLAIN SELECT film_id,language_id FROM sakila.film
-> WHERE NOT EXISTS(SELECT * FROM sakila.film_actor WHERE film_actor.film_id=film.film_id)\G
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: film
partitions: NULL
type: index
possible_keys: NULL
key: idx_fk_language_id
key_len: 1
ref: NULL
rows: 1000
filtered: 100.00
Extra: Using where; Using index
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: film_actor
partitions: NULL
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 5
filtered: 100.00
Extra: Using index
2 rows in set, 2 warnings (0.00 sec)
```
一般回建议使用左外连接(LEFT OUTER JOIN)重写该查询,以代替子查询。理论上,改写后MySQL的执行计划完全不会改变。我们来看这个例子
```sql
mysql> EXPLAIN SELECT film.film_id,film.language_id
-> FROM sakila.film
-> LEFT OUTER JOIN sakila.film_actor USING(film_id)
-> WHERE film_actor.film_id IS NULL\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: index
possible_keys: NULL
key: idx_fk_language_id
key_len: 1
ref: NULL
rows: 1000
filtered: 100.00
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 5
filtered: 100.00
Extra: Using where; Not exists; Using index
2 rows in set, 1 warning (0.00 sec)
```
可以看到,这里的执行计划基本上是一样,下面是一些微小的区别:
1.表film_actor的访问类型是一个DEPENDENT SUBQUERY,而另一个是SIMPLE.这个不同是由于语句的写法不同导致的,一个是普通查询,一个是子查询。这对底层存储引擎接口来说,没有任何不同
2.对film表,第二个查询的Extra中没有"Using where",但这并不重要,第二个查询的USING子句和第一个查询的WHERE子句实际上是完全一样的。
3.在第二个表film_actor的执行计划的Extra列有"Not exists"。这是前面提到的提前终止算法(early-termination algorithm),MySQL通过使用"Not exists"优化来避免在表film_actor的索引中读取任何额外的行。这完全等效于直接编写NOT EXISTS子查询,这个执行计划中也是一样,一旦匹配到一行数据,就立刻停止扫描
mysql> EXPLAIN SELECT film.film_id,film.language_id
-> FROM sakila.film
-> LEFT OUTER JOIN sakila.film_actor USING(film_id)
-> WHERE film_actor.film_id IS NULL\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: index
possible_keys: NULL
key: idx_fk_language_id
key_len: 1
ref: NULL
rows: 1000
filtered: 100.00
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 5
filtered: 100.00
Extra: Using where; Not exists; Using index
2 rows in set, 1 warning (0.00 sec)
```
可以看到,这里的执行计划基本上是一样,下面是一些微小的区别:
1.表film_actor的访问类型是一个DEPENDENT SUBQUERY,而另一个是SIMPLE.这个不同是由于语句的写法不同导致的,一个是普通查询,一个是子查询。这对底层存储引擎接口来说,没有任何不同
2.对film表,第二个查询的Extra中没有"Using where",但这并不重要,第二个查询的USING子句和第一个查询的WHERE子句实际上是完全一样的。
3.在第二个表film_actor的执行计划的Extra列有"Not exists"。这是前面提到的提前终止算法(early-termination algorithm),MySQL通过使用"Not exists"优化来避免在表film_actor的索引中读取任何额外的行。这完全等效于直接编写NOT EXISTS子查询,这个执行计划中也是一样,一旦匹配到一行数据,就立刻停止扫描
所以,从理论上来讲,MySQL将使用完全相同的执行计划来完成这个查询。现实世界中,建议通过一些测试来判断使用哪种写法速度会更快。针对上面的案例,测试结果也是不同的,如表所示.测试结果显示,使用子查询的写法要略微慢些!不过每个具体的案例会各有不同,有时候子查询写法也会快些。例如,当返回结果中只有一个表中的某些列的时候。听起来,这种情况对于关联查询效率也会更好。具体情况具体分析,例如下面的关联,我们希望返回所有演员参演的电影,因为一个电影会有很多演员参演,所以可能会返回一些重复的记录:
```sql
mysql> SELECT film.film_id FROM sakila.film INNER JOIN sakila.film_actor USING(film_id);
```
我们需要使用DISTINCT和GROUP BY来移除重复的记录:
```sql
mysql> SELECT DISTINCT film.film_id FROM sakila.film INNER JOIN sakila.film_actor USING(film_id);
```
但是,回头看看这个查询,到底这个查询返回的结果集意义是什么?至少这样的写法回访SQL的意义很不明显。如果使用EXISTS则很容易表达"有演员参演"的逻辑,而且不需要使用DISTINCT和GROUP BY,也不会产生重复的结果集,我们知道一旦使用了DISTINCT和GROUP BY,那么在查询的执行过程中,通常需要产生临时中间表。下面我们用子查询的写法替换上面的关联:
```sql
mysql> SELECT film_id FROM sakila.film WHERE EXISTS(SELECT * FROM sakila.film_actor WHERE film.film_id = film_actor.film_id);
```
再一次,我们需要通过测试来比对这两种写法,哪个更快一些,测试结果如表所示.在这个案例中,我们看到子查询速度要比关联查询更快些。通过上面这个案例,主要想说明两点:一时不需要听取那些关于子查询的"绝对真理",二十应该用测试来验证对子查询的执行计划和相应时间的假设。我们应该通过测试来验证猜想
```sql
mysql> SELECT film.film_id FROM sakila.film INNER JOIN sakila.film_actor USING(film_id);
```
我们需要使用DISTINCT和GROUP BY来移除重复的记录:
```sql
mysql> SELECT DISTINCT film.film_id FROM sakila.film INNER JOIN sakila.film_actor USING(film_id);
```
但是,回头看看这个查询,到底这个查询返回的结果集意义是什么?至少这样的写法回访SQL的意义很不明显。如果使用EXISTS则很容易表达"有演员参演"的逻辑,而且不需要使用DISTINCT和GROUP BY,也不会产生重复的结果集,我们知道一旦使用了DISTINCT和GROUP BY,那么在查询的执行过程中,通常需要产生临时中间表。下面我们用子查询的写法替换上面的关联:
```sql
mysql> SELECT film_id FROM sakila.film WHERE EXISTS(SELECT * FROM sakila.film_actor WHERE film.film_id = film_actor.film_id);
```
再一次,我们需要通过测试来比对这两种写法,哪个更快一些,测试结果如表所示.在这个案例中,我们看到子查询速度要比关联查询更快些。通过上面这个案例,主要想说明两点:一时不需要听取那些关于子查询的"绝对真理",二十应该用测试来验证对子查询的执行计划和相应时间的假设。我们应该通过测试来验证猜想
UNION的限制。
有时,MySQL无法将限制条件从外层"下推"到内层,这使得原本能够限制部分返回结果的条件无法应用到内层查询的优化上。如果希望UNION的各个子句能够根据LIMIT只取部分结果集,或者希望能够先排好序再合并结果集的话,就需要在UNION的各个子句中分别使用这些子句。例如,想将两个子查询结果联合起来,然后再取前20条记录,那么MySQL会将两个表都存放到同一个临时表中,然后再取出前20行记录:
```sql
(SELECT first_name,last_name FROM sakila.actor ORDER BY last_name)
UNION ALL
(SELECT first_name,last_name FROM sakila.customer ORDER BY last_name)
LIMIT 20
```
这条查询将会把actor中的200条记录和customer表中的599条记录存放在一个临时表中,然后再从临时表中取出前20条。可以通过在UNION的两个子查询中分别加上一个LIMIT 20来减少临时表中的数据:
```sql
(SELECT first_name,last_name FROM sakila.actor ORDER BY last_name LIMIT 20)
UNION ALL
(SELECT first_name,last_name FROM sakila.customer ORDER BY last_name LIMIT 20)
LIMIT 20
```
现在中间的临时表只会包含40条记录了,除了性能考虑之外,在这里还需要注意一点:从临时表中取出数据的顺序并不是一定的,所以如果想获得正确的顺序,还需要加上一个全局的ORDER BY 和LIMIT操作
有时,MySQL无法将限制条件从外层"下推"到内层,这使得原本能够限制部分返回结果的条件无法应用到内层查询的优化上。如果希望UNION的各个子句能够根据LIMIT只取部分结果集,或者希望能够先排好序再合并结果集的话,就需要在UNION的各个子句中分别使用这些子句。例如,想将两个子查询结果联合起来,然后再取前20条记录,那么MySQL会将两个表都存放到同一个临时表中,然后再取出前20行记录:
```sql
(SELECT first_name,last_name FROM sakila.actor ORDER BY last_name)
UNION ALL
(SELECT first_name,last_name FROM sakila.customer ORDER BY last_name)
LIMIT 20
```
这条查询将会把actor中的200条记录和customer表中的599条记录存放在一个临时表中,然后再从临时表中取出前20条。可以通过在UNION的两个子查询中分别加上一个LIMIT 20来减少临时表中的数据:
```sql
(SELECT first_name,last_name FROM sakila.actor ORDER BY last_name LIMIT 20)
UNION ALL
(SELECT first_name,last_name FROM sakila.customer ORDER BY last_name LIMIT 20)
LIMIT 20
```
现在中间的临时表只会包含40条记录了,除了性能考虑之外,在这里还需要注意一点:从临时表中取出数据的顺序并不是一定的,所以如果想获得正确的顺序,还需要加上一个全局的ORDER BY 和LIMIT操作
索引合并优化。
前面已经讨论过,在5.0和更新的版本中,当WHERE子句中包含多个复杂条件的时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行(索引下推)
前面已经讨论过,在5.0和更新的版本中,当WHERE子句中包含多个复杂条件的时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行(索引下推)
等值传递。
某些时候,等值传递会带来一些意想不到的额外消耗。例如,有一个非常大的IN()列表,而MySQL优化器发现存在WHERE、ON或者USING的子句,将这个人列表值和另一个表的某些列相关联。那么优化器会将IN()列表都复制应用到关联的各个表中。通常,因为各个表新增了过滤条件,优化器可更高效地从存储引擎过滤记录,但是如果这个列表非常大,则会导致优化和执行都会变慢。
某些时候,等值传递会带来一些意想不到的额外消耗。例如,有一个非常大的IN()列表,而MySQL优化器发现存在WHERE、ON或者USING的子句,将这个人列表值和另一个表的某些列相关联。那么优化器会将IN()列表都复制应用到关联的各个表中。通常,因为各个表新增了过滤条件,优化器可更高效地从存储引擎过滤记录,但是如果这个列表非常大,则会导致优化和执行都会变慢。
并行执行。
MySQL无法利用多核特性来并行执行查询。很多其他的关系型数据库能够提供这个特性,但是MySQL做不到。这里特别指出是想说不要花时间取尝试寻找并行执行查询的方法。MySQL在处理OLTP场景下的短查询效果很好,但对于复杂大查询则能力有效。最直接一点就是,对于一个SQL语句,MySQL最多只能使用一个CPU核来处理,在这种场景下无法发挥主机CPU多核的能力。MySQL没有停滞不前,一直在发展,新推出的MySQL8.0.14版本第一次引入了并行查询特性,使得check table和select count(*)类型的语句性能成倍提升。虽然目前使用场景还比较优先,但后续的发展值得期待。
1.使用方式
通过配置参数innodb_parallel_read_threads来设置并发线程数,就能开始并行扫描功能,默认这个值为4.有个简单的实验,通过sysbench导入2亿条数据,分别配置innodb_parallel_read_threads为1,2,4,8,16,32,64,测试并行执行的效果。测试语句为SELECT COUNT(*) FROM sbtest1;
如图所示。横轴是配置并发线程数,纵轴是语句执行时间。从测试结果来看,整个并行表现还是不错的,扫描2亿条记录,从单线程的18s,下降到32线程的1s。后面并发开再多,由于数量优先,多线程的管理消耗超过了并发带来的性能提升,不能再继续缩短SQL执行时间
2.MySQL并行执行
实际上MySQL的并行执行还处于非常初级阶段,如下图所示,左边是之前MySQL串行处理单个SQL形态;中间的是目前MySQL版本提供的并行能力,InnoDB引擎并行扫描的形态;最右边的是未来MySQL要发展的形态,优化器根据系统负载和SQL生成并行计划,并将分区计划下发给执行器并行执行。并行执行不仅仅是并行扫描,以及并行排序等。目前版本MySQL的上层优化器以及执行器并没有配套的修改。InnoDB的并行扫描主要应用在了分区,并行扫描,预读,以及与执行器交互的适配器类(详细分析请见https://www.cnblogs.com/cchust/p/12347166.html)
MySQL无法利用多核特性来并行执行查询。很多其他的关系型数据库能够提供这个特性,但是MySQL做不到。这里特别指出是想说不要花时间取尝试寻找并行执行查询的方法。MySQL在处理OLTP场景下的短查询效果很好,但对于复杂大查询则能力有效。最直接一点就是,对于一个SQL语句,MySQL最多只能使用一个CPU核来处理,在这种场景下无法发挥主机CPU多核的能力。MySQL没有停滞不前,一直在发展,新推出的MySQL8.0.14版本第一次引入了并行查询特性,使得check table和select count(*)类型的语句性能成倍提升。虽然目前使用场景还比较优先,但后续的发展值得期待。
1.使用方式
通过配置参数innodb_parallel_read_threads来设置并发线程数,就能开始并行扫描功能,默认这个值为4.有个简单的实验,通过sysbench导入2亿条数据,分别配置innodb_parallel_read_threads为1,2,4,8,16,32,64,测试并行执行的效果。测试语句为SELECT COUNT(*) FROM sbtest1;
如图所示。横轴是配置并发线程数,纵轴是语句执行时间。从测试结果来看,整个并行表现还是不错的,扫描2亿条记录,从单线程的18s,下降到32线程的1s。后面并发开再多,由于数量优先,多线程的管理消耗超过了并发带来的性能提升,不能再继续缩短SQL执行时间
2.MySQL并行执行
实际上MySQL的并行执行还处于非常初级阶段,如下图所示,左边是之前MySQL串行处理单个SQL形态;中间的是目前MySQL版本提供的并行能力,InnoDB引擎并行扫描的形态;最右边的是未来MySQL要发展的形态,优化器根据系统负载和SQL生成并行计划,并将分区计划下发给执行器并行执行。并行执行不仅仅是并行扫描,以及并行排序等。目前版本MySQL的上层优化器以及执行器并没有配套的修改。InnoDB的并行扫描主要应用在了分区,并行扫描,预读,以及与执行器交互的适配器类(详细分析请见https://www.cnblogs.com/cchust/p/12347166.html)
左边是之前MySQL串行处理单个SQL形态;中间的是目前MySQL版本提供的并行能力,InnoDB引擎并行扫描的形态;最右边的是未来MySQL要发展的形态。
MySQL8.0引入了并行查询虽然还比较初级,但已经让我们看到了MySQL并行查询的潜力,
从实验中我们也看到了开启并行查询执行后,SQL语句执行充分发挥了多核能力,响应时间几急剧下降。
相信在不久的将来,8.0会支持更多并行算子,包括并行聚集,并行连接,并行分组以及并行排序等等
MySQL8.0引入了并行查询虽然还比较初级,但已经让我们看到了MySQL并行查询的潜力,
从实验中我们也看到了开启并行查询执行后,SQL语句执行充分发挥了多核能力,响应时间几急剧下降。
相信在不久的将来,8.0会支持更多并行算子,包括并行聚集,并行连接,并行分组以及并行排序等等
哈希关联。
MySQL并不支持哈希关联——MySQL的所有关联都是嵌套循环关联。不过,可以通过建立一个哈希索引来曲线地实现哈希关联。如果使用的是Memeory存储引擎,则索引都是哈希索引,所以关联的时候也类似于哈希关联,另外,MariaDB已经实现了真正的哈希关联
MySQL并不支持哈希关联——MySQL的所有关联都是嵌套循环关联。不过,可以通过建立一个哈希索引来曲线地实现哈希关联。如果使用的是Memeory存储引擎,则索引都是哈希索引,所以关联的时候也类似于哈希关联,另外,MariaDB已经实现了真正的哈希关联
松散索引扫描。
由于历史原因,MySQL并不支持松散索引扫描,也就无法按照不连续的方式扫描一个索引。通常,MySQL的索引扫描需要先定义一个起点和终点,即使需要的数据只是这段索引中很少数的几个,MySQL仍需要扫描这段儿索引中的每一个条目。下面我们通过一个示例说明这点。假设我们有如下索引(a,b),有下面的查询:
```sql
mysql>SELECT ... FROM tbl WHERE b BETWEEN 2 AND 3;
```
因为索引的前导字段是列a,但是在查询中只指定了字段b,MySQL无法使用这个索引,从而只能通过全表扫描找到匹配的行,如图所示。了解索引的物理结构的话,不难发现还可以有一个更快的办法执行上面的查询。索引的物理结构(不是存储引擎的API)使得可以先扫描a列的第一个值对应的b列的范围,然后再跳到a列不同第二个不同值扫描对应的b列的范围。如图所示展示了如果由MySQL来实现这个过程会怎样。注意到,这时就无须再使用WHERE子句过滤,因为松散索引扫描已经跳过了所有不需要的记录。上面是一个简单的例子,除了松散索引扫描,新增一个合适的索引当然也可以优化上述查询。但对于某些场景,增加索引是没用的,例如,对于第一个索引列是范围条件,第二个索引列是等值条件的查询,靠增加索引就无法解决问题。
MySQL5.0之后的版本,在某些特殊的场景下是可以使用松散索引扫描的,例如,在一个分组查询中需要找到分组的最大值和最小值:
```sql
mysql> EXPLAIN SELECT actor_id, MAX(film_id)
-> FROM sakila.film_actor
-> GROUP BY actor_id\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: range
possible_keys: PRIMARY,idx_fk_film_id
key: PRIMARY
key_len: 2
ref: NULL
rows: 201
filtered: 100.00
Extra: Using index for group-by
```
在EXPLAIN中的Extra字段显示"Using index for group-by",表示这里将使用松散索引扫描,不过如果MySQL能写上"loose index probe",相信会更好理解。在MySQL很好地支持松散索引扫描之前,一个简单的绕过问题的办法就是给前面的列加上可能的常数值。在MySQL5.6之后的版本,关于松散索引扫描的一些限制将会通过"索引下推(index condition pushdown)"的方式解决
由于历史原因,MySQL并不支持松散索引扫描,也就无法按照不连续的方式扫描一个索引。通常,MySQL的索引扫描需要先定义一个起点和终点,即使需要的数据只是这段索引中很少数的几个,MySQL仍需要扫描这段儿索引中的每一个条目。下面我们通过一个示例说明这点。假设我们有如下索引(a,b),有下面的查询:
```sql
mysql>SELECT ... FROM tbl WHERE b BETWEEN 2 AND 3;
```
因为索引的前导字段是列a,但是在查询中只指定了字段b,MySQL无法使用这个索引,从而只能通过全表扫描找到匹配的行,如图所示。了解索引的物理结构的话,不难发现还可以有一个更快的办法执行上面的查询。索引的物理结构(不是存储引擎的API)使得可以先扫描a列的第一个值对应的b列的范围,然后再跳到a列不同第二个不同值扫描对应的b列的范围。如图所示展示了如果由MySQL来实现这个过程会怎样。注意到,这时就无须再使用WHERE子句过滤,因为松散索引扫描已经跳过了所有不需要的记录。上面是一个简单的例子,除了松散索引扫描,新增一个合适的索引当然也可以优化上述查询。但对于某些场景,增加索引是没用的,例如,对于第一个索引列是范围条件,第二个索引列是等值条件的查询,靠增加索引就无法解决问题。
MySQL5.0之后的版本,在某些特殊的场景下是可以使用松散索引扫描的,例如,在一个分组查询中需要找到分组的最大值和最小值:
```sql
mysql> EXPLAIN SELECT actor_id, MAX(film_id)
-> FROM sakila.film_actor
-> GROUP BY actor_id\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
partitions: NULL
type: range
possible_keys: PRIMARY,idx_fk_film_id
key: PRIMARY
key_len: 2
ref: NULL
rows: 201
filtered: 100.00
Extra: Using index for group-by
```
在EXPLAIN中的Extra字段显示"Using index for group-by",表示这里将使用松散索引扫描,不过如果MySQL能写上"loose index probe",相信会更好理解。在MySQL很好地支持松散索引扫描之前,一个简单的绕过问题的办法就是给前面的列加上可能的常数值。在MySQL5.6之后的版本,关于松散索引扫描的一些限制将会通过"索引下推(index condition pushdown)"的方式解决
MySQL通过全表扫描找到需要的记录
使用松散索引扫描效率会更高,但是MySQL现在还不支持这么做
最大值和最小值优化。
对于MIN()和MAX()查询,MySQL的优化做得并不好。这里有一个例子:
```sql
mysql> SELECT MIN(actor_id) FROM sakila.actor WHERE first_name='PENELOPE';
```
因为在first_name字段上并没有索引,因此MySQL将会进行一次全表扫描。如果MySQL能够进行主键扫描,那么理论上,当MySQL读到的第一个满足条件的记录的时候,就是我们需要找到的最小值了,因为主键是严格按照actor_id字段的大小顺序排列的。但是MySQL这时只会做全表扫描,我们可以通过查看SHOW STATUS的全表扫描计数器来验证这一点。一个曲线的优化办法是移除MIN(),然后使用LIMIT来讲查询重写如下:
```sql
mysql> SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY)
-> WHERE first_name = 'PENELOPE' LIMIT 1;
+----------+
| actor_id |
+----------+
| 1 |
+----------+
1 row in set (0.00 sec)
```
这个策略可以让MySQL扫描尽可能少的记录数。如果你是一个完美主义者,可能会说这个SQL已经无法表达她的本意了。一般我们通过SQL告诉服务器我们需要什么数据,由服务器来决定如何最优地获取数据,不过在这个案例中,我们其实是告诉MySQL如何去获取我们需要的数据,通过SQL并不能一眼就看出我们其实是想要一个最小值。确实如此,有时候为了获得更高的性能,我们不得不放弃一些原则
对于MIN()和MAX()查询,MySQL的优化做得并不好。这里有一个例子:
```sql
mysql> SELECT MIN(actor_id) FROM sakila.actor WHERE first_name='PENELOPE';
```
因为在first_name字段上并没有索引,因此MySQL将会进行一次全表扫描。如果MySQL能够进行主键扫描,那么理论上,当MySQL读到的第一个满足条件的记录的时候,就是我们需要找到的最小值了,因为主键是严格按照actor_id字段的大小顺序排列的。但是MySQL这时只会做全表扫描,我们可以通过查看SHOW STATUS的全表扫描计数器来验证这一点。一个曲线的优化办法是移除MIN(),然后使用LIMIT来讲查询重写如下:
```sql
mysql> SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY)
-> WHERE first_name = 'PENELOPE' LIMIT 1;
+----------+
| actor_id |
+----------+
| 1 |
+----------+
1 row in set (0.00 sec)
```
这个策略可以让MySQL扫描尽可能少的记录数。如果你是一个完美主义者,可能会说这个SQL已经无法表达她的本意了。一般我们通过SQL告诉服务器我们需要什么数据,由服务器来决定如何最优地获取数据,不过在这个案例中,我们其实是告诉MySQL如何去获取我们需要的数据,通过SQL并不能一眼就看出我们其实是想要一个最小值。确实如此,有时候为了获得更高的性能,我们不得不放弃一些原则
在同一个表上查询和更新。
MySQL不允许对同一个张表同时进行查询和更新。这其实并不是优化器的限制,如果清楚MySQL是如何执行查询,就可以避免这种情况。下面是一个无法运行的SQL,虽然这是一个符合标准的SQL语句。这个SQL语句尝试将两个表中相似行的数量记录到字段cnt中:
```sql
mysql> UPDATE tbl AS outer_tbl
-> SET cnt = (
-> SELECT COUNT(*) FROM tbl AS inner_tbl
-> WHERE inner_tbl.type = outer_tbl.type
-> );
ERROR 1093(HY000):You can't specify target table 'outer_tbl' for update in FROM clause
```
可以通过使用生成表的形式来绕过上面的限制,因为MySQL只会把这个表当作一个临时表来处理。实际上,这执行了两个查询:一个是子查询中的SELECT语句,另一个是多表关联UPDATE,只是关联的表是一个临时表。子查询会在UPDATE语句打开表之前就完成。所以下面的查询将会正常执行:
```sql
mysql> UPDATE tbl
-> INNER JOIN (
-> SELECT type, count(*) AS cnt
-> FROM tbl
-> GROUP BY type
-> ) AS der USING(type)
-> SET tbl.cnt = der.cnt;
```
MySQL不允许对同一个张表同时进行查询和更新。这其实并不是优化器的限制,如果清楚MySQL是如何执行查询,就可以避免这种情况。下面是一个无法运行的SQL,虽然这是一个符合标准的SQL语句。这个SQL语句尝试将两个表中相似行的数量记录到字段cnt中:
```sql
mysql> UPDATE tbl AS outer_tbl
-> SET cnt = (
-> SELECT COUNT(*) FROM tbl AS inner_tbl
-> WHERE inner_tbl.type = outer_tbl.type
-> );
ERROR 1093(HY000):You can't specify target table 'outer_tbl' for update in FROM clause
```
可以通过使用生成表的形式来绕过上面的限制,因为MySQL只会把这个表当作一个临时表来处理。实际上,这执行了两个查询:一个是子查询中的SELECT语句,另一个是多表关联UPDATE,只是关联的表是一个临时表。子查询会在UPDATE语句打开表之前就完成。所以下面的查询将会正常执行:
```sql
mysql> UPDATE tbl
-> INNER JOIN (
-> SELECT type, count(*) AS cnt
-> FROM tbl
-> GROUP BY type
-> ) AS der USING(type)
-> SET tbl.cnt = der.cnt;
```
查询优化器的提示(hint).
如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终的执行计划。下面将列举一些常见的提示,并简单地给出什么时候使用该提示。通过在查询中加入相应的提示,就可以抗旨该查询的执行计划。关于每个提示的具体用法,建议直接阅读MySQL官方手册。有些提示和版本有直接关系。可以使用的一些提示如下:
1.HIGH_PRIORITY和LOW_PRIORITY
这个提示告诉MySQL,当多个语句同时访问某一个表的时候,那些语句的优先级相对高些、哪些语句的优先级相对低些。HIGH_PRIORITY用于SELECT语句的时候,MySQL会将此SELECT语句重新调度到所有正在等待表锁以便修改的语句之前。实际上MySQL是将其放在表的队列的最前面,而不是按照常规顺序等待。HIGH_PRIORITY还可以用于INSERT语句,其效果只是简单地抵消了全局LOW_PRIORITY设置对该语句的影响。
LOW_PRIORITY则正好相反:它会让该语句一直处于等待状态,只要队列中还有需要访问同一个表的语句——即使是哪些比该语句还晚提交到服务器的语句。这就像一个过于礼貌地人站在餐厅门口,只要还有其他顾客在等待就一直不进去,很明显这容易把自己饿坏。LOW_PRIORITY提示在SELECT、INSERT、UPDATE和DELETE语句中都可以使用。这两个提示只对使用表锁地存储引擎有效,千万不要在InnoDB或者其他有细粒度地锁机制和并发控制的引擎中使用。即使是在MyISAM中使用也要注意,因为这两个提示会导致并发插入被禁用,可能会严重降低性能。HIGH_PRIORITY和LOW_PRIORITY经常让人感到困惑。这两个提示并不会获取更多资源让查询"积极"工作,也不会让少获取资源让查询"消极"工作。它们只是简单地控制了MySQL访问某个数据表的队列顺序。
2.DELAYED
这个提示对INSERT和REPLACE有效。MySQL会将使用该提示的语句立即返回给客户端,并将插入的行数据放入到缓冲区,然后在表空闲时批量将数据写入。日志系统使用这样的提示非常有效,或者是其他需要写入大量数据但是客户端却不需要等待单条语句完成IO的应用。这个用法有一些限制:并不是所有的存储引擎都支持这样的做法;并且该提示会导致函数LAST_INSERT_ID()无法正常工作
3.STRAIGHT_JOIN
这个提示可以放置在SELECT语句的SELECT关键字之后,也可以放置在任何两个关联表的名字之间。第一个用法是让查询中所有表按照在语句中出现的顺序进行关联。第二个用法则是固定其前后两个表的关联顺序。当MySQL没能选择正确的关联顺序的时候,或者由于可能太多的顺序导致MySQL无法评估所有的关联顺序的时候,STRAIGHT_JOIN都会很有用。在后面这种情况,MySQL可能会花费大量时间在"statistics"状态,加上这个提示则会大大减少优化器的搜索空间。可以先使用EXPLAIN语句来查看优化器选择的关联顺序,然后使用该提示来重写查询,再看看它的关联顺序。当你确定无论怎样的where条件,某个固定的关联顺序始终是最佳的时候,使用这个提示可以大大提高优化器的效率。但是在升级MySQL版本的时候,需要重新审视下这类查询,某些新的优化特性可能会因为该提示而失效
4.SQL_SAMLL_RESULT和SQL_BIG_RESULT
这两个提示只对SELECT语句有效。它们告诉优化器对GROUP BY或者DISTINCT查询如何使用临时表及排序。SQL_SMALL_RESULT告诉优化器结果集会很小,可以将结果集放在内存的索引临时表,以避免排序操作。如果是SQL_BIG_RESULT,则告诉优化器结果集可能会非常大,建议使用磁盘临时表做排序工作。
5.SQL_BUFFER_RESULT
这个提示告诉优化器将查询结果放入到一个临时表,然后尽可能地释放表锁。这和前面提到的由客户端缓存结果不同。当你没法使用客户端缓存的时候,使用服务器端的缓存通常很有效。带来的好处是无须在客户端上消耗太多的内存,还可以尽可能地释放对应的表锁。代价是,服务器端将需要更多的内存
如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终的执行计划。下面将列举一些常见的提示,并简单地给出什么时候使用该提示。通过在查询中加入相应的提示,就可以抗旨该查询的执行计划。关于每个提示的具体用法,建议直接阅读MySQL官方手册。有些提示和版本有直接关系。可以使用的一些提示如下:
1.HIGH_PRIORITY和LOW_PRIORITY
这个提示告诉MySQL,当多个语句同时访问某一个表的时候,那些语句的优先级相对高些、哪些语句的优先级相对低些。HIGH_PRIORITY用于SELECT语句的时候,MySQL会将此SELECT语句重新调度到所有正在等待表锁以便修改的语句之前。实际上MySQL是将其放在表的队列的最前面,而不是按照常规顺序等待。HIGH_PRIORITY还可以用于INSERT语句,其效果只是简单地抵消了全局LOW_PRIORITY设置对该语句的影响。
LOW_PRIORITY则正好相反:它会让该语句一直处于等待状态,只要队列中还有需要访问同一个表的语句——即使是哪些比该语句还晚提交到服务器的语句。这就像一个过于礼貌地人站在餐厅门口,只要还有其他顾客在等待就一直不进去,很明显这容易把自己饿坏。LOW_PRIORITY提示在SELECT、INSERT、UPDATE和DELETE语句中都可以使用。这两个提示只对使用表锁地存储引擎有效,千万不要在InnoDB或者其他有细粒度地锁机制和并发控制的引擎中使用。即使是在MyISAM中使用也要注意,因为这两个提示会导致并发插入被禁用,可能会严重降低性能。HIGH_PRIORITY和LOW_PRIORITY经常让人感到困惑。这两个提示并不会获取更多资源让查询"积极"工作,也不会让少获取资源让查询"消极"工作。它们只是简单地控制了MySQL访问某个数据表的队列顺序。
2.DELAYED
这个提示对INSERT和REPLACE有效。MySQL会将使用该提示的语句立即返回给客户端,并将插入的行数据放入到缓冲区,然后在表空闲时批量将数据写入。日志系统使用这样的提示非常有效,或者是其他需要写入大量数据但是客户端却不需要等待单条语句完成IO的应用。这个用法有一些限制:并不是所有的存储引擎都支持这样的做法;并且该提示会导致函数LAST_INSERT_ID()无法正常工作
3.STRAIGHT_JOIN
这个提示可以放置在SELECT语句的SELECT关键字之后,也可以放置在任何两个关联表的名字之间。第一个用法是让查询中所有表按照在语句中出现的顺序进行关联。第二个用法则是固定其前后两个表的关联顺序。当MySQL没能选择正确的关联顺序的时候,或者由于可能太多的顺序导致MySQL无法评估所有的关联顺序的时候,STRAIGHT_JOIN都会很有用。在后面这种情况,MySQL可能会花费大量时间在"statistics"状态,加上这个提示则会大大减少优化器的搜索空间。可以先使用EXPLAIN语句来查看优化器选择的关联顺序,然后使用该提示来重写查询,再看看它的关联顺序。当你确定无论怎样的where条件,某个固定的关联顺序始终是最佳的时候,使用这个提示可以大大提高优化器的效率。但是在升级MySQL版本的时候,需要重新审视下这类查询,某些新的优化特性可能会因为该提示而失效
4.SQL_SAMLL_RESULT和SQL_BIG_RESULT
这两个提示只对SELECT语句有效。它们告诉优化器对GROUP BY或者DISTINCT查询如何使用临时表及排序。SQL_SMALL_RESULT告诉优化器结果集会很小,可以将结果集放在内存的索引临时表,以避免排序操作。如果是SQL_BIG_RESULT,则告诉优化器结果集可能会非常大,建议使用磁盘临时表做排序工作。
5.SQL_BUFFER_RESULT
这个提示告诉优化器将查询结果放入到一个临时表,然后尽可能地释放表锁。这和前面提到的由客户端缓存结果不同。当你没法使用客户端缓存的时候,使用服务器端的缓存通常很有效。带来的好处是无须在客户端上消耗太多的内存,还可以尽可能地释放对应的表锁。代价是,服务器端将需要更多的内存
6.SQL_CACHE和SQL_NO_CACHE
这个提示告诉MySQL这个结果集是否应该缓存在查询缓存中
7.SQL_CALC_FOUND_ROWS
严格来说,这并不是一个优化器提示。它不会告诉优化器任何关于执行计划的东西,它会让MySQL返回的结果集包含更多的信息。查询中加上该提示MySQL会计算除去LIMIT子句后这个查询要返回的结果集的总数,而实际上只返回LIMIT要求的结果集。可以通过函数FOUND_ROW()获得这个值
8.FOR UPDATE和LOCK IN SHARE MODE
这也不是真正的优化器提示。这两个提示主要控制SELECT语句的所机制,但只对实现了行级锁的存储引擎有效。使用该提示会对符合查询条件的数据行加锁。对于INSERT...SELECT语句是不需要这两个提示的,因为对于MySQL5.0和更细那版本会默认给这些记录加上读锁(可以禁用该默认行为,但不是个好主意)。唯一内置的支持这两个提示的引擎就是InnoDB。另外需要记住的是,这两个提示会让某些优化无法正常使用,例如索引覆盖扫描。InnoDB不能再不访问主键的情况下排他地锁定行,因为行的版本信息保存在主键中。糟糕的是,这两个提示经常被滥用,很容易造成服务器的锁争用问题应该尽可能地避免使用这两个提示,通常都有其他更好的方式可以实现同样的目的。
9.USE INDEX、IGNORE INDEX和FORCE INDEX
这几个提示会告诉优化器使用或者不适用哪些索引来查询记录(例如,在决定关联顺序的时候使用哪个索引)。在MySQL5.0和更早的版本,这些提示并不会影响到优化器选择哪个索引进行排序和分组,在MySQL5.1和之后的版本可以通过新增选项FOR ORDER BY和FOR GROUP BY来指定是否对排序和分组有效。FORCE INDEX和USE INDEX基本相同,除了一点:FORCE INDEX会告诉优化器全表扫描的成本会远远高于索引扫描,哪怕实际上该索引用处不大。当发现优化器选择了错误的索引,或者因为某些原因(比如在不适用ORDER BY的时候希望结果有序)要使用另一个索引时,可以使用该提示。
这个提示告诉MySQL这个结果集是否应该缓存在查询缓存中
7.SQL_CALC_FOUND_ROWS
严格来说,这并不是一个优化器提示。它不会告诉优化器任何关于执行计划的东西,它会让MySQL返回的结果集包含更多的信息。查询中加上该提示MySQL会计算除去LIMIT子句后这个查询要返回的结果集的总数,而实际上只返回LIMIT要求的结果集。可以通过函数FOUND_ROW()获得这个值
8.FOR UPDATE和LOCK IN SHARE MODE
这也不是真正的优化器提示。这两个提示主要控制SELECT语句的所机制,但只对实现了行级锁的存储引擎有效。使用该提示会对符合查询条件的数据行加锁。对于INSERT...SELECT语句是不需要这两个提示的,因为对于MySQL5.0和更细那版本会默认给这些记录加上读锁(可以禁用该默认行为,但不是个好主意)。唯一内置的支持这两个提示的引擎就是InnoDB。另外需要记住的是,这两个提示会让某些优化无法正常使用,例如索引覆盖扫描。InnoDB不能再不访问主键的情况下排他地锁定行,因为行的版本信息保存在主键中。糟糕的是,这两个提示经常被滥用,很容易造成服务器的锁争用问题应该尽可能地避免使用这两个提示,通常都有其他更好的方式可以实现同样的目的。
9.USE INDEX、IGNORE INDEX和FORCE INDEX
这几个提示会告诉优化器使用或者不适用哪些索引来查询记录(例如,在决定关联顺序的时候使用哪个索引)。在MySQL5.0和更早的版本,这些提示并不会影响到优化器选择哪个索引进行排序和分组,在MySQL5.1和之后的版本可以通过新增选项FOR ORDER BY和FOR GROUP BY来指定是否对排序和分组有效。FORCE INDEX和USE INDEX基本相同,除了一点:FORCE INDEX会告诉优化器全表扫描的成本会远远高于索引扫描,哪怕实际上该索引用处不大。当发现优化器选择了错误的索引,或者因为某些原因(比如在不适用ORDER BY的时候希望结果有序)要使用另一个索引时,可以使用该提示。
MySQL5.0和更新版本中,新增了一些参数用来控制优化器的行为:
1.optimizer_search_depth
这个参数控制优化器在穷举执行计划时的限度。如果查询长时间处于"Statistics"状态,那么可以考虑调低此参数。
2.optimizer_prune_level
该参数默认是打开的,这让优化器会根据需要扫描的行数来决定是否跳过某些执行计划
3.optimizer_switch
这个变量包含了一些开启/关闭优化器特性的标志位。例如在MySQL5.1中可以通过这个参数来控制禁用索引合并的特性
前两个参数是用来控制优化器可以走的一些"结晶"。这些结晶可以让优化器在处理非常复杂的SQL语句时,仍然可以很搞笑,但这也可能让优化器错过一些真正最优的执行计划。所以应该根据实际需要来修改这些参数。
1.optimizer_search_depth
这个参数控制优化器在穷举执行计划时的限度。如果查询长时间处于"Statistics"状态,那么可以考虑调低此参数。
2.optimizer_prune_level
该参数默认是打开的,这让优化器会根据需要扫描的行数来决定是否跳过某些执行计划
3.optimizer_switch
这个变量包含了一些开启/关闭优化器特性的标志位。例如在MySQL5.1中可以通过这个参数来控制禁用索引合并的特性
前两个参数是用来控制优化器可以走的一些"结晶"。这些结晶可以让优化器在处理非常复杂的SQL语句时,仍然可以很搞笑,但这也可能让优化器错过一些真正最优的执行计划。所以应该根据实际需要来修改这些参数。
MySQL升级后的验证。
在优化器面前耍一些"小聪明"是不好的。这样做收效甚小,但是却给维护带来了很多额外的工作量。在MySQL版本升级的时候,这个问题就很突出了,你设置的"优化器提示"很可能会让新版的优化策略失效。MySQL5.0版本引入了大量优化策略,在还没有正式发布的5.6版本中,优化器的改进也是近些年来最大的一次改进。如果要更新到这些版本,当然希望能够从这些改进中受益。新版MySQL基本上在各个方面都有非常大的改进,5.5和5.6这两个版本尤为突出。升级操作一般来说都很顺利,但仍然建议仔细检查各个细节,以防止一些边界情况影响你的应用程序。不过还好,要避免这些,你不需要符出太多的经历,使用Percona ToolKit中的pt-upgrade工具,就可以检查在新版本中运行的SQL是否和老版本一样,返回相同的结果
在优化器面前耍一些"小聪明"是不好的。这样做收效甚小,但是却给维护带来了很多额外的工作量。在MySQL版本升级的时候,这个问题就很突出了,你设置的"优化器提示"很可能会让新版的优化策略失效。MySQL5.0版本引入了大量优化策略,在还没有正式发布的5.6版本中,优化器的改进也是近些年来最大的一次改进。如果要更新到这些版本,当然希望能够从这些改进中受益。新版MySQL基本上在各个方面都有非常大的改进,5.5和5.6这两个版本尤为突出。升级操作一般来说都很顺利,但仍然建议仔细检查各个细节,以防止一些边界情况影响你的应用程序。不过还好,要避免这些,你不需要符出太多的经历,使用Percona ToolKit中的pt-upgrade工具,就可以检查在新版本中运行的SQL是否和老版本一样,返回相同的结果
优化特定类型的查询。
接下来将介绍如何优化特定类型的查询,对于未来MySQL的版本未必适用。毫无疑问,某一天优化器自己也会实现这里列出的部分或者全部优化技巧。
接下来将介绍如何优化特定类型的查询,对于未来MySQL的版本未必适用。毫无疑问,某一天优化器自己也会实现这里列出的部分或者全部优化技巧。
优化COUNT()查询.
COUNT()聚合函数,以及如何优化使用了该函数的查询,很可能是MySQL中最容易被误解的10个话题之一。在网上随便一搜就能看到很多错误的理解,可能比我们想象的多得多。在做优化之前,先来看看COUNT()函数真正的作用是什么
1.COUNT()的作用
COUNT()是一个特殊的函数,有两种非常不同的作用:它可以统计某个列值的数量,也可以统计行数,在统计列值时要求列值是非空的(不统计NULL).如果在COUNT()的括号中指定了列或者列的表达式,则统计的就是这个表达式有值的结果数(而不是NULL).因为很多人对NULL理解有问题,所以这里很容易产生误解。COUNT()的另一个作用是统计结果集的行数。当MySQL确认括号内的表达式值不可能为空时,实际上就是在统计行数。最简单的就是当我们使用COUNT(*)的时候,这种情况下通配符*并不会像我们猜想的那样扩展成所有的列,实际上,它会忽略所有的列而直接统计所有的行数。
我们发现一个最常见的错误就是,在括号内指定了一个列却希望统计结果集的行数。如果希望知道的是结果集的行数,最好使用COUNT(*),这样些意义清晰,性能也会很好
2.关于MyISAM的神话
一个容易产生的误解就是:MyISAM的COUNT()函数总是非常快,不够这是有前提条件的,即只有没有任何WHERE条件的COUNT(*)才非常快,因为此时无须实际地去计算表的行数。MySQL可以利用存储引擎的特性直接获得这个值。如果MySQL知道某列col不可能为NULL值,那么MySQL内部会将COUNT(col)表达式优化为COUNT(*)。当统计带WHERE子句的结果集行数,可以是统计某个列值得数量时,MyISAM得COUTN()和其他存储引擎没有任何不同,就不再有神话般得速度了,所以在MyISAM引擎表上执行COUNT()有时候比别得引擎块,有时候比别的引擎慢,这受很多因素影响,要视具体情况而定
3.简单的优化
有时候可以使用MyISAM在COUNT(*)全表非常快得这个特性,来加速一些特定条件的COUNT()查询。在下面的例子中,我们使用标准数据库world来看看如何快速查找到所有ID大于5的城市。可以像下面这样来写这个查询:
```sql
mysql> SELECT COUNT(*) FROM world.City WHERE ID > 5;
```
通过SHOW STATUS的结果可以看到该擦汗寻需要扫描4097行数据。如果将条件反转一下,先找到ID小于等于5的城市数,然后用总城市数一减就能得到同样的结果,却可以将扫描的行数减少到5行以内:
```sql
mysql> SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*)
-> FROM world.City WHERE ID <= 5;
```
这样做可以大大减少需要扫描的行数,是因为在查询优化阶段会将其中的子查询当作一个常数来处理。在邮件组和IRC聊天频道中,通常会看到这样的问题:如何在同一个查询中统计同一个列的不同值的数量,以减少查询的语句量。例如,假设可能需要通过一个查询返回各种不同颜色的商品数量,此时不能使用OR语句(比如SELECT COUNT(color='blur' OR color='red') FROM items;)因为这样做就无法区分不同颜色的商品数量;也不能在WHERE条件中指定颜色(比如SELECT COUNT(*) FROM items WHERE color='blue' AND color = 'RED')因为颜色的条件是互斥的。下面的查询可以在一定程度上解决这个问题。
```sql
mysql> SELECT SUM(IF(color='blue', 1,0)) AS blue, SUM(IF(color='red',1,0)) AS red FROM items;
```
也可以使用COUNT()而不是SUM()实现同样的目的,只需要将满足条件设置为真,不满足条件设置为NULL即可
```sql
mysql> SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color= 'read' OR NULL) AS red FROM items;
```
COUNT()聚合函数,以及如何优化使用了该函数的查询,很可能是MySQL中最容易被误解的10个话题之一。在网上随便一搜就能看到很多错误的理解,可能比我们想象的多得多。在做优化之前,先来看看COUNT()函数真正的作用是什么
1.COUNT()的作用
COUNT()是一个特殊的函数,有两种非常不同的作用:它可以统计某个列值的数量,也可以统计行数,在统计列值时要求列值是非空的(不统计NULL).如果在COUNT()的括号中指定了列或者列的表达式,则统计的就是这个表达式有值的结果数(而不是NULL).因为很多人对NULL理解有问题,所以这里很容易产生误解。COUNT()的另一个作用是统计结果集的行数。当MySQL确认括号内的表达式值不可能为空时,实际上就是在统计行数。最简单的就是当我们使用COUNT(*)的时候,这种情况下通配符*并不会像我们猜想的那样扩展成所有的列,实际上,它会忽略所有的列而直接统计所有的行数。
我们发现一个最常见的错误就是,在括号内指定了一个列却希望统计结果集的行数。如果希望知道的是结果集的行数,最好使用COUNT(*),这样些意义清晰,性能也会很好
2.关于MyISAM的神话
一个容易产生的误解就是:MyISAM的COUNT()函数总是非常快,不够这是有前提条件的,即只有没有任何WHERE条件的COUNT(*)才非常快,因为此时无须实际地去计算表的行数。MySQL可以利用存储引擎的特性直接获得这个值。如果MySQL知道某列col不可能为NULL值,那么MySQL内部会将COUNT(col)表达式优化为COUNT(*)。当统计带WHERE子句的结果集行数,可以是统计某个列值得数量时,MyISAM得COUTN()和其他存储引擎没有任何不同,就不再有神话般得速度了,所以在MyISAM引擎表上执行COUNT()有时候比别得引擎块,有时候比别的引擎慢,这受很多因素影响,要视具体情况而定
3.简单的优化
有时候可以使用MyISAM在COUNT(*)全表非常快得这个特性,来加速一些特定条件的COUNT()查询。在下面的例子中,我们使用标准数据库world来看看如何快速查找到所有ID大于5的城市。可以像下面这样来写这个查询:
```sql
mysql> SELECT COUNT(*) FROM world.City WHERE ID > 5;
```
通过SHOW STATUS的结果可以看到该擦汗寻需要扫描4097行数据。如果将条件反转一下,先找到ID小于等于5的城市数,然后用总城市数一减就能得到同样的结果,却可以将扫描的行数减少到5行以内:
```sql
mysql> SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*)
-> FROM world.City WHERE ID <= 5;
```
这样做可以大大减少需要扫描的行数,是因为在查询优化阶段会将其中的子查询当作一个常数来处理。在邮件组和IRC聊天频道中,通常会看到这样的问题:如何在同一个查询中统计同一个列的不同值的数量,以减少查询的语句量。例如,假设可能需要通过一个查询返回各种不同颜色的商品数量,此时不能使用OR语句(比如SELECT COUNT(color='blur' OR color='red') FROM items;)因为这样做就无法区分不同颜色的商品数量;也不能在WHERE条件中指定颜色(比如SELECT COUNT(*) FROM items WHERE color='blue' AND color = 'RED')因为颜色的条件是互斥的。下面的查询可以在一定程度上解决这个问题。
```sql
mysql> SELECT SUM(IF(color='blue', 1,0)) AS blue, SUM(IF(color='red',1,0)) AS red FROM items;
```
也可以使用COUNT()而不是SUM()实现同样的目的,只需要将满足条件设置为真,不满足条件设置为NULL即可
```sql
mysql> SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color= 'read' OR NULL) AS red FROM items;
```
4.使用近似值
有时候某些业务场景并不要求完全精确的COUNT值,此时可以用近似值来代替。EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不需要真正地去执行查询,所以成本很低。很多时候,计算精确值的成本非常高,而计算近似值则非常简单。曾经有一个人希望统计他的网站的当前活跃用户数是多少,这个活跃用户数保存在缓存中,过期时间为30分钟,所以每隔30分钟需要重新计算并放入缓存。因此这个活跃用户数本身就不是精确值,所以使用近似值代替是可以接受的。另外,如果要精确统计在线认数,通常WHERE条件会很复杂,一方面需要剔除当前非活跃用户,另一方面还要剔除系统中某些特定ID的"默认"用户,去掉这些约束条件对总数的影响很小,但却可能很好地提升该查询的性能。更进一步地优化则可以尝试删除DISTINCT这样的约束来避免文件排序。这样重写过的查询要比原来的精确统计的查询快很多,而返回的结果则几乎相同
5.更复杂的优化。
通常来说,COUNT()都需要扫描大量的行(意味着要访问大量数据)才能获得精确的结果,因此是很难优化的。除了前面的方法,在MySQL层面还能做的就只有索引覆盖扫描了,如果这还不够,就需要考虑修改应用的架构,可以增加汇总表,或者增加类似Memcached这样的外部缓存系统。可能很快你就会发现陷入到一个熟悉的困境,"快速,精确和实现简单",三者永远只能满足其二,必须舍掉其中一个
有时候某些业务场景并不要求完全精确的COUNT值,此时可以用近似值来代替。EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不需要真正地去执行查询,所以成本很低。很多时候,计算精确值的成本非常高,而计算近似值则非常简单。曾经有一个人希望统计他的网站的当前活跃用户数是多少,这个活跃用户数保存在缓存中,过期时间为30分钟,所以每隔30分钟需要重新计算并放入缓存。因此这个活跃用户数本身就不是精确值,所以使用近似值代替是可以接受的。另外,如果要精确统计在线认数,通常WHERE条件会很复杂,一方面需要剔除当前非活跃用户,另一方面还要剔除系统中某些特定ID的"默认"用户,去掉这些约束条件对总数的影响很小,但却可能很好地提升该查询的性能。更进一步地优化则可以尝试删除DISTINCT这样的约束来避免文件排序。这样重写过的查询要比原来的精确统计的查询快很多,而返回的结果则几乎相同
5.更复杂的优化。
通常来说,COUNT()都需要扫描大量的行(意味着要访问大量数据)才能获得精确的结果,因此是很难优化的。除了前面的方法,在MySQL层面还能做的就只有索引覆盖扫描了,如果这还不够,就需要考虑修改应用的架构,可以增加汇总表,或者增加类似Memcached这样的外部缓存系统。可能很快你就会发现陷入到一个熟悉的困境,"快速,精确和实现简单",三者永远只能满足其二,必须舍掉其中一个
优化关联查询。
这个话题基本上一直在讨论,这里需要特别提到的是:
1.确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用到c关联的时候,如果优化器的关联顺序是B、A,那么久不需要在B表的对应列上建上索引。没有用到的索引只会带来额外的负担。一般来说,除非有其他理由,否则只需要在关联顺序中的第二个表的相应列上创建索引。
2.确保任何的GROUP BY 和ORDER BY中的表达式只涉及到一个表中的列,这昂MySQL才有可能使用索引来优化这个过程
3.当升级MySQL的时候需要注意:关联语法、运算符优先级等其他可能会发生变化的地方。因为以前是普通关联的地方可能会变成笛卡儿积,不同类型的关联可能会生成不同的结果等
这个话题基本上一直在讨论,这里需要特别提到的是:
1.确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用到c关联的时候,如果优化器的关联顺序是B、A,那么久不需要在B表的对应列上建上索引。没有用到的索引只会带来额外的负担。一般来说,除非有其他理由,否则只需要在关联顺序中的第二个表的相应列上创建索引。
2.确保任何的GROUP BY 和ORDER BY中的表达式只涉及到一个表中的列,这昂MySQL才有可能使用索引来优化这个过程
3.当升级MySQL的时候需要注意:关联语法、运算符优先级等其他可能会发生变化的地方。因为以前是普通关联的地方可能会变成笛卡儿积,不同类型的关联可能会生成不同的结果等
优化子查询
关于子查询优化给出的最重要的优化建议就是尽可能使用关联查询,至少当前的MySQL版本需要这样,"尽可能使用关联"并不是绝对的,如果使用的是MySQL5.6或更新的版本或者MariaDB,那么久可以直接忽略关于子查询的这些建议了
关于子查询优化给出的最重要的优化建议就是尽可能使用关联查询,至少当前的MySQL版本需要这样,"尽可能使用关联"并不是绝对的,如果使用的是MySQL5.6或更新的版本或者MariaDB,那么久可以直接忽略关于子查询的这些建议了
优化GROUP BY和DISTINCT
在很多场景下,MySQL都使用同样的方法优化这两种查询,事实上,MySQL优化器会在内部处理的时候相互转化这两类查询。它们都可以使用索引来优化,这也是最有效的优化办法。在MySQL中,当无法使用索引的时候,GROUP BY使用两种策略来完成:使用临时表或者文件排序来做分组。对于任何查询语句,这两种策略的性能都有可以提升的地方。可以通过使用提示SQL_BIG_RESULT和SQL_SMALL_RESULT来让优化器按照你希望的方式运行。如果需要对关联查询做分组(GROUP BY),并且是按照查找表中的某个列进行分组,那么通常采用查找表的标识列分组的效率会比其他列更高。例如下面的查询效率不会很好:
```sql
mysql> SELECT actor.first_name,actor.last_name, COUNT(*)
-> FROM sakila.film_actor
-> INNER JOIN sakila.actor USING(actor_id)
-> GROUP BY actor.first_name,actor.last_name;
```
如果查询按照下面的写法效率则会更高:
```sql
mysql> SELECT actor.first_name,actor.last_name, COUNT(*) FROM sakila.film_actor INNER JOIN sakila.actor USING(actor_id) GROUP BY film_actor.actor_id;
```
使用actor.actor_id列分组的效率甚至会比使用film_actor.actor_id更好。这点通过简单的测试即可验证。这个查询利用了演员的姓名和ID直接相关的特点,因此改写后的结果不受影响,但显然不是所有的关联语句的分组查询都可以改写成在SELECT中直接使用非分组列的形式的。甚至可能会在服务器上设置SQL_MODE来禁止这样的写法。如果是这样,也可以通过MIN()或者MAX()函数来绕过这种限制,但一定要清楚,SELECT后面出现的非分组列一定是直接依赖分组列,并且在每个组内的值是唯一的,或者是业务上根本不在乎这个值具体是什么:
```sql
mysql>SELECT MIN(actor.first_name), MAX(actor.last_name), .....;
```
较真的人可能会说这样写的分组查询是有问题的,确实如此。从MIN()或者MAX()函数的用法就可以看出这个查询是有问题的。但若更在乎的是MySQL运行查询的效率时这样做也无可厚非。如果实在较真的话也可以改写成下面的形式
```sql
mysql> SELECT actor.first_name,actor.last_name, cnt FROM sakila.actor INNER JOIN ( SELECT actor_id, COUNT(*) AS cnt FROM sakila.film_actor GROUP BY actor_id ) AS c USING(actor_id);
```
这样写更满足关系理论,但成本有点高,因为子查询需要创建和填充临时表,而子查询中创建的临时表是没有任何索引的(值得一提的是,MariaDB修复了这个限制)。在分组查询的SELECT中直接使用非分组列通常不是什么好主意,因为这样的结果通常是补丁的,当索引改变,或者优化器选择不同的优化策略时都可能导致结果不一样。碰到的大多数这种查询最后都导致了故障(因为MySQL不会对这类查询返回错误),而且这种写法大部分是由于偷懒而不是为优化而故意这么设计的。建议始终使用含义明确的语法。事实上,建议对MySQL的SQL_MODE设置为包含ONLY_FULL_GROUP BY,这时MySQL会对这类查询直接返回一个错误,提醒你需要重写这个查询。如果没有通过ORDER BY子句显示地指定排序列,当查询使用GROUP BY子句地时候,结果集会自动按照分组的字段进行排序。如果不关心结果集的顺序,而这种默认排序又导致了需要文件排序,则可以使用ORDER BY NULL,让MySQL不再进行文件排序。也可以在GROUP BY子句中直接使用DESC或者ASC关键字
在很多场景下,MySQL都使用同样的方法优化这两种查询,事实上,MySQL优化器会在内部处理的时候相互转化这两类查询。它们都可以使用索引来优化,这也是最有效的优化办法。在MySQL中,当无法使用索引的时候,GROUP BY使用两种策略来完成:使用临时表或者文件排序来做分组。对于任何查询语句,这两种策略的性能都有可以提升的地方。可以通过使用提示SQL_BIG_RESULT和SQL_SMALL_RESULT来让优化器按照你希望的方式运行。如果需要对关联查询做分组(GROUP BY),并且是按照查找表中的某个列进行分组,那么通常采用查找表的标识列分组的效率会比其他列更高。例如下面的查询效率不会很好:
```sql
mysql> SELECT actor.first_name,actor.last_name, COUNT(*)
-> FROM sakila.film_actor
-> INNER JOIN sakila.actor USING(actor_id)
-> GROUP BY actor.first_name,actor.last_name;
```
如果查询按照下面的写法效率则会更高:
```sql
mysql> SELECT actor.first_name,actor.last_name, COUNT(*) FROM sakila.film_actor INNER JOIN sakila.actor USING(actor_id) GROUP BY film_actor.actor_id;
```
使用actor.actor_id列分组的效率甚至会比使用film_actor.actor_id更好。这点通过简单的测试即可验证。这个查询利用了演员的姓名和ID直接相关的特点,因此改写后的结果不受影响,但显然不是所有的关联语句的分组查询都可以改写成在SELECT中直接使用非分组列的形式的。甚至可能会在服务器上设置SQL_MODE来禁止这样的写法。如果是这样,也可以通过MIN()或者MAX()函数来绕过这种限制,但一定要清楚,SELECT后面出现的非分组列一定是直接依赖分组列,并且在每个组内的值是唯一的,或者是业务上根本不在乎这个值具体是什么:
```sql
mysql>SELECT MIN(actor.first_name), MAX(actor.last_name), .....;
```
较真的人可能会说这样写的分组查询是有问题的,确实如此。从MIN()或者MAX()函数的用法就可以看出这个查询是有问题的。但若更在乎的是MySQL运行查询的效率时这样做也无可厚非。如果实在较真的话也可以改写成下面的形式
```sql
mysql> SELECT actor.first_name,actor.last_name, cnt FROM sakila.actor INNER JOIN ( SELECT actor_id, COUNT(*) AS cnt FROM sakila.film_actor GROUP BY actor_id ) AS c USING(actor_id);
```
这样写更满足关系理论,但成本有点高,因为子查询需要创建和填充临时表,而子查询中创建的临时表是没有任何索引的(值得一提的是,MariaDB修复了这个限制)。在分组查询的SELECT中直接使用非分组列通常不是什么好主意,因为这样的结果通常是补丁的,当索引改变,或者优化器选择不同的优化策略时都可能导致结果不一样。碰到的大多数这种查询最后都导致了故障(因为MySQL不会对这类查询返回错误),而且这种写法大部分是由于偷懒而不是为优化而故意这么设计的。建议始终使用含义明确的语法。事实上,建议对MySQL的SQL_MODE设置为包含ONLY_FULL_GROUP BY,这时MySQL会对这类查询直接返回一个错误,提醒你需要重写这个查询。如果没有通过ORDER BY子句显示地指定排序列,当查询使用GROUP BY子句地时候,结果集会自动按照分组的字段进行排序。如果不关心结果集的顺序,而这种默认排序又导致了需要文件排序,则可以使用ORDER BY NULL,让MySQL不再进行文件排序。也可以在GROUP BY子句中直接使用DESC或者ASC关键字
优化GROUP BY WITH ROLLUP
分组查询的一个变种就是要求MySQL对返回的分组结果再做一次超级聚合。可以使用WITH ROLLUP子句来实现这种逻辑,但可能会不够优化。可以通过EXPLAIN来观察其执行计划,特别要注意是否通过文件排序或者临时表实现的,然后再去掉WITH ROLLUP子句看执行计划是否相同。也可以通过前面介绍的优化器提示来固定执行计划。很多时候,如果可以,在应用程序中做超级聚合是更好的,虽然这需要返回给客户端更多的结果。也可以在FROM子句中嵌套使用子查询,或者是通过一个临时表存放中间数据,然后和临时表执行UNION来得到最终结果。最好的办法是尽可能地将WITH ROLLUP功能转移到应用程序中处理
分组查询的一个变种就是要求MySQL对返回的分组结果再做一次超级聚合。可以使用WITH ROLLUP子句来实现这种逻辑,但可能会不够优化。可以通过EXPLAIN来观察其执行计划,特别要注意是否通过文件排序或者临时表实现的,然后再去掉WITH ROLLUP子句看执行计划是否相同。也可以通过前面介绍的优化器提示来固定执行计划。很多时候,如果可以,在应用程序中做超级聚合是更好的,虽然这需要返回给客户端更多的结果。也可以在FROM子句中嵌套使用子查询,或者是通过一个临时表存放中间数据,然后和临时表执行UNION来得到最终结果。最好的办法是尽可能地将WITH ROLLUP功能转移到应用程序中处理
优化LIMIT分页。
在系统中需要进行分页操作的时候,我们通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY子句。如果有对应的索引,通常效率会不错,否则,MySQL需要做大量的文件排序操作。一个非常常见又令人头疼的问题就是,在偏移量非常大的时候(翻页到非常靠后的页面),例如可能是
LIMIT 10 000,20这样的查询,这时MySQL需要查询10020条记录然后只返回最后20条,前面的10 000条记录都将被抛弃,这样的代价非常高。如果所有的页面被访问的频率都相同,内马尔这样的查询平均访问半个表的数据。要优化这种查询,要么是在页面中限制分页的数量,要么是优化大偏移量的性能。优化此类分页查询的一个最简单的办法就是尽可能地使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作在返回所需的列。对于偏移量很大的时候,这样做的效率提升会非常大。考虑下面的查询:
```sql
mysql> SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50,5;
```
如果这个表非常大,那么这个查询最好改写成下面的样子:
```sql
mysql> SELECT film.film_id,film.description
-> FROM sakila.film
-> INNER JOIN (
-> SELECT film_id FROM sakila.film
-> ORDER BY title LIMIT 50,5
-> ) AS lim USING(film_id);
```
这里的"延迟关联"将大大提升查询效率,它让MySQL扫描尽可能少的页面,获取需要访问的记录后再根据关联列回原表查询需要的所有列。这个技术也可以用于优化关联查询中的LIMIT子句。有时候也可以将LIMIT查询转换为已知位置的查询,让MySQL通过范围扫描获得到对应的结果。例如,如果在一个位置列上有索引,并且预先计算了边界值,上面的查询就可以改写为:
```sql
mysql> SELECT film_id,description FROM sakila.film WHERE position BETWEEN 50 AND 51 ORDER BY position;
```
对数据进行排名的问题也与此类似,但往往还会同时和GROUP BY 混合使用。在这种情况下通常都需要预先计算并存储排名信息。LIMIT和OFFSET的问题,其实是OFFSET的问题,它回导致MySQL扫描大量不需要的行然后再抛弃掉。如果可以使用书签记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用OFFSET.例如,若需要按照租借记录做翻页,那么可以根据最新一条租借记录向后追溯,这种做法可行是因为租借记录的主键是单调递增的。首先使用下面的查询获得第一组结果:
```sql
mysql> SELECT * FROM sakila.rental ORDER BY rental_id DESC LIMIT 20;
```
假设上面的查询返回的是主键为16 049到16 030的租借记录,那么下一页查询就可以从16 030这个点开始:
```sql
mysql> SELECT * FROM sakila.rental WHERE rental_id < 16030 ORDER BY rental_id DESC LIMIT 20;
```
该技术的好处是无论翻页到多么后面,其性能都会很好。其他优化办法还包括使用预先计算的汇总表,或者关联到一个冗余表,冗余表只包含主键列和需要做排序的数据列。还可以使用Sphinx优化一些搜索操作
在系统中需要进行分页操作的时候,我们通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY子句。如果有对应的索引,通常效率会不错,否则,MySQL需要做大量的文件排序操作。一个非常常见又令人头疼的问题就是,在偏移量非常大的时候(翻页到非常靠后的页面),例如可能是
LIMIT 10 000,20这样的查询,这时MySQL需要查询10020条记录然后只返回最后20条,前面的10 000条记录都将被抛弃,这样的代价非常高。如果所有的页面被访问的频率都相同,内马尔这样的查询平均访问半个表的数据。要优化这种查询,要么是在页面中限制分页的数量,要么是优化大偏移量的性能。优化此类分页查询的一个最简单的办法就是尽可能地使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作在返回所需的列。对于偏移量很大的时候,这样做的效率提升会非常大。考虑下面的查询:
```sql
mysql> SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50,5;
```
如果这个表非常大,那么这个查询最好改写成下面的样子:
```sql
mysql> SELECT film.film_id,film.description
-> FROM sakila.film
-> INNER JOIN (
-> SELECT film_id FROM sakila.film
-> ORDER BY title LIMIT 50,5
-> ) AS lim USING(film_id);
```
这里的"延迟关联"将大大提升查询效率,它让MySQL扫描尽可能少的页面,获取需要访问的记录后再根据关联列回原表查询需要的所有列。这个技术也可以用于优化关联查询中的LIMIT子句。有时候也可以将LIMIT查询转换为已知位置的查询,让MySQL通过范围扫描获得到对应的结果。例如,如果在一个位置列上有索引,并且预先计算了边界值,上面的查询就可以改写为:
```sql
mysql> SELECT film_id,description FROM sakila.film WHERE position BETWEEN 50 AND 51 ORDER BY position;
```
对数据进行排名的问题也与此类似,但往往还会同时和GROUP BY 混合使用。在这种情况下通常都需要预先计算并存储排名信息。LIMIT和OFFSET的问题,其实是OFFSET的问题,它回导致MySQL扫描大量不需要的行然后再抛弃掉。如果可以使用书签记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用OFFSET.例如,若需要按照租借记录做翻页,那么可以根据最新一条租借记录向后追溯,这种做法可行是因为租借记录的主键是单调递增的。首先使用下面的查询获得第一组结果:
```sql
mysql> SELECT * FROM sakila.rental ORDER BY rental_id DESC LIMIT 20;
```
假设上面的查询返回的是主键为16 049到16 030的租借记录,那么下一页查询就可以从16 030这个点开始:
```sql
mysql> SELECT * FROM sakila.rental WHERE rental_id < 16030 ORDER BY rental_id DESC LIMIT 20;
```
该技术的好处是无论翻页到多么后面,其性能都会很好。其他优化办法还包括使用预先计算的汇总表,或者关联到一个冗余表,冗余表只包含主键列和需要做排序的数据列。还可以使用Sphinx优化一些搜索操作
优化SQL_CALC_FOUND_ROWS
分页的时候,另一个常用的技巧是在LIMIT语句中加上SQL_CALC_FOUND_ROWS提示(hint),这样就可以获得去掉LIMIT以后满足条件的行数,因此可以作为分页的总数。看起来,MySQL做了一些非常"高深"的优化,像是通过某种方法预测了总行数。但实际上,MySQL只有在扫描了所有满足条件的行以后,才会知道行数,所以加上这个提示以后,不管是否需要,MySQL都会扫描所有满足条件的行,然后再抛弃掉不需要的行,而不是在满足LIMIT的行数后就会终止扫描。所以该提示的代价可能非常高。一个更好的设计是将具体的页数换成"下一页"按钮,假设每页显示20条记录,那么我们每次查询时都是用LIMIT返回21条记录并只显示20条,如果第21条存在,那么我们就显示"下一页"按钮,否则就说明没有更多的数据,也就无须显示"下一页"按钮了。另一种做法时先获取并缓存较多的数据——例如,缓存1 000条——然后每次分页都从这个缓存中获取。这样做可以让应用程序根据结果集的大小采取不同的策略,如果结果集少于1000,就可以在页面上显示所有的分页链接,因为数据都在缓存中,所以这样做性能不会有问题。如果结果集大于1 000,则可以在页面上设计一个额外的"找到的结果多于1 000条"之类的按钮。这两种策略都比每次生成全部结果集再抛弃掉不需要的数据的效率要高很多。有时候也可以考虑使用EXPLAIN的结果中的rows列的值来作为结果集总数的近似值(实际上Google的搜索结果总数也是个近似值)。当需要精确结果的时候,再单独使用COUNT(*)来满足需求,这时如果能够使用索引覆盖扫描则通常也会比SQL_CALC_FOUND_ROWS块得多
分页的时候,另一个常用的技巧是在LIMIT语句中加上SQL_CALC_FOUND_ROWS提示(hint),这样就可以获得去掉LIMIT以后满足条件的行数,因此可以作为分页的总数。看起来,MySQL做了一些非常"高深"的优化,像是通过某种方法预测了总行数。但实际上,MySQL只有在扫描了所有满足条件的行以后,才会知道行数,所以加上这个提示以后,不管是否需要,MySQL都会扫描所有满足条件的行,然后再抛弃掉不需要的行,而不是在满足LIMIT的行数后就会终止扫描。所以该提示的代价可能非常高。一个更好的设计是将具体的页数换成"下一页"按钮,假设每页显示20条记录,那么我们每次查询时都是用LIMIT返回21条记录并只显示20条,如果第21条存在,那么我们就显示"下一页"按钮,否则就说明没有更多的数据,也就无须显示"下一页"按钮了。另一种做法时先获取并缓存较多的数据——例如,缓存1 000条——然后每次分页都从这个缓存中获取。这样做可以让应用程序根据结果集的大小采取不同的策略,如果结果集少于1000,就可以在页面上显示所有的分页链接,因为数据都在缓存中,所以这样做性能不会有问题。如果结果集大于1 000,则可以在页面上设计一个额外的"找到的结果多于1 000条"之类的按钮。这两种策略都比每次生成全部结果集再抛弃掉不需要的数据的效率要高很多。有时候也可以考虑使用EXPLAIN的结果中的rows列的值来作为结果集总数的近似值(实际上Google的搜索结果总数也是个近似值)。当需要精确结果的时候,再单独使用COUNT(*)来满足需求,这时如果能够使用索引覆盖扫描则通常也会比SQL_CALC_FOUND_ROWS块得多
优化UNION查询。
MySQL总是通过创建并填充临时表的方式来执行UNION查询。因此很多优化策略在UNION查询中都没法很好地使用。经常需要手工地将WHERE、LIMIT、ORDER BY等子句“下推”到UNION的各个子查询中,以便优化器可以充分利用这些条件进行优化(例如,直接将这些子句冗余地写一份到各个子查询)。除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,这一点很重要。如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这回导致对整个临时表的数据做唯一性检查。这样做的代价非常高。即使有ALL关键字,MySQL仍然会用临时表存储结果。事实上,MySQL总是将结果放入临时表,然后再独处,再返回给客户端。虽然很多时候这样做是没有必要的(例如,MySQL可以直接把这些结果返回给客户端)
MySQL总是通过创建并填充临时表的方式来执行UNION查询。因此很多优化策略在UNION查询中都没法很好地使用。经常需要手工地将WHERE、LIMIT、ORDER BY等子句“下推”到UNION的各个子查询中,以便优化器可以充分利用这些条件进行优化(例如,直接将这些子句冗余地写一份到各个子查询)。除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,这一点很重要。如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这回导致对整个临时表的数据做唯一性检查。这样做的代价非常高。即使有ALL关键字,MySQL仍然会用临时表存储结果。事实上,MySQL总是将结果放入临时表,然后再独处,再返回给客户端。虽然很多时候这样做是没有必要的(例如,MySQL可以直接把这些结果返回给客户端)
静态查询分析。
Percona Toolkit中的pt-query-advisor能够解析查询日志、分析查询模式,然后给出所有可能存在潜在问题的查询,并给出足够详细的建议。这像是给MySQL所有的查询做一次全面的健康检查。它能检测出许多常见的问题
Percona Toolkit中的pt-query-advisor能够解析查询日志、分析查询模式,然后给出所有可能存在潜在问题的查询,并给出足够详细的建议。这像是给MySQL所有的查询做一次全面的健康检查。它能检测出许多常见的问题
使用用户自定义变量。
用户自定义变量是一个容易被遗忘的MySQL特性,但是如果能够用好,发挥其潜力,再某些场景可以写出非常高效的查询语句。在查询中混合使用过程话和关系化逻辑的时候,自定义变量可能会非常有用。单纯的关系查询将所有的东西都当成无序的数据集合,并且一次性操作它们。MySQL则采用了更加程序化的处理方式。MySQL的这种方式有它的弱点,但如果能熟练地掌握,则会发现其强大之处,而用户自定义变量也可以给这种方式带来很大的帮助。用户自定义变量是一个用来存储内容的临时容器,在连接MySQL的整个过程中都存在,可以使用下面的SET和SELECT语句来定义它们
```sql
mysql> SET @one :=1;
Query OK, 0 rows affected (18.96 sec)
mysql> SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
Query OK, 0 rows affected (0.02 sec)
mysql> SELECT @last_week := CURRENT_DATE - INTERVAL 1 WEEK;
```
然后可以在任何可以使用表达式的地方使用这些自定义变量:
```sql
mysql> SELECT ... WHERE col <= @last_week;
```
在了解自定义变量的强大之前,我们再看看它自身的一些属性和限制,看可能在哪些场景下我们不能使用用户自定义变量:
1.使用自定义变量的查询,无法使用查询缓存
2.不能再使用常量或者标识符的地方使用自定义变量,例如表明、列明和LIMIT子句中
3.用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通信
4.如果使用连接池或者持久化连接,自定义变量可能让看起来毫无关系的代码发生交互(如果是这样,通常是代码bug或者连接池bug,这类情况确实可能发生)
5.在5.0之前的版本,是大小写敏感的,所以要注意代码在不同MySQL版本间的兼容性问题
6.不能显式地声明自定义变量的类型。确定未定义变量的具体类型的时机在不同MySQL版本中也可能不一样。如果你希望变量是整数类型,那么最好在初始化的时候就赋值为0,如果希望是浮点型则赋值为0.0,如果希望是字符串则赋值为'',用户自定义变量的类型在赋值的时候会改变。MySQL的用户自定义变量是一个动态类型
7.MySQL优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想的方式运行
8.赋值的顺序和赋值的时间点并不总是固定的,这依赖于优化器的决定。实际情况可能很让人困惑
9.赋值符号:=的优先级非常低,所以需要注意,赋值赋值表达式应该使用明确的括号
10.使用未定义变量不会产生任何语法错误,如果没有意识到这一点,非常容易犯错
用户自定义变量是一个容易被遗忘的MySQL特性,但是如果能够用好,发挥其潜力,再某些场景可以写出非常高效的查询语句。在查询中混合使用过程话和关系化逻辑的时候,自定义变量可能会非常有用。单纯的关系查询将所有的东西都当成无序的数据集合,并且一次性操作它们。MySQL则采用了更加程序化的处理方式。MySQL的这种方式有它的弱点,但如果能熟练地掌握,则会发现其强大之处,而用户自定义变量也可以给这种方式带来很大的帮助。用户自定义变量是一个用来存储内容的临时容器,在连接MySQL的整个过程中都存在,可以使用下面的SET和SELECT语句来定义它们
```sql
mysql> SET @one :=1;
Query OK, 0 rows affected (18.96 sec)
mysql> SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
Query OK, 0 rows affected (0.02 sec)
mysql> SELECT @last_week := CURRENT_DATE - INTERVAL 1 WEEK;
```
然后可以在任何可以使用表达式的地方使用这些自定义变量:
```sql
mysql> SELECT ... WHERE col <= @last_week;
```
在了解自定义变量的强大之前,我们再看看它自身的一些属性和限制,看可能在哪些场景下我们不能使用用户自定义变量:
1.使用自定义变量的查询,无法使用查询缓存
2.不能再使用常量或者标识符的地方使用自定义变量,例如表明、列明和LIMIT子句中
3.用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通信
4.如果使用连接池或者持久化连接,自定义变量可能让看起来毫无关系的代码发生交互(如果是这样,通常是代码bug或者连接池bug,这类情况确实可能发生)
5.在5.0之前的版本,是大小写敏感的,所以要注意代码在不同MySQL版本间的兼容性问题
6.不能显式地声明自定义变量的类型。确定未定义变量的具体类型的时机在不同MySQL版本中也可能不一样。如果你希望变量是整数类型,那么最好在初始化的时候就赋值为0,如果希望是浮点型则赋值为0.0,如果希望是字符串则赋值为'',用户自定义变量的类型在赋值的时候会改变。MySQL的用户自定义变量是一个动态类型
7.MySQL优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想的方式运行
8.赋值的顺序和赋值的时间点并不总是固定的,这依赖于优化器的决定。实际情况可能很让人困惑
9.赋值符号:=的优先级非常低,所以需要注意,赋值赋值表达式应该使用明确的括号
10.使用未定义变量不会产生任何语法错误,如果没有意识到这一点,非常容易犯错
优化排名语句。
使用用户自定义变量的一个特性是你可以在给一个变量赋值的同时使用这个变量,换句话说,用户自定义变量的赋值具有"左值"特性。下面的例子展示了如何使用变量来实现一个类似"行号(row number)"的功能:
```sql
mysql> SET @rownum := 0;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT actor_id, @rownum := @rownum + 1 AS rownum FROM sakila.actor ORDER BY actor_id ASC LIMIT 3;
+----------+--------+
| actor_id | rownum |
+----------+--------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+----------+--------+
3 rows in set (0.01 sec)
```
这个例子的实际意义不打,它只是实现了一个和该主键一样的列。不过,我们也可以把这当作是一个排名。现在我们来看一个更复杂的用法。我们先编写一个查询获取演过最多电影的前10位演员,然后根据它们的出演电影次数做一个排名,如果出演的电影数量一样,则排名相同,我们先编写一个查询,返回每隔演员参演电影的数量:
```sql
mysql> SELECT actor_id,COUNT(*) AS cnt
-> FROM sakila.film_actor
-> GROUP BY actor_id
-> ORDER BY cnt DESC
-> LIMIT 10;
+----------+-----+
| actor_id | cnt |
+----------+-----+
| 107 | 42 |
| 102 | 41 |
| 198 | 40 |
| 181 | 39 |
| 23 | 37 |
| 81 | 36 |
| 60 | 35 |
| 13 | 35 |
| 158 | 35 |
| 144 | 35 |
+----------+-----+
10 rows in set (0.00 sec)
```
使用用户自定义变量的一个特性是你可以在给一个变量赋值的同时使用这个变量,换句话说,用户自定义变量的赋值具有"左值"特性。下面的例子展示了如何使用变量来实现一个类似"行号(row number)"的功能:
```sql
mysql> SET @rownum := 0;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT actor_id, @rownum := @rownum + 1 AS rownum FROM sakila.actor ORDER BY actor_id ASC LIMIT 3;
+----------+--------+
| actor_id | rownum |
+----------+--------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+----------+--------+
3 rows in set (0.01 sec)
```
这个例子的实际意义不打,它只是实现了一个和该主键一样的列。不过,我们也可以把这当作是一个排名。现在我们来看一个更复杂的用法。我们先编写一个查询获取演过最多电影的前10位演员,然后根据它们的出演电影次数做一个排名,如果出演的电影数量一样,则排名相同,我们先编写一个查询,返回每隔演员参演电影的数量:
```sql
mysql> SELECT actor_id,COUNT(*) AS cnt
-> FROM sakila.film_actor
-> GROUP BY actor_id
-> ORDER BY cnt DESC
-> LIMIT 10;
+----------+-----+
| actor_id | cnt |
+----------+-----+
| 107 | 42 |
| 102 | 41 |
| 198 | 40 |
| 181 | 39 |
| 23 | 37 |
| 81 | 36 |
| 60 | 35 |
| 13 | 35 |
| 158 | 35 |
| 144 | 35 |
+----------+-----+
10 rows in set (0.00 sec)
```
现在我们再把排名加上去,这里看到有四名演员都参演了35部电影,所以它们的排名应该是相同的。我们使用三个变量来实现:一个用来记录当前的排名,一个用来记录前一个演员的排名,还有一个用来记录当前演员参演的电影数量。只有当前演员参演的电影的数量和前一个演员不同时,排名才变化。我们先试试下面的写法:
```sql
mysql> SELECT actor_id,
-> @curr_cnt :=COUNT(*) AS cnt,
-> @rank :=IF(@prev_cnt <> @curr_cnt, @rank +1, @rank) AS rank,
-> @prev_cnt := @curr_cnt AS dummy
-> FROM sakila.film_actor
-> GROUP BY actor_id
-> ORDER BY cnt DESC
-> LIMIT 10;
+----------+-----+------+-------+
| actor_id | cnt | rank | dummy |
+----------+-----+------+-------+
| 107 | 42 | 0 | 0 |
| 102 | 41 | 0 | 0 |
| 198 | 40 | 0 | 0 |
| 181 | 39 | 0 | 0 |
| 23 | 37 | 0 | 0 |
| 81 | 36 | 0 | 0 |
| 106 | 35 | 0 | 0 |
| 60 | 35 | 0 | 0 |
| 13 | 35 | 0 | 0 |
| 37 | 35 | 0 | 0 |
+----------+-----+------+-------+
10 rows in set (0.00 sec)
```
Oops——排名和统计列一直都无法更新,这是什么原因?对于这类问题,是没法给出一个放之四海皆准的答案的,例如,一个变量名的拼写错误就可鞥导致这样的问题(这个案例中并不是这个原因),具体问题要具体分析。这里,通过EXPLAIN我们看到将会使用临时表和文件排序,所以可能是由于变量赋值的时间和我们预料的不同。在使用用户自定义变量的时候,经常会遇到一些"诡异"的现象,要揪出这些问题的原因通常都不容易,但是相比其带来的好处,深究这些问题是值得的。使用SQL语句生成排名值通常需要做两次计算,例如,需要额外计算一次出演过相同数量电影的演员有哪些。使用变量则可一次完成——这对性能是一个很大的提升。针对这个案例,另一个简单的方案是在FROM子句中使用子查询生成一个中间的临时表:
```sql
mysql> SELECT actor_id, @curr_cnt :=cnt AS cnt, @rank:= IF(@prev_cnt <> @curr_cnt, @rank +1, @rank + 1) AS rank, @prev_cnt := @curr_cnt AS dummy FROM ( SELECT actor_id, COUNT(*)
AS cnt FROM sakila.film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10 ) as der;
+----------+-----+------+-------+
| actor_id | cnt | rank | dummy |
+----------+-----+------+-------+
| 107 | 42 | 1 | 42 |
| 102 | 41 | 2 | 41 |
| 198 | 40 | 3 | 40 |
| 181 | 39 | 4 | 39 |
| 23 | 37 | 5 | 37 |
| 81 | 36 | 6 | 36 |
| 60 | 35 | 7 | 35 |
| 13 | 35 | 8 | 35 |
| 158 | 35 | 9 | 35 |
| 144 | 35 | 10 | 35 |
+----------+-----+------+-------+
10 rows in set (0.01 sec)
```
```sql
mysql> SELECT actor_id,
-> @curr_cnt :=COUNT(*) AS cnt,
-> @rank :=IF(@prev_cnt <> @curr_cnt, @rank +1, @rank) AS rank,
-> @prev_cnt := @curr_cnt AS dummy
-> FROM sakila.film_actor
-> GROUP BY actor_id
-> ORDER BY cnt DESC
-> LIMIT 10;
+----------+-----+------+-------+
| actor_id | cnt | rank | dummy |
+----------+-----+------+-------+
| 107 | 42 | 0 | 0 |
| 102 | 41 | 0 | 0 |
| 198 | 40 | 0 | 0 |
| 181 | 39 | 0 | 0 |
| 23 | 37 | 0 | 0 |
| 81 | 36 | 0 | 0 |
| 106 | 35 | 0 | 0 |
| 60 | 35 | 0 | 0 |
| 13 | 35 | 0 | 0 |
| 37 | 35 | 0 | 0 |
+----------+-----+------+-------+
10 rows in set (0.00 sec)
```
Oops——排名和统计列一直都无法更新,这是什么原因?对于这类问题,是没法给出一个放之四海皆准的答案的,例如,一个变量名的拼写错误就可鞥导致这样的问题(这个案例中并不是这个原因),具体问题要具体分析。这里,通过EXPLAIN我们看到将会使用临时表和文件排序,所以可能是由于变量赋值的时间和我们预料的不同。在使用用户自定义变量的时候,经常会遇到一些"诡异"的现象,要揪出这些问题的原因通常都不容易,但是相比其带来的好处,深究这些问题是值得的。使用SQL语句生成排名值通常需要做两次计算,例如,需要额外计算一次出演过相同数量电影的演员有哪些。使用变量则可一次完成——这对性能是一个很大的提升。针对这个案例,另一个简单的方案是在FROM子句中使用子查询生成一个中间的临时表:
```sql
mysql> SELECT actor_id, @curr_cnt :=cnt AS cnt, @rank:= IF(@prev_cnt <> @curr_cnt, @rank +1, @rank + 1) AS rank, @prev_cnt := @curr_cnt AS dummy FROM ( SELECT actor_id, COUNT(*)
AS cnt FROM sakila.film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10 ) as der;
+----------+-----+------+-------+
| actor_id | cnt | rank | dummy |
+----------+-----+------+-------+
| 107 | 42 | 1 | 42 |
| 102 | 41 | 2 | 41 |
| 198 | 40 | 3 | 40 |
| 181 | 39 | 4 | 39 |
| 23 | 37 | 5 | 37 |
| 81 | 36 | 6 | 36 |
| 60 | 35 | 7 | 35 |
| 13 | 35 | 8 | 35 |
| 158 | 35 | 9 | 35 |
| 144 | 35 | 10 | 35 |
+----------+-----+------+-------+
10 rows in set (0.01 sec)
```
避免重复查询刚刚更新的数据。
如果在更新行的同时又希望获得该行的信息,要怎么做才能避免重复的查询呢?不幸的是,MySQL并不支持像PostgreSQL那样的UPDATE RETURNING语法,这个语法可以帮你在更新行的时候同时返回该行的信息。还好在MySQL中你可以使用变量来解决这个问题。例如,一个用户希望能够更高效地更新一条记录地时间戳,同时希望查询当前记录中存放地时间戳是什么。简单地,可以用下面地代码来实现:
```sql
UPDATE t1 SET lastUpdated = NO() WHERE id =1;
SELECT lastUpdated FROM t1 WHERE id = 1;
```
使用变量,我们可以按如下方式重写查询:
```sql
UDPATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now:=NOW();
```
上面查询看起来仍然需要两个查询,需要两次网络来回,但是这里的第二个查询无须访问任何数据表,所以会快非常多。(如果网络延迟非常大,那么这个优化的意义可能不大,不过这对这个用户,这样做的效果很好)
如果在更新行的同时又希望获得该行的信息,要怎么做才能避免重复的查询呢?不幸的是,MySQL并不支持像PostgreSQL那样的UPDATE RETURNING语法,这个语法可以帮你在更新行的时候同时返回该行的信息。还好在MySQL中你可以使用变量来解决这个问题。例如,一个用户希望能够更高效地更新一条记录地时间戳,同时希望查询当前记录中存放地时间戳是什么。简单地,可以用下面地代码来实现:
```sql
UPDATE t1 SET lastUpdated = NO() WHERE id =1;
SELECT lastUpdated FROM t1 WHERE id = 1;
```
使用变量,我们可以按如下方式重写查询:
```sql
UDPATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now:=NOW();
```
上面查询看起来仍然需要两个查询,需要两次网络来回,但是这里的第二个查询无须访问任何数据表,所以会快非常多。(如果网络延迟非常大,那么这个优化的意义可能不大,不过这对这个用户,这样做的效果很好)
统计更新和插入的数量
当使用了INSERT ON DUPLICATE KEY UPDATE的时候,如果想知道到底插入了多少行数据,到底有多少数据是因为冲突而改写成更新操作的?Kerstian Kohntopp在他的博客上给出了一个解决这个问题的办法,实现办法的本质如下:
```sql
INSERT INTO t1 (c1, c2) VALUES(4,4),(2,1), (3,1)
ON DUPLICATE KEY UPDATE
c1 = VALUES(c1) + () * (@x:=@x+1));
```
当每次由于冲突导致更新时对变量@x自增一次。然后通过对这个表达式乘以0来让其不影响要更新的内容。另外,MySQL的协议会返回被更改的总行数,所以不需要单独统计这个值
当使用了INSERT ON DUPLICATE KEY UPDATE的时候,如果想知道到底插入了多少行数据,到底有多少数据是因为冲突而改写成更新操作的?Kerstian Kohntopp在他的博客上给出了一个解决这个问题的办法,实现办法的本质如下:
```sql
INSERT INTO t1 (c1, c2) VALUES(4,4),(2,1), (3,1)
ON DUPLICATE KEY UPDATE
c1 = VALUES(c1) + () * (@x:=@x+1));
```
当每次由于冲突导致更新时对变量@x自增一次。然后通过对这个表达式乘以0来让其不影响要更新的内容。另外,MySQL的协议会返回被更改的总行数,所以不需要单独统计这个值
确定取值的顺序
使用用户自定义变量的一个最常见的问题就是没有注意到在赋值和读取变量的时候可能是在查询的不同的阶段。例如,在SELECT子句中进行赋值然后在WHERE子句中读取变量,则可能变量取值并不如你所想。下面的查询看起来只返回一个结果,但事实并非如此:
```sql
mysql> SET @rownum := 0;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt
-> FROM sakila.actor
-> WHERE @rownum <= 1;
+----------+------+
| actor_id | cnt |
+----------+------+
| 58 | 1 |
| 92 | 2 |
+----------+------+
2 rows in set (0.00 sec)
```
因为WHERE和SELECT是在查询执行的不同阶段被执行的。如果在查询中再加入ORDER BY的化,结果可能会更不同:
```sql
mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt FROM sakila.actor WHERE @rownum <= 1 ORDER BY first_name;
```
这是因为ORDER BY 引入了文件排序,而WHERE条件是文件排序操作之前取值的,所以这条查询会返回表中的全部记录。解决这个问题的办法是让变量的赋值和取值发生在执行查询的同一阶段:
```sql
mysql> SET @rownum :=0;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT actor_id, @rownum AS rownum
-> FROM sakila.actor
-> WHERE (@rownum := @rownum + 1) <= 1;
+----------+--------+
| actor_id | rownum |
+----------+--------+
| 58 | 1 |
+----------+--------+
1 row in set (0.00 sec)
```
使用用户自定义变量的一个最常见的问题就是没有注意到在赋值和读取变量的时候可能是在查询的不同的阶段。例如,在SELECT子句中进行赋值然后在WHERE子句中读取变量,则可能变量取值并不如你所想。下面的查询看起来只返回一个结果,但事实并非如此:
```sql
mysql> SET @rownum := 0;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt
-> FROM sakila.actor
-> WHERE @rownum <= 1;
+----------+------+
| actor_id | cnt |
+----------+------+
| 58 | 1 |
| 92 | 2 |
+----------+------+
2 rows in set (0.00 sec)
```
因为WHERE和SELECT是在查询执行的不同阶段被执行的。如果在查询中再加入ORDER BY的化,结果可能会更不同:
```sql
mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt FROM sakila.actor WHERE @rownum <= 1 ORDER BY first_name;
```
这是因为ORDER BY 引入了文件排序,而WHERE条件是文件排序操作之前取值的,所以这条查询会返回表中的全部记录。解决这个问题的办法是让变量的赋值和取值发生在执行查询的同一阶段:
```sql
mysql> SET @rownum :=0;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT actor_id, @rownum AS rownum
-> FROM sakila.actor
-> WHERE (@rownum := @rownum + 1) <= 1;
+----------+--------+
| actor_id | rownum |
+----------+--------+
| 58 | 1 |
+----------+--------+
1 row in set (0.00 sec)
```
小测试:如果在上面再加入ORDER BY ,那会返回什么结果?试试看吧,如果得出的结果出乎你的意料,想想为什么?再看下面这个查询会返回什么,下面的查询中ORDER BY子句会改变变量值,那WHERE语句执行时变量是多少。
```sql
mysql> SELECT actor_id, first_name, @rownum AS rownum FROM sakila.actor WHERE @rownum <= 1 ORDER BY first_name, LEAST(0, @rownum := @rownum + 1);
+----------+------------+--------+
| actor_id | first_name | rownum |
+----------+------------+--------+
| 2 | NICK | 2 |
| 1 | PENELOPE | 1 |
+----------+------------+--------+
2 rows in set (0.01 sec)
```
这个最出人意料的变量行为的答案可以在EXPLAIN语句中找到,注意看在Extra列中的"Using where"、"Using temporary"或者"Using filesort".
在上面的最后一个例子中,我们引入了一个新的技巧:我们将赋值语句放到LEAST()函数中,这样就可以在完全不改变排序顺序的时候完成赋值操作(在上面例子中,LEAST()函数总是返回0).这个技巧在不希望对子句的执行结果有影响却又要完成变量赋值的时候很有用。这个例子中,无须在返回值中新增额外列。这样的函数还有GREATEST()、LENGTH()、ISNULL()、NULLIFL()、IF()和COALESCE(),可以单独使用也可以组合使用。例如,COALESCE()可以在一组参数中取第一个已经被定义的变量/
```sql
mysql> SELECT actor_id, first_name, @rownum AS rownum FROM sakila.actor WHERE @rownum <= 1 ORDER BY first_name, LEAST(0, @rownum := @rownum + 1);
+----------+------------+--------+
| actor_id | first_name | rownum |
+----------+------------+--------+
| 2 | NICK | 2 |
| 1 | PENELOPE | 1 |
+----------+------------+--------+
2 rows in set (0.01 sec)
```
这个最出人意料的变量行为的答案可以在EXPLAIN语句中找到,注意看在Extra列中的"Using where"、"Using temporary"或者"Using filesort".
在上面的最后一个例子中,我们引入了一个新的技巧:我们将赋值语句放到LEAST()函数中,这样就可以在完全不改变排序顺序的时候完成赋值操作(在上面例子中,LEAST()函数总是返回0).这个技巧在不希望对子句的执行结果有影响却又要完成变量赋值的时候很有用。这个例子中,无须在返回值中新增额外列。这样的函数还有GREATEST()、LENGTH()、ISNULL()、NULLIFL()、IF()和COALESCE(),可以单独使用也可以组合使用。例如,COALESCE()可以在一组参数中取第一个已经被定义的变量/
编写偷懒的UNIO。
假设需要编写一个UNION查询,其第一个子查询作为分支条件先执行,如果找到了匹配的行,则跳过第二个分支。在某些业务场景中确实会有这样的需求,比如先在一个频繁访问的表中查找"热"数据,找不到再去另外一个较少访问的表中查找"冷"数据(区分热数据和冷叔是一个很好的提高缓存命中率的方法)。
下面的查询会在两个地方查找一个用户——一个主用户表,一个长事件不活跃的用户表,不活跃用户表的目的是为了实现更高效的归档(Baron认为在一些社交网站上归档一些常见不活跃用户后,用户重新回到网站时有这样的需求,当用户再次登录时,一方面我们需要将其从归档中重新拿出来,另外,还可以给他发送一份欢迎邮件。这对一些不活跃的用户是非常好的一个优化)
```sql
SELECT id FROM WHERE id = 123
UNION ALL
SELECT id FROM users_archived WHERE id = 123;
```
上面这个查询是可以正常工作的,但是即使在users表中找到了记录,上面的查询还是会去归档表user_archived中再查找一次。我们可以用一个偷懒的UNION查询来抑制这样的数据返回,而且只有当第一个表中没有数据时,我们才在第二个表中查询。一旦在第一个表中找到记录,我们就定义一个变量@found.我们通过在结果列中做一次赋值来实现,然后将赋值放在CREATEST中来避免返回额外的数据。为了明确我们的结果到底来自哪张表,我们新增了一个包含表名的列。最后我们需要在查询的末尾将变量重置为NULL。这样保证遍历时不会干扰后面的结果。完成的查询如下:
```sql
SELECT GREATEST(@found := -1, id) AS id, 'users' AS which_tbl
FROM users WHERE id = 1
UNION ALL
SELECT id, 'users_archived'
FROM users_archived WHERE id = 1 AND @found IS NULL
UNION ALL
SELECT 1, 'reset' FROM DUAL WHERE (@found := NULL) IS NOT NULL;
```
假设需要编写一个UNION查询,其第一个子查询作为分支条件先执行,如果找到了匹配的行,则跳过第二个分支。在某些业务场景中确实会有这样的需求,比如先在一个频繁访问的表中查找"热"数据,找不到再去另外一个较少访问的表中查找"冷"数据(区分热数据和冷叔是一个很好的提高缓存命中率的方法)。
下面的查询会在两个地方查找一个用户——一个主用户表,一个长事件不活跃的用户表,不活跃用户表的目的是为了实现更高效的归档(Baron认为在一些社交网站上归档一些常见不活跃用户后,用户重新回到网站时有这样的需求,当用户再次登录时,一方面我们需要将其从归档中重新拿出来,另外,还可以给他发送一份欢迎邮件。这对一些不活跃的用户是非常好的一个优化)
```sql
SELECT id FROM WHERE id = 123
UNION ALL
SELECT id FROM users_archived WHERE id = 123;
```
上面这个查询是可以正常工作的,但是即使在users表中找到了记录,上面的查询还是会去归档表user_archived中再查找一次。我们可以用一个偷懒的UNION查询来抑制这样的数据返回,而且只有当第一个表中没有数据时,我们才在第二个表中查询。一旦在第一个表中找到记录,我们就定义一个变量@found.我们通过在结果列中做一次赋值来实现,然后将赋值放在CREATEST中来避免返回额外的数据。为了明确我们的结果到底来自哪张表,我们新增了一个包含表名的列。最后我们需要在查询的末尾将变量重置为NULL。这样保证遍历时不会干扰后面的结果。完成的查询如下:
```sql
SELECT GREATEST(@found := -1, id) AS id, 'users' AS which_tbl
FROM users WHERE id = 1
UNION ALL
SELECT id, 'users_archived'
FROM users_archived WHERE id = 1 AND @found IS NULL
UNION ALL
SELECT 1, 'reset' FROM DUAL WHERE (@found := NULL) IS NOT NULL;
```
用户自定义变量的其他用处
不仅是在SELECT语句中,在其他任何类型的SQL语句中都可以对变量进行赋值。事实上,这也是用户自定义变量最大的用途。例如,可以像前面使用子查询的方式改进排名语句一样改进UPDATE语句。不过我们需要使用一些技巧来获得我们希望的效果。有时,优化器会把变量当作一个编译时常量来对待,而不是对其进行赋值。将函数放在类似于LEAST()这样的函数中通常可以避免这样的问题。另一个办法是在查询被执行前检查变量是否被赋值。不同的场景下使用不同的办法。通过一些实际,可以了解所有用户自定义变量能够做的有趣的事情,例如下面这些用法:
1.查询运行时计算总数和平均值
2.模拟GROUP 语句中的函数FIRST()和LAST()
3.对大量数据做一些数据计算
4.计算一个大表的MD5散列值
5.编写一个样本处理函数,当样本中的数值超过某个边界值的时候将其变成0
6.模拟读/写游标
7.在SHOW语句的WHERE子句中加入变量值
不仅是在SELECT语句中,在其他任何类型的SQL语句中都可以对变量进行赋值。事实上,这也是用户自定义变量最大的用途。例如,可以像前面使用子查询的方式改进排名语句一样改进UPDATE语句。不过我们需要使用一些技巧来获得我们希望的效果。有时,优化器会把变量当作一个编译时常量来对待,而不是对其进行赋值。将函数放在类似于LEAST()这样的函数中通常可以避免这样的问题。另一个办法是在查询被执行前检查变量是否被赋值。不同的场景下使用不同的办法。通过一些实际,可以了解所有用户自定义变量能够做的有趣的事情,例如下面这些用法:
1.查询运行时计算总数和平均值
2.模拟GROUP 语句中的函数FIRST()和LAST()
3.对大量数据做一些数据计算
4.计算一个大表的MD5散列值
5.编写一个样本处理函数,当样本中的数值超过某个边界值的时候将其变成0
6.模拟读/写游标
7.在SHOW语句的WHERE子句中加入变量值
案例学习。
通常我们要做的不是查询优化,不是库表结构优化,不是索引优化也不是应用设计优化——在实践中可能要面对所有这些搅和在一起的情况。
通常我们要做的不是查询优化,不是库表结构优化,不是索引优化也不是应用设计优化——在实践中可能要面对所有这些搅和在一起的情况。
使用MySQL构建一个对列表。
使用MySQL来实现对列表是一个取巧的做法,我们看到很多系统在高流量、高并发的情况下表现并不好。典型的模式是一个表包含多种类型的记录:未处理记录、已处理记录、正在处理记录等。一个或者多个消费者线程在表中查找未处理的记录,然后声称正在处理、当处理完成后,再将记录更新成已处理状态。一般的,例如邮件发送、多命令处理、评论修改等会使用类似模式。通常有两个原因使得大家认为这样的处理方式并不合适。第一,随者队列表越来越大和索引深度的增加,找到未处理记录的速度会随之变慢。你可以通过将队列表分成两部分来解决这个问题,就是将已处理记录归档或者存放到历史表,这可以始终保证队列表很小。第二,一般的处理过程分两步,先找到未处理记录然后加锁。找到记录会增加服务器的压力,而加锁操作则会让各个消费者线程增加竞争,因为这时一个串行化的操作。找到未处理记录一般来说都没问题,如果有问题则可以通过使用消息的方式来通知各个消费者。具体的,可以使用一个带有注释的SLEEP()函数做超时处理,如下:
```sql
SELECT /* wating on unsent_email */ SLEEP(10000);
```
这让线程一直阻塞,直到两个条件之一满足:10 000秒后超时,或者另一个线程使用KILL QUERY结束当前的SLEEP。因此,当再向队列表中新增一批数据后,可以通过SHOW PROCESSLIST,根据注释找到当前正在休眠的线程,并将其KILL。你可以使用函数GET_LOCK()和RELEASE_LOCK()来实现通知,或者可以在数据库之外实现,例如使用一个消息服务。最后需要解决的问题是如何让消费者标记正在处理的记录,而不至于让多个消费者重复处理一个记录。我们看到大家一般使用SELECT FOR UPDATE来实现。这通常是扩展性问题的根源,这会导致大量的事务阻塞并等待。
一般,我们要尽量避免使用SELECT FOR UPDATE。不光是队列表,任何情况下都要尽量避免。总是有别的更好的办法来实现你的目的。在队列表的案例中,可以直接使用UPDATE来更新记录,然后检查是否还有其他的记录需要处理。先建立如下的表:
```sql
CREATE TABLE unsent_emails (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`status` ENUM ( 'unset', 'claimed', 'sent' ),
`owner` INT UNSIGNED NOT NULL DEFAULT 0,
ts TIMESTAMP,
KEY ( owner,status, ts )
);
```
该表的列owner用来存储当前正在处理这个记录的连接ID,即由函数CONNECTION_ID()返回的ID。如果当前记录没有被任何消费者处理,则该值为0.我们经常看到的一个办法是,如下面所示的一次处理10条记录:
```sql
BEGIN;
SELECT
id
FROM
unsent_emails
WHERE
`owner` = 0
AND `status` = 'unsent'
LIMIT 10 FOR UPDATE;-- result : 123,456,789
UPDATE unsent_emails
SET STATUS = 'claimed',
`owner` = CONNECTION_ID()
WHERE
id IN ( 123, 456, 789 );
COMMIT;
```
看到这里的SELECT查询可以使用索引的两个列,因此理论上查找的效率应该更快。问题是,在上面两个查询之间的"间隙时间",这里的锁会让所有其他同样的查询全部被阻塞。所有的这样的查询将使用相同的索引,扫描索引相同的部分,所以很可能会被阻塞。如果该进程下面的写法,则会更加高效:
使用MySQL来实现对列表是一个取巧的做法,我们看到很多系统在高流量、高并发的情况下表现并不好。典型的模式是一个表包含多种类型的记录:未处理记录、已处理记录、正在处理记录等。一个或者多个消费者线程在表中查找未处理的记录,然后声称正在处理、当处理完成后,再将记录更新成已处理状态。一般的,例如邮件发送、多命令处理、评论修改等会使用类似模式。通常有两个原因使得大家认为这样的处理方式并不合适。第一,随者队列表越来越大和索引深度的增加,找到未处理记录的速度会随之变慢。你可以通过将队列表分成两部分来解决这个问题,就是将已处理记录归档或者存放到历史表,这可以始终保证队列表很小。第二,一般的处理过程分两步,先找到未处理记录然后加锁。找到记录会增加服务器的压力,而加锁操作则会让各个消费者线程增加竞争,因为这时一个串行化的操作。找到未处理记录一般来说都没问题,如果有问题则可以通过使用消息的方式来通知各个消费者。具体的,可以使用一个带有注释的SLEEP()函数做超时处理,如下:
```sql
SELECT /* wating on unsent_email */ SLEEP(10000);
```
这让线程一直阻塞,直到两个条件之一满足:10 000秒后超时,或者另一个线程使用KILL QUERY结束当前的SLEEP。因此,当再向队列表中新增一批数据后,可以通过SHOW PROCESSLIST,根据注释找到当前正在休眠的线程,并将其KILL。你可以使用函数GET_LOCK()和RELEASE_LOCK()来实现通知,或者可以在数据库之外实现,例如使用一个消息服务。最后需要解决的问题是如何让消费者标记正在处理的记录,而不至于让多个消费者重复处理一个记录。我们看到大家一般使用SELECT FOR UPDATE来实现。这通常是扩展性问题的根源,这会导致大量的事务阻塞并等待。
一般,我们要尽量避免使用SELECT FOR UPDATE。不光是队列表,任何情况下都要尽量避免。总是有别的更好的办法来实现你的目的。在队列表的案例中,可以直接使用UPDATE来更新记录,然后检查是否还有其他的记录需要处理。先建立如下的表:
```sql
CREATE TABLE unsent_emails (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`status` ENUM ( 'unset', 'claimed', 'sent' ),
`owner` INT UNSIGNED NOT NULL DEFAULT 0,
ts TIMESTAMP,
KEY ( owner,status, ts )
);
```
该表的列owner用来存储当前正在处理这个记录的连接ID,即由函数CONNECTION_ID()返回的ID。如果当前记录没有被任何消费者处理,则该值为0.我们经常看到的一个办法是,如下面所示的一次处理10条记录:
```sql
BEGIN;
SELECT
id
FROM
unsent_emails
WHERE
`owner` = 0
AND `status` = 'unsent'
LIMIT 10 FOR UPDATE;-- result : 123,456,789
UPDATE unsent_emails
SET STATUS = 'claimed',
`owner` = CONNECTION_ID()
WHERE
id IN ( 123, 456, 789 );
COMMIT;
```
看到这里的SELECT查询可以使用索引的两个列,因此理论上查找的效率应该更快。问题是,在上面两个查询之间的"间隙时间",这里的锁会让所有其他同样的查询全部被阻塞。所有的这样的查询将使用相同的索引,扫描索引相同的部分,所以很可能会被阻塞。如果该进程下面的写法,则会更加高效:
```sql
SET AUTOCOMMIT = 1;
COMMIT;
UPDATE unsent_emails
SET `status` = 'claimed', `owner` = CONNECTION_ID()
WHERE `owner` = 0 AND `status` = 'unsent'
LIMIT 10;
SET AUTOCOMMIT = 0;
SELECT id FROM unsent_emails
WHERE `owner`=CONNECTION_ID() AND `status` = 'claimed';
-- result:123,456,789
```
根本就无须使用SELECT查询去找到哪些记录还没有被处理。客户端的协议会告诉你更新了几条记录,所以可以直到这次需要处理多少条记录。所有的
SELECT FOR UPDATE都可以使用类似的办法改写。最后还需要处理一种特殊情况:那些正在被进程处理,而进程本身却由于某种原因退出的情况。这种情况处理起来很简单。你只需要定期运行UPDATE语句将它都更新成原始状态就可以了,然后执行SHOW PROCESSLIST,获取当前正在工作的线程ID,并使用一些WHERE条件避免取到那些刚开始处理的进程。假设我们获取的线程ID有(10、20、30),下面的更新语句会将处理时间超过10分钟的记录状态都更新成初始化状态:
```sql
UPDATE unsent_emails
SET `owner` = 0, `status` = 'unsent'
WHERE `owner` NOT IN (0,10,20,30) AND `status` = 'claimed'
AND ts < CURRENT_TIMESTAMP - INTERVAL 10 MINUTE;
```
另外,注意看看是如何巧妙地设计索引让这个查询更加高效的。因为我们将范围条件放在WHERE条件的末尾,这个查询恰好能够使用索引的全部列。其他的查询也都能用上这个索引,这就避免了再新增一个额外的索引来满足其他的查询。
这里我们将总结一下这个案例中的一些基础原则:
1.尽量少做事,可以的话就不要做任何事情。除非不得已,否则不要使用轮询,因为这会增加负载,而且还会带来很多低产出的工作
2.尽可能快地完成需要做的事情。尽量使用UPDATE 代替先SELECT FOR UPDATE再UPDATE的写法,因为事务提交的速度越快,持有的锁时间就越短,可以大大减少竞争和加速串行执行效率。将已经处理完成和未处理的数据分开,保证数据集足够小
3.这个案例的另一个启发是,某些查询是无法优化的;考虑使用不同的查询或者不同的策略去实现相同的目的。通常对于SELECT FOR UPDATE就需要这样处理。
有时,最好的办法就是将任务队列从数据库中迁移出来。Redis就是一个很好地队列容器,也可以使用memcached来实现。另一个选择是使用Q4M存储引擎,RabbitMQ等其他消息中间件也可以实现类似的功能
SET AUTOCOMMIT = 1;
COMMIT;
UPDATE unsent_emails
SET `status` = 'claimed', `owner` = CONNECTION_ID()
WHERE `owner` = 0 AND `status` = 'unsent'
LIMIT 10;
SET AUTOCOMMIT = 0;
SELECT id FROM unsent_emails
WHERE `owner`=CONNECTION_ID() AND `status` = 'claimed';
-- result:123,456,789
```
根本就无须使用SELECT查询去找到哪些记录还没有被处理。客户端的协议会告诉你更新了几条记录,所以可以直到这次需要处理多少条记录。所有的
SELECT FOR UPDATE都可以使用类似的办法改写。最后还需要处理一种特殊情况:那些正在被进程处理,而进程本身却由于某种原因退出的情况。这种情况处理起来很简单。你只需要定期运行UPDATE语句将它都更新成原始状态就可以了,然后执行SHOW PROCESSLIST,获取当前正在工作的线程ID,并使用一些WHERE条件避免取到那些刚开始处理的进程。假设我们获取的线程ID有(10、20、30),下面的更新语句会将处理时间超过10分钟的记录状态都更新成初始化状态:
```sql
UPDATE unsent_emails
SET `owner` = 0, `status` = 'unsent'
WHERE `owner` NOT IN (0,10,20,30) AND `status` = 'claimed'
AND ts < CURRENT_TIMESTAMP - INTERVAL 10 MINUTE;
```
另外,注意看看是如何巧妙地设计索引让这个查询更加高效的。因为我们将范围条件放在WHERE条件的末尾,这个查询恰好能够使用索引的全部列。其他的查询也都能用上这个索引,这就避免了再新增一个额外的索引来满足其他的查询。
这里我们将总结一下这个案例中的一些基础原则:
1.尽量少做事,可以的话就不要做任何事情。除非不得已,否则不要使用轮询,因为这会增加负载,而且还会带来很多低产出的工作
2.尽可能快地完成需要做的事情。尽量使用UPDATE 代替先SELECT FOR UPDATE再UPDATE的写法,因为事务提交的速度越快,持有的锁时间就越短,可以大大减少竞争和加速串行执行效率。将已经处理完成和未处理的数据分开,保证数据集足够小
3.这个案例的另一个启发是,某些查询是无法优化的;考虑使用不同的查询或者不同的策略去实现相同的目的。通常对于SELECT FOR UPDATE就需要这样处理。
有时,最好的办法就是将任务队列从数据库中迁移出来。Redis就是一个很好地队列容器,也可以使用memcached来实现。另一个选择是使用Q4M存储引擎,RabbitMQ等其他消息中间件也可以实现类似的功能
MySQL高级特性
外键约束。
InnoDB是目前MySQL中唯一支持外键的内置存储引擎,所以如果需要外键支持那选择就不多了。使用外键是有成本的。比如外键通常都要求每次在修改数据时都要在另一张表中多执行一次查找操作。虽然InnoDB强制外键使用索引,但还是无法消除这种约束检查的开销。如果外键列的选择性很低,则会导致一个非常大且选择性很低的索引。例如,在一个非常大的表上有status列,并希望限制这个状态列的取值,如果该列只能取三个值——虽然这个列本身很小,但是如果主键很大,那么这个索引就会很大——而且这个索引除了做这个外键限制,也没有任何其他的作用了。不过,在某些场景下,外键会提升一些性能。如果想确保两个相关表始终有一致的数据,那么使用外键比在应用程序中检查一致性的性能要高得多,此外,外键在相关数据得删除和更新上,也比在应用中维护要更高效,不过,外键维护操作是逐行进行得,所以这样得更新会比批量删除和更新要慢些。外键约束使得查询需要额外访问一些别的表,这也意味着需要额外的锁。如果向子表中写入一条记录,外键约束会让InnoDB检查对应的父表的记录,也就需要对父表对应记录进行加锁操作,来确保这条记录不会在这个事务完成之时就被删除了。这会导致额外的锁等待,甚至会导致一些死锁。因为没有直接访问这些表,所以这类死锁问题往往难以排查。有时,可以使用触发器来代替外键。对于相关数据的同时更新外键更合适,但是如果外键只是用作数值约束,那么触发器或者显式地限制取值会更好些。如果只是使用外键做约束,那通常在应用程序里实现该约束会更好。外键会带来很大的额外消耗。碰到过很多案例,在对性能进行剖析时发现外键约束就是瓶颈所在,删除外键后性能立即大幅提升。
InnoDB是目前MySQL中唯一支持外键的内置存储引擎,所以如果需要外键支持那选择就不多了。使用外键是有成本的。比如外键通常都要求每次在修改数据时都要在另一张表中多执行一次查找操作。虽然InnoDB强制外键使用索引,但还是无法消除这种约束检查的开销。如果外键列的选择性很低,则会导致一个非常大且选择性很低的索引。例如,在一个非常大的表上有status列,并希望限制这个状态列的取值,如果该列只能取三个值——虽然这个列本身很小,但是如果主键很大,那么这个索引就会很大——而且这个索引除了做这个外键限制,也没有任何其他的作用了。不过,在某些场景下,外键会提升一些性能。如果想确保两个相关表始终有一致的数据,那么使用外键比在应用程序中检查一致性的性能要高得多,此外,外键在相关数据得删除和更新上,也比在应用中维护要更高效,不过,外键维护操作是逐行进行得,所以这样得更新会比批量删除和更新要慢些。外键约束使得查询需要额外访问一些别的表,这也意味着需要额外的锁。如果向子表中写入一条记录,外键约束会让InnoDB检查对应的父表的记录,也就需要对父表对应记录进行加锁操作,来确保这条记录不会在这个事务完成之时就被删除了。这会导致额外的锁等待,甚至会导致一些死锁。因为没有直接访问这些表,所以这类死锁问题往往难以排查。有时,可以使用触发器来代替外键。对于相关数据的同时更新外键更合适,但是如果外键只是用作数值约束,那么触发器或者显式地限制取值会更好些。如果只是使用外键做约束,那通常在应用程序里实现该约束会更好。外键会带来很大的额外消耗。碰到过很多案例,在对性能进行剖析时发现外键约束就是瓶颈所在,删除外键后性能立即大幅提升。
全文索引。
通过数值比较、范围过滤等就可以完成绝大多数我们需要的查询了。但是,如果你希望通过关键字的匹配来进行查询过滤,那么就需要基于相似度的查询,而不是原来的精确数值比较。全文索引就是为这种场景设计的。全文索引有着自己独特的语法。没有索引也可以工作,如果有索引效率会更高。用于全文搜索的索引有着独特的结构,帮助这类查询找到匹配某些关键字的记录。
你可能没有在意过全文索引,不过至少应该对一种全文索引技术比较熟悉:互联网搜索引擎。虽然这类搜索引擎的索引对象是超大量的数据,并且通过其背后都不是关系型数据库,不过全文索引的基本原理都是一样的。全文索引可以支持各种字符内容的搜索(包括CHAR、VARCHAR和TEXT类型),也支持自然语言搜索和布尔搜索。在MySQL中全文索引有很多的限制,其实现也很复杂,但是因为它是MySQL内置的功能,而且满足很多基本的搜索需求,所以它的应用仍然非常广泛。
在标准的MySQL中,只有MyISAM引擎支持全文索引。事实上,MyISAM对全文索引的支持有很多的限制,例如表级别锁对性能的影响、数据文件的崩溃、崩溃后的恢复等,这使得MyISAM的全文索引对于很多应用场景并不合适。所以,多数情况下建议使用别的解决方案,例如Sphinx、Lucene、Solr、Groonga、Xapian或者Senna。
MyISAM的全文索引作用对象是一个"集合",这可能是某个数据表的义列,也可能是多个列。具体的,对数据表的某一条记录,MySQL会将需要索引的列全部拼接成一个字符串,然后进行索引,MyISAM的全文索引是一类特殊的B-Tree索引,共有两层。第一层是所有关键字,然后对于每一个关键字的第二层,包含的是一组相关的"文档指针"。全文索引不会索引文档对象中的所有词语,它会根据如下规则过滤一些词语:
1.停用词列表中的词都不会被索引。默认的停用词根据通用英语的使用来设置,可以使用参数ft_stopword_file指定一组外部文件来使用自定义的停用词。
2.对于长度小于ft_min_word_len的词语和长度大于ft_max_word_len的词语,都不会被索引。
全文索引并不会存储关键字具体匹配在那一列,如果需要根据不同的列来进行组合查询,那么不需要针对每一列来建立多个这类索引。这也意味着不能在MATCH AGAINST子句中指定哪个列的相关行更重要。通常构建一个网站的搜索引擎是需要这样的功能,例如,你可能希望优先搜索出那些在标题中出现过的文档对象。如果需要这样的功能,则需要编写更复杂的查询语句。
通过数值比较、范围过滤等就可以完成绝大多数我们需要的查询了。但是,如果你希望通过关键字的匹配来进行查询过滤,那么就需要基于相似度的查询,而不是原来的精确数值比较。全文索引就是为这种场景设计的。全文索引有着自己独特的语法。没有索引也可以工作,如果有索引效率会更高。用于全文搜索的索引有着独特的结构,帮助这类查询找到匹配某些关键字的记录。
你可能没有在意过全文索引,不过至少应该对一种全文索引技术比较熟悉:互联网搜索引擎。虽然这类搜索引擎的索引对象是超大量的数据,并且通过其背后都不是关系型数据库,不过全文索引的基本原理都是一样的。全文索引可以支持各种字符内容的搜索(包括CHAR、VARCHAR和TEXT类型),也支持自然语言搜索和布尔搜索。在MySQL中全文索引有很多的限制,其实现也很复杂,但是因为它是MySQL内置的功能,而且满足很多基本的搜索需求,所以它的应用仍然非常广泛。
在标准的MySQL中,只有MyISAM引擎支持全文索引。事实上,MyISAM对全文索引的支持有很多的限制,例如表级别锁对性能的影响、数据文件的崩溃、崩溃后的恢复等,这使得MyISAM的全文索引对于很多应用场景并不合适。所以,多数情况下建议使用别的解决方案,例如Sphinx、Lucene、Solr、Groonga、Xapian或者Senna。
MyISAM的全文索引作用对象是一个"集合",这可能是某个数据表的义列,也可能是多个列。具体的,对数据表的某一条记录,MySQL会将需要索引的列全部拼接成一个字符串,然后进行索引,MyISAM的全文索引是一类特殊的B-Tree索引,共有两层。第一层是所有关键字,然后对于每一个关键字的第二层,包含的是一组相关的"文档指针"。全文索引不会索引文档对象中的所有词语,它会根据如下规则过滤一些词语:
1.停用词列表中的词都不会被索引。默认的停用词根据通用英语的使用来设置,可以使用参数ft_stopword_file指定一组外部文件来使用自定义的停用词。
2.对于长度小于ft_min_word_len的词语和长度大于ft_max_word_len的词语,都不会被索引。
全文索引并不会存储关键字具体匹配在那一列,如果需要根据不同的列来进行组合查询,那么不需要针对每一列来建立多个这类索引。这也意味着不能在MATCH AGAINST子句中指定哪个列的相关行更重要。通常构建一个网站的搜索引擎是需要这样的功能,例如,你可能希望优先搜索出那些在标题中出现过的文档对象。如果需要这样的功能,则需要编写更复杂的查询语句。
自然语言的全文索引。
自然语言搜索引擎将计算每一个文档对象和查询的相关度。这里,相关度是基于匹配的关键词个数,以及关键词在文档中出现的次数。在整个索引中出现次数越少的词语,匹配时的相关度就越高。相反,非常常见的单词将不会搜索,即使不再停用词列表中出现,如果一个词语在超过50%的记录中都出现了,那么自然语言搜索不会搜索这类词语(在测试使用的一个常见错误就是,只是用很小的数据结合进行全文索引,所以总是无法返回结果,原因在于,每隔搜索u干建祠都可能在一半以上的记录里面出现过)。
全文索引的语法和普通查询略有不同。可以根据WHERE子句中的MATCH AGAINST来区分查询是否使用全文索引。我们来看一个示例。在标准的数据库Sakila中,数据表film_text在字段title和description上建立了全文索引:如图所示。下面时一个使用自然语言搜索的查询:
```sql
mysql> SELECT film_id, title, RIGHT(description ,25),
-> MATCH (title, description) AGAINST('factory casualties') AS relevance
-> FROM sakila.film_text
-> WHERE MATCH(title, description) AGAINST('factory casualties');
+---------+-----------------------+---------------------------+-------------------+
| film_id | title | RIGHT(description ,25) | relevance |
+---------+-----------------------+---------------------------+-------------------+
| 831 | SPIRITED CASUALTIES | a Car in A Baloon Factory | 8.640907287597656 |
| 126 | CASUALTIES ENCINO | Face a Boy in A Monastery | 6.364917278289795 |
| 193 | CROSSROADS CASUALTIES | a Composer in The Outback | 6.364917278289795 |
| 3 | ADAPTATION HOLES | rjack in A Baloon Factory | 2.275989532470703 |
| 103 | BUCKET BROTHERHOOD | rjack in A Baloon Factory | 2.275989532470703 |
| 110 | CABIN FLASH | Shark in A Baloon Factory | 2.275989532470703 |
| 186 | CRAFT OUTFIELD | rator in A Baloon Factory | 2.275989532470703 |
| 187 | CRANES RESERVOIR | ogist in A Baloon Factory | 2.275989532470703 |
| 291 | EVOLUTION ALTER | lorer in A Baloon Factory | 2.275989532470703 |
| 299 | FACTORY DRAGON | jack in The Sahara Desert | 2.275989532470703 |
| 345 | GABLES METROPOLIS | Chef in A Baloon Factory | 2.275989532470703 |
| 365 | GOLD RIVER | ntist in A Baloon Factory | 2.275989532470703 |
| 369 | GOODFELLAS SALUTE | d Cow in A Baloon Factory | 2.275989532470703 |
| 370 | GORGEOUS BINGO | tress in A Baloon Factory | 2.275989532470703 |
```
MySQL将搜索词语分成两个独立的关键词进行搜索,搜索在title和description字段组成的全文索引上进行。注意,只有一条记录同时包含全部的两个关键词,查询结果时根据与关键词的相似度来进行排序的。(和普通查询不同,这类查询自动按照相似度进行排序。在使用全文索引进行排序的时候,MySQL无法再使用索引排序。所以如果不想使用文件排序的话,那么就不要在查询中使用ORDER BY 子句)。从上面的例子中可以看到,函数MATCH()将返回关键词匹配的相关度,是一个浮点数字。你可以根据相关度进行匹配,或者将词直接展现给用户。在一个查询中使用两次MATCH()函数并不会有额外的消耗,MySQL会自动识别并只进行一次搜索。不过,如果你将MATCH()函数放在ORDER BY 子句中,MySQL将会使用文件排序。在MATCH()函数中指定的列必须和在全文索引中指定的列完全相同,否则就无法啊使用全文索引。这是因为全文索引不会记录关键字是来自哪一列的。这也意味着无法使用完全索引来查询某个关键字是否在某一列上存在。这里介绍一个绕过该问题的办法:根据关键词在多个不同列的全文索引上的相关度来算出排名值,然后依次来排序。我们可以在某一列上加上如下索引:
```sql
mysql>ALTER TABLE film_text ADD FULLTEXT KEY(title);
```
这样,我们可以将title匹配乘以2来提高它的相似度的权重:
```sql
mysql> SELECT film_id, RIGHT ( description, 25 ), ROUND( MATCH ( title, description ) AGAINST ( 'factor casualties' ), 3 ) AS full_rel, ROUND( MATCH ( title ) AGAINST ( 'factory
casualties' ), 3 ) AS title_rel FROM sakila.film_text WHERE MATCH ( title, description ) AGAINST ( 'factory casualties' ) ORDER BY (2 * MATCH ( title ) AGAINST ( 'factory casualties' )) DESC;
+---------+---------------------------+----------+-----------+
| film_id | RIGHT ( description, 25 ) | full_rel | title_rel |
+---------+---------------------------+----------+-----------+
| 299 | jack in The Sahara Desert | 0.000 | 9.000 |
| 831 | a Car in A Baloon Factory | 6.365 | 6.365 |
| 126 | Face a Boy in A Monastery | 6.365 | 6.365 |
| 193 | a Composer in The Outback | 6.365 |
```
因为上面的查询需要做文件排序,所以这并不是一个高效的做法
自然语言搜索引擎将计算每一个文档对象和查询的相关度。这里,相关度是基于匹配的关键词个数,以及关键词在文档中出现的次数。在整个索引中出现次数越少的词语,匹配时的相关度就越高。相反,非常常见的单词将不会搜索,即使不再停用词列表中出现,如果一个词语在超过50%的记录中都出现了,那么自然语言搜索不会搜索这类词语(在测试使用的一个常见错误就是,只是用很小的数据结合进行全文索引,所以总是无法返回结果,原因在于,每隔搜索u干建祠都可能在一半以上的记录里面出现过)。
全文索引的语法和普通查询略有不同。可以根据WHERE子句中的MATCH AGAINST来区分查询是否使用全文索引。我们来看一个示例。在标准的数据库Sakila中,数据表film_text在字段title和description上建立了全文索引:如图所示。下面时一个使用自然语言搜索的查询:
```sql
mysql> SELECT film_id, title, RIGHT(description ,25),
-> MATCH (title, description) AGAINST('factory casualties') AS relevance
-> FROM sakila.film_text
-> WHERE MATCH(title, description) AGAINST('factory casualties');
+---------+-----------------------+---------------------------+-------------------+
| film_id | title | RIGHT(description ,25) | relevance |
+---------+-----------------------+---------------------------+-------------------+
| 831 | SPIRITED CASUALTIES | a Car in A Baloon Factory | 8.640907287597656 |
| 126 | CASUALTIES ENCINO | Face a Boy in A Monastery | 6.364917278289795 |
| 193 | CROSSROADS CASUALTIES | a Composer in The Outback | 6.364917278289795 |
| 3 | ADAPTATION HOLES | rjack in A Baloon Factory | 2.275989532470703 |
| 103 | BUCKET BROTHERHOOD | rjack in A Baloon Factory | 2.275989532470703 |
| 110 | CABIN FLASH | Shark in A Baloon Factory | 2.275989532470703 |
| 186 | CRAFT OUTFIELD | rator in A Baloon Factory | 2.275989532470703 |
| 187 | CRANES RESERVOIR | ogist in A Baloon Factory | 2.275989532470703 |
| 291 | EVOLUTION ALTER | lorer in A Baloon Factory | 2.275989532470703 |
| 299 | FACTORY DRAGON | jack in The Sahara Desert | 2.275989532470703 |
| 345 | GABLES METROPOLIS | Chef in A Baloon Factory | 2.275989532470703 |
| 365 | GOLD RIVER | ntist in A Baloon Factory | 2.275989532470703 |
| 369 | GOODFELLAS SALUTE | d Cow in A Baloon Factory | 2.275989532470703 |
| 370 | GORGEOUS BINGO | tress in A Baloon Factory | 2.275989532470703 |
```
MySQL将搜索词语分成两个独立的关键词进行搜索,搜索在title和description字段组成的全文索引上进行。注意,只有一条记录同时包含全部的两个关键词,查询结果时根据与关键词的相似度来进行排序的。(和普通查询不同,这类查询自动按照相似度进行排序。在使用全文索引进行排序的时候,MySQL无法再使用索引排序。所以如果不想使用文件排序的话,那么就不要在查询中使用ORDER BY 子句)。从上面的例子中可以看到,函数MATCH()将返回关键词匹配的相关度,是一个浮点数字。你可以根据相关度进行匹配,或者将词直接展现给用户。在一个查询中使用两次MATCH()函数并不会有额外的消耗,MySQL会自动识别并只进行一次搜索。不过,如果你将MATCH()函数放在ORDER BY 子句中,MySQL将会使用文件排序。在MATCH()函数中指定的列必须和在全文索引中指定的列完全相同,否则就无法啊使用全文索引。这是因为全文索引不会记录关键字是来自哪一列的。这也意味着无法使用完全索引来查询某个关键字是否在某一列上存在。这里介绍一个绕过该问题的办法:根据关键词在多个不同列的全文索引上的相关度来算出排名值,然后依次来排序。我们可以在某一列上加上如下索引:
```sql
mysql>ALTER TABLE film_text ADD FULLTEXT KEY(title);
```
这样,我们可以将title匹配乘以2来提高它的相似度的权重:
```sql
mysql> SELECT film_id, RIGHT ( description, 25 ), ROUND( MATCH ( title, description ) AGAINST ( 'factor casualties' ), 3 ) AS full_rel, ROUND( MATCH ( title ) AGAINST ( 'factory
casualties' ), 3 ) AS title_rel FROM sakila.film_text WHERE MATCH ( title, description ) AGAINST ( 'factory casualties' ) ORDER BY (2 * MATCH ( title ) AGAINST ( 'factory casualties' )) DESC;
+---------+---------------------------+----------+-----------+
| film_id | RIGHT ( description, 25 ) | full_rel | title_rel |
+---------+---------------------------+----------+-----------+
| 299 | jack in The Sahara Desert | 0.000 | 9.000 |
| 831 | a Car in A Baloon Factory | 6.365 | 6.365 |
| 126 | Face a Boy in A Monastery | 6.365 | 6.365 |
| 193 | a Composer in The Outback | 6.365 |
```
因为上面的查询需要做文件排序,所以这并不是一个高效的做法
布尔全文索引。
在布尔搜索中,用户可以查询中自定义某个被搜索的词语的相关性。布尔搜索通过停用词列表过滤掉那些"噪声"词,除此之外,布尔搜索还要求搜索关键词长度必须大于ft_min_word_len,同时小于ft_max_word_len(事实上,全文索引根本不会对太短或者太长的词语进行索引,但是这里说的不是一回事。一般地,MySQL本身并不会因为搜索关键词过长或过短而忽略这些词语,但是查询优化器的某些部分却可能这样做)。搜索返回的结果是未经排序的。当编写一个布尔搜索查询时,可以通过一些前缀修改时符来定制搜索,表中列出了最常用的修饰符。如表所示。还可以使用其他的操作,例如使用括号分组。基于此,就可以构造出一些复杂的搜索查询。还是继续使用sakila.film_text来举例,现在我们需要搜索既包含词"factory"又包含"casualties"的记录。在前面我们已经使用自然语言搜索查询实现找到这两个词中的任何一个SQL写法。使用布尔搜索查询,我们可以指定返回结果必须同时包含"factory"和"casualties":
```sql
mysql> SELECT film_id, title, RIGHT(description, 25) FROM sakila.film_text WHERE MATCH(title, description) AGAINST ('+factory +casualties' IN BOOLEAN MODE);
+---------+---------------------+---------------------------+
| film_id | title | RIGHT(description, 25) |
+---------+---------------------+---------------------------+
| 831 | SPIRITED CASUALTIES | a Car in A Baloon Factory |
+---------+---------------------+---------------------------+
1 row in set (0.00 sec)
```
查询中还可以使用括号进行"短语搜索",让返回结果精确匹配指定的短语:
```sql
mysql> SELECT film_id,title, RIGHT(description, 25) FROM sakila.film_text WHERE MATCH(title, description) AGAINST ('"spirited casualties"' IN BOOLEAN MODE);
+---------+---------------------+---------------------------+
| film_id | title | RIGHT(description, 25) |
+---------+---------------------+---------------------------+
| 831 | SPIRITED CASUALTIES | a Car in A Baloon Factory |
+---------+---------------------+---------------------------+
1 row in set (0.00 sec)
```
短语搜索的速度会比较慢。只使用全文索引是无法判断是否精确匹配短语的,通常还需要查询原文确定记录中是否包含完整的短语。由于需要进行回表过滤,所以速度会比较慢慢。要完成上面的查询,MySQL需要先从索引中找出所有同时包含"spirited"和"casualties"的索引条目,然后取出这些记录再判断是否精确匹配短语,因为这个操作会先从索引中过滤出一些记录,所以通常认为这样做的速度是很快的——比LIKE操作要快很多。事实上,这样做的确很快,但是搜索的关键词不能是太常见的词语。如果搜索的关键词太常见,因为前一步的过滤会返回太多的记录需要判断,因此LIKE操作反而更快。这种情况下LIKE操作是完全的顺序读,相比索引返回值的随机读。会快很多。只有MyISAM引擎才能使用布尔全文索引,但并不是一定要有全文索引才能使用布尔全文搜索。当没有全文搜索的时候,MySQL就通过全表扫描来实现。所以,你甚至还可以在多表上使用布尔全文索引,例如在一个关联结果上进行。只不过,因为是全表扫描速度可能会很慢
在布尔搜索中,用户可以查询中自定义某个被搜索的词语的相关性。布尔搜索通过停用词列表过滤掉那些"噪声"词,除此之外,布尔搜索还要求搜索关键词长度必须大于ft_min_word_len,同时小于ft_max_word_len(事实上,全文索引根本不会对太短或者太长的词语进行索引,但是这里说的不是一回事。一般地,MySQL本身并不会因为搜索关键词过长或过短而忽略这些词语,但是查询优化器的某些部分却可能这样做)。搜索返回的结果是未经排序的。当编写一个布尔搜索查询时,可以通过一些前缀修改时符来定制搜索,表中列出了最常用的修饰符。如表所示。还可以使用其他的操作,例如使用括号分组。基于此,就可以构造出一些复杂的搜索查询。还是继续使用sakila.film_text来举例,现在我们需要搜索既包含词"factory"又包含"casualties"的记录。在前面我们已经使用自然语言搜索查询实现找到这两个词中的任何一个SQL写法。使用布尔搜索查询,我们可以指定返回结果必须同时包含"factory"和"casualties":
```sql
mysql> SELECT film_id, title, RIGHT(description, 25) FROM sakila.film_text WHERE MATCH(title, description) AGAINST ('+factory +casualties' IN BOOLEAN MODE);
+---------+---------------------+---------------------------+
| film_id | title | RIGHT(description, 25) |
+---------+---------------------+---------------------------+
| 831 | SPIRITED CASUALTIES | a Car in A Baloon Factory |
+---------+---------------------+---------------------------+
1 row in set (0.00 sec)
```
查询中还可以使用括号进行"短语搜索",让返回结果精确匹配指定的短语:
```sql
mysql> SELECT film_id,title, RIGHT(description, 25) FROM sakila.film_text WHERE MATCH(title, description) AGAINST ('"spirited casualties"' IN BOOLEAN MODE);
+---------+---------------------+---------------------------+
| film_id | title | RIGHT(description, 25) |
+---------+---------------------+---------------------------+
| 831 | SPIRITED CASUALTIES | a Car in A Baloon Factory |
+---------+---------------------+---------------------------+
1 row in set (0.00 sec)
```
短语搜索的速度会比较慢。只使用全文索引是无法判断是否精确匹配短语的,通常还需要查询原文确定记录中是否包含完整的短语。由于需要进行回表过滤,所以速度会比较慢慢。要完成上面的查询,MySQL需要先从索引中找出所有同时包含"spirited"和"casualties"的索引条目,然后取出这些记录再判断是否精确匹配短语,因为这个操作会先从索引中过滤出一些记录,所以通常认为这样做的速度是很快的——比LIKE操作要快很多。事实上,这样做的确很快,但是搜索的关键词不能是太常见的词语。如果搜索的关键词太常见,因为前一步的过滤会返回太多的记录需要判断,因此LIKE操作反而更快。这种情况下LIKE操作是完全的顺序读,相比索引返回值的随机读。会快很多。只有MyISAM引擎才能使用布尔全文索引,但并不是一定要有全文索引才能使用布尔全文搜索。当没有全文搜索的时候,MySQL就通过全表扫描来实现。所以,你甚至还可以在多表上使用布尔全文索引,例如在一个关联结果上进行。只不过,因为是全表扫描速度可能会很慢
MySQL5.1中全文索引的变化。
在MySQL5.1中引入了一些和全文索引相关的改进,包括一些性能上的提升和新增插件式的解析,通过此用户可以自己定制增强搜索功能。例如,插件可以该笔那索引文本的方式。可以用更灵活的方式进行分词(例如,可以指定C++作为一个单独的词语)、预处理可以对不同的文档类型进行索引(如PDF),还可以做一些自定义的词干规则。插件还可以直接影响全文搜索的工作方式——例如,直接使用词干进行搜索。
在MySQL5.1中引入了一些和全文索引相关的改进,包括一些性能上的提升和新增插件式的解析,通过此用户可以自己定制增强搜索功能。例如,插件可以该笔那索引文本的方式。可以用更灵活的方式进行分词(例如,可以指定C++作为一个单独的词语)、预处理可以对不同的文档类型进行索引(如PDF),还可以做一些自定义的词干规则。插件还可以直接影响全文搜索的工作方式——例如,直接使用词干进行搜索。
全文索引的限制和替代方案。
MySQL的全文索引实现有很多的设计本身带来的限制。在某些场景下这些限制是致命的,不过也有很多办法绕过限制。例如,MySQL全文索引中只有一种判断相关性的方法:词频。索引也不会记录索引词在字符串中的位置,所以位置也就无法用在相关性上。虽然大多数情况下,尤其是数据量很小的时候,这些限制都不会影响使用,但也可能不是你锁想要的。而且MySQL的全文索引也没有提供其他可选的相关性排序算法(它无法存储基于相对位置的相关性排序数据)。数据量的大小也是一个问题。MySQL的全文索引只有全部在内存的时候,性能才非常好。如果内存无法装在全部索引,那么搜索速度可能会非常慢。当你使用精确短语搜索时,想要好的性能,数据和索引都需要在内存中。相比其他的索引类型,当INSERT、UPDATE和DELETE操作进行时,全文索引的操作代价都很大:
1.修改一段文本中的100个单词,需要100词索引操作,而不是一次
2.一半来说列长度并不会太影响其他的索引类型,但是如果是全文索引,三个单词的文本和10 000个单词的文本,性能可能会相差几个数量级
3.全文索引会有更多的碎片,可能需要做跟更多的OPTIMIZE TABLE操作
全文索引还会影响查询优化器的工作。索引选择、WHERE子句、ORDER BY都有可能不是按照你所预想的方式来工作:
1.如果查询中使用了MATCH AGAINST子句,而对应列上又有可用的全文索引,那么MySQL就一定会使用这个全文索引。这时,即使有其他列的索引可以使用,MySQL也不会比较到底哪个索引的性能更好。所以,即使这时有更合适的索引可以使用,MySQL仍然会置之不理
2.全文索引只能用作全文搜索匹配。任何其他操作,如WHERE条件比较,都必须在MySQL完成全文搜索返回记录后才能进行。这和其他普通索引不同,例如,在处理WHERE条件时,MySQL可以使用普通索引一次-判断多个表达式。
3.全文索引不存储索引列的实际值。也就不可能用作索引覆盖扫描
4.除了相关行排序,全文索引不能用作其他的排序。如果查询需要做相关性以外的排序操作,都需要使用文件排序。
让我们看看这些限制如何影响查询语句。来看一个例子,假设有一百万个文档记录,在文档的作者author字段上有一个普通的索引,在文档内容字段content上有全文索引。先在我们要搜索作者123,文档中又包含特定词语的文档。很多人可能会按照下面的方式来写查询语句
```sql
... WHERE MATCH(content) AGAINST('High Performance MySQL') AND author = 123;
```
而实际上,这样做的效率非常低。因为这里使用MATCH AGAINST,而且恰好上面有全文索引,所以MySQL优先选择使用全文索引,即先搜索所有的文档,查找是否有包含关键词的文档,然后返回记录看看作者是否是123.所以这里也就没有使用author字段上的索引。一个替代方案时将author列包含到全文索引中。可以在author列的值前面附上一个不常见的前缀,然后将这个带前缀的值存放到一个单独的filters列中,并单独维护该列(也许可以使用触发器来做维护工作)。
```sql
... WHERE MATCH(content, filters) AGAINST ('High Performance MySQL +author_id_123' IN BOOLEAN MODE);
``
这个案例中,如果author列的选择性非常高,那么MySQL能够根据作者信息很快地将需要过滤的文档记录限制在一个很小的范围内,这个查询的效率也就会非常耗。如果author列的选择性很低,那么这个替代方案的效率会比前面那个更糟,所以使用的时候要谨慎。全文索引有时候还可以实现一些简单的"边框"搜索。例如,希望搜索某个坐标范围时,将坐标按某种方式转换成文本再进行全文搜索。假设某条记录的坐标为X=123和Y=456.可以按照这样的方式交错存储坐标XY123456,然后对此进行全文搜索。这时,希望查询某矩形——X取值100至199,Y取值400至499——范围时,可以再查询直接搜索"+XY14*".这比使用WHERE条件过滤的效率要高很多。全文索引的另一个常用技巧时缓存全文索引返回的主键值,这在分页显示的时候经常使用。当应用程序真的需要输出结果时,才通过主键值将所有需要的数据返回。这个查询就可以自由地使用其他索引、或者自由地关联其他表。在早期版本中虽然只有MyISMA表支持全文索引,但是如果仍然希望使用InnoDB或其他引擎,可以将原表赋值到一个备库,再将备库上的表改成MyISAM并建上相应的全文索引。如果不希望再另一个服务器上完成查询,还可以对表进行垂直拆分,将需要索引的列放到一个单独的MyISAM表中。将需要索引的列额外地冗余再另一个MyISAM表中也是一个办法。再测试库sakila.film_text就是使用这个策略,这里使用触发器来维护这个表的数据。最后,你还可以使用一个包含内置全文索引的引擎,,如Lucene或者Sphinx。因为使用全文索引的时候,通常会返回大量结果并产生大量随机IO,如何和GROUP BY 一起使用的话,还需要通过临时表或者文件排序进行分组,性能会非常非常糟糕。这类查询通常只是希望查询分组后的前几名结果,所以一个有效的优化办法是对结果进行抽样而不是精确计算。例如,仅查询前面的
1 000条记录,进行分组并返回前几名的结果。
MySQL的全文索引实现有很多的设计本身带来的限制。在某些场景下这些限制是致命的,不过也有很多办法绕过限制。例如,MySQL全文索引中只有一种判断相关性的方法:词频。索引也不会记录索引词在字符串中的位置,所以位置也就无法用在相关性上。虽然大多数情况下,尤其是数据量很小的时候,这些限制都不会影响使用,但也可能不是你锁想要的。而且MySQL的全文索引也没有提供其他可选的相关性排序算法(它无法存储基于相对位置的相关性排序数据)。数据量的大小也是一个问题。MySQL的全文索引只有全部在内存的时候,性能才非常好。如果内存无法装在全部索引,那么搜索速度可能会非常慢。当你使用精确短语搜索时,想要好的性能,数据和索引都需要在内存中。相比其他的索引类型,当INSERT、UPDATE和DELETE操作进行时,全文索引的操作代价都很大:
1.修改一段文本中的100个单词,需要100词索引操作,而不是一次
2.一半来说列长度并不会太影响其他的索引类型,但是如果是全文索引,三个单词的文本和10 000个单词的文本,性能可能会相差几个数量级
3.全文索引会有更多的碎片,可能需要做跟更多的OPTIMIZE TABLE操作
全文索引还会影响查询优化器的工作。索引选择、WHERE子句、ORDER BY都有可能不是按照你所预想的方式来工作:
1.如果查询中使用了MATCH AGAINST子句,而对应列上又有可用的全文索引,那么MySQL就一定会使用这个全文索引。这时,即使有其他列的索引可以使用,MySQL也不会比较到底哪个索引的性能更好。所以,即使这时有更合适的索引可以使用,MySQL仍然会置之不理
2.全文索引只能用作全文搜索匹配。任何其他操作,如WHERE条件比较,都必须在MySQL完成全文搜索返回记录后才能进行。这和其他普通索引不同,例如,在处理WHERE条件时,MySQL可以使用普通索引一次-判断多个表达式。
3.全文索引不存储索引列的实际值。也就不可能用作索引覆盖扫描
4.除了相关行排序,全文索引不能用作其他的排序。如果查询需要做相关性以外的排序操作,都需要使用文件排序。
让我们看看这些限制如何影响查询语句。来看一个例子,假设有一百万个文档记录,在文档的作者author字段上有一个普通的索引,在文档内容字段content上有全文索引。先在我们要搜索作者123,文档中又包含特定词语的文档。很多人可能会按照下面的方式来写查询语句
```sql
... WHERE MATCH(content) AGAINST('High Performance MySQL') AND author = 123;
```
而实际上,这样做的效率非常低。因为这里使用MATCH AGAINST,而且恰好上面有全文索引,所以MySQL优先选择使用全文索引,即先搜索所有的文档,查找是否有包含关键词的文档,然后返回记录看看作者是否是123.所以这里也就没有使用author字段上的索引。一个替代方案时将author列包含到全文索引中。可以在author列的值前面附上一个不常见的前缀,然后将这个带前缀的值存放到一个单独的filters列中,并单独维护该列(也许可以使用触发器来做维护工作)。
```sql
... WHERE MATCH(content, filters) AGAINST ('High Performance MySQL +author_id_123' IN BOOLEAN MODE);
``
这个案例中,如果author列的选择性非常高,那么MySQL能够根据作者信息很快地将需要过滤的文档记录限制在一个很小的范围内,这个查询的效率也就会非常耗。如果author列的选择性很低,那么这个替代方案的效率会比前面那个更糟,所以使用的时候要谨慎。全文索引有时候还可以实现一些简单的"边框"搜索。例如,希望搜索某个坐标范围时,将坐标按某种方式转换成文本再进行全文搜索。假设某条记录的坐标为X=123和Y=456.可以按照这样的方式交错存储坐标XY123456,然后对此进行全文搜索。这时,希望查询某矩形——X取值100至199,Y取值400至499——范围时,可以再查询直接搜索"+XY14*".这比使用WHERE条件过滤的效率要高很多。全文索引的另一个常用技巧时缓存全文索引返回的主键值,这在分页显示的时候经常使用。当应用程序真的需要输出结果时,才通过主键值将所有需要的数据返回。这个查询就可以自由地使用其他索引、或者自由地关联其他表。在早期版本中虽然只有MyISMA表支持全文索引,但是如果仍然希望使用InnoDB或其他引擎,可以将原表赋值到一个备库,再将备库上的表改成MyISAM并建上相应的全文索引。如果不希望再另一个服务器上完成查询,还可以对表进行垂直拆分,将需要索引的列放到一个单独的MyISAM表中。将需要索引的列额外地冗余再另一个MyISAM表中也是一个办法。再测试库sakila.film_text就是使用这个策略,这里使用触发器来维护这个表的数据。最后,你还可以使用一个包含内置全文索引的引擎,,如Lucene或者Sphinx。因为使用全文索引的时候,通常会返回大量结果并产生大量随机IO,如何和GROUP BY 一起使用的话,还需要通过临时表或者文件排序进行分组,性能会非常非常糟糕。这类查询通常只是希望查询分组后的前几名结果,所以一个有效的优化办法是对结果进行抽样而不是精确计算。例如,仅查询前面的
1 000条记录,进行分组并返回前几名的结果。
全文索引的配置和优化。
全文索引的日常维护通常能够大大提升性能。"双B-Tree"的特殊结构、再某些文档中比其他文档要包含多得多的关键字,这都使得全文索引比起普通索引有更多的碎片问题,所以需要经常使用OPTIMIZE TABLE来减少碎片。如果应用时IO密集型的,那么定期地进行全文索引重建可以让性能提升很多。如果希望全文索引能够高效地工作,还需要保证索引缓存足够大,从而保证所有的全文索引都能够缓存在内存中。通常,可以为全文索引设置单独的键缓存(Key Cache),保证不会被其他的索引缓存挤出内存。
提供一个好的停用词表也很重要。默认的停用词表对常用英语来说可能还不错,但是如果时其他语言或者某些专业文档就不合适了,例如技术文档。例如,若要索引一批MySQL相关的文档,那么最好将mysql放入停用词表,因为在这类文档中,这个词会出现得非常频繁。忽略一些太短的单词也可以提升全文索引的效率。索引单词的最小长度可以通过参数ft_min_word_len配置。修改该参数可以过滤更多的单词,让查询速度更快,但是也会降低精确度。还需要注意一些特殊的而场景,有时确实需要索引某些非常短的词语。例如,对一个电子消费品文档进行索引,除非我们允许对很短的单词进行索引,否则搜索
"cd player"可能会返回大量的结果。因为单词"cd"比默认允许的最短长度4还要小,所以这里只会对"Player"进行搜索,而通常搜索"cd player"的客户,其实对MP3或者DVD播放器并不感兴趣。停用词表和最小词长都可以通过减少索引词语来提升全文索引的效率,但是同时地也会降低搜索的精确度。这需要根据实际的应用场景找到合适的平衡点。如果你希望同时获得好的性能和好的搜索质量,那么需要自己定制这些参数。一个好的办法时通过日志系统来研究用户的搜索行为,看看一些异常的查询,包括没有结果返回的查询或者返回过多结果的用户查询。通过这些用户行为和被搜索的内容来判断应该如何调整索引策略。
(需要注意,当调整"允许最小词长"后,需要通过OPTIMIZE TABLE来重建索引才会生效。另一个参数ft_max_word_len和该参数行为类似,它限制了允许索引的最大词长)。
当向一个有全文索引的表中导入大量数据的时候,最好先通过命令DISABLE KEYS来禁用全文索引,然后再导入结束后使用ENABLE KEYS来建立全文索引。因为全文索引的更新是一个消耗很大的操作,所以上面的细节会帮你节省大量时间。另外,这样还顺便为全文索引做了一次碎片整理工作。如果数据集特别大,则需要对数据进行手动分区,然后将数据分布到不同的节点,再做并行的搜索。这是一个复杂的工作,最好通过一些外部的搜索引擎来实现,如Lucene或者Sphinx,经验显示这样做性能会有指数级的提升
全文索引的日常维护通常能够大大提升性能。"双B-Tree"的特殊结构、再某些文档中比其他文档要包含多得多的关键字,这都使得全文索引比起普通索引有更多的碎片问题,所以需要经常使用OPTIMIZE TABLE来减少碎片。如果应用时IO密集型的,那么定期地进行全文索引重建可以让性能提升很多。如果希望全文索引能够高效地工作,还需要保证索引缓存足够大,从而保证所有的全文索引都能够缓存在内存中。通常,可以为全文索引设置单独的键缓存(Key Cache),保证不会被其他的索引缓存挤出内存。
提供一个好的停用词表也很重要。默认的停用词表对常用英语来说可能还不错,但是如果时其他语言或者某些专业文档就不合适了,例如技术文档。例如,若要索引一批MySQL相关的文档,那么最好将mysql放入停用词表,因为在这类文档中,这个词会出现得非常频繁。忽略一些太短的单词也可以提升全文索引的效率。索引单词的最小长度可以通过参数ft_min_word_len配置。修改该参数可以过滤更多的单词,让查询速度更快,但是也会降低精确度。还需要注意一些特殊的而场景,有时确实需要索引某些非常短的词语。例如,对一个电子消费品文档进行索引,除非我们允许对很短的单词进行索引,否则搜索
"cd player"可能会返回大量的结果。因为单词"cd"比默认允许的最短长度4还要小,所以这里只会对"Player"进行搜索,而通常搜索"cd player"的客户,其实对MP3或者DVD播放器并不感兴趣。停用词表和最小词长都可以通过减少索引词语来提升全文索引的效率,但是同时地也会降低搜索的精确度。这需要根据实际的应用场景找到合适的平衡点。如果你希望同时获得好的性能和好的搜索质量,那么需要自己定制这些参数。一个好的办法时通过日志系统来研究用户的搜索行为,看看一些异常的查询,包括没有结果返回的查询或者返回过多结果的用户查询。通过这些用户行为和被搜索的内容来判断应该如何调整索引策略。
(需要注意,当调整"允许最小词长"后,需要通过OPTIMIZE TABLE来重建索引才会生效。另一个参数ft_max_word_len和该参数行为类似,它限制了允许索引的最大词长)。
当向一个有全文索引的表中导入大量数据的时候,最好先通过命令DISABLE KEYS来禁用全文索引,然后再导入结束后使用ENABLE KEYS来建立全文索引。因为全文索引的更新是一个消耗很大的操作,所以上面的细节会帮你节省大量时间。另外,这样还顺便为全文索引做了一次碎片整理工作。如果数据集特别大,则需要对数据进行手动分区,然后将数据分布到不同的节点,再做并行的搜索。这是一个复杂的工作,最好通过一些外部的搜索引擎来实现,如Lucene或者Sphinx,经验显示这样做性能会有指数级的提升
分布式(XA)事务。
存储引擎的事务特性能够保证在存储引擎级别实现ACID,而分布式事务则让存储引擎级别的ACID可以扩展到数据库层面,甚至可以扩展到多个数据库之间——这需要通过两阶段提交实现。MySQL5.0和更新版本的数据库已经开始支持XA事务了。XA事务中需要有一个事务协调器来保证所有的事务参与者都完成了准备工作(第一阶段)。如果协调器受到所有的参与者都准备好的消息,就会告诉所有的事务可以提交了,这时第二阶段。MySQL在这个XA事务过程中扮演一个参与者的角色,而不是协调者。实际上,在MySQL中有两种XA事务。一方面,MySQL可以参与到外部的分布式事务中;另一方面,还可以通过XA事务来协调存储引擎和二进制日志。
存储引擎的事务特性能够保证在存储引擎级别实现ACID,而分布式事务则让存储引擎级别的ACID可以扩展到数据库层面,甚至可以扩展到多个数据库之间——这需要通过两阶段提交实现。MySQL5.0和更新版本的数据库已经开始支持XA事务了。XA事务中需要有一个事务协调器来保证所有的事务参与者都完成了准备工作(第一阶段)。如果协调器受到所有的参与者都准备好的消息,就会告诉所有的事务可以提交了,这时第二阶段。MySQL在这个XA事务过程中扮演一个参与者的角色,而不是协调者。实际上,在MySQL中有两种XA事务。一方面,MySQL可以参与到外部的分布式事务中;另一方面,还可以通过XA事务来协调存储引擎和二进制日志。
内部XA事务。
MySQL本身的插件式架构导致在其内部需要使用XA事务。MySQL中各个存储引擎是完全独立的,彼此不知道对方的存在,所以一个跨存储引擎的事务就需要一个外部的协调者,如果不使用XA协议,例如,跨存储引擎的事务提交就只是顺序地要求每个存储引擎各自提交。如果在某个存储提交过程中发生系统崩溃,就会破坏事务的特性(要么全部提交,要么就不做任何操作)如果将MySQL记录的二进制日志操作看作一个独立的"存储引擎",就不难理解为什么即使是一个存储引擎参与的事务仍然需要XA事务了。在存储引擎提交的同时,需要将"提交"的信息写入二进制日志,这就是一个分布式事务,只不过二进制日志的参与者是MySQL本身。XA事务为MySQL带来巨大的性能下降。从MySQL5.0开始,它破坏了MySQL内部的"批量提交"()一种通过单磁盘IO操作完成多个事务提交的技术),使得MySQL不得不进行多次额外的fsync()调用。具体的,一个事务如果开启了二进制日志,则不仅需要对二进制日志进行持久化操作,InnoDB事务日志还需要两次日志持久化操作。换句话说,如果希望有二进制日志安全的事务实现,则至少需要做三次fsync()操作。唯一避免这个问题的办法就是关闭二进制日志,并将innodb_support_xa设置为0.(一个常见的误区是认为innodb_support_xa只有在需要XA事务的时候才需要打开。这是醋无的:该参数还会控制MySQL内部存储引擎和二进制日志之间的分布式事务。如果你真正关心你的数据,你需要将这个参数打开)。
但这样的设置是非常不安全的,而且这回导致MySQL赋值也没法正常工作。复制需要二进制日志和XA事务的支持,另外——如果希望数据尽可能安全——最好还要将sync_binlog设置成1, 这时存储引擎和二进制日志才是真正同步的(否则,XA事务支持就没有意义了,因为事务提交了二进制日志却可能没有"提交"到磁盘)。这也是为什么强烈建议使用带电池保护的RAID卡写缓存:这个缓存可以大大加快fsync()操作的效率
MySQL本身的插件式架构导致在其内部需要使用XA事务。MySQL中各个存储引擎是完全独立的,彼此不知道对方的存在,所以一个跨存储引擎的事务就需要一个外部的协调者,如果不使用XA协议,例如,跨存储引擎的事务提交就只是顺序地要求每个存储引擎各自提交。如果在某个存储提交过程中发生系统崩溃,就会破坏事务的特性(要么全部提交,要么就不做任何操作)如果将MySQL记录的二进制日志操作看作一个独立的"存储引擎",就不难理解为什么即使是一个存储引擎参与的事务仍然需要XA事务了。在存储引擎提交的同时,需要将"提交"的信息写入二进制日志,这就是一个分布式事务,只不过二进制日志的参与者是MySQL本身。XA事务为MySQL带来巨大的性能下降。从MySQL5.0开始,它破坏了MySQL内部的"批量提交"()一种通过单磁盘IO操作完成多个事务提交的技术),使得MySQL不得不进行多次额外的fsync()调用。具体的,一个事务如果开启了二进制日志,则不仅需要对二进制日志进行持久化操作,InnoDB事务日志还需要两次日志持久化操作。换句话说,如果希望有二进制日志安全的事务实现,则至少需要做三次fsync()操作。唯一避免这个问题的办法就是关闭二进制日志,并将innodb_support_xa设置为0.(一个常见的误区是认为innodb_support_xa只有在需要XA事务的时候才需要打开。这是醋无的:该参数还会控制MySQL内部存储引擎和二进制日志之间的分布式事务。如果你真正关心你的数据,你需要将这个参数打开)。
但这样的设置是非常不安全的,而且这回导致MySQL赋值也没法正常工作。复制需要二进制日志和XA事务的支持,另外——如果希望数据尽可能安全——最好还要将sync_binlog设置成1, 这时存储引擎和二进制日志才是真正同步的(否则,XA事务支持就没有意义了,因为事务提交了二进制日志却可能没有"提交"到磁盘)。这也是为什么强烈建议使用带电池保护的RAID卡写缓存:这个缓存可以大大加快fsync()操作的效率
RedoLog日志和Binlog日志
外部XA事务。
MySQL能够作为参与者完成一个外部的分布式事务。但它对XA协议支持并不完整。例如XA协议要求在一个事务中的多个连接可以做关联,但目前的MysQL版本还不能支持。因为通信延迟和参与者本身可能失败,所以外部XA事务比内部消耗会更大。如果在广域网中使用XA事务,通常会因为不可预测的网络性能导致事务失败。如果有太多不可控因素,例如,不稳定的网络通信或者用户长时间等待而不提交,则最好避免使用XA事务。任何可能让事务提交发生延迟的操作代价都很大,因为它影响的不仅仅是自己本身,它还会让所有参与者都在等待。
通常,还可以使用别的方式实现高性能的分布式事务。例如,可以在本地写入数据,并将其放入队列,然后在一个更小、更快的事务中自动分发。还可以使用MySQL本身的复制机制来发送数据。我们看到很多应用程序都可以完全彼岸使用分布式事务。也就是说,XA事务是一种在多个服务器之间同步的方法。如果由于某些原因不能使用MySQL本身的复制,或者性能并不是瓶颈的时候,可以尝试使用。
MySQL能够作为参与者完成一个外部的分布式事务。但它对XA协议支持并不完整。例如XA协议要求在一个事务中的多个连接可以做关联,但目前的MysQL版本还不能支持。因为通信延迟和参与者本身可能失败,所以外部XA事务比内部消耗会更大。如果在广域网中使用XA事务,通常会因为不可预测的网络性能导致事务失败。如果有太多不可控因素,例如,不稳定的网络通信或者用户长时间等待而不提交,则最好避免使用XA事务。任何可能让事务提交发生延迟的操作代价都很大,因为它影响的不仅仅是自己本身,它还会让所有参与者都在等待。
通常,还可以使用别的方式实现高性能的分布式事务。例如,可以在本地写入数据,并将其放入队列,然后在一个更小、更快的事务中自动分发。还可以使用MySQL本身的复制机制来发送数据。我们看到很多应用程序都可以完全彼岸使用分布式事务。也就是说,XA事务是一种在多个服务器之间同步的方法。如果由于某些原因不能使用MySQL本身的复制,或者性能并不是瓶颈的时候,可以尝试使用。
查询缓存。
很多数据库产品都能够缓存查询的执行计划,对于相同类型的SQL就可以跳过SQL解析和执行计划生成阶段。MySQL在某些场景下也可以实现,但是MySQL还有另一种不同的缓存类型:缓存完整的SELECT查询结果,也就是"查询缓存"。
MySQL查询缓存保存查询返回的完整结果。当查询命中该缓存,MySQL会立刻返回结果,跳过了解析、优化和执行解读那。查询缓存系统会跟踪查询中涉及的每个表,如果这些表发生变化,那么和这个表相关的所有的缓存数据都将失效。这种机制效率看起来比较低,因为数据表变化时很有可能对应的查询结果并没有变更,但是这种简单实现代价很小,而这点对于一个非常繁忙的系统来说非常重要。
查询缓存对应用程序是完全透明的。应用程序无须关心MySQL是通过查询缓存返回的结果还是实际执行返回的结果。事实上,这两种方式执行的结果是完全相同的。换句话说,查询缓存无须使用任何语法。无论是MySQL开启或关闭查询缓存,对应用程序都是透明的。(有一种方式查询缓存可能和原生的SQL工作方式有所不同:默认的,当要查询的表被LOCK TABLES锁住时,查询仍然可以通过查询缓存返回数据。你可以通过参数query_cache_wlock_invaidate打开或者关闭这种行为)。随者现在的通用服务器越来越强大,查询缓存被发现是一个影响服务器扩展性的因素。他可能成为整个服务器的资源竞争单点,在多核服务器上还可能导致服务器僵死。后面再详细介绍如何配合查询缓存,但是很多时候我们还是认为应该默认关闭查询缓存,如果查询缓存作用很大的话,那就配置一个很小的查询缓存空间(如几十兆)。后面再解释如何判断再系统压力下打开查询缓存是否有好处。
很多数据库产品都能够缓存查询的执行计划,对于相同类型的SQL就可以跳过SQL解析和执行计划生成阶段。MySQL在某些场景下也可以实现,但是MySQL还有另一种不同的缓存类型:缓存完整的SELECT查询结果,也就是"查询缓存"。
MySQL查询缓存保存查询返回的完整结果。当查询命中该缓存,MySQL会立刻返回结果,跳过了解析、优化和执行解读那。查询缓存系统会跟踪查询中涉及的每个表,如果这些表发生变化,那么和这个表相关的所有的缓存数据都将失效。这种机制效率看起来比较低,因为数据表变化时很有可能对应的查询结果并没有变更,但是这种简单实现代价很小,而这点对于一个非常繁忙的系统来说非常重要。
查询缓存对应用程序是完全透明的。应用程序无须关心MySQL是通过查询缓存返回的结果还是实际执行返回的结果。事实上,这两种方式执行的结果是完全相同的。换句话说,查询缓存无须使用任何语法。无论是MySQL开启或关闭查询缓存,对应用程序都是透明的。(有一种方式查询缓存可能和原生的SQL工作方式有所不同:默认的,当要查询的表被LOCK TABLES锁住时,查询仍然可以通过查询缓存返回数据。你可以通过参数query_cache_wlock_invaidate打开或者关闭这种行为)。随者现在的通用服务器越来越强大,查询缓存被发现是一个影响服务器扩展性的因素。他可能成为整个服务器的资源竞争单点,在多核服务器上还可能导致服务器僵死。后面再详细介绍如何配合查询缓存,但是很多时候我们还是认为应该默认关闭查询缓存,如果查询缓存作用很大的话,那就配置一个很小的查询缓存空间(如几十兆)。后面再解释如何判断再系统压力下打开查询缓存是否有好处。
MySQL如何判断缓存命中。
MySQL判断缓存命中的办法很简单:缓存放在一个引用表,通过一个哈希值引用,整个哈希值包括了如下因素,即查询本身、当前要查询的数据库、客户端协议的版本等一些其他可能会影响返回结果的信息。当判断缓存是否命中时,MySQL不会解析、"正规化"或者参数化查询语句,而是直接使用SQL语句和客户端发送过来的其他原始信息。任何字符上的不同,例如空格、注释——任何的不同——都会导致缓存的不命中。(对于这个规则,Percona Server是个例外。它会先将所有的注释语句删除,然后再比较查询语句是否有缓存。这是一个通用的需求,这样可以在查询语句中带入更多的处理过程信息)。所以在编写SQL语句的时候,需要特别注意这点。通常使用统一的编码规则是一个好的习惯,在这里这个好习惯会让你系统运行得更快。当查询语句中有一些不确定的数据时,则不会被缓存。例如包含函数NOW()或者CURRENT_DATE()的查询不会被缓存。类似的,包含CURRENT_USER或者CONNECTION_ID()的查询语句因为会根据不同的用户返回不同的结果,所以也不会被缓存。事实上,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,或者任何包含列级别权限的表,都不会被缓存。
我们常听到:"如果查询中包含一个不确定的函数,MySQL则不会检查查询缓存"。这个说法是不正确的。因为在检查查询缓存的时候,还没有解析SQL语句,所以MySQL并不知道查询语句中是否包含这类函数。在检查查询缓存之前,MySQL只做一件事情,就是通过一个大小写不敏感的检查看看SQL语句是不是以SEL开头。准确的说法应该是:"如果查询语句中包含任何的不确定函数,那么在查询缓存中是不可能找到缓存结果的"。因为即使之前刚刚执行了这样的查询,结果也不会放在查询缓存中。MySQL在任何时候只要发现不能被缓存的部分,就会禁止这个查询被缓存。所以,如果希望换成一个带日期的查询,那么最好将日期提前计算好,而不是直接使用函数。例如:
```sql
... DATE_SUB(CURRENT_DATE, INTERVAL 1 DAY) -- Not cacheable!
... DATE_SUB('2007-07-14', INTERVAL 1 DAY) -- Cacheable
```
因为查询缓存是在完整的SELECT语句基础上的,而且只是在刚刚收到SQL语句的时候才检查,所以子查询和存储过程都没办法使用查询缓存。在MySQL5.1之前的版本中,绑定变量也无法使用查询缓存。MySQL的查询缓存在狠毒哦时候可以提升查询性能,在使用的时候,有一些问题需要特别注意。手下你打开查询缓存对读和写操作都会带来额外的消耗:
1.读查询在开始之前必须先检查是否命中缓存
2.如果这个读查询可以被缓存,那么当完成执行后,MSQL若发现查询缓存中没有这个查询,会将其结果存入查询缓存,这回带来额外的系统消耗
3.这对写操作也会有影响,因为当向这某个表写入数据的时候,MySQL必须将对应表的所有缓存都设置失效。如果查询缓存非常大或者碎片很多,这个操作就可能带来很大系统消耗(设置了很多的内存给查询缓存用的时候).
虽然如此,查询缓存仍然可能给系统带来性能提升。但是,如上所述,这些额外的消耗也可能不断增加,再加上对查询缓存操作是一个加锁排他操作,这个消耗可能不容小觑。对InnoDB用户来说,事务的一些特性会限制查询缓存的使用。当一个语句再事务中修改了某个表,MySQL会将这个表对应的查询缓存都设置失效,而事实上,InnoDB的多版本特性会暂时将这个修改对其他事务屏蔽。在这个事务提交之前,这个表的相关查询是无法被缓存的,所以所有在这个表上的查询——内部或外部的事务——都只能在该事务提交后才能被缓存。因此,长事件运行的事务,会大大降低查询缓存的命中率。
如果查询缓存使用了很大量鞥多内存,缓存失效操作就可能成为一个非常严重的问题瓶颈。如果缓存中存放了大量的查询结果,那么缓存失效操作时整个系统都可能会僵死一会儿。因为这个操作是靠一个全局锁操作保护的,所有需要做该操作的查询都要等待这个锁,而且无论是检测是否命中缓存、还是缓存失效检测都需要等待这个全局锁。
MySQL判断缓存命中的办法很简单:缓存放在一个引用表,通过一个哈希值引用,整个哈希值包括了如下因素,即查询本身、当前要查询的数据库、客户端协议的版本等一些其他可能会影响返回结果的信息。当判断缓存是否命中时,MySQL不会解析、"正规化"或者参数化查询语句,而是直接使用SQL语句和客户端发送过来的其他原始信息。任何字符上的不同,例如空格、注释——任何的不同——都会导致缓存的不命中。(对于这个规则,Percona Server是个例外。它会先将所有的注释语句删除,然后再比较查询语句是否有缓存。这是一个通用的需求,这样可以在查询语句中带入更多的处理过程信息)。所以在编写SQL语句的时候,需要特别注意这点。通常使用统一的编码规则是一个好的习惯,在这里这个好习惯会让你系统运行得更快。当查询语句中有一些不确定的数据时,则不会被缓存。例如包含函数NOW()或者CURRENT_DATE()的查询不会被缓存。类似的,包含CURRENT_USER或者CONNECTION_ID()的查询语句因为会根据不同的用户返回不同的结果,所以也不会被缓存。事实上,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,或者任何包含列级别权限的表,都不会被缓存。
我们常听到:"如果查询中包含一个不确定的函数,MySQL则不会检查查询缓存"。这个说法是不正确的。因为在检查查询缓存的时候,还没有解析SQL语句,所以MySQL并不知道查询语句中是否包含这类函数。在检查查询缓存之前,MySQL只做一件事情,就是通过一个大小写不敏感的检查看看SQL语句是不是以SEL开头。准确的说法应该是:"如果查询语句中包含任何的不确定函数,那么在查询缓存中是不可能找到缓存结果的"。因为即使之前刚刚执行了这样的查询,结果也不会放在查询缓存中。MySQL在任何时候只要发现不能被缓存的部分,就会禁止这个查询被缓存。所以,如果希望换成一个带日期的查询,那么最好将日期提前计算好,而不是直接使用函数。例如:
```sql
... DATE_SUB(CURRENT_DATE, INTERVAL 1 DAY) -- Not cacheable!
... DATE_SUB('2007-07-14', INTERVAL 1 DAY) -- Cacheable
```
因为查询缓存是在完整的SELECT语句基础上的,而且只是在刚刚收到SQL语句的时候才检查,所以子查询和存储过程都没办法使用查询缓存。在MySQL5.1之前的版本中,绑定变量也无法使用查询缓存。MySQL的查询缓存在狠毒哦时候可以提升查询性能,在使用的时候,有一些问题需要特别注意。手下你打开查询缓存对读和写操作都会带来额外的消耗:
1.读查询在开始之前必须先检查是否命中缓存
2.如果这个读查询可以被缓存,那么当完成执行后,MSQL若发现查询缓存中没有这个查询,会将其结果存入查询缓存,这回带来额外的系统消耗
3.这对写操作也会有影响,因为当向这某个表写入数据的时候,MySQL必须将对应表的所有缓存都设置失效。如果查询缓存非常大或者碎片很多,这个操作就可能带来很大系统消耗(设置了很多的内存给查询缓存用的时候).
虽然如此,查询缓存仍然可能给系统带来性能提升。但是,如上所述,这些额外的消耗也可能不断增加,再加上对查询缓存操作是一个加锁排他操作,这个消耗可能不容小觑。对InnoDB用户来说,事务的一些特性会限制查询缓存的使用。当一个语句再事务中修改了某个表,MySQL会将这个表对应的查询缓存都设置失效,而事实上,InnoDB的多版本特性会暂时将这个修改对其他事务屏蔽。在这个事务提交之前,这个表的相关查询是无法被缓存的,所以所有在这个表上的查询——内部或外部的事务——都只能在该事务提交后才能被缓存。因此,长事件运行的事务,会大大降低查询缓存的命中率。
如果查询缓存使用了很大量鞥多内存,缓存失效操作就可能成为一个非常严重的问题瓶颈。如果缓存中存放了大量的查询结果,那么缓存失效操作时整个系统都可能会僵死一会儿。因为这个操作是靠一个全局锁操作保护的,所有需要做该操作的查询都要等待这个锁,而且无论是检测是否命中缓存、还是缓存失效检测都需要等待这个全局锁。
查询缓存如何使用内存。
查询缓存是完全存储在内存中的,所以在配置和使用它之前,我们需要先了解它是如何使用内存的。除了查询结果之外,需要缓存的还有很多别的维护相关的数据。这和文件系统有些类似:需要一些内存专门用来确定哪些内存目前是可用的、哪些是已经用掉的、哪些用来存储数据表和查询结果之前的映射、哪些用来存储查询字符串和查询结果。这些基本的管理维护数据结构大需要需要40KB的内存资源,除此之外,MySQL用于查询缓存的内存被分成一个个的数据块,数据块是变长的。每一个数据块中,存储了自己的类型、大小和存储的数据本身,还外加指向前一个和后一个数据块的指针。数据块的类型有:存储查询结果、存储查询和数据表的映射、存储查询文本,等等。不同的存储快,在内存使用上并没有什么不同,从用户角度来看无须区分它们。当服务器启动的时候,它先初始化查询缓存需要的内存。这个内存池初始是一个完整的空闲块。这个空闲块的大小就是你所配置的查询缓存大小再减去用于维护元数据的数据结构所消耗的空间。当有查询结果需要缓存的时候,MySQL先从大的空间块中申请一个数据块用于存储结果。这个数据块需要大于参数query_cache_min_res_unit的配置,即使查询结果远远小于此,仍需要至少申请query_cache_min_res_unit空间。因为需要在查询开始返回结果的时候就分配空间,而此时是无法预知查询结果到底多大的,所以MySQL无法为每一个查询结果精确分配大小恰好匹配的缓存空间。
因为需要先锁住空间块,然后找到合适大小数据块,所以相对来说,分配内存块是一个非常慢的操作。MySQL尽量避免这个操作的次数。当需要缓存一个查询结果的时候,它先选择一个尽可能小的内存块(也可能选择较大的),然后将结果存入其中。如果数据块全部用完,但仍有剩余数据需要存储,那么MySQL会申请一块新数据块——仍然是尽可能小的数据块——继续存储结果数据。当查询完成时,如果申请的内存空间还有剩余,MySQL会将其释放,并放入空闲内存部分。该过程如图所示。
我们上面说的"分配内存块",并不是指通过函数malloc()向操作系统申请内存,这个操作只在初次创建查询缓存的时候执行一次。这里"分配内存块"是指在空闲块列表中找到一个合适的内存块,或者从正在使用的、待淘汰的内存块中回收再使用。也就是说这里MySQL自己管理一大块内存,而不依赖操作系统的内存管理。至此,一些都看起来很简单。不过实际情况要比上图更复杂。例如,我们假设平均查询结果非常小,服务器在并发地向不同的两个连接返回结果,返回完结果MySQL回收剩余数据块空间时发现,回收的数据块小于query_cache_min_res_unit,所以不能够直接在后续的内存块分配中使用。如果考虑到这种情况,数据块的分配就更复杂些,
查询缓存是完全存储在内存中的,所以在配置和使用它之前,我们需要先了解它是如何使用内存的。除了查询结果之外,需要缓存的还有很多别的维护相关的数据。这和文件系统有些类似:需要一些内存专门用来确定哪些内存目前是可用的、哪些是已经用掉的、哪些用来存储数据表和查询结果之前的映射、哪些用来存储查询字符串和查询结果。这些基本的管理维护数据结构大需要需要40KB的内存资源,除此之外,MySQL用于查询缓存的内存被分成一个个的数据块,数据块是变长的。每一个数据块中,存储了自己的类型、大小和存储的数据本身,还外加指向前一个和后一个数据块的指针。数据块的类型有:存储查询结果、存储查询和数据表的映射、存储查询文本,等等。不同的存储快,在内存使用上并没有什么不同,从用户角度来看无须区分它们。当服务器启动的时候,它先初始化查询缓存需要的内存。这个内存池初始是一个完整的空闲块。这个空闲块的大小就是你所配置的查询缓存大小再减去用于维护元数据的数据结构所消耗的空间。当有查询结果需要缓存的时候,MySQL先从大的空间块中申请一个数据块用于存储结果。这个数据块需要大于参数query_cache_min_res_unit的配置,即使查询结果远远小于此,仍需要至少申请query_cache_min_res_unit空间。因为需要在查询开始返回结果的时候就分配空间,而此时是无法预知查询结果到底多大的,所以MySQL无法为每一个查询结果精确分配大小恰好匹配的缓存空间。
因为需要先锁住空间块,然后找到合适大小数据块,所以相对来说,分配内存块是一个非常慢的操作。MySQL尽量避免这个操作的次数。当需要缓存一个查询结果的时候,它先选择一个尽可能小的内存块(也可能选择较大的),然后将结果存入其中。如果数据块全部用完,但仍有剩余数据需要存储,那么MySQL会申请一块新数据块——仍然是尽可能小的数据块——继续存储结果数据。当查询完成时,如果申请的内存空间还有剩余,MySQL会将其释放,并放入空闲内存部分。该过程如图所示。
我们上面说的"分配内存块",并不是指通过函数malloc()向操作系统申请内存,这个操作只在初次创建查询缓存的时候执行一次。这里"分配内存块"是指在空闲块列表中找到一个合适的内存块,或者从正在使用的、待淘汰的内存块中回收再使用。也就是说这里MySQL自己管理一大块内存,而不依赖操作系统的内存管理。至此,一些都看起来很简单。不过实际情况要比上图更复杂。例如,我们假设平均查询结果非常小,服务器在并发地向不同的两个连接返回结果,返回完结果MySQL回收剩余数据块空间时发现,回收的数据块小于query_cache_min_res_unit,所以不能够直接在后续的内存块分配中使用。如果考虑到这种情况,数据块的分配就更复杂些,
如图所示。在收缩第一个查询结果使用的缓存空间时,就会在第二个查询结果之间留下一个"空隙"——一个非常小的空闲空间,因为小于query_cache_min_res_unit而不能再次被查询缓存使用。这类"空隙"我们成为碎片,这在内存管理、文件系统管理上都是经典问题。有很多种情况都会导致碎片,例如缓存失效时,可能导致留下太小的数据块无法在后续缓存中管使用
什么情况下查询缓存能发挥作用。
并不是什么情况下查询缓存都会提高系统性能的。缓存和失效都会带来额外的消耗,所以只有当缓存带来的资源节约大于本身的资源消耗时才会给系统带来性能提升。这跟具体的服务器压力模型有关。理论上,可以通过观察打开或者关闭查询缓存时候的系统效率来决定是否需要开启查询缓存。关闭查询缓存时,每个查询都需要完整的执行,每一次写操作执行完成后立刻返回;打开查询缓存时,每次读请求先检查缓存是否命中,如果命中则立刻返回,否则就完整地执行查询,每次写操作则需要检查查询缓存中是否有需要失效的缓存,然后再返回。这个过程还比较简单明了,但是要评估打开查询缓存是否能够带来性能提升却并不容易。还有一些外部的因素需要考虑,例如,查询缓存可以降低查询执行的时间,但是却不能减少查询结果传输的网络消耗,如果这个消耗是系统的主要瓶颈,那么查询缓存的作用也很小。
因为MySQL再SHOW STATUS中只能提供一个全局的性能指标,所以很难根据此来判断查询缓存是否能够提升性能(Percona和MariaDB对MySQL慢日志进行了改进,会记录慢日志中的查询是否命中查询缓存)。很多时候,全局平均不能反应实际情况。例如,打开查询缓存可以使得一个很慢的查询变得非常快,但是也会让其他查询稍微慢一点点。有时候如果能够让某些关键的查询速度更快,稍微降低一下其他查询的速度是值得的。不过这种情况我们推荐使用SQL_CACHE来优化对查询缓存的使用。
对于哪些需要消耗大量资源的查询通常都是非常适合缓存的。例如一些汇总计算查询,具体的如COUNT()等。总的来说,对于复杂的SELECT语句都可以使用查询缓存,例如多表JOIN后还需要做排序和分页,这类查询每次执行消耗都很大,但是返回的结果集却很小,非常适合查询缓存。不过需要注意的是,涉及的表上UPDATE、DELETE和INSERT操作相比SELECT来说要非常少才行。
一个判断查询缓存是否有效地直接数据是命中率,就是使用查询缓存返回结果占总查询的比率。当MySQL接收到一个SELECT查询的时候,要么增加Qcache_hits的指,要么增加Com_select的值。所以查询缓存命中率可以由如下公式计算:Qcache_hits/(Qcache_hits+Com_select)
不过,查询缓存命中率是一个很难判断的数值。命中率多大才是好的命中率?具体情况要具体分析。只要查询缓存带来的效率提升大于查询缓存带来的额外消耗,即使30%命中率对系统性能提升也有很大好处。另外,缓存了哪些查询也很重要,例如,被缓存的查询本身消耗非常巨大,那么即使缓存命中率非常低,也仍然会对系统性能提升有好处。所以,没有一个简单的规则可以判断查询缓存是否对系统有好处。
任何SELECT语句没有从查询缓存中返回都成为"缓存未命中"。缓存未命中可能有如下几种原因:
1.查询语句无法被缓存,可能是因为查询种包含一个不确定的函数(如CURRENT_DATE),或者查询结果太大而无法缓存。这都会导致状态值Qcache_not_cached增加
2.MySQL从未处理这个查询,所以结果也从不曾被缓存过
3.还有一种情况是虽然之前缓存了查询结果,但是由于查询缓存的内存用完了,MySQL需要将某些缓存"逐出",或者由于数据表被修改导致缓存失效。
如果你的服务器上有大量缓存未命中,但是实际上绝大数查询都被缓存了,那么一定是有如下情况发生:
1.查询缓存还没有完成预热。也就是说,MySQL还没有机会将查询结果都缓存起来。
2.查询语句之前从未执行过,如果你的应用程序不会重复执行一条查询语句,那么即使完成预热仍然会有很多缓存未命中
3.缓存失效操作太多了
缓存碎片、内存不足、数据修改都会造成缓存失效。如果配置了足够的缓存空间,而且query_cache_min_res_unit设置也合理的化,那么缓存失效应该主要是数据修改导致的。可以通过参数Com_*来查看数据修改的情况(包括Com_update,Com_delete,等等),还可以通过Qcache_lowmem_prunes来查看多少次失效是由于内存不足导致的。
在考虑缓存命中率的同时,通常还需要考虑缓存失效带来的额外消耗。一个极端的办法是,对某一个表先做一次只有查询的测试,并且所有的查询都命中缓存,而另一个相同的表只做修改操作。这是,查询缓存的命中率就是100%。但因为会给更新操作带来额外的消耗,所以查询缓存并不一定会带来总体效率的提升。这里,所有的更新语句都回做一次缓存失效检查,而检查的结果都是相同的,这会给系统带来额外的资源浪费。所以,如果你只是观察查询缓存的命中率的话,可能完全不会发现这样的问题。
在MySQL张入股更新操作和带缓存的读操作混合,那么查询缓存带来的好处通常很难很亮。更新操作会不断地使得缓存失效,而同时每次查询还会像缓存中再写入新的数据。所以甚至有当后续的查询能够在缓存失效前使用缓存才会有效地利用查询缓存。
并不是什么情况下查询缓存都会提高系统性能的。缓存和失效都会带来额外的消耗,所以只有当缓存带来的资源节约大于本身的资源消耗时才会给系统带来性能提升。这跟具体的服务器压力模型有关。理论上,可以通过观察打开或者关闭查询缓存时候的系统效率来决定是否需要开启查询缓存。关闭查询缓存时,每个查询都需要完整的执行,每一次写操作执行完成后立刻返回;打开查询缓存时,每次读请求先检查缓存是否命中,如果命中则立刻返回,否则就完整地执行查询,每次写操作则需要检查查询缓存中是否有需要失效的缓存,然后再返回。这个过程还比较简单明了,但是要评估打开查询缓存是否能够带来性能提升却并不容易。还有一些外部的因素需要考虑,例如,查询缓存可以降低查询执行的时间,但是却不能减少查询结果传输的网络消耗,如果这个消耗是系统的主要瓶颈,那么查询缓存的作用也很小。
因为MySQL再SHOW STATUS中只能提供一个全局的性能指标,所以很难根据此来判断查询缓存是否能够提升性能(Percona和MariaDB对MySQL慢日志进行了改进,会记录慢日志中的查询是否命中查询缓存)。很多时候,全局平均不能反应实际情况。例如,打开查询缓存可以使得一个很慢的查询变得非常快,但是也会让其他查询稍微慢一点点。有时候如果能够让某些关键的查询速度更快,稍微降低一下其他查询的速度是值得的。不过这种情况我们推荐使用SQL_CACHE来优化对查询缓存的使用。
对于哪些需要消耗大量资源的查询通常都是非常适合缓存的。例如一些汇总计算查询,具体的如COUNT()等。总的来说,对于复杂的SELECT语句都可以使用查询缓存,例如多表JOIN后还需要做排序和分页,这类查询每次执行消耗都很大,但是返回的结果集却很小,非常适合查询缓存。不过需要注意的是,涉及的表上UPDATE、DELETE和INSERT操作相比SELECT来说要非常少才行。
一个判断查询缓存是否有效地直接数据是命中率,就是使用查询缓存返回结果占总查询的比率。当MySQL接收到一个SELECT查询的时候,要么增加Qcache_hits的指,要么增加Com_select的值。所以查询缓存命中率可以由如下公式计算:Qcache_hits/(Qcache_hits+Com_select)
不过,查询缓存命中率是一个很难判断的数值。命中率多大才是好的命中率?具体情况要具体分析。只要查询缓存带来的效率提升大于查询缓存带来的额外消耗,即使30%命中率对系统性能提升也有很大好处。另外,缓存了哪些查询也很重要,例如,被缓存的查询本身消耗非常巨大,那么即使缓存命中率非常低,也仍然会对系统性能提升有好处。所以,没有一个简单的规则可以判断查询缓存是否对系统有好处。
任何SELECT语句没有从查询缓存中返回都成为"缓存未命中"。缓存未命中可能有如下几种原因:
1.查询语句无法被缓存,可能是因为查询种包含一个不确定的函数(如CURRENT_DATE),或者查询结果太大而无法缓存。这都会导致状态值Qcache_not_cached增加
2.MySQL从未处理这个查询,所以结果也从不曾被缓存过
3.还有一种情况是虽然之前缓存了查询结果,但是由于查询缓存的内存用完了,MySQL需要将某些缓存"逐出",或者由于数据表被修改导致缓存失效。
如果你的服务器上有大量缓存未命中,但是实际上绝大数查询都被缓存了,那么一定是有如下情况发生:
1.查询缓存还没有完成预热。也就是说,MySQL还没有机会将查询结果都缓存起来。
2.查询语句之前从未执行过,如果你的应用程序不会重复执行一条查询语句,那么即使完成预热仍然会有很多缓存未命中
3.缓存失效操作太多了
缓存碎片、内存不足、数据修改都会造成缓存失效。如果配置了足够的缓存空间,而且query_cache_min_res_unit设置也合理的化,那么缓存失效应该主要是数据修改导致的。可以通过参数Com_*来查看数据修改的情况(包括Com_update,Com_delete,等等),还可以通过Qcache_lowmem_prunes来查看多少次失效是由于内存不足导致的。
在考虑缓存命中率的同时,通常还需要考虑缓存失效带来的额外消耗。一个极端的办法是,对某一个表先做一次只有查询的测试,并且所有的查询都命中缓存,而另一个相同的表只做修改操作。这是,查询缓存的命中率就是100%。但因为会给更新操作带来额外的消耗,所以查询缓存并不一定会带来总体效率的提升。这里,所有的更新语句都回做一次缓存失效检查,而检查的结果都是相同的,这会给系统带来额外的资源浪费。所以,如果你只是观察查询缓存的命中率的话,可能完全不会发现这样的问题。
在MySQL张入股更新操作和带缓存的读操作混合,那么查询缓存带来的好处通常很难很亮。更新操作会不断地使得缓存失效,而同时每次查询还会像缓存中再写入新的数据。所以甚至有当后续的查询能够在缓存失效前使用缓存才会有效地利用查询缓存。
如果缓存的结果在失效前没有被任何其他的SELECT语句使用,那么这次缓存操作就是浪费时间和内存。我们可以通过查看Com_select和Qcache_inserts的相对值来看看是否一直有这种情况发生。如果每次查询查询都是缓存未命中,然后需要将查询结果放到缓存中,那么Qcache_inserts的大小应该和Com_select相当。所以在缓存完成预热后,我们总希望看到Qcache_inserts远远小于Com_select。不过由于缓存和服务器内部的复杂和多样性,仍然很难说,这个比率是多少才是一个合适的值。
所以,上面的"命中率"和"INSERTS和SELECT比率"都无法直观地反应查询缓存的效率,那么还有什么直观的办法能够反应查询缓存是否对系统有好处?这里推荐查看一个指标"命中和写入"的比率。即Qcache_hits和Qcache_inserts的壁纸。根据经验来看,当这个比值大于3:1时通常查询缓存是有效的,不过这个比率最好能够达到10:1.如果你的应用没有达到这个比率,那么就可以考虑禁用查询缓存了。除非你能够通过精确的计算得知:命中带来的性能提升大于缓存失效的消耗,并且查询缓存并没有成为系统的瓶颈。
每一个应用程序都会有一个"最大缓存空间",甚至对一些纯读的应用来说也一样。最大缓存空间是能够缓存所有可能查询结果的缓存空间综合。理论上,对多数应用来说,这个数值都会非常大。而实际上,由于缓存失效的原因,大多数应用最后使用的缓存空间都比预想的要小。即使你配置了足够大的缓存空间,由于不断地失效,导致缓存空间一直都不会接近"最大缓存空间"。
通常可以观察查询缓存内存的实际使用情况,来确定是否需要缩小或者扩大查询缓存。如果查询缓存空间长事件都有剩余,那么建议索引;如果经常由于空间不足而导致查询缓存失效,那么则需要增大查询缓存。不过需要注意,如果查询缓存达到了几十兆这样的数量级,是有潜在危险的。
另外,可能还需要和系统的其他缓存一起考虑,例如InnoDB的缓存池,或者MyISAM的索引缓存。关于这点是没法简单给出一个公式或者比率来判断的,因为真正的平衡点与应用程序有很大的关系。
最好的判断查询缓存是否有效的办法还是通过查询某类查询时间消耗是否增大或者减少来判断。Percona Server通过扩展慢查询可以观察到一个查询是否命中缓存,如果查询缓存没有为系统节省时间,那么最好禁用它
所以,上面的"命中率"和"INSERTS和SELECT比率"都无法直观地反应查询缓存的效率,那么还有什么直观的办法能够反应查询缓存是否对系统有好处?这里推荐查看一个指标"命中和写入"的比率。即Qcache_hits和Qcache_inserts的壁纸。根据经验来看,当这个比值大于3:1时通常查询缓存是有效的,不过这个比率最好能够达到10:1.如果你的应用没有达到这个比率,那么就可以考虑禁用查询缓存了。除非你能够通过精确的计算得知:命中带来的性能提升大于缓存失效的消耗,并且查询缓存并没有成为系统的瓶颈。
每一个应用程序都会有一个"最大缓存空间",甚至对一些纯读的应用来说也一样。最大缓存空间是能够缓存所有可能查询结果的缓存空间综合。理论上,对多数应用来说,这个数值都会非常大。而实际上,由于缓存失效的原因,大多数应用最后使用的缓存空间都比预想的要小。即使你配置了足够大的缓存空间,由于不断地失效,导致缓存空间一直都不会接近"最大缓存空间"。
通常可以观察查询缓存内存的实际使用情况,来确定是否需要缩小或者扩大查询缓存。如果查询缓存空间长事件都有剩余,那么建议索引;如果经常由于空间不足而导致查询缓存失效,那么则需要增大查询缓存。不过需要注意,如果查询缓存达到了几十兆这样的数量级,是有潜在危险的。
另外,可能还需要和系统的其他缓存一起考虑,例如InnoDB的缓存池,或者MyISAM的索引缓存。关于这点是没法简单给出一个公式或者比率来判断的,因为真正的平衡点与应用程序有很大的关系。
最好的判断查询缓存是否有效的办法还是通过查询某类查询时间消耗是否增大或者减少来判断。Percona Server通过扩展慢查询可以观察到一个查询是否命中缓存,如果查询缓存没有为系统节省时间,那么最好禁用它
如何配置和维护查询缓存。
一旦理解查询缓存工作的原理,配置起来就很容了。它也只有少数的参数可供配置。如下所示:
1.query_cache_type
是否打开查询缓存。可以设置成OFF、ON或DEMAND。DEMAND表示只有在查询语句中明确写明SQL_CACHE的语句才放入查询缓存。这个变量可以是会话级别的也可以是全局级别的
2.query_cache_size
查询缓存使用的总内存空间,单位是字节。这个值必须是1024的整数倍,否则MySQL实际分配的数据会和你指定的略有不同
3.query_cache_min_res_unit
在查询缓存中分配内存块时的最小单位。
4.query_cache_limit
MySQL能够缓存的最大查询结果。如果查询结果大于这个值,则不会被缓存。因为查询缓存在数据生成的时候就开始尝试缓存数据,所以只有当结果全部返回后,MySQL才直到查询结果是否超出限制。如果超出,MySQL则增加状态值Qcache_not_cached,并将结果从查询缓存张删除。如果你事先直到有很多这样的情况发生,那么建议在查询语句中加入SQL_NO_CACHE来避免查询缓存带来的额外消耗
5.query_cache_wlock_invalidate
如果某个数据表被其他的连接锁住,是否仍然从查询缓存中返回结果。这个参数默认是OFF,这可能在一定程序上会改变服务器的行为,因为这使得数据库可能返回其他线程锁住的数据,将参数设置成ON,则不会从缓存中读取这类数据,但是这可能会增加锁等待。对于绝大数应用来说无须注意这个细节,所以默认设置通常是没有问题的。
配置查询缓存通常很简单,但是如果想直到修改这些参数会带来哪些改变,则是一项很复杂的工作。
一旦理解查询缓存工作的原理,配置起来就很容了。它也只有少数的参数可供配置。如下所示:
1.query_cache_type
是否打开查询缓存。可以设置成OFF、ON或DEMAND。DEMAND表示只有在查询语句中明确写明SQL_CACHE的语句才放入查询缓存。这个变量可以是会话级别的也可以是全局级别的
2.query_cache_size
查询缓存使用的总内存空间,单位是字节。这个值必须是1024的整数倍,否则MySQL实际分配的数据会和你指定的略有不同
3.query_cache_min_res_unit
在查询缓存中分配内存块时的最小单位。
4.query_cache_limit
MySQL能够缓存的最大查询结果。如果查询结果大于这个值,则不会被缓存。因为查询缓存在数据生成的时候就开始尝试缓存数据,所以只有当结果全部返回后,MySQL才直到查询结果是否超出限制。如果超出,MySQL则增加状态值Qcache_not_cached,并将结果从查询缓存张删除。如果你事先直到有很多这样的情况发生,那么建议在查询语句中加入SQL_NO_CACHE来避免查询缓存带来的额外消耗
5.query_cache_wlock_invalidate
如果某个数据表被其他的连接锁住,是否仍然从查询缓存中返回结果。这个参数默认是OFF,这可能在一定程序上会改变服务器的行为,因为这使得数据库可能返回其他线程锁住的数据,将参数设置成ON,则不会从缓存中读取这类数据,但是这可能会增加锁等待。对于绝大数应用来说无须注意这个细节,所以默认设置通常是没有问题的。
配置查询缓存通常很简单,但是如果想直到修改这些参数会带来哪些改变,则是一项很复杂的工作。
减少碎片。
没什么办法能够完全避免碎片,但是选择合适的query_cache_min_res_unit可以帮你减少由碎片导致的内存空间浪费。设置合适的值可以平衡每个数据块的大小和每次存储结果时内存块申请的次数。这个值太小,则浪费的空间更少,但是会导致更频繁的内存块申请操作;如果这个值设置得太大,那么碎片会很多。调整合适得值其实是在平衡内存浪费和CPU消耗。这个参数的最合适的大小和应用程序的查询结果的平均大小直接相关。可以通过内存实际消耗(query_cache_szie_Qcache_free_memory)除以Qcache_queries_in_cache计算单个查询的平均缓存大小。如果你的应用程序的查询结果很不均匀,有的结果很大,有的结果很小,那么碎片和反复的的内存块分配可能无法避免。如果你发现缓存一个非常大的结果并没有什么意义(通常确实是这样),那么你可以通过参数query_cache_limit限制可以缓存的最大查询结果,借此大大减少大的查询结果的缓存,最终减少内存碎片的发生。还可以通过参数Qcache_free_blocks来观察碎片。参数Qcache_free_blocks反应了查询缓存中空闲块的多少,在图中的配置我们看到,有两个空闲块。最糟糕的情况是,任何两个存储结果的数据块之间都有一个非常小的空闲块。所以如果Qcache_free_blocks大小恰好达到Qcache_total_blocks/2,那么查询缓存就有严重的碎片问题。而如果你还有很多空闲块,而状态值Qcache_lowmem_prunes还不断地增加,则说ing由于碎片导致了过早地在删除查询缓存结果。可以使用命令FLUSH QUERY CACHE完成碎片整理。这个命令会将所有的查询缓存重新排序,并将所有的空闲空间都聚集到查询缓存的一块区域上。不过需要注意,这个命令并不会将查询缓存清空,清空缓存由命令RESET QUERY CACHE完成。FLUSH QUERY CACHE会访问所有的查询缓存,在这期间任何其他的连接都无法访问查询缓存,从而会导致服务器僵死一段时间,使用这个命令的时候需要特别小心这点。另外,根据经验,建议保持查询缓存空间足够小,以便在维护时可以将服务器僵死控制在非常短的时间内。
没什么办法能够完全避免碎片,但是选择合适的query_cache_min_res_unit可以帮你减少由碎片导致的内存空间浪费。设置合适的值可以平衡每个数据块的大小和每次存储结果时内存块申请的次数。这个值太小,则浪费的空间更少,但是会导致更频繁的内存块申请操作;如果这个值设置得太大,那么碎片会很多。调整合适得值其实是在平衡内存浪费和CPU消耗。这个参数的最合适的大小和应用程序的查询结果的平均大小直接相关。可以通过内存实际消耗(query_cache_szie_Qcache_free_memory)除以Qcache_queries_in_cache计算单个查询的平均缓存大小。如果你的应用程序的查询结果很不均匀,有的结果很大,有的结果很小,那么碎片和反复的的内存块分配可能无法避免。如果你发现缓存一个非常大的结果并没有什么意义(通常确实是这样),那么你可以通过参数query_cache_limit限制可以缓存的最大查询结果,借此大大减少大的查询结果的缓存,最终减少内存碎片的发生。还可以通过参数Qcache_free_blocks来观察碎片。参数Qcache_free_blocks反应了查询缓存中空闲块的多少,在图中的配置我们看到,有两个空闲块。最糟糕的情况是,任何两个存储结果的数据块之间都有一个非常小的空闲块。所以如果Qcache_free_blocks大小恰好达到Qcache_total_blocks/2,那么查询缓存就有严重的碎片问题。而如果你还有很多空闲块,而状态值Qcache_lowmem_prunes还不断地增加,则说ing由于碎片导致了过早地在删除查询缓存结果。可以使用命令FLUSH QUERY CACHE完成碎片整理。这个命令会将所有的查询缓存重新排序,并将所有的空闲空间都聚集到查询缓存的一块区域上。不过需要注意,这个命令并不会将查询缓存清空,清空缓存由命令RESET QUERY CACHE完成。FLUSH QUERY CACHE会访问所有的查询缓存,在这期间任何其他的连接都无法访问查询缓存,从而会导致服务器僵死一段时间,使用这个命令的时候需要特别小心这点。另外,根据经验,建议保持查询缓存空间足够小,以便在维护时可以将服务器僵死控制在非常短的时间内。
提高查询缓存的使用率。
如果查询缓存不再有碎片问题,但你仍然发现命中率很低,还可能是查询缓存的内存空间太小导致的。如果MySQL无法为一个新的查询缓存结果的时候,则会选择删除某个老的缓存结果。当由于这个原因导致删除老的结果时,会增加状态值Qcache_lowmem_prunes.如果这个值增加得很快,那么可能是由下面两个原因导致的:
1.如果还有很多空闲块,那么碎片可能是罪魁祸首
2.如果这是没有什么空闲块了,就说明在这个系统压力下,你分配的查询缓存空间不够大。你可以通过检查状态值Qcache_free_memory来查看还有多少没有使用的内存。
如果空闲块很多,碎片很少,也没什么由于内存导致的缓存失效,但是命中率仍然很低,那么很可能说ing,在你的系统压力下,查询缓存并没有什么好处。一定是什么原因导致查询缓存无法为系统服务,例如有大量的更新或者查询语句本身都不能被缓存。如果在观察命中率时,仍然无法确定查询缓存是否给系统带来了好处,那么可以通过禁用它,然后观察系统的性能,再重新打开它,观察性能变化,据此来判断查询缓存是否给系统带来了好处。可以通过将query_cache_size设置成0,来关闭查询缓存。(改变query_cache_type的全局值并不会影响已经打开的连接,也不会将查询缓存的内存释放给系统)你还可以系统测试来验证,不过一般都很难精确地模拟实际情况。如图展示了一个用来分析配置查询缓存的流程图
如果查询缓存不再有碎片问题,但你仍然发现命中率很低,还可能是查询缓存的内存空间太小导致的。如果MySQL无法为一个新的查询缓存结果的时候,则会选择删除某个老的缓存结果。当由于这个原因导致删除老的结果时,会增加状态值Qcache_lowmem_prunes.如果这个值增加得很快,那么可能是由下面两个原因导致的:
1.如果还有很多空闲块,那么碎片可能是罪魁祸首
2.如果这是没有什么空闲块了,就说明在这个系统压力下,你分配的查询缓存空间不够大。你可以通过检查状态值Qcache_free_memory来查看还有多少没有使用的内存。
如果空闲块很多,碎片很少,也没什么由于内存导致的缓存失效,但是命中率仍然很低,那么很可能说ing,在你的系统压力下,查询缓存并没有什么好处。一定是什么原因导致查询缓存无法为系统服务,例如有大量的更新或者查询语句本身都不能被缓存。如果在观察命中率时,仍然无法确定查询缓存是否给系统带来了好处,那么可以通过禁用它,然后观察系统的性能,再重新打开它,观察性能变化,据此来判断查询缓存是否给系统带来了好处。可以通过将query_cache_size设置成0,来关闭查询缓存。(改变query_cache_type的全局值并不会影响已经打开的连接,也不会将查询缓存的内存释放给系统)你还可以系统测试来验证,不过一般都很难精确地模拟实际情况。如图展示了一个用来分析配置查询缓存的流程图
InnoDB和查询缓存。
因为InnoDB有自己的MVCC机制,所以相比其他存储引擎,InnoDB和查询缓存的交互要更加复杂。MySQL4.0版本中,在事务处理中查询缓存是被禁用的,从4.1和更新的InnoDB版本开始,InnoDB会控制在一个事务中是否可以使用查询缓存,InnoDB会同时控制对查询缓存的读(从缓存中获取查询结果)和写操作(向查询缓存写入结果)。事务是否可以访问查询缓存取决于当前事务ID,以及对应的数据表上是否有锁。每一个InnoDB表的内存数据字典都保存了一个事务ID号,如果当前事务ID小于该事务ID,则无法访问查询缓存。如果表上有任何的锁,那么对这个表的任何查询语句都是无法被缓存的。例如,某个事务执行了SELECT FOR UPDATE语句,那么在这个锁释放之前,任何其他的事务都无法从查询缓存中读取与这个表相关的缓存结果。当事务提交时,InnoDB持有锁,并使用当前的一个系统事务ID更新当前表的计数器。锁一定程度上说明事务需要对表进行修改操作,当然有可能事务获得锁,却不进行任何更新操作,但是如果想要更新任何表的内容,获得相应锁则是前提条件。InnoDB将每个表的计数器设置成某个事务ID,而这个事务ID就代表了当前存在的且修改了该表的最大的事务ID.那么下面的一些事实也就成立:
1.所有大于该表计数器的事务才可以使用查询缓存。例如当前系统系统的事务ID是5,且事务获取了该表的某些记录的锁,然后进行事务提交操作,那么事务1至4,都不应该再读取或者向查询缓存写入任何相关的数据
2.该表的计数器并不是直接更新为对该表进行加锁的事务ID,而是被更新成一个系统事务ID,而是被更新成一个系统事务ID.搜易,会发现该事务自身后续的更新操作也无法读取和修改查询缓存。
查询缓存存储、检索和失效操作都是在MySQL层面完成,InnoDB无法绕过或者延迟这个行为。但InnoDB可以在事务中显式地告诉MySQL何时应该让讴歌表地查询缓存都失效。在有外键限制地时候这是必须地,例如某个SQL语句有ON DELEETE CASCADE那么相关关联表地查询缓存也是要一起失效的。
原则上,在InnoDB的MVCC架构下,当某些修改不影响其他事务读取一致的数据时,是可以使用查询缓存的。但是这样实现起来会非常复杂,InnoDB做了一个简化,让所有有加锁操作的事务都不适用任何查询缓存,这个限制其实并不是必须的。
因为InnoDB有自己的MVCC机制,所以相比其他存储引擎,InnoDB和查询缓存的交互要更加复杂。MySQL4.0版本中,在事务处理中查询缓存是被禁用的,从4.1和更新的InnoDB版本开始,InnoDB会控制在一个事务中是否可以使用查询缓存,InnoDB会同时控制对查询缓存的读(从缓存中获取查询结果)和写操作(向查询缓存写入结果)。事务是否可以访问查询缓存取决于当前事务ID,以及对应的数据表上是否有锁。每一个InnoDB表的内存数据字典都保存了一个事务ID号,如果当前事务ID小于该事务ID,则无法访问查询缓存。如果表上有任何的锁,那么对这个表的任何查询语句都是无法被缓存的。例如,某个事务执行了SELECT FOR UPDATE语句,那么在这个锁释放之前,任何其他的事务都无法从查询缓存中读取与这个表相关的缓存结果。当事务提交时,InnoDB持有锁,并使用当前的一个系统事务ID更新当前表的计数器。锁一定程度上说明事务需要对表进行修改操作,当然有可能事务获得锁,却不进行任何更新操作,但是如果想要更新任何表的内容,获得相应锁则是前提条件。InnoDB将每个表的计数器设置成某个事务ID,而这个事务ID就代表了当前存在的且修改了该表的最大的事务ID.那么下面的一些事实也就成立:
1.所有大于该表计数器的事务才可以使用查询缓存。例如当前系统系统的事务ID是5,且事务获取了该表的某些记录的锁,然后进行事务提交操作,那么事务1至4,都不应该再读取或者向查询缓存写入任何相关的数据
2.该表的计数器并不是直接更新为对该表进行加锁的事务ID,而是被更新成一个系统事务ID,而是被更新成一个系统事务ID.搜易,会发现该事务自身后续的更新操作也无法读取和修改查询缓存。
查询缓存存储、检索和失效操作都是在MySQL层面完成,InnoDB无法绕过或者延迟这个行为。但InnoDB可以在事务中显式地告诉MySQL何时应该让讴歌表地查询缓存都失效。在有外键限制地时候这是必须地,例如某个SQL语句有ON DELEETE CASCADE那么相关关联表地查询缓存也是要一起失效的。
原则上,在InnoDB的MVCC架构下,当某些修改不影响其他事务读取一致的数据时,是可以使用查询缓存的。但是这样实现起来会非常复杂,InnoDB做了一个简化,让所有有加锁操作的事务都不适用任何查询缓存,这个限制其实并不是必须的。
通用查询缓存优化。
库表结构的涉及、查询语句、应用程序涉及都可能会影响到查询缓存的效率。除了前面的介绍之外,还有一些要点需要注意:
1.用多个淆表代替一个大表对查询缓存有好处。这个涉及将会使得失效策略能够在一个更合适的粒度上进行。当然,不要让这个原则过分影响你的涉及,毕竟其他的一些有时可能很容易就弥补了这个问题
2.批量写入时只需要做一次缓存失效,所以相比单条写入效率更好(另外需要注意,不要同时做延迟写和批量写,否则可能会因为失效导致服务器僵死较长时间)
3.因为缓存空间太大,在过期操作的时候可能会导致服务器僵死。一个简单的解决办法就是控制缓存空间的大小(query_cache_size)或者直接禁用查询缓存
4.无法在数据库或者表级别控制查询缓存,但是可以通过SQL_CACHE和SQL_NO_CACHE来控制某个SELECT语句是否需要进行缓存。你还可以通过修改会话级别的变量query_cache_type来控制查询缓存
5.对于写密集型的应用来说,直接禁用查询缓存可能会提高系统的性能。关闭查询缓存可以移除所有相关的消耗。例如将query_cache_size设置成0,那么至少这部分就不再消耗任何内存了
6.因为对互斥信号量的竞争,有时直接关闭查询缓存对读密集型的应用也会有好处。如果你希望提高系统的并发,那么最好做一个相关的测试,对比打开和关闭查询缓存时候的性能差异。
如果不像所有的查询都进入查询缓存,但是有希望某些查询走查询缓存,那么可以将query_cache_type设置成DEMAND,然后在希望缓存的查询中加上SQL_CACHE,这虽然需要在查询中加入一些额外的语法,但是可以让你非常自由地控制哪些查询需要被缓存。相反,如果希望缓存多数查询,而少数查询又不希望缓存,那么你可以使用关键字SQL_NO_CACHE
库表结构的涉及、查询语句、应用程序涉及都可能会影响到查询缓存的效率。除了前面的介绍之外,还有一些要点需要注意:
1.用多个淆表代替一个大表对查询缓存有好处。这个涉及将会使得失效策略能够在一个更合适的粒度上进行。当然,不要让这个原则过分影响你的涉及,毕竟其他的一些有时可能很容易就弥补了这个问题
2.批量写入时只需要做一次缓存失效,所以相比单条写入效率更好(另外需要注意,不要同时做延迟写和批量写,否则可能会因为失效导致服务器僵死较长时间)
3.因为缓存空间太大,在过期操作的时候可能会导致服务器僵死。一个简单的解决办法就是控制缓存空间的大小(query_cache_size)或者直接禁用查询缓存
4.无法在数据库或者表级别控制查询缓存,但是可以通过SQL_CACHE和SQL_NO_CACHE来控制某个SELECT语句是否需要进行缓存。你还可以通过修改会话级别的变量query_cache_type来控制查询缓存
5.对于写密集型的应用来说,直接禁用查询缓存可能会提高系统的性能。关闭查询缓存可以移除所有相关的消耗。例如将query_cache_size设置成0,那么至少这部分就不再消耗任何内存了
6.因为对互斥信号量的竞争,有时直接关闭查询缓存对读密集型的应用也会有好处。如果你希望提高系统的并发,那么最好做一个相关的测试,对比打开和关闭查询缓存时候的性能差异。
如果不像所有的查询都进入查询缓存,但是有希望某些查询走查询缓存,那么可以将query_cache_type设置成DEMAND,然后在希望缓存的查询中加上SQL_CACHE,这虽然需要在查询中加入一些额外的语法,但是可以让你非常自由地控制哪些查询需要被缓存。相反,如果希望缓存多数查询,而少数查询又不希望缓存,那么你可以使用关键字SQL_NO_CACHE
查询缓存的替代方案。
MySQL查询缓存工作的原则是:执行查询最快的方式就是不执行,但是查询仍然需要发送到服务器,服务器也还需要做一点点工作。如果对于某些查询完全不需要与服务器通信效果会如何呢?这是客户端的缓存可以很大程度上帮你分担MySQL服务器的压力。
MySQL查询缓存工作的原则是:执行查询最快的方式就是不执行,但是查询仍然需要发送到服务器,服务器也还需要做一点点工作。如果对于某些查询完全不需要与服务器通信效果会如何呢?这是客户端的缓存可以很大程度上帮你分担MySQL服务器的压力。
优化服务器设置
配置MySQL的IO行为。
有一些配置影响着MySQL怎样同步数据到磁盘以及如何做恢复操作。这些操作对性能的影响非常大,因为都涉及到昂贵的IO操作。它们也表现了性能和数据安全之间的权衡。通常,保证数据立刻并且一致地写到磁盘是很昂贵的。如果能够冒一点磁盘写可能没有真正持久化到磁盘的风险,就可以增加并发性和减少IO等待,但是必须决定可以容忍多大的风险
有一些配置影响着MySQL怎样同步数据到磁盘以及如何做恢复操作。这些操作对性能的影响非常大,因为都涉及到昂贵的IO操作。它们也表现了性能和数据安全之间的权衡。通常,保证数据立刻并且一致地写到磁盘是很昂贵的。如果能够冒一点磁盘写可能没有真正持久化到磁盘的风险,就可以增加并发性和减少IO等待,但是必须决定可以容忍多大的风险
InnoDB的IO配置.
InnoDB不仅允许控制怎么恢复,还允许控制怎么打开和刷新数据(文件),这会对恢复和整体性能产生巨大的影响。尽管可以影响它的行为,InnoDB的恢复流程实际上是自动的,并且经常在InnoDB启动时运行。撇开恢复并假设InnoDB没有崩溃或者出错,InnoDB依然有很多需要配置的地方。它有一系列复杂的缓存和文件涉及可以提升性能,以及保证ACID特性,并且每一部分都是可配置的,如图所示。对于常见的应用,最重要的一小部分内容是InnoDB日志文件大小、InnoDB怎样刷新它的日志缓冲,以及InnoDB怎样执行IO.
InnoDB不仅允许控制怎么恢复,还允许控制怎么打开和刷新数据(文件),这会对恢复和整体性能产生巨大的影响。尽管可以影响它的行为,InnoDB的恢复流程实际上是自动的,并且经常在InnoDB启动时运行。撇开恢复并假设InnoDB没有崩溃或者出错,InnoDB依然有很多需要配置的地方。它有一系列复杂的缓存和文件涉及可以提升性能,以及保证ACID特性,并且每一部分都是可配置的,如图所示。对于常见的应用,最重要的一小部分内容是InnoDB日志文件大小、InnoDB怎样刷新它的日志缓冲,以及InnoDB怎样执行IO.
InnoDB事务日志(包含:Redo log 重做日志和Undo log回滚日志)。
InnoDB使用日志来减少提交事务时的开销,因为日志中已经记录了事务,就无须在每个事务提交时把缓冲池的脏块刷新(flush)到磁盘中。事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些变更到磁盘需要很多随机IO.InnoDB假设使用的时常规磁盘(机械磁盘),随机IO比顺序IO要昂贵很多,因为一个IO请求需要时间把磁头移到正确的位置,然后等待磁盘上读出需要的部分,再转到开始位置。InnoDB用日志把随机IO变成顺序IO.一旦日志安全写到磁盘,事务就持久化了,即使变更还没写到数据文件,如果一些糟糕的事情发生了(例如断电了),InnoDB可以重放日志并且恢复已经提交的事务。当然,InnoDB最后还是必须把变更写到数据文件,因为日志有固定的大小。InnoDB的日志是环形方式写的:当写到日志的尾部,会重新跳转到开头继续写,但不胡覆盖还没应用到数据文件的日志记录,因为这样做会清掉已提交事务的唯一持久化记录。
InnoDB使用一个后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。实际上,事务日志把数据文件的随机IO转换为几乎顺序的日志文件和数据文件IO.把刷新操作转移到后台使查询可以更快完成,并且缓和查询高峰时IO系统的压力。
整体的日志文件受控于innodb_log_file_size和innodb_log_files_in_group两个参数,这对写性能非常重要。日志文件的总大小是每个文件的大小之和。默认情况下,只有两个5MB的文件,总共10MB。对高性能工作来说这太小了。至少需要几百MB,或者甚至上GB的日志文件。
InnoDB使用多个文件作为一组循环日志,通常不需要修改默认的日志数量,只修改每个日志文件的大小即可。要修改日志文件大小,需要完全关闭MySQL,将旧的日志文件移到其他地方保存,重新配置参数,然后重启。一定要确保MySQL干净地关闭了,或者还有日志文件可以保证需要应用到数据文件的事务记录,否则数据库就无法恢复了。当重启服务器的时候,查看MySQL的错误日志。在重启成功之后,才可以删除旧的日志文件。
日志文件大小和日志缓存。要确定理想的日志文件大小,必须权衡正常数据变更的开销和崩溃恢复需要鞥多时间。如果日志太小,InnoDB将必须做更多的检查点,导致更多的日志写。在极个别情况下,写语句可能被拖累,在日志没有空间继续写入前,必须等待变更被应用到数据文件。另一方面,如果日志太大了,在崩溃恢复时InnoDB可能不得不做大量的工作。这可能极大地增加恢复时间,尽管这个处理在新的MySQL版本中已经改善很多。
数据大小和访问模式也将影响恢复时间。假设有一个1TB的数据和16GB的缓冲池,并且全部日志大小是128MB。如果缓冲池里面有很多脏页(例如,页被修改了还没被刷写回数据文件),并且它们均匀地分布在1TB数据中,崩溃后恢复将需要相当长一段时间。InnoDB必须从头到尾扫描日志,仔细检查数据文件,如果需要还要应用变更到数据文件。这是很庞大的读写操作!另一方面,如果变更是局部性的——就是说,如果只有几百MB数据被频繁地变更——恢复可能就很快,即使数据和日志文件很大。恢复时间也依赖普通修改操作的大小,这跟数据行地平均长度有关系。较短的行使得更多的修改而已放在同样的日志中,所以InnoDB可能必须在恢复时重放更多修改操作。
当InnoDB变更任何数据时,会写一条变更记录到内存日志缓冲区。在缓冲区满的时候、事务提交的时候,或者每一秒钟,InnoDB都会刷写缓冲区的内容打磁盘日志文件——无论上述三个条件哪个先达到。如果有大事务,增加日志缓冲区(默认1MB)大小可以帮助减少IO.变量innodb_log_buffer_size可以控制日志缓冲区的大小。
通常不需要把日志缓冲区设置得非常大。推荐的范围是1MB~8MB,一般来说足够了,除非要写很多相当大的BLOB记录。相对于InnoDB的普通数据,日志条目是非常紧凑的,它们不是基于页的,所以不会浪费空间来一次存储整个页。InnoDB也使得日志条目尽可能地端。有时甚至会保存为函数号和C函数的参数。
较大的日志缓冲区在某些情况下也是有好处的:可以减少缓冲区中空间分配的争用。当配置一台有打内存的服务器时,有时简单地分配32MB~128MB的日志缓冲,因为花费这么点相对(整机)而言比较小的内存并没有什么不好,还可以帮助避免压力瓶颈。如果有问题,瓶颈一般会表现为日志缓冲Mutex的竞争。
InnoDB使用日志来减少提交事务时的开销,因为日志中已经记录了事务,就无须在每个事务提交时把缓冲池的脏块刷新(flush)到磁盘中。事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些变更到磁盘需要很多随机IO.InnoDB假设使用的时常规磁盘(机械磁盘),随机IO比顺序IO要昂贵很多,因为一个IO请求需要时间把磁头移到正确的位置,然后等待磁盘上读出需要的部分,再转到开始位置。InnoDB用日志把随机IO变成顺序IO.一旦日志安全写到磁盘,事务就持久化了,即使变更还没写到数据文件,如果一些糟糕的事情发生了(例如断电了),InnoDB可以重放日志并且恢复已经提交的事务。当然,InnoDB最后还是必须把变更写到数据文件,因为日志有固定的大小。InnoDB的日志是环形方式写的:当写到日志的尾部,会重新跳转到开头继续写,但不胡覆盖还没应用到数据文件的日志记录,因为这样做会清掉已提交事务的唯一持久化记录。
InnoDB使用一个后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。实际上,事务日志把数据文件的随机IO转换为几乎顺序的日志文件和数据文件IO.把刷新操作转移到后台使查询可以更快完成,并且缓和查询高峰时IO系统的压力。
整体的日志文件受控于innodb_log_file_size和innodb_log_files_in_group两个参数,这对写性能非常重要。日志文件的总大小是每个文件的大小之和。默认情况下,只有两个5MB的文件,总共10MB。对高性能工作来说这太小了。至少需要几百MB,或者甚至上GB的日志文件。
InnoDB使用多个文件作为一组循环日志,通常不需要修改默认的日志数量,只修改每个日志文件的大小即可。要修改日志文件大小,需要完全关闭MySQL,将旧的日志文件移到其他地方保存,重新配置参数,然后重启。一定要确保MySQL干净地关闭了,或者还有日志文件可以保证需要应用到数据文件的事务记录,否则数据库就无法恢复了。当重启服务器的时候,查看MySQL的错误日志。在重启成功之后,才可以删除旧的日志文件。
日志文件大小和日志缓存。要确定理想的日志文件大小,必须权衡正常数据变更的开销和崩溃恢复需要鞥多时间。如果日志太小,InnoDB将必须做更多的检查点,导致更多的日志写。在极个别情况下,写语句可能被拖累,在日志没有空间继续写入前,必须等待变更被应用到数据文件。另一方面,如果日志太大了,在崩溃恢复时InnoDB可能不得不做大量的工作。这可能极大地增加恢复时间,尽管这个处理在新的MySQL版本中已经改善很多。
数据大小和访问模式也将影响恢复时间。假设有一个1TB的数据和16GB的缓冲池,并且全部日志大小是128MB。如果缓冲池里面有很多脏页(例如,页被修改了还没被刷写回数据文件),并且它们均匀地分布在1TB数据中,崩溃后恢复将需要相当长一段时间。InnoDB必须从头到尾扫描日志,仔细检查数据文件,如果需要还要应用变更到数据文件。这是很庞大的读写操作!另一方面,如果变更是局部性的——就是说,如果只有几百MB数据被频繁地变更——恢复可能就很快,即使数据和日志文件很大。恢复时间也依赖普通修改操作的大小,这跟数据行地平均长度有关系。较短的行使得更多的修改而已放在同样的日志中,所以InnoDB可能必须在恢复时重放更多修改操作。
当InnoDB变更任何数据时,会写一条变更记录到内存日志缓冲区。在缓冲区满的时候、事务提交的时候,或者每一秒钟,InnoDB都会刷写缓冲区的内容打磁盘日志文件——无论上述三个条件哪个先达到。如果有大事务,增加日志缓冲区(默认1MB)大小可以帮助减少IO.变量innodb_log_buffer_size可以控制日志缓冲区的大小。
通常不需要把日志缓冲区设置得非常大。推荐的范围是1MB~8MB,一般来说足够了,除非要写很多相当大的BLOB记录。相对于InnoDB的普通数据,日志条目是非常紧凑的,它们不是基于页的,所以不会浪费空间来一次存储整个页。InnoDB也使得日志条目尽可能地端。有时甚至会保存为函数号和C函数的参数。
较大的日志缓冲区在某些情况下也是有好处的:可以减少缓冲区中空间分配的争用。当配置一台有打内存的服务器时,有时简单地分配32MB~128MB的日志缓冲,因为花费这么点相对(整机)而言比较小的内存并没有什么不好,还可以帮助避免压力瓶颈。如果有问题,瓶颈一般会表现为日志缓冲Mutex的竞争。
可以通过检查SHOW INNODB STATUS的输出中LOG部分来监控InnoDB的日志和日志缓冲区的IO性能,通过观察Innodb_os_log_written状态变量来查看InnoDB对日志文件写出了多少数据。一个好用的经验法则是,查看10~100秒间隔的数字,然后记录分支。可以用这个来判断日志缓冲是否设置得正好。例如,若看到峰值是每秒写100KB数据到日志,那么1MB的日志缓冲可能足够了,也可以使用这个衡量标准来决定日志文件设置多大会比较号。如果峰值是100KB/s,那么256MB的日志我呢见足够存储至少2560秒的日志记录。这看起来足够了。作为一个经验法则,日志文件的全部大小,应该足够容纳服务器一个小时的活动内容。
InnoDB怎样刷新日志缓冲。当InnoDB把日志缓冲刷新到磁盘文件时,先回使用一个Mutex锁住缓冲区,刷新到所需要的位置,然后移动剩下的条目到缓冲区的前面。当Mutex释放时,可能有超过一个事务已经准备好刷新其日志记录。InnoDB有一个Group Commit功能,可以在一个IO操作内提交多个事务,但是MySQL5.0中打开二进制日志时这个功能就不能用了。
日志缓冲必须被刷新到持久化存储,以确保提交的事务完全被持久化了。如果和持久化相比更在乎性能,可以修改innodb_flush_log_at_trx_commit变量来控制日志缓冲区刷新的频繁程度。可能的设置如下:
0
把日志缓冲写到日志文件中,并且每秒钟刷新一次,但是事务提交时不做任何事
1
将日志缓冲写到日志文件,并且每次事务提交都刷新到持久化存储。这是默认的(并且是最安全的)
设置,该设置能保证不会丢失任何已经提交的事务,除非磁盘或者操作系统是"伪"刷新。
2
每次提交时把日志缓冲写到日志文件,但是并不刷新。InnoDB每秒钟做一次刷新。0与2最重要的
不同是(也是为什么2是更合适的设置),如果MySQL进程"挂了",2不会丢失任何事务。如果整个服务器
"挂了"或者断电了,则还是可能会丢失一些事务
InnoDB怎样刷新日志缓冲。当InnoDB把日志缓冲刷新到磁盘文件时,先回使用一个Mutex锁住缓冲区,刷新到所需要的位置,然后移动剩下的条目到缓冲区的前面。当Mutex释放时,可能有超过一个事务已经准备好刷新其日志记录。InnoDB有一个Group Commit功能,可以在一个IO操作内提交多个事务,但是MySQL5.0中打开二进制日志时这个功能就不能用了。
日志缓冲必须被刷新到持久化存储,以确保提交的事务完全被持久化了。如果和持久化相比更在乎性能,可以修改innodb_flush_log_at_trx_commit变量来控制日志缓冲区刷新的频繁程度。可能的设置如下:
0
把日志缓冲写到日志文件中,并且每秒钟刷新一次,但是事务提交时不做任何事
1
将日志缓冲写到日志文件,并且每次事务提交都刷新到持久化存储。这是默认的(并且是最安全的)
设置,该设置能保证不会丢失任何已经提交的事务,除非磁盘或者操作系统是"伪"刷新。
2
每次提交时把日志缓冲写到日志文件,但是并不刷新。InnoDB每秒钟做一次刷新。0与2最重要的
不同是(也是为什么2是更合适的设置),如果MySQL进程"挂了",2不会丢失任何事务。如果整个服务器
"挂了"或者断电了,则还是可能会丢失一些事务
了解清楚"把日志缓冲写到日中文件"和"把日志刷新到持久化存储"之间的不同是很重要的。在大部分操作系统中,把缓冲写到日志只是简单地把数据从InnoDB的内存缓冲转移到了操作系统的缓存,也是在内存里,并没有真的把数据写到持久化存储。因此,如果MySQL崩溃了或者电源断电了,设置0和2通常会导致最多一秒的丢失,因为数据可能只存在于操作系统的缓存。我们说"通常",因为不论如何InnoDB会每秒尝试刷新日志文件到磁盘,但是在一些场景下也可能丢失超过1秒的事务,例如当刷新被推迟了。
与此相反,把日志刷新到持久化存储意味着InnoDB请求操作系统把数据刷出缓存,并且确认写到磁盘了。这是一个阻塞IO的调用,直到数据被完全写回才会完成。因为写数据到磁盘比较满,当innodb_flush_log_at_trx_commit被设置为1时,可能明显地降低InnoDB每秒可以提交的事务数。今天的高速驱动器可能每秒只能执行一两百个磁盘事务,受限于磁盘旋转速度和寻道时间。有时硬盘控制器或者操作系统假装做了刷新,其实只是把数据放到了另一个缓存,例如磁盘自己的缓存。这更快但是很危险,因为u如果驱动器断电,数据依然可能丢失。者甚至比设置innodb_flush_log_at_trx_commit为不为1的值更糟糕,因为这可能导致数据损坏,不仅仅时丢失事务。
设置innodb_flush_log_at_trx为不为1的值可能导致丢失事务。然而,如果不在意持久性(ACID中的D),那么设置为其他的值也是有用的。也许你只是想拥有InnoDB的其他一些功能,例如聚簇索引、防止数据损坏,以及行锁。但仅仅因为性能原因用InnoDB替换MyISAM的情况也并不少见。
高性能事务处理需要的最佳配置是把innodb_flush_log_at_trx_commit设置为1且把日志i文件放到一个有电池保护的写缓存的RAID卷中。这兼顾了安全和速度。事实上,哦我们敢说任何希望能扛过高负荷工作负载的产品数据库服务器,都需要有这种类型的硬件。
Percona Server扩展了innodb_flush_log_at_trx_commit变量,使得它成为一个会话级变量,而不是一个全局变量。这允许有不同的性能和持久化要求的应用,可以使用同样的数据库,同时又避免了标准MySQL提供的一刀切的解决方案。
与此相反,把日志刷新到持久化存储意味着InnoDB请求操作系统把数据刷出缓存,并且确认写到磁盘了。这是一个阻塞IO的调用,直到数据被完全写回才会完成。因为写数据到磁盘比较满,当innodb_flush_log_at_trx_commit被设置为1时,可能明显地降低InnoDB每秒可以提交的事务数。今天的高速驱动器可能每秒只能执行一两百个磁盘事务,受限于磁盘旋转速度和寻道时间。有时硬盘控制器或者操作系统假装做了刷新,其实只是把数据放到了另一个缓存,例如磁盘自己的缓存。这更快但是很危险,因为u如果驱动器断电,数据依然可能丢失。者甚至比设置innodb_flush_log_at_trx_commit为不为1的值更糟糕,因为这可能导致数据损坏,不仅仅时丢失事务。
设置innodb_flush_log_at_trx为不为1的值可能导致丢失事务。然而,如果不在意持久性(ACID中的D),那么设置为其他的值也是有用的。也许你只是想拥有InnoDB的其他一些功能,例如聚簇索引、防止数据损坏,以及行锁。但仅仅因为性能原因用InnoDB替换MyISAM的情况也并不少见。
高性能事务处理需要的最佳配置是把innodb_flush_log_at_trx_commit设置为1且把日志i文件放到一个有电池保护的写缓存的RAID卷中。这兼顾了安全和速度。事实上,哦我们敢说任何希望能扛过高负荷工作负载的产品数据库服务器,都需要有这种类型的硬件。
Percona Server扩展了innodb_flush_log_at_trx_commit变量,使得它成为一个会话级变量,而不是一个全局变量。这允许有不同的性能和持久化要求的应用,可以使用同样的数据库,同时又避免了标准MySQL提供的一刀切的解决方案。
InnoDB 怎样打开和刷新日志以及数据文件。
使用innodb_flush_method选项可以配置InnoDB如何跟文件系统相互作用。从名字来看,会以为只能影响InnoDB怎么写数据,实际上还影响了InnoDB怎么读数据。Windows和非Windows的操作系统对这个选项的值是互斥的:async_unbuffered、unbuffered和norm只能在Windows下使用,并且Windows下不能使用其他的值。在Windows下默认值是unbuffered,其他操作系统都是fdatasync(如果SHOW GLOBAL VARIABLES 显式这个变量为空,意味着它被设置为默认值了)。
改变Innodb执行IO操作的方式可以显著地影响性能,所以请确认你明白了在做什么后再去做改动。
这是个有点难以理解的选项,因为它既影响日志文件,也应该想数据文件,而且有时候对不同类型的文件的处理也不一样。如果有一个选项来配置日志,另一个选项来配置数据为念,这样最好了,但实际上它们混合在同一个配置项中。下面是一些可能的值:
1.fdatasync
这在非Windows系统上是默认值:InnoDB用fsync()来刷新数据和日志文件。InnoDB通常用fsync()代替fdatasync(),即使这个值似乎表达的是相反的意思。fdatasync()跟fsync()相似,但是只刷新文件的数据,而不包括元数据(最后修改时间,等等)。因此,fsync()会导致更多的IO,然而InnoDB的开发者都很保守,它们发现某些场景下fdatasync()会导致数据损坏。InnoDB决定了哪些方法可以安全地使用,有一些是编译时设置的,也有一些是运行时设置的。它使用尽可能最快的安全方法。使用fsync()的缺点是操作系统至少会在自己的缓存中缓冲一些苏剧。理论上,这种双重缓冲是浪费的,因为InnoDB管理自己的缓冲比操作系统能做的更加智能。然而,最后的影响跟操作系统和文件系统非常相关。如果能让文件系统做更智能的IO调度和批量操作,双重缓冲可能并不是坏事。有的文件系统和操作系统可以积累写操作后合并执行,通过对IO重新排序来提升效率,或者并发写入多个设备。它们也可能做预读优化,例如,若连续请求了几个顺序的块,它会通知硬盘预读下一个块。有时这些优化有帮助,有时没有。如果你好奇你的系统中的fsync()会做哪些具体的事,可以阅读系统的帮助手册,看下fsync(2).innodb_file_per_table选项会导致每个文件独立地做fsync(),这意味着些多个表不能合并到一个IO操作。这可能导致InnoDB执行更多的fsync()操作
2.0_DIRECT
InnoDB对数据文件使用0_DIRECT标记或directio()函数,这依赖于操作系统。这个设置并不影响文件并且不是在所有的类UNIX系统上都有效。但至少GNU/Linux、FreeBSD。以及Solaris(5.0以后的新版本)是支持的。不像0_DSYNC标记,它会同时影响读和写。这个设置依然使用fsync()来刷新文件到磁盘,但是会通知操作系统不要缓存数据,也不要使用预读。这个选项完全关闭了操作系统缓存,并且使所有的读和写都直接通过存储设备,避免了双重缓冲。在大部分系统上,这个实现用fcntl()调用来设置文件描述符的0_DIRECT标记,所以可以阅读fcntl(2)的手册页来了解系统上这个函数的细节。在Solaris系统,这个选项用directio().如果RAID卡支持预读,这个设置不会关闭RAID卡的预读。这个设置只能关闭操作系统和文件系统的预读(RAID卡的预读控制必须在RAID卡的设置中调整)。如果使用0_DIRECT选项,通常需要带有写缓存的RAID卡,并且设置为Write-Back策略(就是写入会在RAID卡缓存上进行缓冲,不直接写道硬盘)因为这是典型的唯一能保持好性能的方法。当InnoDB和实际存储设备之间没有缓冲时使用0_DIRECT,例如当RAID卡没有写缓存时,可能导致严重的性能下降。现在有了多个写线程,这个问题稍微小一点(并且MySQL5.5提供了原生异步IO)但是通常还是有问题。这个选项可能导致服务器预热时间变长,特别时草走系统的缓存很大的时候,也可能导致小容量的缓存池(例如,默认大小的缓冲池)比缓冲IO(Buffered IO)方式操作要慢的多。这是因为操作系统不会通过保持更多数据在自己的缓存中来"帮助"提升性能.如果需要的数据不在缓冲池,InnoDB将不得不直接从磁盘读取。这个选项不会对innodb_file_per_table产生任何额外的损失。相反,如果不用innodb_file_per_table,当使用0_DIRECT时,可能由于一些顺序IO而遭受性能损失。这种情况的发生是因为一些文件系统(包括Linux所有的ext文件系统)每个inode有一个Mutex。当在这些文件系统上使用0_DIRECT时,确实需要打开innodb_file_per_table
使用innodb_flush_method选项可以配置InnoDB如何跟文件系统相互作用。从名字来看,会以为只能影响InnoDB怎么写数据,实际上还影响了InnoDB怎么读数据。Windows和非Windows的操作系统对这个选项的值是互斥的:async_unbuffered、unbuffered和norm只能在Windows下使用,并且Windows下不能使用其他的值。在Windows下默认值是unbuffered,其他操作系统都是fdatasync(如果SHOW GLOBAL VARIABLES 显式这个变量为空,意味着它被设置为默认值了)。
改变Innodb执行IO操作的方式可以显著地影响性能,所以请确认你明白了在做什么后再去做改动。
这是个有点难以理解的选项,因为它既影响日志文件,也应该想数据文件,而且有时候对不同类型的文件的处理也不一样。如果有一个选项来配置日志,另一个选项来配置数据为念,这样最好了,但实际上它们混合在同一个配置项中。下面是一些可能的值:
1.fdatasync
这在非Windows系统上是默认值:InnoDB用fsync()来刷新数据和日志文件。InnoDB通常用fsync()代替fdatasync(),即使这个值似乎表达的是相反的意思。fdatasync()跟fsync()相似,但是只刷新文件的数据,而不包括元数据(最后修改时间,等等)。因此,fsync()会导致更多的IO,然而InnoDB的开发者都很保守,它们发现某些场景下fdatasync()会导致数据损坏。InnoDB决定了哪些方法可以安全地使用,有一些是编译时设置的,也有一些是运行时设置的。它使用尽可能最快的安全方法。使用fsync()的缺点是操作系统至少会在自己的缓存中缓冲一些苏剧。理论上,这种双重缓冲是浪费的,因为InnoDB管理自己的缓冲比操作系统能做的更加智能。然而,最后的影响跟操作系统和文件系统非常相关。如果能让文件系统做更智能的IO调度和批量操作,双重缓冲可能并不是坏事。有的文件系统和操作系统可以积累写操作后合并执行,通过对IO重新排序来提升效率,或者并发写入多个设备。它们也可能做预读优化,例如,若连续请求了几个顺序的块,它会通知硬盘预读下一个块。有时这些优化有帮助,有时没有。如果你好奇你的系统中的fsync()会做哪些具体的事,可以阅读系统的帮助手册,看下fsync(2).innodb_file_per_table选项会导致每个文件独立地做fsync(),这意味着些多个表不能合并到一个IO操作。这可能导致InnoDB执行更多的fsync()操作
2.0_DIRECT
InnoDB对数据文件使用0_DIRECT标记或directio()函数,这依赖于操作系统。这个设置并不影响文件并且不是在所有的类UNIX系统上都有效。但至少GNU/Linux、FreeBSD。以及Solaris(5.0以后的新版本)是支持的。不像0_DSYNC标记,它会同时影响读和写。这个设置依然使用fsync()来刷新文件到磁盘,但是会通知操作系统不要缓存数据,也不要使用预读。这个选项完全关闭了操作系统缓存,并且使所有的读和写都直接通过存储设备,避免了双重缓冲。在大部分系统上,这个实现用fcntl()调用来设置文件描述符的0_DIRECT标记,所以可以阅读fcntl(2)的手册页来了解系统上这个函数的细节。在Solaris系统,这个选项用directio().如果RAID卡支持预读,这个设置不会关闭RAID卡的预读。这个设置只能关闭操作系统和文件系统的预读(RAID卡的预读控制必须在RAID卡的设置中调整)。如果使用0_DIRECT选项,通常需要带有写缓存的RAID卡,并且设置为Write-Back策略(就是写入会在RAID卡缓存上进行缓冲,不直接写道硬盘)因为这是典型的唯一能保持好性能的方法。当InnoDB和实际存储设备之间没有缓冲时使用0_DIRECT,例如当RAID卡没有写缓存时,可能导致严重的性能下降。现在有了多个写线程,这个问题稍微小一点(并且MySQL5.5提供了原生异步IO)但是通常还是有问题。这个选项可能导致服务器预热时间变长,特别时草走系统的缓存很大的时候,也可能导致小容量的缓存池(例如,默认大小的缓冲池)比缓冲IO(Buffered IO)方式操作要慢的多。这是因为操作系统不会通过保持更多数据在自己的缓存中来"帮助"提升性能.如果需要的数据不在缓冲池,InnoDB将不得不直接从磁盘读取。这个选项不会对innodb_file_per_table产生任何额外的损失。相反,如果不用innodb_file_per_table,当使用0_DIRECT时,可能由于一些顺序IO而遭受性能损失。这种情况的发生是因为一些文件系统(包括Linux所有的ext文件系统)每个inode有一个Mutex。当在这些文件系统上使用0_DIRECT时,确实需要打开innodb_file_per_table
3.0_DSYNC
这个选项使用日志文件调用open()函数时设置0_SYNC标记。它使得所有的写同步——换个说法,只有数据写道磁盘后写操作才返回。这个选项不影响数据文件。0_SYNC标记和0_DIRECT标记的不同之处在于0_SYNC没有禁用操作系统层的缓存。因此,它没有避免双重缓冲,并且它没有使写操作直接操作到磁盘。用了0_SYNC标记,在缓存中写数据,然后发送到磁盘。使用0_SYNC标记做同步写操作,听起来可能跟fsync()作得事情非常相似,但是它们两个的实现无论在操作系统层还是在硬件层都非常不同。用了0_SYNC标记后,操作系统可能把"使用同步IO"标记下传给硬件层,告诉设备不要使用缓存,另一方面,fsync()告诉操作系统把修改过的缓冲数据刷写到设备上,如果设备支持,紧接着会传递一个指令给设备刷新它自己的缓存,所以,毫无疑问,数据肯定记录在了物理媒介上。另一个不同是,用了0_SYNC的话,每个write()或pwrite()操作都会在函数完成之前把数据同步到磁盘,完成前函数调用是阻塞的。相对来看,不用0_SYNC标记的写入调用fsync()允许写操作积累在缓存(使得每个写更快),然后一次性刷新所有的数据。再一次吐槽下这个名称,这个选项设置0_SYNC标记,不是0_DSYNC标记,因为InnoDB开发者发现了0_DSYNC的Bug。0_SYNC和0_DSYNC类似于fsync()和fdatasync(),0_SYNC同时同步数据和元数据,但是0_DSYNC只同步数据。
4.async_unbuffered
这是Windows下的默认值。这个选项让InnoDB对大部分写使用没有缓冲的IO;例外是当innodb_flush_log_at_trx_commit设置为2的时候,对日志文件使用缓冲IO。这个选项使得InnoDB在Windows 2000 、XP,以及更新版本中对数据读写都是用操作系统的原生(重叠的)IO.在更老的Windows版本哪种,InnoDB使用自己用多线程模拟的异步IO
5.unbuffered
只对Windows有效。这个选项于async_unbuffered类似,但是不适用原生异步IO
6.normal
只对Windows有效。这个选项让InnoDB不要使用原生异步IO或者无缓冲IO
7.Nosync和littersync
只为开发使用。这个两个选项在文档中没有并且对生产环境来说不安全,不应该使用这个。
如果这些看起来像是一堆不带建议的说明,那么下面是一些建议:如果使用类UNIX操作系统并且RAID控制器带有电池保护的写缓存,建议使用0_DIRECT。如果不是这样,默认值或者0_DIRECT都可能是最好的选择
这个选项使用日志文件调用open()函数时设置0_SYNC标记。它使得所有的写同步——换个说法,只有数据写道磁盘后写操作才返回。这个选项不影响数据文件。0_SYNC标记和0_DIRECT标记的不同之处在于0_SYNC没有禁用操作系统层的缓存。因此,它没有避免双重缓冲,并且它没有使写操作直接操作到磁盘。用了0_SYNC标记,在缓存中写数据,然后发送到磁盘。使用0_SYNC标记做同步写操作,听起来可能跟fsync()作得事情非常相似,但是它们两个的实现无论在操作系统层还是在硬件层都非常不同。用了0_SYNC标记后,操作系统可能把"使用同步IO"标记下传给硬件层,告诉设备不要使用缓存,另一方面,fsync()告诉操作系统把修改过的缓冲数据刷写到设备上,如果设备支持,紧接着会传递一个指令给设备刷新它自己的缓存,所以,毫无疑问,数据肯定记录在了物理媒介上。另一个不同是,用了0_SYNC的话,每个write()或pwrite()操作都会在函数完成之前把数据同步到磁盘,完成前函数调用是阻塞的。相对来看,不用0_SYNC标记的写入调用fsync()允许写操作积累在缓存(使得每个写更快),然后一次性刷新所有的数据。再一次吐槽下这个名称,这个选项设置0_SYNC标记,不是0_DSYNC标记,因为InnoDB开发者发现了0_DSYNC的Bug。0_SYNC和0_DSYNC类似于fsync()和fdatasync(),0_SYNC同时同步数据和元数据,但是0_DSYNC只同步数据。
4.async_unbuffered
这是Windows下的默认值。这个选项让InnoDB对大部分写使用没有缓冲的IO;例外是当innodb_flush_log_at_trx_commit设置为2的时候,对日志文件使用缓冲IO。这个选项使得InnoDB在Windows 2000 、XP,以及更新版本中对数据读写都是用操作系统的原生(重叠的)IO.在更老的Windows版本哪种,InnoDB使用自己用多线程模拟的异步IO
5.unbuffered
只对Windows有效。这个选项于async_unbuffered类似,但是不适用原生异步IO
6.normal
只对Windows有效。这个选项让InnoDB不要使用原生异步IO或者无缓冲IO
7.Nosync和littersync
只为开发使用。这个两个选项在文档中没有并且对生产环境来说不安全,不应该使用这个。
如果这些看起来像是一堆不带建议的说明,那么下面是一些建议:如果使用类UNIX操作系统并且RAID控制器带有电池保护的写缓存,建议使用0_DIRECT。如果不是这样,默认值或者0_DIRECT都可能是最好的选择
InnoDB表空间。
InnoDB把数据保存在表空间内,本质上是一个由一个或多个磁盘文件组成的虚拟文件系统。InnoDB用表空间实现很多功能,并不只是存储表和索引。它还保存了回滚日志(旧版本行)、插入缓冲(Insert Buffer)、双写缓冲(Doublewrite Buffer),以及其他内部数据结构。配置表空间。通过innodb_data_file_path配置项可以定制表空间文件。这些文件都放在innodb_dta_home_dir指定的目录下。这是一个例子:
```c
innodb_data_home_dir = /var/lib/mysql
innodb_data_file_path = ibdata1:1G;ibdata2:1G;ibdata3:1G
```
这里再三个为文件中创建了3GB的表空间。有时人们并不清楚可以使用多个文件分散驱动器的负载,像这样:
```c
innodb_data_file_path = /disk1/ibdata1:1G;/disk2/ibdata2:1G;....
```
在这个例子中,表空间确实放在代表不同驱动器的不同目录中,InnoDB把这些文件首尾相连组合起来。因此,通常这种方式并不能获得太多收益。InnoDB先填满第一个文件,当第一个文件满了再用第二个文件,如此循环;负载并没有按照希望的高性能方式分布。用RAID控制器是分布负载更聪明的方式。为了允许表空间再超过了分配的空间时还能增长,可以像这样配置最后一个文件自动扩展:
```c
...ibdata3:1G:autoextend
```
默认的行为是创建单个10MB的自动扩展文件。如果让文件可以自动扩展,那么最好给表空间大小设置一个上线,别让它扩展得太大,因为一旦扩展了,就不能收缩回来。例如,下面得例子限制了自动扩展文件最多到2GB:
```c
...ibdata3:1G:autoextend:max:2G
```
管理一个单独得表空间可能有点麻烦,尤其是如果它是自动扩展的,并且希望回收空间时(因为这个原因,我们建议关闭自动扩展功能,至少设置一个合理的空间范围)。回收空间唯一的方式是导出数据,关闭MySQL,删除所有文件,修改配置,重启,让InnoDB创建新的数据文件,然后导入数据。InnoDB这种表空间管理方式很让人头疼——不能简单地删除文件或者改变大小。如果表空间损坏了,InnoDB会拒绝启动。对日志文件也一样的严格。如果像MyISAM一样随便移动文件,千万要谨慎。
innodb_file_per_table选项让InnoDB为每张表使用一个文件,MySQL4.1和之后的版本都支持。它在数据字典存储为"表明.ibd"的数据。这使得删除一张表时回收空间简单多了,并且可以容易地分散表到不同的磁盘上。然而,把数据放到多个文件,总体来说可能导致更多的空间浪费,因为把单个InnoDB表空间的内部碎片浪费分布到了多个.ibd文件,对于非常小的表,这个问题更大,因为InnoDB的页大小是16KB.即使表只有1KB的数据,仍然需要至少16KB的磁盘空间。即使打开innodb_file_per_table选项,依然需要为回滚日志和其他系统数据创建共享表空间。没有把所有数据存在其中是明智的做法,但最好还是关闭它的自动增长,因为无法在不重新导入全部数据的情况下给共享表空间瘦身。一些人喜欢使用innodb_file_per_table,只是因为特别容易管理,并且可以看到每个表的文件。例如,可以通过查看文件的大小来确认表的大小,这比用SHOW TABLE STATUS来看快多了,这个命令需要执行很多复杂的工作来判断给一个表分配了多少页面。设置innodb_file_per_table也有不好的一面:更差的DROP TABLE性能。这可能足以导致显而易见的服务器端阻塞。因为由如下两个原因:
1.删除表需要从文件系统层去掉(删除)文件,这可能在某些文件熊(ext3,说的就是你)上会很慢。可以通过欺骗文件系统来缩短这个过程:把.ibd文件链接欸到一个0字节的文件,然后手动删除这个文件,而不用等待MySQL来做。
2.当打开这个选项,每张表都在InnoDB中使用自己的表空间。结果时,移除表空间实际上需要InnoDB锁定和扫描缓冲池,查找术语这个表空间的页面,在一个有庞大的缓冲池的服务器上做这个操作是非常慢的。如果打算删除很多InnoDB表(包括临时表)并且用了innodb_file_per_table,可能会从Percona Server包含的一个修复中获益,他可以让服务器慢慢地清理掉术语被删除的页面。只需要设置innodb_lazy_drop_table这个选项。
行的旧版本和表空间。
在一个写压力大的环境下,InnoDB的表空间可能增长得非常大。如果事务保持打开状态很久(即使它们没有做任何事),并且使用默认得REPETABLE READ事务隔离级别,InnoDB将不能删除旧得行版本,因为没提交得事务依然需要看到它们。InnoDB把旧版本存在共享表空间,所以如果有更多的数据在更新,共享表空间会持续增长。有时这个问题并非是没有提交的事务的原因,也可能是工作负载的问题:清理过程只有一个线程处理,直到最近的MySQL版本才改进,这可能导致清理线程处理速度跟不上旧版本行数增加的速度。
无论发生何种情况,SHOW INNODB STATUS的数据都可以帮助定位问题。查看历史链表的长度会显示了回滚日志的大小,以页为单位。分析TRANSACTIONS部分的第一行和第二行可以证实这个观点,这部分展示了当前事务号以及清理线程完成到了哪个点。如果这个表差距很大,可能有大量的没有清理的事务。这有个例子:
```sql
---------
TRANSACTIONS
---------
Trx id counter 0 80157601
Purge done for trx's n:o < 0 80154573 undo n:o < 0 0
```
事务标识是一个64比特的数字,由两个32比特的数字(在更新版本的InnoDB中这是一个十六进制的数组)组成,所以需要这一点数学计算来计算差距。在这个例子中旧很简单了,因为最高为是0:那么由80 157 601 - 80 154 573 = 3028个"潜在的"没有被清理的事务。我们说"潜在的",是因为这跟很多没有清理的行是由很大区别的。只有改变了数据的事务才会创建旧版本的行,但是有很多事务并没有修改过数据(相反的,一个事务也可能修改很多行)
如果有个很大的回滚日志并且表空间因此增长得很快,可以强制MySQL减速来使InnoDB的清理线程可以跟得上。这听起来不怎么样,但是没办法。否则,InnoDB将保持数据写入,填充磁盘直到最后磁盘空间饱满,或者表空间大于定义的上线。
为了控制写入速度,可以设置innodb_max_purge_lag变量为一个大于0的只。这个只标识InnoDB开始延迟后面的语句更新数据之前,可以等待被清除的最大的事务数量。你必须直到工作负载以决定一个合理的只。例如事务平均影响1KB的行,并且可以容许表空间里有100MB的未清理行,那么可以设置这个值为100 000.牢记,没有清理的行版本会对所有的查询产生影响,因为它们事实上使得表和索引更大了。如果清理线程确实跟不上,性能可能显著地下降。设置innodb_max_purge_lag变量也会降低性能,但是它的伤害较少。在更新版本的MySQL中,甚至在更早版本的Percona Server和MariaDB,清理过程已经显著地提升了性能,并且从其他内部工作任务中分离出来。甚至可以创建多个专用地清理线程来更快地做这个后台工作。如果可以利用这些特性,会比限制服务器地服务能力要好得多
InnoDB把数据保存在表空间内,本质上是一个由一个或多个磁盘文件组成的虚拟文件系统。InnoDB用表空间实现很多功能,并不只是存储表和索引。它还保存了回滚日志(旧版本行)、插入缓冲(Insert Buffer)、双写缓冲(Doublewrite Buffer),以及其他内部数据结构。配置表空间。通过innodb_data_file_path配置项可以定制表空间文件。这些文件都放在innodb_dta_home_dir指定的目录下。这是一个例子:
```c
innodb_data_home_dir = /var/lib/mysql
innodb_data_file_path = ibdata1:1G;ibdata2:1G;ibdata3:1G
```
这里再三个为文件中创建了3GB的表空间。有时人们并不清楚可以使用多个文件分散驱动器的负载,像这样:
```c
innodb_data_file_path = /disk1/ibdata1:1G;/disk2/ibdata2:1G;....
```
在这个例子中,表空间确实放在代表不同驱动器的不同目录中,InnoDB把这些文件首尾相连组合起来。因此,通常这种方式并不能获得太多收益。InnoDB先填满第一个文件,当第一个文件满了再用第二个文件,如此循环;负载并没有按照希望的高性能方式分布。用RAID控制器是分布负载更聪明的方式。为了允许表空间再超过了分配的空间时还能增长,可以像这样配置最后一个文件自动扩展:
```c
...ibdata3:1G:autoextend
```
默认的行为是创建单个10MB的自动扩展文件。如果让文件可以自动扩展,那么最好给表空间大小设置一个上线,别让它扩展得太大,因为一旦扩展了,就不能收缩回来。例如,下面得例子限制了自动扩展文件最多到2GB:
```c
...ibdata3:1G:autoextend:max:2G
```
管理一个单独得表空间可能有点麻烦,尤其是如果它是自动扩展的,并且希望回收空间时(因为这个原因,我们建议关闭自动扩展功能,至少设置一个合理的空间范围)。回收空间唯一的方式是导出数据,关闭MySQL,删除所有文件,修改配置,重启,让InnoDB创建新的数据文件,然后导入数据。InnoDB这种表空间管理方式很让人头疼——不能简单地删除文件或者改变大小。如果表空间损坏了,InnoDB会拒绝启动。对日志文件也一样的严格。如果像MyISAM一样随便移动文件,千万要谨慎。
innodb_file_per_table选项让InnoDB为每张表使用一个文件,MySQL4.1和之后的版本都支持。它在数据字典存储为"表明.ibd"的数据。这使得删除一张表时回收空间简单多了,并且可以容易地分散表到不同的磁盘上。然而,把数据放到多个文件,总体来说可能导致更多的空间浪费,因为把单个InnoDB表空间的内部碎片浪费分布到了多个.ibd文件,对于非常小的表,这个问题更大,因为InnoDB的页大小是16KB.即使表只有1KB的数据,仍然需要至少16KB的磁盘空间。即使打开innodb_file_per_table选项,依然需要为回滚日志和其他系统数据创建共享表空间。没有把所有数据存在其中是明智的做法,但最好还是关闭它的自动增长,因为无法在不重新导入全部数据的情况下给共享表空间瘦身。一些人喜欢使用innodb_file_per_table,只是因为特别容易管理,并且可以看到每个表的文件。例如,可以通过查看文件的大小来确认表的大小,这比用SHOW TABLE STATUS来看快多了,这个命令需要执行很多复杂的工作来判断给一个表分配了多少页面。设置innodb_file_per_table也有不好的一面:更差的DROP TABLE性能。这可能足以导致显而易见的服务器端阻塞。因为由如下两个原因:
1.删除表需要从文件系统层去掉(删除)文件,这可能在某些文件熊(ext3,说的就是你)上会很慢。可以通过欺骗文件系统来缩短这个过程:把.ibd文件链接欸到一个0字节的文件,然后手动删除这个文件,而不用等待MySQL来做。
2.当打开这个选项,每张表都在InnoDB中使用自己的表空间。结果时,移除表空间实际上需要InnoDB锁定和扫描缓冲池,查找术语这个表空间的页面,在一个有庞大的缓冲池的服务器上做这个操作是非常慢的。如果打算删除很多InnoDB表(包括临时表)并且用了innodb_file_per_table,可能会从Percona Server包含的一个修复中获益,他可以让服务器慢慢地清理掉术语被删除的页面。只需要设置innodb_lazy_drop_table这个选项。
innod_file_per_table默认是打开的
什么是最终的建议?我们建议使用innodb_file_per_table并且给共享表空间设置大小范围,这样可以过得舒服点(不用处理那些空间回收的事)。如果遇到人恶化头痛的场景,就像上面说的,考虑用下Percona的那个修复。提醒一下,事实上没有必要把InnoDB文件放在传统的文件系统上,像许多的传统数据库服务器一样,InnoDB提供使用裸设备的选项——例如,一个没有格式化的分区——作为它的存储。然而,今天的文件系统已经可以存放足够大的文件,所以经没有必要使用这个选项。使用裸设备可能提升几个百分点的性能,但是我们不认为这点小提升足以抵消这样做带来的坏处,我们不能直接使用文件管理数据。当把数据存在一个裸设备分区时,不能使用mv、cp或其他任何工具来操作它。最终,这点小的性能收益显然不值得。
行的旧版本和表空间。
在一个写压力大的环境下,InnoDB的表空间可能增长得非常大。如果事务保持打开状态很久(即使它们没有做任何事),并且使用默认得REPETABLE READ事务隔离级别,InnoDB将不能删除旧得行版本,因为没提交得事务依然需要看到它们。InnoDB把旧版本存在共享表空间,所以如果有更多的数据在更新,共享表空间会持续增长。有时这个问题并非是没有提交的事务的原因,也可能是工作负载的问题:清理过程只有一个线程处理,直到最近的MySQL版本才改进,这可能导致清理线程处理速度跟不上旧版本行数增加的速度。
无论发生何种情况,SHOW INNODB STATUS的数据都可以帮助定位问题。查看历史链表的长度会显示了回滚日志的大小,以页为单位。分析TRANSACTIONS部分的第一行和第二行可以证实这个观点,这部分展示了当前事务号以及清理线程完成到了哪个点。如果这个表差距很大,可能有大量的没有清理的事务。这有个例子:
```sql
---------
TRANSACTIONS
---------
Trx id counter 0 80157601
Purge done for trx's n:o < 0 80154573 undo n:o < 0 0
```
事务标识是一个64比特的数字,由两个32比特的数字(在更新版本的InnoDB中这是一个十六进制的数组)组成,所以需要这一点数学计算来计算差距。在这个例子中旧很简单了,因为最高为是0:那么由80 157 601 - 80 154 573 = 3028个"潜在的"没有被清理的事务。我们说"潜在的",是因为这跟很多没有清理的行是由很大区别的。只有改变了数据的事务才会创建旧版本的行,但是有很多事务并没有修改过数据(相反的,一个事务也可能修改很多行)
如果有个很大的回滚日志并且表空间因此增长得很快,可以强制MySQL减速来使InnoDB的清理线程可以跟得上。这听起来不怎么样,但是没办法。否则,InnoDB将保持数据写入,填充磁盘直到最后磁盘空间饱满,或者表空间大于定义的上线。
为了控制写入速度,可以设置innodb_max_purge_lag变量为一个大于0的只。这个只标识InnoDB开始延迟后面的语句更新数据之前,可以等待被清除的最大的事务数量。你必须直到工作负载以决定一个合理的只。例如事务平均影响1KB的行,并且可以容许表空间里有100MB的未清理行,那么可以设置这个值为100 000.牢记,没有清理的行版本会对所有的查询产生影响,因为它们事实上使得表和索引更大了。如果清理线程确实跟不上,性能可能显著地下降。设置innodb_max_purge_lag变量也会降低性能,但是它的伤害较少。在更新版本的MySQL中,甚至在更早版本的Percona Server和MariaDB,清理过程已经显著地提升了性能,并且从其他内部工作任务中分离出来。甚至可以创建多个专用地清理线程来更快地做这个后台工作。如果可以利用这些特性,会比限制服务器地服务能力要好得多
双写缓冲(Doublewrite Buffer)
InnoDB用双写缓冲来避免页没写完整所导致的数据损坏。当一个磁盘写操作不能完整地完成时,不完整的页写入就可能发生,16KB的页可能只有一部分被写到磁盘上。有多种多样的原因(崩溃、Bug,等等)可能导致页没有写完整。双写缓冲区在这种秦广发生时可以保证数据完整性。
双写缓冲是表空间的一个特殊保留区域,在一些连续的块中足够保存100个页。本质上是一个最近写回的页面的备份拷贝。当InnoDB从缓冲池刷新页面到磁盘时,首先把它们写(或者刷新)到双写缓冲,然后再把它们写道其所属的数据区域中。这可以保证每个页面的写入都是原子并且持久化的。
这意味着每个页都要写两遍?是的,但是因为InnoDB写页面到双写缓冲是顺序的,并且只调用一次fsync()刷新到磁盘,所以实际上对性能的冲击是比较小的)通常只有几个百分点,肯定没有一半那么多,尽管这个开销在SSD上更明显。更重要的是,这个策略允许日志文件更加高效。因为双写缓冲给了InnoDB一个非常牢固的保证,数据页不会损坏,InnoDB日志记录没必要包含整个页,它们更像是页面的二进制变化量。
如果有一个不完整的页写到了双写缓冲,原始的页依然会在磁盘上它的真实位置,当InnoDB恢复时,它将用原始页面替换掉双写缓冲中的损坏页面。然而,如果双写缓冲成功写入,但写到页的真实位置失败了,InnoDB在恢复时将使用双写缓冲中的拷贝来替换。InnoDb知道什么时候页面损坏了,因为每个页面在末尾都有校验值(Checksum)。校验值时最后写道页面的东西,所以如果页面的内容跟校验值不匹配,说明这个页面是损坏的。因此,在恢复的时候,InnoDB只需要读取双写缓冲中每个页面并且验证校验值,如果一个页面的校验值不对,就从它的原始位置读取这个页面。
有些场景下,双写缓冲确实没必要——例如,你也许想在备库上进制双写缓冲。此外一些文件系统(例如ZFS)做了同样的事,所以没必要再让InnoDB做一遍。可以通过innodb_doublewrite为0来关闭双写缓冲。在Percona Server中,可以配置双写缓冲到独立的文件中,所以可以把这部分工作压力分离出来放在单独的磁盘上。
InnoDB用双写缓冲来避免页没写完整所导致的数据损坏。当一个磁盘写操作不能完整地完成时,不完整的页写入就可能发生,16KB的页可能只有一部分被写到磁盘上。有多种多样的原因(崩溃、Bug,等等)可能导致页没有写完整。双写缓冲区在这种秦广发生时可以保证数据完整性。
双写缓冲是表空间的一个特殊保留区域,在一些连续的块中足够保存100个页。本质上是一个最近写回的页面的备份拷贝。当InnoDB从缓冲池刷新页面到磁盘时,首先把它们写(或者刷新)到双写缓冲,然后再把它们写道其所属的数据区域中。这可以保证每个页面的写入都是原子并且持久化的。
这意味着每个页都要写两遍?是的,但是因为InnoDB写页面到双写缓冲是顺序的,并且只调用一次fsync()刷新到磁盘,所以实际上对性能的冲击是比较小的)通常只有几个百分点,肯定没有一半那么多,尽管这个开销在SSD上更明显。更重要的是,这个策略允许日志文件更加高效。因为双写缓冲给了InnoDB一个非常牢固的保证,数据页不会损坏,InnoDB日志记录没必要包含整个页,它们更像是页面的二进制变化量。
如果有一个不完整的页写到了双写缓冲,原始的页依然会在磁盘上它的真实位置,当InnoDB恢复时,它将用原始页面替换掉双写缓冲中的损坏页面。然而,如果双写缓冲成功写入,但写到页的真实位置失败了,InnoDB在恢复时将使用双写缓冲中的拷贝来替换。InnoDb知道什么时候页面损坏了,因为每个页面在末尾都有校验值(Checksum)。校验值时最后写道页面的东西,所以如果页面的内容跟校验值不匹配,说明这个页面是损坏的。因此,在恢复的时候,InnoDB只需要读取双写缓冲中每个页面并且验证校验值,如果一个页面的校验值不对,就从它的原始位置读取这个页面。
有些场景下,双写缓冲确实没必要——例如,你也许想在备库上进制双写缓冲。此外一些文件系统(例如ZFS)做了同样的事,所以没必要再让InnoDB做一遍。可以通过innodb_doublewrite为0来关闭双写缓冲。在Percona Server中,可以配置双写缓冲到独立的文件中,所以可以把这部分工作压力分离出来放在单独的磁盘上。
其他的IO配置项
sync_binlog选项控制MySQL怎么刷新二进制日志到磁盘。默认值是0(更新版本中是1),意味着MySQL并不刷新,由操作系统自己决定什么时候刷新缓存到持久化设备。如果这个值比0大,它制定了两次刷新到磁盘的动作之间间隔多少次二进制日志写操作(如果autocommit被设置了,每个独立的语句都是一次血,否则就是一个事务一次写)。把它设置为0和1以外的值是很罕见的。
如果没有设置sync_binlog为1,那么崩溃以后可能导致二进制日志没有同步事务数据。这可以轻易地导致复制中断,并且使得及时恢复变得不可能。无论如何,可以把这个值设置为1来获得安全地保障。这样就会要求MySQL同步把二进制日志和事务日志两个文件刷新到两个不同地位置。这可能需要要磁盘寻道,相对来说是个很慢的操作。
像InnoDB日志文件一样,把二进制日志放到一个带有电池保护的写缓存的RAID卷,可以极大地提升性能。事实上,写和刷新二进制日志缓存其实比InnoDB事务日志要昂贵多了,因为不像InnoDB事务日志,每次写二进制日志都会增加它们的大小。这需要每次写入文件系统都更新元信息。所以,设置sync_binlog=1可能比innodb_flush_log_at_trx_commit=1对性能你的损害要大得多,尤其是网络文件系统,例如NFS.
一个跟性能无关的提示,关于二进制日志:如果希望使用expire_logs_days选项来自动清理旧的二进制日志,就不要用rm命令去删。服务器会感到困惑并且拒绝自动删除它们,并且PURGE MASTER LOGS也将停止工作。解决的办法是,如果发现了这种情况,就手动重新同步"主机名-bin.index"文件,可以用磁盘上现有日志文件的列表来更新。
把带有电池保护写缓存的高质量RAID控制器设置为使用写回(Writeback)策略,可以支持每秒数千的写入,并且依然会保证写到持久化存储。数据写到了带有电池的高速缓存,所以即使系统断电它也能存在。但电源恢复时,RAID控制器会在磁盘被设置可用前,把数据从缓存中写到磁盘。因此,一个带有电池保护写缓存的RAID控制器可以显著地提升性能,,这是非常值得的投资。当然,SSD存储是另一个选择。
sync_binlog选项控制MySQL怎么刷新二进制日志到磁盘。默认值是0(更新版本中是1),意味着MySQL并不刷新,由操作系统自己决定什么时候刷新缓存到持久化设备。如果这个值比0大,它制定了两次刷新到磁盘的动作之间间隔多少次二进制日志写操作(如果autocommit被设置了,每个独立的语句都是一次血,否则就是一个事务一次写)。把它设置为0和1以外的值是很罕见的。
如果没有设置sync_binlog为1,那么崩溃以后可能导致二进制日志没有同步事务数据。这可以轻易地导致复制中断,并且使得及时恢复变得不可能。无论如何,可以把这个值设置为1来获得安全地保障。这样就会要求MySQL同步把二进制日志和事务日志两个文件刷新到两个不同地位置。这可能需要要磁盘寻道,相对来说是个很慢的操作。
像InnoDB日志文件一样,把二进制日志放到一个带有电池保护的写缓存的RAID卷,可以极大地提升性能。事实上,写和刷新二进制日志缓存其实比InnoDB事务日志要昂贵多了,因为不像InnoDB事务日志,每次写二进制日志都会增加它们的大小。这需要每次写入文件系统都更新元信息。所以,设置sync_binlog=1可能比innodb_flush_log_at_trx_commit=1对性能你的损害要大得多,尤其是网络文件系统,例如NFS.
一个跟性能无关的提示,关于二进制日志:如果希望使用expire_logs_days选项来自动清理旧的二进制日志,就不要用rm命令去删。服务器会感到困惑并且拒绝自动删除它们,并且PURGE MASTER LOGS也将停止工作。解决的办法是,如果发现了这种情况,就手动重新同步"主机名-bin.index"文件,可以用磁盘上现有日志文件的列表来更新。
把带有电池保护写缓存的高质量RAID控制器设置为使用写回(Writeback)策略,可以支持每秒数千的写入,并且依然会保证写到持久化存储。数据写到了带有电池的高速缓存,所以即使系统断电它也能存在。但电源恢复时,RAID控制器会在磁盘被设置可用前,把数据从缓存中写到磁盘。因此,一个带有电池保护写缓存的RAID控制器可以显著地提升性能,,这是非常值得的投资。当然,SSD存储是另一个选择。
MyISAM的IO配置。
让我们从分析MyISAM怎么为索引操作IO开始。MyISAM通常每次操作之后就把索引变更刷新磁盘。如你打算在一张表上做很多修改,那么毫无疑问,批量操作会更快一些,一种办法是用LOCK TABLES延迟写入,知道解锁这些表。这是个提升性能的很有价值的技巧,因为它使得你精确控制哪些写被延迟,以及什么时候把它们刷到磁盘。可以精确延迟那些希望延迟的语句。
通过设置delay_key_write变量,也可以延迟索引的写入。如果这么做,修改的键缓冲块直到表被关闭才会刷新。(表可能因为多种原因被关闭。例如,服务器因为表缓存没有空间了就会关闭表,或者有人执行了FLUSH TABLES).可能的配置如下:
1.OFF
MyISAM每次写操作后刷新键缓冲(键缓存,Key Buffer)中的脏块到磁盘,除非表被LOCK TABLES锁定了
2.ON
打开延迟键写入,但是只对用DELAY_KEY_WRITE选项创建的表有效
3.ALL
所有的MyISAM表都会使用延迟键写入
延迟键写入在某些场景下可能很有帮助,但是通常不会带来很大的性能提升。但键缓冲的杜明中很好但谢明中不好时,数据又比较小,这可能很有用。当然也有一小部分缺点:
1.如果服务器缓存并且块没有被刷到磁盘,索引可能会损坏
2.如果很多写被延迟了,MySQL可能需要花费更长时间去关闭表,因为必须等待缓冲刷新到磁盘。在MySQL5.0这可能引起很长的表缓存锁
3.由于上面提到的原因,FLUSH TABLES可能需要很长时间。如果为了做逻辑卷(LVM)快照或者其他备份操作,而执行FLUSH TABLE WITH READ LOCK,那可能增加操作的时间
4.键缓冲中没有刷灰去的脏块可能占用空间,导致从磁盘上读取的新块没有空间存放,因此,查询语句可能需要等待MyISAM释放一些键缓存的空间。
另外,除了配置MyISAM的索引IO还可以配置MyISAM怎样尝试从损坏中恢复,myisam_recover选项控制MyISAM怎样寻找和修复错误。需要在配置文件或者命令中设置这个选项。可以通过下面的SQL语句查看选项的值,但是不能修改
```sql
mysql>SHOW VARIBLES LIKE 'myisam_recover_options';
```
打开这个选项通知MySQL在表打开时,检查是否损坏,并且在找到问题的时候进行修复。可以设置的值如下:
1.DEFAULT(或者不设置)
使MySQL尝试修复任何被标记为崩溃或者没有标记为完全关闭的表。默认值不要求在恢复时执行其他动作。跟大多数变量不同,这里DEFAULT值不是重置变量的值为编译值,它本质上意味着没有设置
2.BACKUP
让MySQL将数据文件的备份写到.BAK文件,以便随后进行检查
3.QUICK
除非又删除块,否则跳过恢复。块中有已经删除的行也依然会占用空间,但是可以被后面的INSERT语句宠用。这可能比较有用,因为MyISAM大表的恢复可能花费相当长的时间
让我们从分析MyISAM怎么为索引操作IO开始。MyISAM通常每次操作之后就把索引变更刷新磁盘。如你打算在一张表上做很多修改,那么毫无疑问,批量操作会更快一些,一种办法是用LOCK TABLES延迟写入,知道解锁这些表。这是个提升性能的很有价值的技巧,因为它使得你精确控制哪些写被延迟,以及什么时候把它们刷到磁盘。可以精确延迟那些希望延迟的语句。
通过设置delay_key_write变量,也可以延迟索引的写入。如果这么做,修改的键缓冲块直到表被关闭才会刷新。(表可能因为多种原因被关闭。例如,服务器因为表缓存没有空间了就会关闭表,或者有人执行了FLUSH TABLES).可能的配置如下:
1.OFF
MyISAM每次写操作后刷新键缓冲(键缓存,Key Buffer)中的脏块到磁盘,除非表被LOCK TABLES锁定了
2.ON
打开延迟键写入,但是只对用DELAY_KEY_WRITE选项创建的表有效
3.ALL
所有的MyISAM表都会使用延迟键写入
延迟键写入在某些场景下可能很有帮助,但是通常不会带来很大的性能提升。但键缓冲的杜明中很好但谢明中不好时,数据又比较小,这可能很有用。当然也有一小部分缺点:
1.如果服务器缓存并且块没有被刷到磁盘,索引可能会损坏
2.如果很多写被延迟了,MySQL可能需要花费更长时间去关闭表,因为必须等待缓冲刷新到磁盘。在MySQL5.0这可能引起很长的表缓存锁
3.由于上面提到的原因,FLUSH TABLES可能需要很长时间。如果为了做逻辑卷(LVM)快照或者其他备份操作,而执行FLUSH TABLE WITH READ LOCK,那可能增加操作的时间
4.键缓冲中没有刷灰去的脏块可能占用空间,导致从磁盘上读取的新块没有空间存放,因此,查询语句可能需要等待MyISAM释放一些键缓存的空间。
另外,除了配置MyISAM的索引IO还可以配置MyISAM怎样尝试从损坏中恢复,myisam_recover选项控制MyISAM怎样寻找和修复错误。需要在配置文件或者命令中设置这个选项。可以通过下面的SQL语句查看选项的值,但是不能修改
```sql
mysql>SHOW VARIBLES LIKE 'myisam_recover_options';
```
打开这个选项通知MySQL在表打开时,检查是否损坏,并且在找到问题的时候进行修复。可以设置的值如下:
1.DEFAULT(或者不设置)
使MySQL尝试修复任何被标记为崩溃或者没有标记为完全关闭的表。默认值不要求在恢复时执行其他动作。跟大多数变量不同,这里DEFAULT值不是重置变量的值为编译值,它本质上意味着没有设置
2.BACKUP
让MySQL将数据文件的备份写到.BAK文件,以便随后进行检查
3.QUICK
除非又删除块,否则跳过恢复。块中有已经删除的行也依然会占用空间,但是可以被后面的INSERT语句宠用。这可能比较有用,因为MyISAM大表的恢复可能花费相当长的时间
可以使用多个设置,用逗号分割。例如"BACKUP,FORCE"会强制恢复并且创建备份。建议打开这个选项,尤其是只有一些小的MyISAM表时。服务器运行着一些损坏的MyISAM表是很危险的,因为它们有时可以导致更多数据损坏,甚至服务器崩溃。然而,如果有很大的表,原子恢复时不切实际的:它导致服务器打开所有的MyISAM表时都会检查和修复,这是低效的做法。在这段时间,MySQL会阻止连接做任何工作。如果有一大堆的MyISAM表,比较好的注意还是启动CHEKC TABLES和REPAIR TABLES命令来做,这样对服务器影响比较少。不管哪种方式,检查和修复表都是很中重要的。
打开数据文件的内存映射(MMAP)访问是另一个有用的MyISAM选项。内存映射使得MyISAM直接通过操作系统的页面缓存访问.MYD文件,避免系统调用的开销。在MySQL5.1和更新版本重,可以通过myisam_use_mmap选项打开内存映射。更老的版本的MySQL只能对压缩的MyISAM表使用内存映射
打开数据文件的内存映射(MMAP)访问是另一个有用的MyISAM选项。内存映射使得MyISAM直接通过操作系统的页面缓存访问.MYD文件,避免系统调用的开销。在MySQL5.1和更新版本重,可以通过myisam_use_mmap选项打开内存映射。更老的版本的MySQL只能对压缩的MyISAM表使用内存映射
高级InnoDB设置
innodb_old_blocks_time。
InnoDB有两段缓冲池LRU(最近最少使用)链表,设计目的是防止换出长期很多次的页面。像mysqldump产生的这种一次性的(大)查询,通常会读取页面到缓冲池的LRU列表,从中读取需要的行,然后移动到下一页。理论上,两段LRU链表将阻止此页取代很长一段时间内都需要用到的页面被放入"年轻(Young)"子链表,并且只在它被已被浏览过多次后将其移动到"年老(Old)"子链表。但是InnoDB默认没有配置为防止这种情况,因为页内有很多行,所以从页面读取的行的多次访问,会导致它立即被转移到"年老(Old)"子链表,对哪些需要长时间缓存的页面带来换出的压力。这个变量指定一个页面从LRU链表的"年轻"部分转移到"年老"部分之前必须经过的毫秒数。默认情况下将它设置为0,将它设为诸如1000毫秒(一秒)这样的小一点的值,在基准测试中已被证明非常有效。
InnoDB有两段缓冲池LRU(最近最少使用)链表,设计目的是防止换出长期很多次的页面。像mysqldump产生的这种一次性的(大)查询,通常会读取页面到缓冲池的LRU列表,从中读取需要的行,然后移动到下一页。理论上,两段LRU链表将阻止此页取代很长一段时间内都需要用到的页面被放入"年轻(Young)"子链表,并且只在它被已被浏览过多次后将其移动到"年老(Old)"子链表。但是InnoDB默认没有配置为防止这种情况,因为页内有很多行,所以从页面读取的行的多次访问,会导致它立即被转移到"年老(Old)"子链表,对哪些需要长时间缓存的页面带来换出的压力。这个变量指定一个页面从LRU链表的"年轻"部分转移到"年老"部分之前必须经过的毫秒数。默认情况下将它设置为0,将它设为诸如1000毫秒(一秒)这样的小一点的值,在基准测试中已被证明非常有效。
线程。
MySQL每个连接使用一个线程,另外还有一个内部处理线程、特殊用途的线程,以及所有存储引擎创建的线程。在MySQL5.5中,Oracle提供了一个线程池插件,但目前尚不清楚在实际应用中能获得什么好处。无论哪种方式,MySQL都需要大量的线程才能有效地工作。MySQL确实需要内核级线程地支持,而不只是用户级线程,这样才能更有效地使用多个CPU.另外也需要有效的同步原子,例如互斥变量。操作系统的线程库必须提供所有的这些功能。
GNU/Linux提供两个线程库:LinuxThreads和新的原生POSIX线程库(NPTL).LinuxThreads在某些情况下仍然使用,但现在的发行版已经切换到NPTL,并且大部分应用已经不再加载LinuxThreads.NPTL更轻量,更高效,也不会有哪些LinuxThreads遇到的问题。
FreeBSD会加载许多线程库。从历史上看,它对县城的支持很弱,但现在已经宾得好多了,在一些测试中,甚至由于SMP系统上的GNU/Linux。在FreeBSD6和更新版本,土建的线程库是libthr,早期版本使用的linuxthreads,是FreeBSD从GNU/Linux上一致的Linux/Threads库。
通常,线程问题都是过去的事了,现在GNU/Linux和FreeBSD都提供了很好的线程库,Solaris和Windows一直对线程有很好的支持,尽管直到5.5发布之前,MyISAM都不能在Windows下很好地使用线程,但5.5里有显著的提升
MySQL每个连接使用一个线程,另外还有一个内部处理线程、特殊用途的线程,以及所有存储引擎创建的线程。在MySQL5.5中,Oracle提供了一个线程池插件,但目前尚不清楚在实际应用中能获得什么好处。无论哪种方式,MySQL都需要大量的线程才能有效地工作。MySQL确实需要内核级线程地支持,而不只是用户级线程,这样才能更有效地使用多个CPU.另外也需要有效的同步原子,例如互斥变量。操作系统的线程库必须提供所有的这些功能。
GNU/Linux提供两个线程库:LinuxThreads和新的原生POSIX线程库(NPTL).LinuxThreads在某些情况下仍然使用,但现在的发行版已经切换到NPTL,并且大部分应用已经不再加载LinuxThreads.NPTL更轻量,更高效,也不会有哪些LinuxThreads遇到的问题。
FreeBSD会加载许多线程库。从历史上看,它对县城的支持很弱,但现在已经宾得好多了,在一些测试中,甚至由于SMP系统上的GNU/Linux。在FreeBSD6和更新版本,土建的线程库是libthr,早期版本使用的linuxthreads,是FreeBSD从GNU/Linux上一致的Linux/Threads库。
通常,线程问题都是过去的事了,现在GNU/Linux和FreeBSD都提供了很好的线程库,Solaris和Windows一直对线程有很好的支持,尽管直到5.5发布之前,MyISAM都不能在Windows下很好地使用线程,但5.5里有显著的提升
内存交换区。
当操作系统因为没有足够的内存而将一些虚拟内存写到磁盘就会发生内存交换(内存交换有时称为页面交换,从技术上来说,它们是不同的东西,但是人么你通常把它们作为同义词)内存交换对操作系统中运行的进程是透明的。只有操作系统知道特定的虚拟内存地址是在物理内存还是硬盘。
内存交换对MySQL性能影响是很糟糕的,它破坏了缓存在内存的目的,并且相对于使用很小的内存做缓存,使用交换区的性能更差。MySQL和存储引擎有很多算法来区别对待内存中的数据和硬盘上的数据,因为一般都假设内存数据访问代价更低。
因为内存交换对用户进程不可见,MySQL(或存储引擎)并不知道数据实际上已经移动到磁盘,还会以为在内存中。
结果会导致很差的性能。例如,若存储引擎认为数据依然在内存,可能觉得为"短暂"的内存操作锁定一个全局互斥变量(例如InnoDB缓冲池Mutex)是OK的。如果这个操作实际上引起了硬盘IO,直到IO操作完成前任何操作都会被挂起。这意味着内存交换比直接做硬盘IO操作还要糟糕。
在GNU/Linux上,可以用vmstat来监控内存交换。最好查看si和so列报告的内存交换IO活动,这比看swap列报告的交换区利用率更重要。swapd列可以展示那些被载入了但是没有被使用的进程,它们并不是真的会称为问题。我们喜欢si和so列的值为0,并且一定要保证它们低于每秒10个块。
极端的场景下,太多的内存交换可能导致操作系统交换空间溢出。如果发生了这种情况,缺乏虚拟内存可能让MySQL崩溃。但是即使交换空间没有移除,非常活跃的内存交换也会导致整个操作变得无法响应,到这种时候甚至不能登录系统去杀掉MySQL进程。有时当交换空间溢出时,甚至Linux内核都会完全Hang住。绝不要让系统的虚拟内存溢出!对交换空间利用率做好监控和报警。如果不知道需要多少交换空间,就在硬盘上尽可能多地分配空间,这不会对性能造成冲击,只是消耗了硬盘空间。有些大的组织清楚地直到内存消耗将有多大,并且内存交换被非常严格地控制,但是对于只有少量多用途的MySQL实例,并且工作负载页多种多样的环境,通常不切实际。如果后者的描述更符合实际情况,确认给服务器一些"呼吸"的空间,分配足够的交换空间。
在特别大的内存压力下经常发生的另一件事是内存不足(OOM),这会导致踢掉和杀掉一些进程。在MySQ进程这很常见。在另外的进程上也挺常见,比如SSH,甚至会让系统不能从网络访问。可以通过设置SSH进程的oom_adj或oom_score_adj值来避免这种情况。
可以通过正确地配置MySQL缓冲来解决大部分内存交换问题,但是有时候操作系统的虚拟内存系统还是会决定交换MySQL的内存。这通常发生在操作系统看到MySQL发出了大量的IO,因此尝试增加文件缓存来保存更多数据时。如果没有足够的内存,有些东西就必须交换出去,有些可能就是MySQL本身。有些老的Linux内核版本也有一些适得其反鞥多优先级,导致本不应该被交换的被交换出去,但是在最近的内核都被缓解了。
有些人主张完全禁用交换文件。尽管这样做有时在某些内核拒绝工作的极端场景下是可行的,但这降低了操作系统的性能(在理论上不会,但是实际上会的)。同时这样做也是很危险的,因为禁用内存交换就相当于给虚拟内存设置了一个不可动摇的限制。如果MySQL需要临时使用很大一块内存,或者有很耗内存的进程运行在同一台机器(如夜间批量任务),MySQL会内存溢出、崩溃或者被操作系统杀死。
操作系统通常允许对虚拟内存和IO进行一些限制。我们提供过一些GNU/Linux上控制它们的办法。最基本的办法是修改/proc/sys/vm/swappiness为一个很小的值,例如0或1.这告诉内核除非虚拟内存完全满了,否则不要使用交换区。下面如何检查这个值的例子。这个值显示为0,这是默认的设置(范围是0~100),如果它不是0,则对服务器而言这是一个跟糟糕的默认值,服务器应该设置为0:
另一个选项是修改存储引擎怎么读取和写入数据。例如,使用innodb_flush_method=0_DIRECT,减轻IO压力,DirectIO并不缓存,因此操作系统并不能把MySQL视为增加文件缓存的原因。这个参数只对InnoDB有效。你也可以使用大页,不参与换入换出。这对MyISAM和InnoDB都有效。
另一个选择是使用MySQL的memlock配置项,可以把MySQL锁定在内存。这可以避免交换,但是也可能带来危险:如果没有足够的可锁定内存,MySQL在尝试分配更多内存时会崩溃。这也可能导致锁定的内存太多而没有足够的内存留给操作系统。很多技巧都是对于特定内核版本的,因此要小心,尤其是当升级的时候。在某些工作负载下,很难让操作系统的行为合情合理,并且仅有的资源可能让缓冲大小达不到最满意的值
当操作系统因为没有足够的内存而将一些虚拟内存写到磁盘就会发生内存交换(内存交换有时称为页面交换,从技术上来说,它们是不同的东西,但是人么你通常把它们作为同义词)内存交换对操作系统中运行的进程是透明的。只有操作系统知道特定的虚拟内存地址是在物理内存还是硬盘。
内存交换对MySQL性能影响是很糟糕的,它破坏了缓存在内存的目的,并且相对于使用很小的内存做缓存,使用交换区的性能更差。MySQL和存储引擎有很多算法来区别对待内存中的数据和硬盘上的数据,因为一般都假设内存数据访问代价更低。
因为内存交换对用户进程不可见,MySQL(或存储引擎)并不知道数据实际上已经移动到磁盘,还会以为在内存中。
结果会导致很差的性能。例如,若存储引擎认为数据依然在内存,可能觉得为"短暂"的内存操作锁定一个全局互斥变量(例如InnoDB缓冲池Mutex)是OK的。如果这个操作实际上引起了硬盘IO,直到IO操作完成前任何操作都会被挂起。这意味着内存交换比直接做硬盘IO操作还要糟糕。
在GNU/Linux上,可以用vmstat来监控内存交换。最好查看si和so列报告的内存交换IO活动,这比看swap列报告的交换区利用率更重要。swapd列可以展示那些被载入了但是没有被使用的进程,它们并不是真的会称为问题。我们喜欢si和so列的值为0,并且一定要保证它们低于每秒10个块。
极端的场景下,太多的内存交换可能导致操作系统交换空间溢出。如果发生了这种情况,缺乏虚拟内存可能让MySQL崩溃。但是即使交换空间没有移除,非常活跃的内存交换也会导致整个操作变得无法响应,到这种时候甚至不能登录系统去杀掉MySQL进程。有时当交换空间溢出时,甚至Linux内核都会完全Hang住。绝不要让系统的虚拟内存溢出!对交换空间利用率做好监控和报警。如果不知道需要多少交换空间,就在硬盘上尽可能多地分配空间,这不会对性能造成冲击,只是消耗了硬盘空间。有些大的组织清楚地直到内存消耗将有多大,并且内存交换被非常严格地控制,但是对于只有少量多用途的MySQL实例,并且工作负载页多种多样的环境,通常不切实际。如果后者的描述更符合实际情况,确认给服务器一些"呼吸"的空间,分配足够的交换空间。
在特别大的内存压力下经常发生的另一件事是内存不足(OOM),这会导致踢掉和杀掉一些进程。在MySQ进程这很常见。在另外的进程上也挺常见,比如SSH,甚至会让系统不能从网络访问。可以通过设置SSH进程的oom_adj或oom_score_adj值来避免这种情况。
可以通过正确地配置MySQL缓冲来解决大部分内存交换问题,但是有时候操作系统的虚拟内存系统还是会决定交换MySQL的内存。这通常发生在操作系统看到MySQL发出了大量的IO,因此尝试增加文件缓存来保存更多数据时。如果没有足够的内存,有些东西就必须交换出去,有些可能就是MySQL本身。有些老的Linux内核版本也有一些适得其反鞥多优先级,导致本不应该被交换的被交换出去,但是在最近的内核都被缓解了。
有些人主张完全禁用交换文件。尽管这样做有时在某些内核拒绝工作的极端场景下是可行的,但这降低了操作系统的性能(在理论上不会,但是实际上会的)。同时这样做也是很危险的,因为禁用内存交换就相当于给虚拟内存设置了一个不可动摇的限制。如果MySQL需要临时使用很大一块内存,或者有很耗内存的进程运行在同一台机器(如夜间批量任务),MySQL会内存溢出、崩溃或者被操作系统杀死。
操作系统通常允许对虚拟内存和IO进行一些限制。我们提供过一些GNU/Linux上控制它们的办法。最基本的办法是修改/proc/sys/vm/swappiness为一个很小的值,例如0或1.这告诉内核除非虚拟内存完全满了,否则不要使用交换区。下面如何检查这个值的例子。这个值显示为0,这是默认的设置(范围是0~100),如果它不是0,则对服务器而言这是一个跟糟糕的默认值,服务器应该设置为0:
另一个选项是修改存储引擎怎么读取和写入数据。例如,使用innodb_flush_method=0_DIRECT,减轻IO压力,DirectIO并不缓存,因此操作系统并不能把MySQL视为增加文件缓存的原因。这个参数只对InnoDB有效。你也可以使用大页,不参与换入换出。这对MyISAM和InnoDB都有效。
另一个选择是使用MySQL的memlock配置项,可以把MySQL锁定在内存。这可以避免交换,但是也可能带来危险:如果没有足够的可锁定内存,MySQL在尝试分配更多内存时会崩溃。这也可能导致锁定的内存太多而没有足够的内存留给操作系统。很多技巧都是对于特定内核版本的,因此要小心,尤其是当升级的时候。在某些工作负载下,很难让操作系统的行为合情合理,并且仅有的资源可能让缓冲大小达不到最满意的值
操作系统状态。
操作系统会提供一些帮助发现操作系统和硬件正在做什么的工具。其中包括最常用的工具iostat和vmstat。如果系统不能提供它们中的任何一个,有可能提供了相似的替代品。当然目的不是让大家熟练使用iostat和vmstat,而是告诉大家用类似的工具诊断问题时应该看什么指标。除了这些,操作系统也许还提供了其他的工具,如mpstat或者sar.如果对系统的其他部分感兴趣,例如网络,你可能希望使用ifconfig(除了其他信息,它能显示了多少次网络错误)或者netstat.
默认情况下,vmstat和iostat只是生成一个报告,展示自系统启动依赖很多计数器的平均值,这其实没什么用。然而,两个工具都可以给出一个间隔参数,让它们生成增量值得报告,展示服务器正在做什么,这更有用
操作系统会提供一些帮助发现操作系统和硬件正在做什么的工具。其中包括最常用的工具iostat和vmstat。如果系统不能提供它们中的任何一个,有可能提供了相似的替代品。当然目的不是让大家熟练使用iostat和vmstat,而是告诉大家用类似的工具诊断问题时应该看什么指标。除了这些,操作系统也许还提供了其他的工具,如mpstat或者sar.如果对系统的其他部分感兴趣,例如网络,你可能希望使用ifconfig(除了其他信息,它能显示了多少次网络错误)或者netstat.
默认情况下,vmstat和iostat只是生成一个报告,展示自系统启动依赖很多计数器的平均值,这其实没什么用。然而,两个工具都可以给出一个间隔参数,让它们生成增量值得报告,展示服务器正在做什么,这更有用
如何阅读vmstat的输出。
我们先看一个vmstat的例子。用下面的命令让它每5秒钟打印出一个报告:
```c
vmstat 5
```
可以用Ctrl +C停止vmstat,可以看到输出依赖于所用的操作系统,因此可能需要阅读一下手册来解读报告。刚启动不久,即使采用增量报告,第一行的值还是显示自系统启动以来的平均值,第二行展示现在正在发生的情况,接下来的行会展示每5秒的间隔内发生了什么。每一列的含义在头部,如下所示:
1.procs
r这一列展示了多少进程正在等待CPU。b列显示多少进程正在不可中断地休眠(通常意味着它们在等待IO,例如磁盘、网络、用户输入,等等)。
2.memory
swapd列显示多少块被换出到了磁盘(页面交换)/剩下地三列显示了多少块时空闲的(未被使用)、多少块正在被用作缓冲,以及多少正在被用作操作系统的缓存。
3.swap
这些列显示页面交换活动:每秒有多少块正在被换入(从磁盘)和换出(到磁盘)。它们比监控swapd列重要多了。大部分时间我们喜欢看到si和so列时0,并且我们很明确不希望看到每秒超过10个块。突发性的高峰一样很糟糕。
4.io
这些列显示有多少块从块设备读取(bi)和写出(bo)。这通常反应了硬盘IO
5.system
这些列显示了每秒中断(in)和上下文切换(cs)的数量
6.cpu
这些列显示所有的CPU时间花费在各类操作的百分比,包括执行用户代码(非内核)、执行系统代码(内核)、空闲,以及等待IO.如果正在使用虚拟化,则第五个列可能是st,显示了从虚拟机中"偷走"的百分比。这关系到那些虚拟机想运行但是系统管理程序转而运行其他的对象的时间。如果虚拟机不希望运行任何对象,但是系统管理程序运行了其他对象,这不算被偷走的CPU时间
vmstat的输出跟系统有关,所以如果看到跟展示的例子不同的输出,应该阅读系统的vmstat(8)手册。一个重要的提示是:内存、交换区,以及IO统计是块数而不是i字节。在GNU/Linux,块大小通常是1024字节
我们先看一个vmstat的例子。用下面的命令让它每5秒钟打印出一个报告:
```c
vmstat 5
```
可以用Ctrl +C停止vmstat,可以看到输出依赖于所用的操作系统,因此可能需要阅读一下手册来解读报告。刚启动不久,即使采用增量报告,第一行的值还是显示自系统启动以来的平均值,第二行展示现在正在发生的情况,接下来的行会展示每5秒的间隔内发生了什么。每一列的含义在头部,如下所示:
1.procs
r这一列展示了多少进程正在等待CPU。b列显示多少进程正在不可中断地休眠(通常意味着它们在等待IO,例如磁盘、网络、用户输入,等等)。
2.memory
swapd列显示多少块被换出到了磁盘(页面交换)/剩下地三列显示了多少块时空闲的(未被使用)、多少块正在被用作缓冲,以及多少正在被用作操作系统的缓存。
3.swap
这些列显示页面交换活动:每秒有多少块正在被换入(从磁盘)和换出(到磁盘)。它们比监控swapd列重要多了。大部分时间我们喜欢看到si和so列时0,并且我们很明确不希望看到每秒超过10个块。突发性的高峰一样很糟糕。
4.io
这些列显示有多少块从块设备读取(bi)和写出(bo)。这通常反应了硬盘IO
5.system
这些列显示了每秒中断(in)和上下文切换(cs)的数量
6.cpu
这些列显示所有的CPU时间花费在各类操作的百分比,包括执行用户代码(非内核)、执行系统代码(内核)、空闲,以及等待IO.如果正在使用虚拟化,则第五个列可能是st,显示了从虚拟机中"偷走"的百分比。这关系到那些虚拟机想运行但是系统管理程序转而运行其他的对象的时间。如果虚拟机不希望运行任何对象,但是系统管理程序运行了其他对象,这不算被偷走的CPU时间
vmstat的输出跟系统有关,所以如果看到跟展示的例子不同的输出,应该阅读系统的vmstat(8)手册。一个重要的提示是:内存、交换区,以及IO统计是块数而不是i字节。在GNU/Linux,块大小通常是1024字节
使用如下命令查看系统手册:
```c
man vmstat
```
vmstat(8)使用手册如图
```c
man vmstat
```
vmstat(8)使用手册如图
如何阅读iostat的输出。
现在让我们转移到iostat。默认情况下,它显示了与vmstat相同的CPU使用信息。我们通常只是对IO统计感兴趣,所以使用下面的命令之战是扩展的设备统计:
```c
iostat -dx 5
```
与vmstat一样,第一行报告显示的是自系统启动以来的平均值(通常删掉它节省空间),然后接下来的报告显示了增量的平均值,每个设备一行。有多种选项显示和隐藏列。官方文档有点难以理解,因此我们必须从源码中挖掘真正显示的内容是什么。说明的列信息如下:
1.rrqm/s和wrqm/s
每秒合并的读和写请求。"合并的"意味着操作系统从队列中拿出多个逻辑请求合并为一个请求到实际磁盘
2.r/s和w/s
每秒发送到设备的读和写请求
3.rsec/s和wsec/s
每秒读和写的扇区数。有些系统也输出为rkB/s和wkB/s,意味着每秒读写的千字节数。为了简洁,省略了那些指标说明。
4.avgrq-sz
请求的扇区数
5.avgqu-sz
在设备队列中等待的请求数
6.await
磁盘队列上花费的毫秒数。很不幸,iostat没有独立统计读和写的请求,它们实际上不应该被一起平均
7.svctm
服务请求花费的毫秒数,不包括排队时间
8.%util
至少有一个活跃请求所占时间的百分比。如果熟悉队列理论张利用率的标准定义,那么这个命名很莫名其妙。它其实不是设备的利用率。超过一块硬盘的设备(例如RAID控制器)比一块硬盘的设备可以支持更高的并发,但是%util从来不会超过100%,除非在计算机时有四舍五入的错误。因此,这个指标无法真实反映设备的利用率,实际上跟文档说的相反,除非只有一块物理磁盘的特殊例子。
可以用iostat的输出推断某些关于机器IO自系统的实际情况。一个重要的度量标准时请求服务的并发数。因为读写的单位时每秒而服务时间的单位是千分之一秒,所以可以利用利特尔法则(Littele's Law)得到下面的公式,计算出设备服务的并发请求数(另一种计算并发的方式是通过平均队列大小、服务时间,以及平均等大i时间:(avuqu_sz * svctm) / await):
```c
concurrency = (r/s + w/s) * (svctm/1000)
```
把数字带入并发公式,可以得到(0.38 + 0.02) * (0.43 / 1000) = 0.000172.由于我这台服务器配置是乞丐版的,所以得到的并发数比较低。这意味着在一个采样周期内,这个设备平均要服务0.000172次的请求。
现在让我们转移到iostat。默认情况下,它显示了与vmstat相同的CPU使用信息。我们通常只是对IO统计感兴趣,所以使用下面的命令之战是扩展的设备统计:
```c
iostat -dx 5
```
与vmstat一样,第一行报告显示的是自系统启动以来的平均值(通常删掉它节省空间),然后接下来的报告显示了增量的平均值,每个设备一行。有多种选项显示和隐藏列。官方文档有点难以理解,因此我们必须从源码中挖掘真正显示的内容是什么。说明的列信息如下:
1.rrqm/s和wrqm/s
每秒合并的读和写请求。"合并的"意味着操作系统从队列中拿出多个逻辑请求合并为一个请求到实际磁盘
2.r/s和w/s
每秒发送到设备的读和写请求
3.rsec/s和wsec/s
每秒读和写的扇区数。有些系统也输出为rkB/s和wkB/s,意味着每秒读写的千字节数。为了简洁,省略了那些指标说明。
4.avgrq-sz
请求的扇区数
5.avgqu-sz
在设备队列中等待的请求数
6.await
磁盘队列上花费的毫秒数。很不幸,iostat没有独立统计读和写的请求,它们实际上不应该被一起平均
7.svctm
服务请求花费的毫秒数,不包括排队时间
8.%util
至少有一个活跃请求所占时间的百分比。如果熟悉队列理论张利用率的标准定义,那么这个命名很莫名其妙。它其实不是设备的利用率。超过一块硬盘的设备(例如RAID控制器)比一块硬盘的设备可以支持更高的并发,但是%util从来不会超过100%,除非在计算机时有四舍五入的错误。因此,这个指标无法真实反映设备的利用率,实际上跟文档说的相反,除非只有一块物理磁盘的特殊例子。
可以用iostat的输出推断某些关于机器IO自系统的实际情况。一个重要的度量标准时请求服务的并发数。因为读写的单位时每秒而服务时间的单位是千分之一秒,所以可以利用利特尔法则(Littele's Law)得到下面的公式,计算出设备服务的并发请求数(另一种计算并发的方式是通过平均队列大小、服务时间,以及平均等大i时间:(avuqu_sz * svctm) / await):
```c
concurrency = (r/s + w/s) * (svctm/1000)
```
把数字带入并发公式,可以得到(0.38 + 0.02) * (0.43 / 1000) = 0.000172.由于我这台服务器配置是乞丐版的,所以得到的并发数比较低。这意味着在一个采样周期内,这个设备平均要服务0.000172次的请求。
CPU密集型的机器。
CPU密集型服务器的vmstat输出通常在us列会有一个很高的值,报告了花费在非内核代码上的CPU时钟;也可能在sy列有很高的值,表示系统CPU利用率,超过20%就足以令人不安了。在大部分情况下,也会有进程队列排队时间(在r列报告的)。如图所示。注意,这里也可能有合理数量的上下文切换(cs列),除非每秒超过100 000次或更多,一般都不用担心上下文切换。当操作系统停止一个进程转而运行另一个进程时,就会产生上下文切换。例如,一查询语句在MyISAM上执行了一个非覆盖索引扫描,就会先从索引中读取元素,然后根据索引再从磁盘上读取页面。如果页面不在操作系统缓存中,就需要从磁盘进行物理读取,着就会导致上下文切换中断进程处理,直到IO完成。这样一个查询可以导致大量的上下文切换。
CPU密集型服务器的vmstat输出通常在us列会有一个很高的值,报告了花费在非内核代码上的CPU时钟;也可能在sy列有很高的值,表示系统CPU利用率,超过20%就足以令人不安了。在大部分情况下,也会有进程队列排队时间(在r列报告的)。如图所示。注意,这里也可能有合理数量的上下文切换(cs列),除非每秒超过100 000次或更多,一般都不用担心上下文切换。当操作系统停止一个进程转而运行另一个进程时,就会产生上下文切换。例如,一查询语句在MyISAM上执行了一个非覆盖索引扫描,就会先从索引中读取元素,然后根据索引再从磁盘上读取页面。如果页面不在操作系统缓存中,就需要从磁盘进行物理读取,着就会导致上下文切换中断进程处理,直到IO完成。这样一个查询可以导致大量的上下文切换。
如果我们在同一台机器观察iostat的输出(再次剔除显示启动以来平均值的第一行),可以发现磁盘利用率可能低于50%,如图所示。
这台机器不是IO密集型的,但是依然有相当数量的IO发生,在数据库服务器中这种情况很少见。另一方面,传统的Web服务器会消耗大量CPU资源,但是很少发生IO,所以Web服务器的输出不会像这个例子。
这台机器不是IO密集型的,但是依然有相当数量的IO发生,在数据库服务器中这种情况很少见。另一方面,传统的Web服务器会消耗大量CPU资源,但是很少发生IO,所以Web服务器的输出不会像这个例子。
IO密集型的机器。
在IO密集型工作负载下,CPU花费大量时间在等待IO请求完成。这意味着vmstat会显示很多处理器在非中断休眠(b列)状态,并且在wa这一列的值很高,如果你查询这台机器的iostat输出显示硬盘一直很忙。%util的值可能因为四舍五入的错误超过100%.什么迹象意味着机器是IO密集的呢?只要有足够的缓冲来服务写请求,即使机器正在做大量的写操作,也可能满足,但是却通常意味着可能会无法满足读请求。这听起来好像违反直觉,但是如果思考读和写的本质,就不会这儿认为了:
1.写请求能够缓冲或同步操作。它们可以被任意一层缓冲:操作系统层、RAID控制器层,等等
2.读请求就其本身而言都是同步的。当然程序可以猜测可能需要某些数据,并异步地提前读取(预读)。无论如何,通常程序在继续工作前得到它们需要的数据。这就强制读请求为同步操作:程序必须被阻塞直到请求完成。
想想这种方式:你可以发出一个写请求到缓冲区的某个地方,然后过一会儿完成。甚至可以每秒发出很多这样的请求。如果缓冲区正确工作,并且有足够的空间,每个请求都可以很快地完成,并且实际上写到物理硬盘是被重新排序后更有效地批量操作的。然而,没有办法对读操作这么做——不管多小或多少的请求,都不可能让硬盘响应说"这是你的数据,我等一会儿读它"。这就是为什么读需要IO等待是可以理解的原因
在IO密集型工作负载下,CPU花费大量时间在等待IO请求完成。这意味着vmstat会显示很多处理器在非中断休眠(b列)状态,并且在wa这一列的值很高,如果你查询这台机器的iostat输出显示硬盘一直很忙。%util的值可能因为四舍五入的错误超过100%.什么迹象意味着机器是IO密集的呢?只要有足够的缓冲来服务写请求,即使机器正在做大量的写操作,也可能满足,但是却通常意味着可能会无法满足读请求。这听起来好像违反直觉,但是如果思考读和写的本质,就不会这儿认为了:
1.写请求能够缓冲或同步操作。它们可以被任意一层缓冲:操作系统层、RAID控制器层,等等
2.读请求就其本身而言都是同步的。当然程序可以猜测可能需要某些数据,并异步地提前读取(预读)。无论如何,通常程序在继续工作前得到它们需要的数据。这就强制读请求为同步操作:程序必须被阻塞直到请求完成。
想想这种方式:你可以发出一个写请求到缓冲区的某个地方,然后过一会儿完成。甚至可以每秒发出很多这样的请求。如果缓冲区正确工作,并且有足够的空间,每个请求都可以很快地完成,并且实际上写到物理硬盘是被重新排序后更有效地批量操作的。然而,没有办法对读操作这么做——不管多小或多少的请求,都不可能让硬盘响应说"这是你的数据,我等一会儿读它"。这就是为什么读需要IO等待是可以理解的原因
发生内存交换的机器。
一台正在发生内存交换的机器可能在swapd列有一个很高的值,也可能不高。但是可以看到si和so列有很高的值,这是我们不希望看到的
一台正在发生内存交换的机器可能在swapd列有一个很高的值,也可能不高。但是可以看到si和so列有很高的值,这是我们不希望看到的
空闲的机器。
为完整期间,下面也给出一台空闲机器上的vmstat输出。注意,没有在运行或或被阻塞的进程,idle列显示CPU是100%空闲,st列展示了从"虚拟机"偷来的时间
为完整期间,下面也给出一台空闲机器上的vmstat输出。注意,没有在运行或或被阻塞的进程,idle列显示CPU是100%空闲,st列展示了从"虚拟机"偷来的时间
复制
概述。
MySQL内建的复制功能是构建基于MySQL的大规模、高性能应用的基础,这类应用使用所谓的"水平扩展"的架构。我们可以通过为服务器配置一个或多个备库(可能有的地方将会复制备库(replica)称为从库(slave))的方式来进行数据同步。复制功能不仅有利于构建高性能的应用,同时也是提高可用性、可扩展性、灾难恢复、备份以及数据仓库等工作的基础。事实上,可扩展性和高可用性通常是相关联的话题。
复制解决的基本问题是让一台服务器的数据与其他服务器保持同步。一台主库的数据可以同步到堕胎备库上,备库背身也可以被配置成另外一台服务器的主库。主库和备库之间可以有多种不同的组合方式。MySQL支持两种复制方式:基于行的复制和基于语句的复制。基于语句的复制(也称逻辑复制)早在MySQL3.23版本中就存在,而基于行的复制方式在5.1版本中才被加进来,这两种方式都是通过在主库上记录二进制日志,在备库重放日志的方式来实现异步的数据复制。这意味着,在同一时间点备库上的数据可能与主库存在不一致,并且无法保证主备之间的延迟。一些大的语句可能导致备库产生几秒、几分钟甚至几个小时的延迟。MySQL复制大部分是向后兼容的,新版本的服务器可以作为老版本服务器的备库,但反过来,将老版本作为新版本服务器的备库通常是不可行的,因为他可能无法解析新版本所采用的新的特性或语法,另外所使用的二进制文件的格式也可能不相同。例如,不能从MySQL5.1复制到MySQL4.0。在进行大的版本升级前,例如从4.1升级到5.0,或从5.1升级到5.5,最好先对复制的设置进行测试。但对于小版本号升级,如从5.151升级到5.1.58,则通常是兼容的。通过阅读每次版本更新的ChangeLog可以找到不同版本间做了什么修改。
复制通常不会增加主库的开销,主要是启用二进制日志带来的开销,但出于备份或及时从崩溃中恢复的目的,这点开销也是必要的。除此之外,每个备库也会对主库增加一些负载(例如网络IO开销),尤其当备库请求从主库读取旧的二进制日志文件时,可能会造成更高的IO开销。另外锁竞争也可能阻碍事务的提交。最后,如果时从一个高吞吐量(例如5 000或更高的TPS)的主库上复制到多个备库,唤醒多个复制线程发送时间的开销将会累加。
通过复制可以将读操作指向备库来获得更好的读扩展,但对于写操作,除非设计得当,否则并不适合通过复制来扩展写操作。在一主库多备库的架构中,写操作会被执行多次,这时候整个系统的性能取决于写入最慢的那部分。
当使用一主库多备库的架构时,可能会造成一些浪费,因为本质上它会复制大量不必要的重复数据。例如,对于一台主库和10台备库,会有11份数据拷贝,并且这11台服务器的缓存中存储了大部分相同的数据。这和在服务器上有11路RAID 1类似,这不是一种经济的硬件使用方式,但这种复制架构却很常见
MySQL内建的复制功能是构建基于MySQL的大规模、高性能应用的基础,这类应用使用所谓的"水平扩展"的架构。我们可以通过为服务器配置一个或多个备库(可能有的地方将会复制备库(replica)称为从库(slave))的方式来进行数据同步。复制功能不仅有利于构建高性能的应用,同时也是提高可用性、可扩展性、灾难恢复、备份以及数据仓库等工作的基础。事实上,可扩展性和高可用性通常是相关联的话题。
复制解决的基本问题是让一台服务器的数据与其他服务器保持同步。一台主库的数据可以同步到堕胎备库上,备库背身也可以被配置成另外一台服务器的主库。主库和备库之间可以有多种不同的组合方式。MySQL支持两种复制方式:基于行的复制和基于语句的复制。基于语句的复制(也称逻辑复制)早在MySQL3.23版本中就存在,而基于行的复制方式在5.1版本中才被加进来,这两种方式都是通过在主库上记录二进制日志,在备库重放日志的方式来实现异步的数据复制。这意味着,在同一时间点备库上的数据可能与主库存在不一致,并且无法保证主备之间的延迟。一些大的语句可能导致备库产生几秒、几分钟甚至几个小时的延迟。MySQL复制大部分是向后兼容的,新版本的服务器可以作为老版本服务器的备库,但反过来,将老版本作为新版本服务器的备库通常是不可行的,因为他可能无法解析新版本所采用的新的特性或语法,另外所使用的二进制文件的格式也可能不相同。例如,不能从MySQL5.1复制到MySQL4.0。在进行大的版本升级前,例如从4.1升级到5.0,或从5.1升级到5.5,最好先对复制的设置进行测试。但对于小版本号升级,如从5.151升级到5.1.58,则通常是兼容的。通过阅读每次版本更新的ChangeLog可以找到不同版本间做了什么修改。
复制通常不会增加主库的开销,主要是启用二进制日志带来的开销,但出于备份或及时从崩溃中恢复的目的,这点开销也是必要的。除此之外,每个备库也会对主库增加一些负载(例如网络IO开销),尤其当备库请求从主库读取旧的二进制日志文件时,可能会造成更高的IO开销。另外锁竞争也可能阻碍事务的提交。最后,如果时从一个高吞吐量(例如5 000或更高的TPS)的主库上复制到多个备库,唤醒多个复制线程发送时间的开销将会累加。
通过复制可以将读操作指向备库来获得更好的读扩展,但对于写操作,除非设计得当,否则并不适合通过复制来扩展写操作。在一主库多备库的架构中,写操作会被执行多次,这时候整个系统的性能取决于写入最慢的那部分。
当使用一主库多备库的架构时,可能会造成一些浪费,因为本质上它会复制大量不必要的重复数据。例如,对于一台主库和10台备库,会有11份数据拷贝,并且这11台服务器的缓存中存储了大部分相同的数据。这和在服务器上有11路RAID 1类似,这不是一种经济的硬件使用方式,但这种复制架构却很常见
复制解决的问题。
下面是复制常见的用途。
1.数据分布
MySQL复制通常不会对贷款造成很大的压力,但在5.1版本引入的基于行的复制会比传统的基于语句的复制模式的带宽压力大,你可以随意地停止或开始复制,并在不同的地理位置来分布数据备份,例如不同的数据中心。即使在不稳定的网络环境下,远程复制也可以工作。但如果为了保持很低的复制延迟,最好有一个稳定的低延迟连接
2.负载均衡
通过MySQL复制可以将读操作分布到多个服务器上,实现对读密集型应用的优化,并且实现很方便,通过简单的代码修改就能实现基本的负载均衡。对于小规模的应用,可以简单地对机器名做硬编码或使用DNS轮询(将一个机器指向多个IP地址)。当然也可以使用更复杂的方法,例如网络负载均衡这一类的标准负载均衡解决方案,能够很好地将负载分配到不同的MySQL服务器上。Linux虚拟服务器(Linux Virtual Server,LVS)也能很好地工作,
3.备份
对于备份来说,复制是一项很有意义的技术补充,但复制既不是备份也不能够取代备份
4.高可用性和故障切换
复制能够帮助应用程序避免MySQL单点失败,一个包含复制的设计良好的故障切换系统能够显著地缩短宕机时间
5.MySQL升级测试
这种做法比较普遍,使用一个更高版本的MySQL作为备库,保证在升级全部实例前,查询能够在备库按照预期执行。
下面是复制常见的用途。
1.数据分布
MySQL复制通常不会对贷款造成很大的压力,但在5.1版本引入的基于行的复制会比传统的基于语句的复制模式的带宽压力大,你可以随意地停止或开始复制,并在不同的地理位置来分布数据备份,例如不同的数据中心。即使在不稳定的网络环境下,远程复制也可以工作。但如果为了保持很低的复制延迟,最好有一个稳定的低延迟连接
2.负载均衡
通过MySQL复制可以将读操作分布到多个服务器上,实现对读密集型应用的优化,并且实现很方便,通过简单的代码修改就能实现基本的负载均衡。对于小规模的应用,可以简单地对机器名做硬编码或使用DNS轮询(将一个机器指向多个IP地址)。当然也可以使用更复杂的方法,例如网络负载均衡这一类的标准负载均衡解决方案,能够很好地将负载分配到不同的MySQL服务器上。Linux虚拟服务器(Linux Virtual Server,LVS)也能很好地工作,
3.备份
对于备份来说,复制是一项很有意义的技术补充,但复制既不是备份也不能够取代备份
4.高可用性和故障切换
复制能够帮助应用程序避免MySQL单点失败,一个包含复制的设计良好的故障切换系统能够显著地缩短宕机时间
5.MySQL升级测试
这种做法比较普遍,使用一个更高版本的MySQL作为备库,保证在升级全部实例前,查询能够在备库按照预期执行。
复制如何工作。
在详细介绍如何设置复制之前,让我们先看看MySQL实际上是如何复制数据的。总的来说,复制有三个步骤:
1.在主库上把数据更改记录到二进制日志(Binary Log)中(这些记录被称为二进制日志事件)
2.备库将主库上的日志复制到自己的中继日志(Relay Log)中
3.备库读取中继日志中的事件,将其重放到备库数据之上
以上只是概述,实际上每一步都很复杂,如图所示
第一步是在主库上记录二进制日志.在每次准备提交事务完成数据更新前,主库将数据更新的事件记录到二进制日志中。MySQL会按事务提交的顺序而非每条语句的执行顺序来记录二进制日志。在记录二进制日志后,主库会告诉存储引擎可以提交事务了。
下一步,备库将主库的二进制日志复制到其本地的中继日志中。首先,备库会启动一个共组哦县城,称为IO线程,IO线程跟主库建立一个普通的客户端连接,然后在主库上启动一个特殊的二进制转储(binlog dump)线程(该线程没有对应的SQL命令),这个二进制转储线程会读取主库上二进制日中的事件。它不会对事件进行轮询。如果该线程追赶上了主库,它将进入睡眠状态,直到主库发送信号量通知其有新的事件产生时才会被幻行,贝克IO线程会将接收到的事件记录到中继日志中。
MySQL4.0之前的复制与之后的版本相比改变很大,例如MySLQ最初的复制功能没有使用中继日志,所以复制只用到了两个线程,而不是现在的三个线程。目前大部分人都是使用的最新版本。
备库的SQL线程执行最后异步,该线程从中继日志中读取事件并在备库执行,从而实现备库数据的更新。当SQL线程追赶上IO线程时,终极日志通常已经在系统缓存中,所以中继日志的开销很低。SQL线程执行的事件也可以通过配置选项来决定是否写入其自己的二进制日志中。
上图显示了在备库有两个运行的线程,在主库上也有一个运行的线程:和其他普通连接一样,由备库发起的连接,在主库上同样拥有一个线程。
这种复制架构实现了获取事件和重放事件的解耦,允许这两个过程异步进行。也就是说IO线程能够独立于SQL线程之外工作。但这种架构也限制了复制的过程,其中最重要的一点时在主库上并发运行的查询在备库只能串行化执行,因为只有一个SQL线程来重放中继日志中的事件。后面我们将会看到,这是很多工作负载的性能瓶颈所在,虽然有一些针对该问题的解决方案,但大多数用户仍然会受制于单线程
在详细介绍如何设置复制之前,让我们先看看MySQL实际上是如何复制数据的。总的来说,复制有三个步骤:
1.在主库上把数据更改记录到二进制日志(Binary Log)中(这些记录被称为二进制日志事件)
2.备库将主库上的日志复制到自己的中继日志(Relay Log)中
3.备库读取中继日志中的事件,将其重放到备库数据之上
以上只是概述,实际上每一步都很复杂,如图所示
第一步是在主库上记录二进制日志.在每次准备提交事务完成数据更新前,主库将数据更新的事件记录到二进制日志中。MySQL会按事务提交的顺序而非每条语句的执行顺序来记录二进制日志。在记录二进制日志后,主库会告诉存储引擎可以提交事务了。
下一步,备库将主库的二进制日志复制到其本地的中继日志中。首先,备库会启动一个共组哦县城,称为IO线程,IO线程跟主库建立一个普通的客户端连接,然后在主库上启动一个特殊的二进制转储(binlog dump)线程(该线程没有对应的SQL命令),这个二进制转储线程会读取主库上二进制日中的事件。它不会对事件进行轮询。如果该线程追赶上了主库,它将进入睡眠状态,直到主库发送信号量通知其有新的事件产生时才会被幻行,贝克IO线程会将接收到的事件记录到中继日志中。
MySQL4.0之前的复制与之后的版本相比改变很大,例如MySLQ最初的复制功能没有使用中继日志,所以复制只用到了两个线程,而不是现在的三个线程。目前大部分人都是使用的最新版本。
备库的SQL线程执行最后异步,该线程从中继日志中读取事件并在备库执行,从而实现备库数据的更新。当SQL线程追赶上IO线程时,终极日志通常已经在系统缓存中,所以中继日志的开销很低。SQL线程执行的事件也可以通过配置选项来决定是否写入其自己的二进制日志中。
上图显示了在备库有两个运行的线程,在主库上也有一个运行的线程:和其他普通连接一样,由备库发起的连接,在主库上同样拥有一个线程。
这种复制架构实现了获取事件和重放事件的解耦,允许这两个过程异步进行。也就是说IO线程能够独立于SQL线程之外工作。但这种架构也限制了复制的过程,其中最重要的一点时在主库上并发运行的查询在备库只能串行化执行,因为只有一个SQL线程来重放中继日志中的事件。后面我们将会看到,这是很多工作负载的性能瓶颈所在,虽然有一些针对该问题的解决方案,但大多数用户仍然会受制于单线程
配置复制。
为MySQL服务器配置复制非常简单。但由于场景不同,基本的步骤还是有所差异的。最基本的场景是新安装的主库和备库,总的来说分为以下几步:
1.在每台服务器上创建复制账号
2.配置主库和备库
3.通知备库连接到主库并从主库复制数据
这里我们假定大部分配置采用默认值即可,在主库和备库都是全新安装并且拥有同样的数据(默认MySQL数据库)时这样的假设时合理的。假设有服务器server1(IP地址192.168.0.1)和服务器server2(IP地址192.168.0.2).
为MySQL服务器配置复制非常简单。但由于场景不同,基本的步骤还是有所差异的。最基本的场景是新安装的主库和备库,总的来说分为以下几步:
1.在每台服务器上创建复制账号
2.配置主库和备库
3.通知备库连接到主库并从主库复制数据
这里我们假定大部分配置采用默认值即可,在主库和备库都是全新安装并且拥有同样的数据(默认MySQL数据库)时这样的假设时合理的。假设有服务器server1(IP地址192.168.0.1)和服务器server2(IP地址192.168.0.2).
创建复制账号
MySQL会赋予一些特殊的权限给复制线程。在备库运行的IO线程会建立一个到主库的TCP/IP连接,这意味着必须在主库创建一个用户,并赋予其合适的权限。备库IO线程以该用户名连接到主库并读取其二进制日志。通过如下语句创建用户账号:
```sql
mysql>GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO repl@'192.168.0.%' IDENTIFIED BY 'p4ssword';
```
我们在主库和备库都创建该账号。注意我们把这个账户限制在本地网络,因为这是一个特权账号(尽管该账号无法执行select或修改数据,但仍然能从二进制日中获得一些数据)。复制账户事实上只需要有主库上的REPLICATION SLAVE权限,并不一定需要每一端服务器都有REPLICATION CLIENT权限,那为什么我们要把这两种权限给主/备库都赋予呢?这有两个原因:
1.用来监控和管理复制的账号需要REPLICATION CLIENT权限,并且针对这两种目的的使用同一个账号更加容易(而不是为某个目的单独创建一个账号)
2.如果在主库上建立了账号,然后从主库将数据克隆到备库上时,备库也就设置好了——变成主库所需要的配置。这样后续有需要可以方便地交换主备库的角色
MySQL会赋予一些特殊的权限给复制线程。在备库运行的IO线程会建立一个到主库的TCP/IP连接,这意味着必须在主库创建一个用户,并赋予其合适的权限。备库IO线程以该用户名连接到主库并读取其二进制日志。通过如下语句创建用户账号:
```sql
mysql>GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO repl@'192.168.0.%' IDENTIFIED BY 'p4ssword';
```
我们在主库和备库都创建该账号。注意我们把这个账户限制在本地网络,因为这是一个特权账号(尽管该账号无法执行select或修改数据,但仍然能从二进制日中获得一些数据)。复制账户事实上只需要有主库上的REPLICATION SLAVE权限,并不一定需要每一端服务器都有REPLICATION CLIENT权限,那为什么我们要把这两种权限给主/备库都赋予呢?这有两个原因:
1.用来监控和管理复制的账号需要REPLICATION CLIENT权限,并且针对这两种目的的使用同一个账号更加容易(而不是为某个目的单独创建一个账号)
2.如果在主库上建立了账号,然后从主库将数据克隆到备库上时,备库也就设置好了——变成主库所需要的配置。这样后续有需要可以方便地交换主备库的角色
配置主库和备库。
下一步需要在主库上开启一些设置,假设主库是服务器server1,需要打开二进制日志并指定一个独一无二的服务器ID(serverID),在主库的my.cnf文件中增加或修改如下内容:
```c
log_bin = mysql-bin
server_id = 10
```
实际取值由你决定,这里只是为了简单起见,当然也可以设置更多需要的配置。必须明确地指定一个唯一的服务器ID,默认服务器ID通常为1(这和版本相关,一些MySQL版本根本不允许使用这个值)。使用默认值可能会导致和其他服务器的ID冲突,因此这里选择10来作为服务器ID.一种通用的做法是使用服务器IP地址的末8位,但要保证它是不变且唯一的(例如,服务器都在一个子网里)。最好选择一些有意义的约定并遵循。
如果之前没有在MySQL的配置文件中指定log_bin选项,就需要重新启动MySQL.为了确认二进制日志吻技安是否已经在主库上创建,使用SHOW MASTER STATUS命令,检查输出是否与如下的一直。MySQL会为文件名增加一些数字,所以这里看到的文件名和你定义的会有点不一样:
```sql
mysql>SHOW MASTER STATUS;
File:mysql-bin.000001
Position:98
Binlog_Do_DB:
Binlog_Ignore_DB:
```
备库上也需要在my.cnf中增加类似的配置,并且同样需要重启服务器。
```c
log_bin = mysql-bin
server_id = 2
relay_log = /var/lib/mysql/mysql-relay-bin
log_slave_updates = 1
read_only =1
```
从技术上来说,这些选项并不总是必要的。其中一些选项只是显式地列出了默认值。事实上只有server_id是必需地。这里我们同样也使用了log_bin,并赋予一个明确的名字。默认情况下,它是根据机器名来命名的,但如果机器名变化了可能会导致问题。为了简便起见,将主库和备库上的log-bin设置为相同的值。当然如果你愿意的化,也可以设置成别的值。另外我们还增加了两个配置选项:relay_log(指定中继日志的位置和命名)和log_salve_updates(允许备库将其重放的事件也记录到自身的二进制日中中),后一个选项会给备库增加额外的工作,但正如后面将会看到的,我们有理由为每个备库设置该选项。
有时候只开启了二进制日志,但却没有开启log_slave_updates,可能会碰到一些奇怪的现象,例如,当配置错误时可能会导致备库数据被修改。如果可能的化,最好使用read_only配置选项,该选项会阻止任何没有特权权限的线程修改数据(所以最好不要给予用户超出需要的权限)。但read_only选项常常不是很使用,特别是对于那些需要在备库建表的应用。
(不要再配置文件my.cnf中设置master_port或master_host这些选项,这是老的配置方式,已经被废弃,它只会导致问题,不会有任何好处)
下一步需要在主库上开启一些设置,假设主库是服务器server1,需要打开二进制日志并指定一个独一无二的服务器ID(serverID),在主库的my.cnf文件中增加或修改如下内容:
```c
log_bin = mysql-bin
server_id = 10
```
实际取值由你决定,这里只是为了简单起见,当然也可以设置更多需要的配置。必须明确地指定一个唯一的服务器ID,默认服务器ID通常为1(这和版本相关,一些MySQL版本根本不允许使用这个值)。使用默认值可能会导致和其他服务器的ID冲突,因此这里选择10来作为服务器ID.一种通用的做法是使用服务器IP地址的末8位,但要保证它是不变且唯一的(例如,服务器都在一个子网里)。最好选择一些有意义的约定并遵循。
如果之前没有在MySQL的配置文件中指定log_bin选项,就需要重新启动MySQL.为了确认二进制日志吻技安是否已经在主库上创建,使用SHOW MASTER STATUS命令,检查输出是否与如下的一直。MySQL会为文件名增加一些数字,所以这里看到的文件名和你定义的会有点不一样:
```sql
mysql>SHOW MASTER STATUS;
File:mysql-bin.000001
Position:98
Binlog_Do_DB:
Binlog_Ignore_DB:
```
备库上也需要在my.cnf中增加类似的配置,并且同样需要重启服务器。
```c
log_bin = mysql-bin
server_id = 2
relay_log = /var/lib/mysql/mysql-relay-bin
log_slave_updates = 1
read_only =1
```
从技术上来说,这些选项并不总是必要的。其中一些选项只是显式地列出了默认值。事实上只有server_id是必需地。这里我们同样也使用了log_bin,并赋予一个明确的名字。默认情况下,它是根据机器名来命名的,但如果机器名变化了可能会导致问题。为了简便起见,将主库和备库上的log-bin设置为相同的值。当然如果你愿意的化,也可以设置成别的值。另外我们还增加了两个配置选项:relay_log(指定中继日志的位置和命名)和log_salve_updates(允许备库将其重放的事件也记录到自身的二进制日中中),后一个选项会给备库增加额外的工作,但正如后面将会看到的,我们有理由为每个备库设置该选项。
有时候只开启了二进制日志,但却没有开启log_slave_updates,可能会碰到一些奇怪的现象,例如,当配置错误时可能会导致备库数据被修改。如果可能的化,最好使用read_only配置选项,该选项会阻止任何没有特权权限的线程修改数据(所以最好不要给予用户超出需要的权限)。但read_only选项常常不是很使用,特别是对于那些需要在备库建表的应用。
(不要再配置文件my.cnf中设置master_port或master_host这些选项,这是老的配置方式,已经被废弃,它只会导致问题,不会有任何好处)
启动复制。
下一步是告诉备库如何连接到主库并重放其二进制日志。这一步要通过修改my.cnf来配置,而是使用CHANGE MASTER TO语句,该语句完全替代了my.cnf中相应的设置,并且允许以后指向别的主库时无须重启备库。下面时开始复制的基本命令:
```sql
mysql> CHANGE MASTER TO MASATER_HOST = 'server1', MASTER_USER='repl',MASTER_PASSWORD='p4ssword',MASTER_LOG_FILE='mysql-bin.000001',MASTER_LOG_POS=0;
```
MASTER_LOG_POS参数被设置为0,因为要从日志的开头独起。当执行完这条语句后,可以通过SHOW SLAVE STATUS语句来检查复制是否正确执行
```sql
mysql>SHOW SLAVE STATUS\G
*************************** 1. row ***************************
Slave_IO_State:
Master_Host:server1
Master_User:repl
Master_Port:3306
Connect_Retry:60
Master_Log_File:mysql-bin.000001
Read_Master_Log_Pos:4
Relay_Log_File:mysql-relay-bin.000001
Relay_Log_Pos:4
Relay_Master_Log_File:mysql-bin.000001
Slave_IO_Running:No
Slave_SQL_Running:No
.......
Seconds_Behind_Master:NULL
```
Slave_IO_State、Slave_IO_Running和Slave_SQL_Running这三列显示当前备库复制尚未运行。可鞥已经注意到日志的开头是4而不是0,这是因为0其实不是日志真正开始的位置,它仅仅意味着"在日志文件头",MySQL知道第一个事件从文件的第4位(事实上,正如之前从SHOW MASTER STAUS看到的,真正的日志起始位置是98,一旦备库连接到主库就开始工作,现在连接还未发生)开始读。
下一步是告诉备库如何连接到主库并重放其二进制日志。这一步要通过修改my.cnf来配置,而是使用CHANGE MASTER TO语句,该语句完全替代了my.cnf中相应的设置,并且允许以后指向别的主库时无须重启备库。下面时开始复制的基本命令:
```sql
mysql> CHANGE MASTER TO MASATER_HOST = 'server1', MASTER_USER='repl',MASTER_PASSWORD='p4ssword',MASTER_LOG_FILE='mysql-bin.000001',MASTER_LOG_POS=0;
```
MASTER_LOG_POS参数被设置为0,因为要从日志的开头独起。当执行完这条语句后,可以通过SHOW SLAVE STATUS语句来检查复制是否正确执行
```sql
mysql>SHOW SLAVE STATUS\G
*************************** 1. row ***************************
Slave_IO_State:
Master_Host:server1
Master_User:repl
Master_Port:3306
Connect_Retry:60
Master_Log_File:mysql-bin.000001
Read_Master_Log_Pos:4
Relay_Log_File:mysql-relay-bin.000001
Relay_Log_Pos:4
Relay_Master_Log_File:mysql-bin.000001
Slave_IO_Running:No
Slave_SQL_Running:No
.......
Seconds_Behind_Master:NULL
```
Slave_IO_State、Slave_IO_Running和Slave_SQL_Running这三列显示当前备库复制尚未运行。可鞥已经注意到日志的开头是4而不是0,这是因为0其实不是日志真正开始的位置,它仅仅意味着"在日志文件头",MySQL知道第一个事件从文件的第4位(事实上,正如之前从SHOW MASTER STAUS看到的,真正的日志起始位置是98,一旦备库连接到主库就开始工作,现在连接还未发生)开始读。
运行下面的命令开始复制:
```sql
mysql> START SLAVE;
```
执行该命令没有显示错误,现在我们再用SHOW SLAVE STATUS命令检查:
```sql
mysql>SHOW SLAVE STATUS\G
*************************** 1. row ***************************
Slave_IO_State:
Master_Host:server1
Master_User:repl
Master_Port:3306
Connect_Retry:60
Master_Log_File:mysql-bin.000001
Read_Master_Log_Pos:164
Relay_Log_File:mysql-relay-bin.000001
Relay_Log_Pos:164
Relay_Master_Log_File:mysql-bin.000001
Slave_IO_Running:Yes
Slave_SQL_Running:Yes
.......
Seconds_Behind_Master:0
```
从输出可以看到IO线程和SQL线程都已经开始运行,Seconds_Behind_Master的值也不再为NULL。IO线程正在等待从主库传递过来的事件,这意味着IO线程已经读取了主库所有的事件。日志位置发生了变化,表明已经从主库获取和执行了一些事件.如果在主库上做一些数据更新,就会看到备库的文件或者日志位置都可能会增加。备库的数据同样会随之更新。我们还可以从线程列表中看到复制线程。在主库上可以看到由备库IO线程向主库发起的连接:
```sql
mysql>SHOW PROCESSLIST\G
*************************** 1. row ***************************
Id:55
User:repl
Host:replica1.webcluster_1:54813
db:NULL
Command:Binlog Dump
Time:610237
State:Has sent all binlog to slave;waiting for binlog to be updated
Info:NULL
```
```sql
mysql> START SLAVE;
```
执行该命令没有显示错误,现在我们再用SHOW SLAVE STATUS命令检查:
```sql
mysql>SHOW SLAVE STATUS\G
*************************** 1. row ***************************
Slave_IO_State:
Master_Host:server1
Master_User:repl
Master_Port:3306
Connect_Retry:60
Master_Log_File:mysql-bin.000001
Read_Master_Log_Pos:164
Relay_Log_File:mysql-relay-bin.000001
Relay_Log_Pos:164
Relay_Master_Log_File:mysql-bin.000001
Slave_IO_Running:Yes
Slave_SQL_Running:Yes
.......
Seconds_Behind_Master:0
```
从输出可以看到IO线程和SQL线程都已经开始运行,Seconds_Behind_Master的值也不再为NULL。IO线程正在等待从主库传递过来的事件,这意味着IO线程已经读取了主库所有的事件。日志位置发生了变化,表明已经从主库获取和执行了一些事件.如果在主库上做一些数据更新,就会看到备库的文件或者日志位置都可能会增加。备库的数据同样会随之更新。我们还可以从线程列表中看到复制线程。在主库上可以看到由备库IO线程向主库发起的连接:
```sql
mysql>SHOW PROCESSLIST\G
*************************** 1. row ***************************
Id:55
User:repl
Host:replica1.webcluster_1:54813
db:NULL
Command:Binlog Dump
Time:610237
State:Has sent all binlog to slave;waiting for binlog to be updated
Info:NULL
```
同样,在备库也可以看到两个线程,一个是IO线程,一个是SQL线程:
```sql
mysql>SHOW PROCESSLIST\G
*************************** 1. row ***************************
Id:1
User:system user
Host:
db:NULL
Command:Connect
Time:611116
State:Waiting for master to send event
Info:NULL
*************************** 2. row ***************************
Id:2
User:system user
Host:
db:NULL
Command:Connect
Time:33
State:Has read all relay log; waiting for the slave I/O thread to update it
Info:NULL
```
这些简单的输出来自一台已经运行了一段时间的服务器,所以IO线程在主库和备库上的Time列的值较大,SQL线程已经空闲了33秒。这意味着33秒内没有重放任何事件。这些线程总是运行在"system user"账号下,其他列的值则不相同。例如,在SQL线程回放事件时,Info列可能显示正在执行的查询。
```sql
mysql>SHOW PROCESSLIST\G
*************************** 1. row ***************************
Id:1
User:system user
Host:
db:NULL
Command:Connect
Time:611116
State:Waiting for master to send event
Info:NULL
*************************** 2. row ***************************
Id:2
User:system user
Host:
db:NULL
Command:Connect
Time:33
State:Has read all relay log; waiting for the slave I/O thread to update it
Info:NULL
```
这些简单的输出来自一台已经运行了一段时间的服务器,所以IO线程在主库和备库上的Time列的值较大,SQL线程已经空闲了33秒。这意味着33秒内没有重放任何事件。这些线程总是运行在"system user"账号下,其他列的值则不相同。例如,在SQL线程回放事件时,Info列可能显示正在执行的查询。
从另一个服务器开始复制。
前面的设置都是嘉定主备库均为刚刚安装好且都是默认的数据,也就是说两台服务器上数据相同,并且知道当前主库的二进制日志。这不是典型的案例,大多数情况下有一个已经运行了一段时间的主库,然后用一台新安装的备库与之同步,此时这台备库还没有数据。有几种办法来初始化备库或者从其他服务器克隆到备库。包括从主库复制数据、从另外一台备库克隆数据,以及使用最近的一次备份来启动备库,需要由三个条件来让主库和备库保持同步:
1.在某个时间点的主库的数据快照
2.主库当前的二进制日志文件,和获得数据快照时在该二进制日志文件中的偏移量,我们把这两个值称为日志文件坐标(log file coordinates)。通过这两个值可以确定二进制日志的位置。可以通过SHOW MASTER STATUS命令来获取这些值
3.从快照时间到现在的二进制日志
下面是一些从别的服务器克隆备库的方法:
1.使用冷备份
最基本的方法是关闭主库,把数据复制到备库.重启主库后,会使用一个新的二进制文件,我们在备库通过执行CHANGE MASTER TO 指向这个文件的起始处。这个方法的缺点很明显:在复制数据时需要关闭主库
2.使用热备份
如果仅使用了MyISAM表,可以在主库运行时使用mysqlhotcopy或rsync来复制数据
3.使用mysqldump
如果只包含InnoDB表,那么可以使用以下命令来转储主库数据并将其加载到备库,然后设置相应的二进制日志坐标
```c
$ mysqldump --single-transaction --all-databases --master-data=1--host=server1 \| mysql --host=server2
```
选项--single-transaction使得转储的数据为事务开始前的数据。如果使用的是非事务型表,可以使用--lock-all-tables选项来获得所有表的一致性转储
4.使用快照或备份
只要知道对应的二进制日志坐标,就可以使用主库的快照或者备份来初始化备库(如果使用备份,需要确保从备份的时间点开始的主库二进制日志都要存在)。只需要把备份或快照恢复到备库,然后使用CHANGE MASTER TO指定二进制日志的坐标。也可以使用LVM快照、SAN快照、EBS快照——任何快照都可以
5.使用Percona Xtrabackup
Percona的Xtrabackup是一款开源的热备份工具。它能够再备份时不阻塞服务器的操作,因此可以在不影响主库的情况下设置备库。可以通过克隆主库或另一个已存在的备库的方式来建立备库。创建一个备份(不管是从主库还是从别的备库),并将其转储到目标机器,然后根据备份获得正确的开始复制的位置。
5.1 如果是从主库获得备份,可以从xtrabackup_binlog_pos_innodb文件中获得复制开始的位置
5.2 如果是从另外的备库获得备份,可以从xtrabackup_slave_info文件重获得复制开始的位置。
6.使用另外的备库
可以使用任何一种提及的克隆或者拷贝技术来从任意一台备库上将数据克隆到另外一台服务器。但是如果使用的是mysqldump,--master-data选项就会不起作用。此外,不能使用SHOW MASTER STATUS来获得主库的二进制日志坐标,而是在获取快照时使用SHOW SLAVE STATUS来获取备库在主库上的执行位置。使用另外的备库进行数据克隆最大的缺点是,如果这台备库的数据已经和主库不同步,克隆得到的就是脏数据。
不要使用LOAD DATA FROM MASTER或者LOAD TABLE FROM MASTER!这些命令过时、缓慢,并且非常危险,并且只适用于MyISAM存储引擎。
不管选择哪种技术,都要能熟练运用,要记录详细的文档或编写脚本。因为可能不止一次需要做这样的事情。甚至当错误发生时,也需要能够处理
前面的设置都是嘉定主备库均为刚刚安装好且都是默认的数据,也就是说两台服务器上数据相同,并且知道当前主库的二进制日志。这不是典型的案例,大多数情况下有一个已经运行了一段时间的主库,然后用一台新安装的备库与之同步,此时这台备库还没有数据。有几种办法来初始化备库或者从其他服务器克隆到备库。包括从主库复制数据、从另外一台备库克隆数据,以及使用最近的一次备份来启动备库,需要由三个条件来让主库和备库保持同步:
1.在某个时间点的主库的数据快照
2.主库当前的二进制日志文件,和获得数据快照时在该二进制日志文件中的偏移量,我们把这两个值称为日志文件坐标(log file coordinates)。通过这两个值可以确定二进制日志的位置。可以通过SHOW MASTER STATUS命令来获取这些值
3.从快照时间到现在的二进制日志
下面是一些从别的服务器克隆备库的方法:
1.使用冷备份
最基本的方法是关闭主库,把数据复制到备库.重启主库后,会使用一个新的二进制文件,我们在备库通过执行CHANGE MASTER TO 指向这个文件的起始处。这个方法的缺点很明显:在复制数据时需要关闭主库
2.使用热备份
如果仅使用了MyISAM表,可以在主库运行时使用mysqlhotcopy或rsync来复制数据
3.使用mysqldump
如果只包含InnoDB表,那么可以使用以下命令来转储主库数据并将其加载到备库,然后设置相应的二进制日志坐标
```c
$ mysqldump --single-transaction --all-databases --master-data=1--host=server1 \| mysql --host=server2
```
选项--single-transaction使得转储的数据为事务开始前的数据。如果使用的是非事务型表,可以使用--lock-all-tables选项来获得所有表的一致性转储
4.使用快照或备份
只要知道对应的二进制日志坐标,就可以使用主库的快照或者备份来初始化备库(如果使用备份,需要确保从备份的时间点开始的主库二进制日志都要存在)。只需要把备份或快照恢复到备库,然后使用CHANGE MASTER TO指定二进制日志的坐标。也可以使用LVM快照、SAN快照、EBS快照——任何快照都可以
5.使用Percona Xtrabackup
Percona的Xtrabackup是一款开源的热备份工具。它能够再备份时不阻塞服务器的操作,因此可以在不影响主库的情况下设置备库。可以通过克隆主库或另一个已存在的备库的方式来建立备库。创建一个备份(不管是从主库还是从别的备库),并将其转储到目标机器,然后根据备份获得正确的开始复制的位置。
5.1 如果是从主库获得备份,可以从xtrabackup_binlog_pos_innodb文件中获得复制开始的位置
5.2 如果是从另外的备库获得备份,可以从xtrabackup_slave_info文件重获得复制开始的位置。
6.使用另外的备库
可以使用任何一种提及的克隆或者拷贝技术来从任意一台备库上将数据克隆到另外一台服务器。但是如果使用的是mysqldump,--master-data选项就会不起作用。此外,不能使用SHOW MASTER STATUS来获得主库的二进制日志坐标,而是在获取快照时使用SHOW SLAVE STATUS来获取备库在主库上的执行位置。使用另外的备库进行数据克隆最大的缺点是,如果这台备库的数据已经和主库不同步,克隆得到的就是脏数据。
不要使用LOAD DATA FROM MASTER或者LOAD TABLE FROM MASTER!这些命令过时、缓慢,并且非常危险,并且只适用于MyISAM存储引擎。
不管选择哪种技术,都要能熟练运用,要记录详细的文档或编写脚本。因为可能不止一次需要做这样的事情。甚至当错误发生时,也需要能够处理
推荐的复制配置。
有许多参数来控制复制,其中一些会对数据安全和性能产生影响。推荐一种"安全"的配置,可以最小化问题发生的概率。在主库上二进制最重要的选项是sync_binlog:
```c
sync_binlog=1
```
如果开启该选项,MySQL每次在提交事务前会将二进制日志同步到磁盘上,保证在服务器崩溃时不会丢失事件。如果禁止该选项,服务器会少做一些工作,但二进制日志文件可能在服务器崩溃时损坏或丢失信息。在一个不需要作为主库的备库上,该选项带来了不必要的开销。它只适用于二进制日志,而非中继日志。如果无法容忍服务器崩溃导致表损坏,推荐使用InnoDB。在表损坏无关紧要时,MyISAM是可以接受的,但在一次备库服务器崩溃重启后,MyISAM表可能已经处于不一致状态。一种可能是语句没有完全应用到一个或多个表上,那么即使修复了表,数据也可能是不一致的。
如果使用InnoDB,强烈推荐设置如下选项:
```c
innodb_flush_logs_at_trx_commit #Flush every log write
innodb_support_xa=1 # MySQL 5.0 and newer only
innodb_safe_binlog # MySQL 4.1 only,rounghly equivalent to innodb_support_xa
```
这些事MySQL5.0及最新版本中的默认配置,我们推荐明确指定二进制日志的名字,以保证二进制日志名在所有服务器上是一致的,避免因为服务器名的变化导致日志文件名变化。你可能认为以服务器名来命名二进制日志无关紧要,但经验表明,当在服务器间转移文件、克隆新的备库、转储备份或者其他一些你想象不到的场景下,可能会导致很多问题。为了避免这些问题,需要给log_bin选项指定一个参数。可以随意地给一个绝对路径,但必须明确地指定基本的命名:
```c
log_bin=/var/lib/ysql/mysql-bin # Good,specifies a path and base name
# log_bin # Bad, base name will be server's hostname
```
在备库上,我们同样推荐开启如下配置选项,为中继日志制定绝对路径:
```c
relay_log=/path/to/logs/relay-bin
skip_slave_start
read_only
```
通过设置relay_log可以避免中继日志文件基于机器名来命名,防止之前提到的可能在主库发生的问题。制定绝对路径可以避免多个MySQL把本中存在的Bug,这些Bug可能会导致中继日志在一个意料外的位置创建。skip_slave_start选项能够阻止备库在崩溃后自动启动复制。这可以给你一些机会来修复可能发生的问题。如果备库在崩溃后自动启动并且处于不一致的状态,就可能导致更多的损坏,最后将不得不把所有数据丢弃,并重新开始配置备库。
read_only选项可以阻止大部分用户更改非临时表,除了复制SQL线程和其他拥有超级权限的用户之外,这也是要尽量避免给正常账号授予超级权限的原因之一。
即使开启了所有我们建议的选项,备库仍然可能在崩溃后被中断,因为master.info和中继日志文件都不是崩溃安全的。默认情况下甚至不会刷新到磁盘,直到MySQL5.5版本才有选项来控制这种行为。如果正在使用MySQL5.5并且不会介意额外的fsync()导致的性能开销,最好设置以下选项:
```c
sync_master_info=1
sync_relay_log=1
sync_relay_log_info=1
```
如果备库与主库的延迟很大,备库的IO线程可能会写很多中继日志文件,SQL线程在重放完一个中继日志中的事件后会尽快将其删除(通过relay_log_purge选项来控制)。但如果延迟非常严重,IO线程可能会把整个磁盘撑满。解决办法是配置relay_log_space_limit变量。如果所有中继日志的大小之和超过这个值,IO线程会停止,等待SQL线程释放磁盘空间。
尽管听起来很美好,但有一个隐藏的问题。如果备库没有从主库上获取所有的中继日志,这些日志可能在主库崩溃时丢失。早先这个选项存在一些Bug,使用率也不高,所以用到这个选项遇到Bug的风险会更高。除非磁盘空间真的非常紧张,否则最好让中继日志使用其需要的磁盘空间,这也是为什么没有将relay_log_space_limit列入推荐配置选项的原因
有许多参数来控制复制,其中一些会对数据安全和性能产生影响。推荐一种"安全"的配置,可以最小化问题发生的概率。在主库上二进制最重要的选项是sync_binlog:
```c
sync_binlog=1
```
如果开启该选项,MySQL每次在提交事务前会将二进制日志同步到磁盘上,保证在服务器崩溃时不会丢失事件。如果禁止该选项,服务器会少做一些工作,但二进制日志文件可能在服务器崩溃时损坏或丢失信息。在一个不需要作为主库的备库上,该选项带来了不必要的开销。它只适用于二进制日志,而非中继日志。如果无法容忍服务器崩溃导致表损坏,推荐使用InnoDB。在表损坏无关紧要时,MyISAM是可以接受的,但在一次备库服务器崩溃重启后,MyISAM表可能已经处于不一致状态。一种可能是语句没有完全应用到一个或多个表上,那么即使修复了表,数据也可能是不一致的。
如果使用InnoDB,强烈推荐设置如下选项:
```c
innodb_flush_logs_at_trx_commit #Flush every log write
innodb_support_xa=1 # MySQL 5.0 and newer only
innodb_safe_binlog # MySQL 4.1 only,rounghly equivalent to innodb_support_xa
```
这些事MySQL5.0及最新版本中的默认配置,我们推荐明确指定二进制日志的名字,以保证二进制日志名在所有服务器上是一致的,避免因为服务器名的变化导致日志文件名变化。你可能认为以服务器名来命名二进制日志无关紧要,但经验表明,当在服务器间转移文件、克隆新的备库、转储备份或者其他一些你想象不到的场景下,可能会导致很多问题。为了避免这些问题,需要给log_bin选项指定一个参数。可以随意地给一个绝对路径,但必须明确地指定基本的命名:
```c
log_bin=/var/lib/ysql/mysql-bin # Good,specifies a path and base name
# log_bin # Bad, base name will be server's hostname
```
在备库上,我们同样推荐开启如下配置选项,为中继日志制定绝对路径:
```c
relay_log=/path/to/logs/relay-bin
skip_slave_start
read_only
```
通过设置relay_log可以避免中继日志文件基于机器名来命名,防止之前提到的可能在主库发生的问题。制定绝对路径可以避免多个MySQL把本中存在的Bug,这些Bug可能会导致中继日志在一个意料外的位置创建。skip_slave_start选项能够阻止备库在崩溃后自动启动复制。这可以给你一些机会来修复可能发生的问题。如果备库在崩溃后自动启动并且处于不一致的状态,就可能导致更多的损坏,最后将不得不把所有数据丢弃,并重新开始配置备库。
read_only选项可以阻止大部分用户更改非临时表,除了复制SQL线程和其他拥有超级权限的用户之外,这也是要尽量避免给正常账号授予超级权限的原因之一。
即使开启了所有我们建议的选项,备库仍然可能在崩溃后被中断,因为master.info和中继日志文件都不是崩溃安全的。默认情况下甚至不会刷新到磁盘,直到MySQL5.5版本才有选项来控制这种行为。如果正在使用MySQL5.5并且不会介意额外的fsync()导致的性能开销,最好设置以下选项:
```c
sync_master_info=1
sync_relay_log=1
sync_relay_log_info=1
```
如果备库与主库的延迟很大,备库的IO线程可能会写很多中继日志文件,SQL线程在重放完一个中继日志中的事件后会尽快将其删除(通过relay_log_purge选项来控制)。但如果延迟非常严重,IO线程可能会把整个磁盘撑满。解决办法是配置relay_log_space_limit变量。如果所有中继日志的大小之和超过这个值,IO线程会停止,等待SQL线程释放磁盘空间。
尽管听起来很美好,但有一个隐藏的问题。如果备库没有从主库上获取所有的中继日志,这些日志可能在主库崩溃时丢失。早先这个选项存在一些Bug,使用率也不高,所以用到这个选项遇到Bug的风险会更高。除非磁盘空间真的非常紧张,否则最好让中继日志使用其需要的磁盘空间,这也是为什么没有将relay_log_space_limit列入推荐配置选项的原因
复制的原理
基于语句的复制。
在MySQL5.0及之前的版本中只支持基于语句的复制(也称为逻辑复制),这在数据库领域是很少见的,基于语句的复制模式下,主库会记录那些造成数据更改的查询,当备库读取并重放这些事件时,实际上只是把主库上执行过的SQL再执行一遍。这种方式既有好处、也有缺点。
最明显的好处是实现相当简单。理论上讲,简单地记录和执行这些语句,能够让主备保持同步。另一个好处是二进制日志里的事件更加紧凑,所以相对而言,基于语句的模式不会使用太多带宽。一条更新好几兆数据的语句在二进制日志里可能只占几十个字节。另外mysqlbinlog工具是使用基于语句的日志的最佳工具。但事实上基于语句的方式可能并不如看起来那么便利。因为主库上的数据更新除了执行的语句外,可能还依赖于其他因素。例如,同一条SQL在主库和备库上执行的事件可能稍微或很不相同,因此在传输的二进制日志中,除了查询语句,还包括了一些元数据信息,如当前的时间戳。即便如此,还存在着一些无法被正确复制的SQL.例如使用CURRENT_USER()函数的语句。存储过程和触发器在使用基于语句的复制模式时也可能存在问题。
另外一个问题是更新必须是串行的。这需要更多的锁——有时候要特别关注这一点,另外不是所有的存储引擎都支持这种复制模式。尽管这些存储引擎是包括在MySQL5.5及之前版本中发行的。
在MySQL5.0及之前的版本中只支持基于语句的复制(也称为逻辑复制),这在数据库领域是很少见的,基于语句的复制模式下,主库会记录那些造成数据更改的查询,当备库读取并重放这些事件时,实际上只是把主库上执行过的SQL再执行一遍。这种方式既有好处、也有缺点。
最明显的好处是实现相当简单。理论上讲,简单地记录和执行这些语句,能够让主备保持同步。另一个好处是二进制日志里的事件更加紧凑,所以相对而言,基于语句的模式不会使用太多带宽。一条更新好几兆数据的语句在二进制日志里可能只占几十个字节。另外mysqlbinlog工具是使用基于语句的日志的最佳工具。但事实上基于语句的方式可能并不如看起来那么便利。因为主库上的数据更新除了执行的语句外,可能还依赖于其他因素。例如,同一条SQL在主库和备库上执行的事件可能稍微或很不相同,因此在传输的二进制日志中,除了查询语句,还包括了一些元数据信息,如当前的时间戳。即便如此,还存在着一些无法被正确复制的SQL.例如使用CURRENT_USER()函数的语句。存储过程和触发器在使用基于语句的复制模式时也可能存在问题。
另外一个问题是更新必须是串行的。这需要更多的锁——有时候要特别关注这一点,另外不是所有的存储引擎都支持这种复制模式。尽管这些存储引擎是包括在MySQL5.5及之前版本中发行的。
基于行的复制。
MySQL5.1开始支持基于行的复制,这种方式会将实际数据记录在二进制日志中,跟其他数据库的实现比较相像。它有其自身的一些优点和缺点。最大的好处是可以正确地复制每一行。一些语句可以被更加有效地复制。
(基于行的复制没有向后兼容性,和MySQL 5.1 一起发布的mysqlbinlog工具可以读取基于行的复制的时间格式(它对人是不可读的,但MySQL可以解释),但是早期版本的mysqlbinlog无法识别这类事件,在遇到错误时会退出)
由于无须重放更新主库数据的查询,使用基于行的复制模式能够更高效地复制数据,重放一些查询的代价可能会很高。例如,下面有一个查询将数据从一个大表中汇总到小表:
```sql
mysql> INSERT INTO summary_table(col1,col2,sum_col3)
-> SELECT col1,col2,sum(col3)
-> FROM enormous_table
-> GROUP BY col1, col2;
```
想象一下,如果表enormous_table的列col1和col2有三种组合,这个查询可能在源表上扫描多次,但最终只在目标表上产生三行数据。但使用基于行的复制方式,在备库上开销会小很多。这种情况下,基于行的复制模式更加高效。但在另一方面,下面这条语句使用基于语句的复制方式代价会小很多:
```sql
mysql>UPDATE enormous_table SET col1 =0;
```
由于这条语句做了全表更新,使用基于行的复制开销会很大,因为每一行的数据都会被记录到二进制日志中,这使得二进制日志事件非常庞大。并且会给主库上记录日志和复制增加额外的负载,更慢的日志记录则会降低并发度。
由于没有哪种模式对所有的情况都是完美的,MySQL能够在这两种复制模式间动态切换,默认情况下使用的是基于语句的复制方式,但如果发现语句无法被正确地复制,就切换到基于行的复制模式。还可以根据需求来设置会话级别的变量binlog_format,控制二进制日志格式。对于基于行的复制模式,很难进行时间点恢复,但这并非不可能
MySQL5.1开始支持基于行的复制,这种方式会将实际数据记录在二进制日志中,跟其他数据库的实现比较相像。它有其自身的一些优点和缺点。最大的好处是可以正确地复制每一行。一些语句可以被更加有效地复制。
(基于行的复制没有向后兼容性,和MySQL 5.1 一起发布的mysqlbinlog工具可以读取基于行的复制的时间格式(它对人是不可读的,但MySQL可以解释),但是早期版本的mysqlbinlog无法识别这类事件,在遇到错误时会退出)
由于无须重放更新主库数据的查询,使用基于行的复制模式能够更高效地复制数据,重放一些查询的代价可能会很高。例如,下面有一个查询将数据从一个大表中汇总到小表:
```sql
mysql> INSERT INTO summary_table(col1,col2,sum_col3)
-> SELECT col1,col2,sum(col3)
-> FROM enormous_table
-> GROUP BY col1, col2;
```
想象一下,如果表enormous_table的列col1和col2有三种组合,这个查询可能在源表上扫描多次,但最终只在目标表上产生三行数据。但使用基于行的复制方式,在备库上开销会小很多。这种情况下,基于行的复制模式更加高效。但在另一方面,下面这条语句使用基于语句的复制方式代价会小很多:
```sql
mysql>UPDATE enormous_table SET col1 =0;
```
由于这条语句做了全表更新,使用基于行的复制开销会很大,因为每一行的数据都会被记录到二进制日志中,这使得二进制日志事件非常庞大。并且会给主库上记录日志和复制增加额外的负载,更慢的日志记录则会降低并发度。
由于没有哪种模式对所有的情况都是完美的,MySQL能够在这两种复制模式间动态切换,默认情况下使用的是基于语句的复制方式,但如果发现语句无法被正确地复制,就切换到基于行的复制模式。还可以根据需求来设置会话级别的变量binlog_format,控制二进制日志格式。对于基于行的复制模式,很难进行时间点恢复,但这并非不可能
基于行或基于语句:哪种更优?
上面已经讨论了这两种复制模式的优点和缺点,那么在实际应用中哪种方式更优呢?理论上基于行的复制模式整体上更优,并且在实际应用中也适用于大多数场景,但这种方式太新了以至于没有将一些特殊的功能加入到其中来满足数据库管理员的操作需求。因此一些人直到现在还没有开始使用。以下详细地阐述两种方式的优点和缺点,以帮助你决定哪种方式更合适。
1.基于语句的复制模式的优点
当主备的模式不同时,逻辑复制能够在多种情况下工作。例如,在主备上的表的定义不同但数据类型相兼容、列的顺序不同等情况。这样就很容易现在备库上修改schema,然后将其提升为主库,减少停机事件。基于语句的复制方式一般允许更灵活的操作。
基于语句的方式执行复制的过程基本上就是执行SQL语句。这意味着所有在服务器上发生的变更都以一种容易理解的方式运行。这样当出现问题时可以很好地去定位
2.基于语句的复制模式的缺点
很多情况下通过基于语句的模式无法正确复制,几乎每一个安装的备库都会至少碰到一次。事实上对于存储过程,触发器以及其他的一些语句的复制在5.0和5.1的一系列版本中存在大量的Bug。这些语句的复制的方式已经被修改了很多次,以使其更好地工作。简单地说:如果正在使用触发器或者存储过程,就不要使用基于语句的复制模式,除非能够清楚地确定不会碰到复制问题
3.基于行的复制模式的优点
几乎没有基于行的复制模式无法处理的场景。对于所有的SQL构造、触发器、存储过程等都能正确执行。只是当你试图做一些诸如在备库修改表的schema这样的事情时才可能导致复制失败。这种方式同样可能减少锁的使用,因为它并不要求这种强串行化是可重复的。
基于行的复制模式会记录数据变更,因此在二进制日志中记录的都是实际上在主库上发生了变化的数据。你不需要查看一条语句去猜测它到底修改了哪些数据。在某种程度上,该模式能够更加清楚地直到服务器上发生了哪些更改,并且哟一个更好的数据变更记录。另外在一些情况下基于行的二进制日志还会记录发生改变之前的数据,因此这可能有利于某些数据恢复。
在很多情况下,由于无须向基于语句的复制那样需要为查询建立执行计划并执行查询,因此基于行的复制占用更少的CPU.最后在某些情况下,基于行的复制能够帮助更快地找到并解决数据不一致的情况。举个例子,如果是使用基于语句的复制模式,在备库更新一个不存在的记录时不会失败,但在基于行的复制模式下则会报错并停止复制
4.基于行的复制模式的缺点
由于语句并没有在日志里记录,因此无法判断执行了哪些SQL,除了需要知道行的变化外,这在很多情况下也很重要。使用一种完全不同的方式在备库进行数据变更——而不是执行SQL.事实上,执行基于行的变化的过程就像一个黑盒子,你无法知道服务器正在做什么。并且没有很好的文档和解释,因此当出现问题时,可能很难找到问题所在。例如,若备库使用一个效率低下的方式去寻找行记录并更新,你无法观察到这一点。
如果有多层的复制服务器,并且所有的都被配置成基于行的复制模式,当会话级别的变量@@binlog_format被设置成STATEMENT时,锁执行的语句在源服务器上被记录为基于语句的模式,但第一层的备库可能将其记录成行模式,并传递给其他层的备库,也就是说你期望的基于语句的日志在复制拓扑种将会被切换到基于行的模式。基于行的日志无法处理诸如在备库修改表的schema这样的情况,而基于语句的日志可以。
在某些情况下,例如找不到要修改的行时,基于行的复制可能会导致复制停止,而基于语句的复制则不会。这也可以认为是基于行的复制的一个优点。该行为可以通过slave_exec_mode来进行配置。这些缺点正在被慢慢解决。它们在大多数生产环境中依然存在。
上面已经讨论了这两种复制模式的优点和缺点,那么在实际应用中哪种方式更优呢?理论上基于行的复制模式整体上更优,并且在实际应用中也适用于大多数场景,但这种方式太新了以至于没有将一些特殊的功能加入到其中来满足数据库管理员的操作需求。因此一些人直到现在还没有开始使用。以下详细地阐述两种方式的优点和缺点,以帮助你决定哪种方式更合适。
1.基于语句的复制模式的优点
当主备的模式不同时,逻辑复制能够在多种情况下工作。例如,在主备上的表的定义不同但数据类型相兼容、列的顺序不同等情况。这样就很容易现在备库上修改schema,然后将其提升为主库,减少停机事件。基于语句的复制方式一般允许更灵活的操作。
基于语句的方式执行复制的过程基本上就是执行SQL语句。这意味着所有在服务器上发生的变更都以一种容易理解的方式运行。这样当出现问题时可以很好地去定位
2.基于语句的复制模式的缺点
很多情况下通过基于语句的模式无法正确复制,几乎每一个安装的备库都会至少碰到一次。事实上对于存储过程,触发器以及其他的一些语句的复制在5.0和5.1的一系列版本中存在大量的Bug。这些语句的复制的方式已经被修改了很多次,以使其更好地工作。简单地说:如果正在使用触发器或者存储过程,就不要使用基于语句的复制模式,除非能够清楚地确定不会碰到复制问题
3.基于行的复制模式的优点
几乎没有基于行的复制模式无法处理的场景。对于所有的SQL构造、触发器、存储过程等都能正确执行。只是当你试图做一些诸如在备库修改表的schema这样的事情时才可能导致复制失败。这种方式同样可能减少锁的使用,因为它并不要求这种强串行化是可重复的。
基于行的复制模式会记录数据变更,因此在二进制日志中记录的都是实际上在主库上发生了变化的数据。你不需要查看一条语句去猜测它到底修改了哪些数据。在某种程度上,该模式能够更加清楚地直到服务器上发生了哪些更改,并且哟一个更好的数据变更记录。另外在一些情况下基于行的二进制日志还会记录发生改变之前的数据,因此这可能有利于某些数据恢复。
在很多情况下,由于无须向基于语句的复制那样需要为查询建立执行计划并执行查询,因此基于行的复制占用更少的CPU.最后在某些情况下,基于行的复制能够帮助更快地找到并解决数据不一致的情况。举个例子,如果是使用基于语句的复制模式,在备库更新一个不存在的记录时不会失败,但在基于行的复制模式下则会报错并停止复制
4.基于行的复制模式的缺点
由于语句并没有在日志里记录,因此无法判断执行了哪些SQL,除了需要知道行的变化外,这在很多情况下也很重要。使用一种完全不同的方式在备库进行数据变更——而不是执行SQL.事实上,执行基于行的变化的过程就像一个黑盒子,你无法知道服务器正在做什么。并且没有很好的文档和解释,因此当出现问题时,可能很难找到问题所在。例如,若备库使用一个效率低下的方式去寻找行记录并更新,你无法观察到这一点。
如果有多层的复制服务器,并且所有的都被配置成基于行的复制模式,当会话级别的变量@@binlog_format被设置成STATEMENT时,锁执行的语句在源服务器上被记录为基于语句的模式,但第一层的备库可能将其记录成行模式,并传递给其他层的备库,也就是说你期望的基于语句的日志在复制拓扑种将会被切换到基于行的模式。基于行的日志无法处理诸如在备库修改表的schema这样的情况,而基于语句的日志可以。
在某些情况下,例如找不到要修改的行时,基于行的复制可能会导致复制停止,而基于语句的复制则不会。这也可以认为是基于行的复制的一个优点。该行为可以通过slave_exec_mode来进行配置。这些缺点正在被慢慢解决。它们在大多数生产环境中依然存在。
复制文件。
接下来看看复制会使用到的一些文件,除了前面的二进制日志文件和中继日志文件,起始还有其他的文件会被用到。不同版本的MySQL默认情况下可能将这些文件放到不同的目录里,大多取决具体的配置选项。可能在data目录或者包含服务器.pid文件的目录下(对于类UNIX系统可能是/var/run/mysqld).
1.mysql-bin.index
当在服务器上开启二进制日志时,同时会生成一个和二进制日志同名的但以.index作为后缀的文件,该文件用于记录磁盘上的二进制日志文件。这里的"index"并不是指表的索引,而是说这个文件的每一行包含了二进制文件的文件名,你可能认为这个文件时多余的。可以被删除(毕竟MySQL可以在磁盘上找到它需要的文件)。事实上并非如此,MySQL依赖于这个文件,除非在这个文件里有记录,否则MySQL识别不了二进制日志文件
2.mysql-relay-bin-index
这个文件是中继日志的索引文件,和mysql-bin.index的作用类似
3.master.info
这个文件用于保存备库连接到主库所需要的信息,格式为纯文本(每行一个值),不同的MySQL版本,其记录的信息也可能不同。此文件不能删除,否则备库在重启后无法连接到主库。这个文件以文本的方式记录了复制用户的密码,所以要注意此文件的权限控制
4.relay-log.info
这个文件包含了当前备库复制的二进制日志和中继日志坐标(例如,备库复制在主库上的位置),同样也不要删除这个文件,否则在备库重启后将无法获知从哪个位置开始复制,可能会导致重放已经执行过的语句
使用这些文件来记录MySQL复制和日志状态是一种非常粗糙的方式。更不幸得是,它们不是同步写的。如果服务器断电并且文件数据没有被刷新到磁盘,在重启服务器后,文件中记录的数据可能是错误的。正如之前提到的,这些问题在MySQL5.5里做了改进。以.index作为后缀的文件也与设置expire_logs_days存在交互,该参数定义了MySQL清理过期日志的方式,如果文件mysql-bin.index在磁盘上不存在,在某些MySQL版本自动清理就会不起作用,甚至执行PURGE MASTER LOGS语句也没有用。这个问题的解决方法通常是使用MySQL服务器管理二进制日志,这样就不会产生误解(这意味着不应该使用rm来自己清理日志)。最好能显式地执行一些日志清理策略,比如设置expire_logs_days参数或者其他方式,否则MySQL的二进制日志i可能会将磁盘撑满。当做这些事情时,还需要考虑到备份策略。
接下来看看复制会使用到的一些文件,除了前面的二进制日志文件和中继日志文件,起始还有其他的文件会被用到。不同版本的MySQL默认情况下可能将这些文件放到不同的目录里,大多取决具体的配置选项。可能在data目录或者包含服务器.pid文件的目录下(对于类UNIX系统可能是/var/run/mysqld).
1.mysql-bin.index
当在服务器上开启二进制日志时,同时会生成一个和二进制日志同名的但以.index作为后缀的文件,该文件用于记录磁盘上的二进制日志文件。这里的"index"并不是指表的索引,而是说这个文件的每一行包含了二进制文件的文件名,你可能认为这个文件时多余的。可以被删除(毕竟MySQL可以在磁盘上找到它需要的文件)。事实上并非如此,MySQL依赖于这个文件,除非在这个文件里有记录,否则MySQL识别不了二进制日志文件
2.mysql-relay-bin-index
这个文件是中继日志的索引文件,和mysql-bin.index的作用类似
3.master.info
这个文件用于保存备库连接到主库所需要的信息,格式为纯文本(每行一个值),不同的MySQL版本,其记录的信息也可能不同。此文件不能删除,否则备库在重启后无法连接到主库。这个文件以文本的方式记录了复制用户的密码,所以要注意此文件的权限控制
4.relay-log.info
这个文件包含了当前备库复制的二进制日志和中继日志坐标(例如,备库复制在主库上的位置),同样也不要删除这个文件,否则在备库重启后将无法获知从哪个位置开始复制,可能会导致重放已经执行过的语句
使用这些文件来记录MySQL复制和日志状态是一种非常粗糙的方式。更不幸得是,它们不是同步写的。如果服务器断电并且文件数据没有被刷新到磁盘,在重启服务器后,文件中记录的数据可能是错误的。正如之前提到的,这些问题在MySQL5.5里做了改进。以.index作为后缀的文件也与设置expire_logs_days存在交互,该参数定义了MySQL清理过期日志的方式,如果文件mysql-bin.index在磁盘上不存在,在某些MySQL版本自动清理就会不起作用,甚至执行PURGE MASTER LOGS语句也没有用。这个问题的解决方法通常是使用MySQL服务器管理二进制日志,这样就不会产生误解(这意味着不应该使用rm来自己清理日志)。最好能显式地执行一些日志清理策略,比如设置expire_logs_days参数或者其他方式,否则MySQL的二进制日志i可能会将磁盘撑满。当做这些事情时,还需要考虑到备份策略。
发送复制事件到其他备库。
log_slave_updates选项可以让备库变成其他服务器的主库。在设置该选项后,MySQL会将其执行过的事件记录到它自己的二进制日志中。这样它的备库就可以从其日志中检索并执行事件。如图所示。在这种场景下,主库将数据更新事件写入二进制日志,第一个备库提取并执行这个事件。这时候一个事件的生命周期应该结束了,但由于设置了log_slave_updates,备库会将这个时间写到它自己的二进制日志中。这样第二个备库就可以将事件提取到它的中继日志中并执行。这意味着作为源服务器的主库可以将其数据变化传递给没有与其直接相连的备库上。默认情况下这个选项是被打开的,这样在连接到备库时就不需要重启服务器。
当第一个备库从主库获得鞥多事件写入到其二进制日志中时,这个事件在备库二进制日志中的位置与其在主库二进制日志中的位置几乎肯定是不相同的,可能在不同的日志文件或文件内不同鞥多位置。这意味着你不能嘉定所有拥有统一逻辑复制点的服务器拥有相同的日志坐标。这种情况会使某些任务更加复杂,例如,修改一个备库的主库或将备库提升为主库。
除非你已经注意到要给每个服务器分配一个唯一的唯一的服务器ID,否则按照这种方式配置备库会导致一些奇怪的错误,甚至还会导致复制停止。一个更常见的问题是:为什么要指定服务器ID,难道MySQL在不知道复制命令来源的情况下不能执行吗?为什么MySQL要在意服务器ID是全局唯一的。问题的答案在于MySQL在复制过程中如何防止无限循环。当复制SQL线程读中继日志时,会丢弃事件中记录的服务器ID和该服务器本身ID相同的事件,从而打破了复制过程中的无限循环。在某些复制拓扑结构下打破无限循环非常重要,例如主-主复制结构(语句在无限循环中来回传递也是多服务器环形复制拓扑结构中比较有意思的话题之一)。
如果在设置复制的时候碰到问题,服务器ID应该是需要检查的因素之一。当然只检查@@server_id是不够的,它有一个默认值,除非在my.cnf文件或通过SET命令明确指定它的值,复制才会工作。如果使用SET命令,确保同时也更新了配置文件,否则SET命令的设定可能在服务器重启后丢失。
log_slave_updates选项可以让备库变成其他服务器的主库。在设置该选项后,MySQL会将其执行过的事件记录到它自己的二进制日志中。这样它的备库就可以从其日志中检索并执行事件。如图所示。在这种场景下,主库将数据更新事件写入二进制日志,第一个备库提取并执行这个事件。这时候一个事件的生命周期应该结束了,但由于设置了log_slave_updates,备库会将这个时间写到它自己的二进制日志中。这样第二个备库就可以将事件提取到它的中继日志中并执行。这意味着作为源服务器的主库可以将其数据变化传递给没有与其直接相连的备库上。默认情况下这个选项是被打开的,这样在连接到备库时就不需要重启服务器。
当第一个备库从主库获得鞥多事件写入到其二进制日志中时,这个事件在备库二进制日志中的位置与其在主库二进制日志中的位置几乎肯定是不相同的,可能在不同的日志文件或文件内不同鞥多位置。这意味着你不能嘉定所有拥有统一逻辑复制点的服务器拥有相同的日志坐标。这种情况会使某些任务更加复杂,例如,修改一个备库的主库或将备库提升为主库。
除非你已经注意到要给每个服务器分配一个唯一的唯一的服务器ID,否则按照这种方式配置备库会导致一些奇怪的错误,甚至还会导致复制停止。一个更常见的问题是:为什么要指定服务器ID,难道MySQL在不知道复制命令来源的情况下不能执行吗?为什么MySQL要在意服务器ID是全局唯一的。问题的答案在于MySQL在复制过程中如何防止无限循环。当复制SQL线程读中继日志时,会丢弃事件中记录的服务器ID和该服务器本身ID相同的事件,从而打破了复制过程中的无限循环。在某些复制拓扑结构下打破无限循环非常重要,例如主-主复制结构(语句在无限循环中来回传递也是多服务器环形复制拓扑结构中比较有意思的话题之一)。
如果在设置复制的时候碰到问题,服务器ID应该是需要检查的因素之一。当然只检查@@server_id是不够的,它有一个默认值,除非在my.cnf文件或通过SET命令明确指定它的值,复制才会工作。如果使用SET命令,确保同时也更新了配置文件,否则SET命令的设定可能在服务器重启后丢失。
复制拓扑.
可以在任意个主库和备库之间建立复制,只有一个限制:每一个备库只能有一个主库。有很多复杂的拓扑结构,但即使是最简单的也可能会非常灵活。一种头普可以有多种用途。关于使用复制的不同方式可以很轻易地写一本书。前面已经讨论了如何为主库设置一个备库,接下来讨论其他比较普遍地拓扑结构以及它们的优缺点.记住下面的基本原则:
1.一个MySQL备库实例只能有一个主库
2.每个备库必须有一个唯一的服务器ID
3.一个主库可以有多个备库(或者相应的,一个备库可以有多个兄弟备库)
4.如果打开了log_slave_updates选项,一个备库可以把其主库上的数据变化传播到其他备库
可以在任意个主库和备库之间建立复制,只有一个限制:每一个备库只能有一个主库。有很多复杂的拓扑结构,但即使是最简单的也可能会非常灵活。一种头普可以有多种用途。关于使用复制的不同方式可以很轻易地写一本书。前面已经讨论了如何为主库设置一个备库,接下来讨论其他比较普遍地拓扑结构以及它们的优缺点.记住下面的基本原则:
1.一个MySQL备库实例只能有一个主库
2.每个备库必须有一个唯一的服务器ID
3.一个主库可以有多个备库(或者相应的,一个备库可以有多个兄弟备库)
4.如果打开了log_slave_updates选项,一个备库可以把其主库上的数据变化传播到其他备库
一主库多备库。
除了前面已经提过的两台服务器的主备结构外,这是最简单的拓扑结构。事实上一主多备的结构和基本配置差不多简单,因为备库之间根本没有交互(从技术上讲这并非正确的。但如果有重复的服务器ID,它们将现如竞争,并反复将对方从主库上踢出),它们仅仅是连接到同一个主库上,如图显示了这种结构。
尽管这是非常简单的拓扑结构,但它非常灵活,能满足多种需求。下面是它的一些用途:
1.为不同的角色使用不同的备库(例如添加不同的索引或使用不同的存储引擎)
2.把一台备库当作待用的主库,除了复制没有其他数据传输
3.延迟一个或多个备库,以备灾难恢复
4.使用其中一个备库,作为备份、培训、开发或者测试使用服务器
这种结构流行的原因是它避免了很多其他拓扑结构的复杂性。例如:可以方便地比较不同备库重放的事件在主库二进制日志中的位置。换句话说,如果在同一个逻辑点停止所有备库的复制,它们正在读取的是主库上同一个日志文件的相同物理位置。这是个很好的特性,可以减轻管理员许多工作,例如把备库提升为主库。这种特性只存在于兄弟备库之间。在没有直接的主备或兄弟关系的服务器上去比较日志文件的位置要复杂很多。
除了前面已经提过的两台服务器的主备结构外,这是最简单的拓扑结构。事实上一主多备的结构和基本配置差不多简单,因为备库之间根本没有交互(从技术上讲这并非正确的。但如果有重复的服务器ID,它们将现如竞争,并反复将对方从主库上踢出),它们仅仅是连接到同一个主库上,如图显示了这种结构。
尽管这是非常简单的拓扑结构,但它非常灵活,能满足多种需求。下面是它的一些用途:
1.为不同的角色使用不同的备库(例如添加不同的索引或使用不同的存储引擎)
2.把一台备库当作待用的主库,除了复制没有其他数据传输
3.延迟一个或多个备库,以备灾难恢复
4.使用其中一个备库,作为备份、培训、开发或者测试使用服务器
这种结构流行的原因是它避免了很多其他拓扑结构的复杂性。例如:可以方便地比较不同备库重放的事件在主库二进制日志中的位置。换句话说,如果在同一个逻辑点停止所有备库的复制,它们正在读取的是主库上同一个日志文件的相同物理位置。这是个很好的特性,可以减轻管理员许多工作,例如把备库提升为主库。这种特性只存在于兄弟备库之间。在没有直接的主备或兄弟关系的服务器上去比较日志文件的位置要复杂很多。
主动-主动模式下的主-主复制。
主-主复制(也叫双主复制或双向复制)包含两台服务器,每一个都被配置成对方的主库和备库,换句话说,它们是一对主库,如图显示了该结构。主动-主动模式下主-主复制有一些应用场景,但通常用于特殊的目的。一个可能的应用场景是两个处于不同地理位置的办公室,并且都需要一份可写的数据拷贝。
这种配置最大的问题是如何解决冲突,两个可写的互主服务器导致的问题非常多。这通常发生在两台服务器同时修改一行记录,或同时在两台服务器上向一个包含AUTO_INCREMENT列的表里插入数据(事实上这些问题经常一周发生三次,并且发现需要好几个月才能解决这些问题)
MySQL5.0增加了一些特性,使得这种配置稍微安全了点,就是设置auto_increment_increment和auto_increment_offset。通过这两个选项可以让MySQL自动为INSERT语句选择不互相冲突的值。然而允许向两台主库上写入仍然很危险。在两台机器上根据不同的顺序更新,可能会导致数据不同步。例如,一个只有一列的表,只有一行值为1的记录假设同时i执行下面两条语句:
1.在第一台主库上:
mysql>UPDATE tbl SET col=col +1
2.在第二台主库上:
mysql>UPDATE tbl SET col=col *2;
那么结果呢?一台服务器上值为4,另一台值为3,并且没有报告任何复制错误。数据不通过不还仅仅是开始。当正常的复制发生错误停止了,但应用在同时向两台服务器写入数据,这时候会发生什么呢?你不能简单地把数据从一台服务器复制到另外一台,因为这两台机器上需要复制的数据都可能发生了变化。解决这个问题将会非常困难。
如果足够仔细地配置这种架构,例如很好地划分数据和权限,并且你很清楚自己在做什么,可以避免一些问题(一些,但不是全部),然而这通常很难做好,并且有更好的办法来实现你所需要的。
总地来说,允许向两个服务器上写入带来的麻烦远远大于其带来的好处
主-主复制(也叫双主复制或双向复制)包含两台服务器,每一个都被配置成对方的主库和备库,换句话说,它们是一对主库,如图显示了该结构。主动-主动模式下主-主复制有一些应用场景,但通常用于特殊的目的。一个可能的应用场景是两个处于不同地理位置的办公室,并且都需要一份可写的数据拷贝。
这种配置最大的问题是如何解决冲突,两个可写的互主服务器导致的问题非常多。这通常发生在两台服务器同时修改一行记录,或同时在两台服务器上向一个包含AUTO_INCREMENT列的表里插入数据(事实上这些问题经常一周发生三次,并且发现需要好几个月才能解决这些问题)
MySQL5.0增加了一些特性,使得这种配置稍微安全了点,就是设置auto_increment_increment和auto_increment_offset。通过这两个选项可以让MySQL自动为INSERT语句选择不互相冲突的值。然而允许向两台主库上写入仍然很危险。在两台机器上根据不同的顺序更新,可能会导致数据不同步。例如,一个只有一列的表,只有一行值为1的记录假设同时i执行下面两条语句:
1.在第一台主库上:
mysql>UPDATE tbl SET col=col +1
2.在第二台主库上:
mysql>UPDATE tbl SET col=col *2;
那么结果呢?一台服务器上值为4,另一台值为3,并且没有报告任何复制错误。数据不通过不还仅仅是开始。当正常的复制发生错误停止了,但应用在同时向两台服务器写入数据,这时候会发生什么呢?你不能简单地把数据从一台服务器复制到另外一台,因为这两台机器上需要复制的数据都可能发生了变化。解决这个问题将会非常困难。
如果足够仔细地配置这种架构,例如很好地划分数据和权限,并且你很清楚自己在做什么,可以避免一些问题(一些,但不是全部),然而这通常很难做好,并且有更好的办法来实现你所需要的。
总地来说,允许向两个服务器上写入带来的麻烦远远大于其带来的好处
MySQL不支持多主库复制。
多主库复制(multisource replication)特指一个备库有多个主库。不管之前你知道什么,但MySQL(和其他数据库产品不一样)现在不支持如图所示的结构
多主库复制(multisource replication)特指一个备库有多个主库。不管之前你知道什么,但MySQL(和其他数据库产品不一样)现在不支持如图所示的结构
主动-被动模式下的主-主复制。
这是前面描述的主-主结构的变体,它能够避免上面讨论的问题。这也是构建容错性和高可用性系统的非常强大的方式,主要区别在于其中的一台服务器是只读的被动服务器,如图所示。这种方式使得反复切换主动和被动服务器非常方便,因为服务器的配置是对称的。这使得故障转移和故障恢复很容易。它也可以让你在不关闭服务器的情况下执行维护、优化表、升级操作系统(或者应用程序、硬件等)或其他任务。
例如,执行ALTER TABLE操作可能会锁住整个表,阻塞对表的读和写,这可能会花费很长事件并导致服务中断。然而在主-主配置下,可以先停止主动服务器上的备库复制线程(这样就不会在被动服务器上执行任何更新),然后在被动服务器上执行ALTER操作,交换角色,最后在先前的主动服务器上启动复制线程。这个服务器将会读取中继日志并执行相同的ALTER语句。这可能花费很长时间,但不要紧,因为该服务器没有为任何活跃查询提供服务。
主动-被动模式的主-主结构能够帮助会比许多MySQL的问题和限制,此外还有一些工具可以完成这种类型的操作。
让我们看看如何配置主-主服务器对,在两台服务器上执行如下设置后,会使其拥有对称的设置:
1.确保两台服务器上有相同的数据
2.启用二进制日志,选择唯一的服务器ID,并创建复制账号。
3.启用备库更新的日志记录,后面将会看到,这是故障转移和故障恢复的关键
4.把被动服务器配置成只读,防止可能与主动服务器上的更新产生冲突,这一点是可选的。
5.启动每个服务器的MySQL实例
6.将每个主库设置为对方的备库,使用新创建的二进制日志开始工作
让我们看看主动服务器上更新时会发生什么事情。更新被记录到二进制日志中,通过复制传递给被动服务器的中继日志中。被动服务器执行查询并将其记录到自己的二进制日志中(因为开启了log_slave_updates选项)。由于时间的服务器ID与主动服务器的相同,因此主动服务器将忽略这些时间。
设置主动-被动的主-主拓扑结构在某种意义上类似于创建了一个热备份,但是可以使用这个"备份"来提高性能,例如,用它来执行读操作、备份、"离线"维护以及升级等。真正的热备份做不了这些事情,然而,你不会获得比单台服务器更好的写性能。它是一种非常常见的并且重要的拓扑结构
这是前面描述的主-主结构的变体,它能够避免上面讨论的问题。这也是构建容错性和高可用性系统的非常强大的方式,主要区别在于其中的一台服务器是只读的被动服务器,如图所示。这种方式使得反复切换主动和被动服务器非常方便,因为服务器的配置是对称的。这使得故障转移和故障恢复很容易。它也可以让你在不关闭服务器的情况下执行维护、优化表、升级操作系统(或者应用程序、硬件等)或其他任务。
例如,执行ALTER TABLE操作可能会锁住整个表,阻塞对表的读和写,这可能会花费很长事件并导致服务中断。然而在主-主配置下,可以先停止主动服务器上的备库复制线程(这样就不会在被动服务器上执行任何更新),然后在被动服务器上执行ALTER操作,交换角色,最后在先前的主动服务器上启动复制线程。这个服务器将会读取中继日志并执行相同的ALTER语句。这可能花费很长时间,但不要紧,因为该服务器没有为任何活跃查询提供服务。
主动-被动模式的主-主结构能够帮助会比许多MySQL的问题和限制,此外还有一些工具可以完成这种类型的操作。
让我们看看如何配置主-主服务器对,在两台服务器上执行如下设置后,会使其拥有对称的设置:
1.确保两台服务器上有相同的数据
2.启用二进制日志,选择唯一的服务器ID,并创建复制账号。
3.启用备库更新的日志记录,后面将会看到,这是故障转移和故障恢复的关键
4.把被动服务器配置成只读,防止可能与主动服务器上的更新产生冲突,这一点是可选的。
5.启动每个服务器的MySQL实例
6.将每个主库设置为对方的备库,使用新创建的二进制日志开始工作
让我们看看主动服务器上更新时会发生什么事情。更新被记录到二进制日志中,通过复制传递给被动服务器的中继日志中。被动服务器执行查询并将其记录到自己的二进制日志中(因为开启了log_slave_updates选项)。由于时间的服务器ID与主动服务器的相同,因此主动服务器将忽略这些时间。
设置主动-被动的主-主拓扑结构在某种意义上类似于创建了一个热备份,但是可以使用这个"备份"来提高性能,例如,用它来执行读操作、备份、"离线"维护以及升级等。真正的热备份做不了这些事情,然而,你不会获得比单台服务器更好的写性能。它是一种非常常见的并且重要的拓扑结构
拥有备库的主-主结构。
另外一种相关的配置是为每个主库增加一个备库,如图所示。这种配置的优点是增加了冗余,对于不同地理位置的复制拓扑,能够消除站点单点失效的问题。你也可以像平常一样,将读查询分配到备库上。如果在本地为了故障转移使用主-主结构,这种配置同样有用。当主库失效时,用备库来代替主库还是可行的,虽然这有点复杂。同样也可以把备库指向一个不同的主库,但需要考虑增加的复杂度
另外一种相关的配置是为每个主库增加一个备库,如图所示。这种配置的优点是增加了冗余,对于不同地理位置的复制拓扑,能够消除站点单点失效的问题。你也可以像平常一样,将读查询分配到备库上。如果在本地为了故障转移使用主-主结构,这种配置同样有用。当主库失效时,用备库来代替主库还是可行的,虽然这有点复杂。同样也可以把备库指向一个不同的主库,但需要考虑增加的复杂度
环形复制。
如图所示,双主结构实际上是环形结构的一种特例(也许应该说,是更明智的特例)。环形结构可以有三个或更多的主库。每个服务器都是在它之前的服务器的备库,是它在之后的服务器的主库。这种结构也称为环形复制(circular replication).环形结构没有双主结构的一些优点,例如对称配置和简单的故障转移,并且完全依赖于环上的每一个可用节点,这大大增加了整个系统失效的几率。如果从环中移除一个节点,这个节点发起的时间就会陷入无限循环:它们将永远绕着服务器链循环。总地来说,环形结构非常脆弱,应该尽量避免。可以通过为每个节点增加备库的方式来减少环形复制的风险,如图所示,但这仅仅防范了服务器失效的危险,断电或者其他一些影响到网络连接的问题都可能破坏整个环
如图所示,双主结构实际上是环形结构的一种特例(也许应该说,是更明智的特例)。环形结构可以有三个或更多的主库。每个服务器都是在它之前的服务器的备库,是它在之后的服务器的主库。这种结构也称为环形复制(circular replication).环形结构没有双主结构的一些优点,例如对称配置和简单的故障转移,并且完全依赖于环上的每一个可用节点,这大大增加了整个系统失效的几率。如果从环中移除一个节点,这个节点发起的时间就会陷入无限循环:它们将永远绕着服务器链循环。总地来说,环形结构非常脆弱,应该尽量避免。可以通过为每个节点增加备库的方式来减少环形复制的风险,如图所示,但这仅仅防范了服务器失效的危险,断电或者其他一些影响到网络连接的问题都可能破坏整个环
主库、分发主库以及备库。
我们之前提到当备库足够多时,会对主库造成很大的负载。每个备库会在主库上创建一个线程,并执行binlog dump命令。该命令会读取二进制日志文件中的数据并将其发送给备库。每个备库都会重复这样的工作,它们不会共享binlog dump的资源。如果有很多备库,并且有大的事件时,例如一次很大的LOAD DATA INFILE操作,主库上的负载会显著上升,甚至可能由于备库同时请求同样的事件而耗尽内存并崩溃。另一方面,如果备库请求的数据不在文件系统的缓存中,可能会导致大量的磁盘检索,这同样会影响主库的性能并增加锁的竞争。因此,如果需要多个备库,一个好办法时从主库移除负载并使用分发主库。分发主库事实上也是一个备库,它的唯一目的就是提取和提供主库的二进制日志。多个备库连接到分发主库,这使原来的主库摆脱了负担。为了避免在分发主库上做实际的查询,可以将它的表修改为blackhole存储疫情,如图所示。很难说当备库数据达到多少时需要一个分发主库。按照通用准则,如果主库接近满负载,不应该为其建立10个以上的备库。如果有少量的写操作,或者只复制其中一部分表,主库就可以提供更多的复制。另外,也不一定只适用一个分发主库。如果需要的话,可以使用多个分发主向大量的备库进行复制,或者使用金字塔状的分发主库。在某些情况下,可以通过设置slave_compressed_protocol来节约一些主库带宽。这对跨数据中心复制很有好处。
还可以通过分发主库实现其他目的,例如,对二进制日志事件执行过滤和重写规则。这比在每个备库上重复进行日志记录、重写和过滤要高效得多。如果在分发主库上使用blackhole表,可以支持更多得备库。虽然会在分发主库执行查询,但其代价非常小,因为blackhole表中没有任何数据。blackhole表得缺点是其存在Bug,例如在某些情况下会忘记将自增ID写入到二进制日志中。所以要小心使用blackhole表。
一个比较常见得问题是如何确保分发服务器上得每个表都是blackhole存储引擎。如果有人在主库创建了一个表并指定了不同的存储引擎呢?确实,不管什么时候,在备库上使用不同的存储引擎总会导致同样的问题。常见的解决方案是设置服务器的storage_engine选项:
```c
storage_engine=blackhole
```
这只会影响哪些没有指定存储引擎的CREATE TABLE的语句。如果有一个无法控制的应用,这种拓扑结构可能会非常脆弱。可以通过skip_innodb选项禁止InnoDB,将表退化为MyISAM。但你无法禁止MyISAM或者Memory引擎。使用分发主库另外一个主要的缺点是无法使用一个备库来代替主库,因为由于分发主库的存在,导致各个备库与原始主库的二进制日志坐标已经不相同。
我们之前提到当备库足够多时,会对主库造成很大的负载。每个备库会在主库上创建一个线程,并执行binlog dump命令。该命令会读取二进制日志文件中的数据并将其发送给备库。每个备库都会重复这样的工作,它们不会共享binlog dump的资源。如果有很多备库,并且有大的事件时,例如一次很大的LOAD DATA INFILE操作,主库上的负载会显著上升,甚至可能由于备库同时请求同样的事件而耗尽内存并崩溃。另一方面,如果备库请求的数据不在文件系统的缓存中,可能会导致大量的磁盘检索,这同样会影响主库的性能并增加锁的竞争。因此,如果需要多个备库,一个好办法时从主库移除负载并使用分发主库。分发主库事实上也是一个备库,它的唯一目的就是提取和提供主库的二进制日志。多个备库连接到分发主库,这使原来的主库摆脱了负担。为了避免在分发主库上做实际的查询,可以将它的表修改为blackhole存储疫情,如图所示。很难说当备库数据达到多少时需要一个分发主库。按照通用准则,如果主库接近满负载,不应该为其建立10个以上的备库。如果有少量的写操作,或者只复制其中一部分表,主库就可以提供更多的复制。另外,也不一定只适用一个分发主库。如果需要的话,可以使用多个分发主向大量的备库进行复制,或者使用金字塔状的分发主库。在某些情况下,可以通过设置slave_compressed_protocol来节约一些主库带宽。这对跨数据中心复制很有好处。
还可以通过分发主库实现其他目的,例如,对二进制日志事件执行过滤和重写规则。这比在每个备库上重复进行日志记录、重写和过滤要高效得多。如果在分发主库上使用blackhole表,可以支持更多得备库。虽然会在分发主库执行查询,但其代价非常小,因为blackhole表中没有任何数据。blackhole表得缺点是其存在Bug,例如在某些情况下会忘记将自增ID写入到二进制日志中。所以要小心使用blackhole表。
一个比较常见得问题是如何确保分发服务器上得每个表都是blackhole存储引擎。如果有人在主库创建了一个表并指定了不同的存储引擎呢?确实,不管什么时候,在备库上使用不同的存储引擎总会导致同样的问题。常见的解决方案是设置服务器的storage_engine选项:
```c
storage_engine=blackhole
```
这只会影响哪些没有指定存储引擎的CREATE TABLE的语句。如果有一个无法控制的应用,这种拓扑结构可能会非常脆弱。可以通过skip_innodb选项禁止InnoDB,将表退化为MyISAM。但你无法禁止MyISAM或者Memory引擎。使用分发主库另外一个主要的缺点是无法使用一个备库来代替主库,因为由于分发主库的存在,导致各个备库与原始主库的二进制日志坐标已经不相同。
树或金字塔形。
如果正在将主库复制到大量的备库中,不管是把数据分发到不同的地方,还是提供更高的读性能,使用金字塔结构都能够更好地管理,如图所示。
这种设计地好处是减轻了主库地负担,就像前面提到地分发主库一样。它的缺点是中间层出现的任何错误都会影响到多个服务器。如果每个备库和主库直接相连就不会存在这样的问题。同样,中间层次越多,处理故障会更困难、更复杂。
如果正在将主库复制到大量的备库中,不管是把数据分发到不同的地方,还是提供更高的读性能,使用金字塔结构都能够更好地管理,如图所示。
这种设计地好处是减轻了主库地负担,就像前面提到地分发主库一样。它的缺点是中间层出现的任何错误都会影响到多个服务器。如果每个备库和主库直接相连就不会存在这样的问题。同样,中间层次越多,处理故障会更困难、更复杂。
定制的复制方案。
MySQL的复制非常灵活,可以根据需要定制解决方案。典型的定制方案包括组合过滤、分发和向不同的存储引擎复制。也可以使用"黑客手段",例如,从一个使用blackhole存储引擎的服务器上复制或复制到这样的服务器上。可以根据需要任意设计。这其中最大的限制是合理地监控和管理,以及所拥有资源额约束(网络带宽、CPU能力等)
MySQL的复制非常灵活,可以根据需要定制解决方案。典型的定制方案包括组合过滤、分发和向不同的存储引擎复制。也可以使用"黑客手段",例如,从一个使用blackhole存储引擎的服务器上复制或复制到这样的服务器上。可以根据需要任意设计。这其中最大的限制是合理地监控和管理,以及所拥有资源额约束(网络带宽、CPU能力等)
选择性复制。
为了利用访问局部性原理(locality of reference),并将需要读的工作集驻留在内存中,可以复制少量数据到备库中。如果每个备库只拥有主库的一部分数据,并且将读分配备库,就可以更好地利用备库的内存。并且每个备库也只有主库一部分的写入负载,这样主库的能力更强并能保证备库延迟。
这个方案优点类似水平数据划分,但它的优势在于主库包含了所有的数据集,这意味着无须为了一条写入查询去访问多个服务器。如果读操作无法在备库上找到数据,还可以通过主库来查询。即使不能从备库上读取所有数据,也可以移除大量的主库读负担。
最简单的方法是在主库上将数据划分到不同的数据库里,然后将每个数据库复制到不同的备库上。例如,若需要将公司的每一个部门的数据复制到不同的备库,可以创建名为sales、marketing、procurement等的数据库,每个备库通过选项replicate_wild_do_table选项来限制给定数据库的数据。下面是sales数据库的配置:
```c
replicate_wild_do_table=sales.%
```
也可以通过一台分发主库进行分发。举个例子,如果想通过一个很慢或者非常昂贵的网络,从一台负载很高的数据库上复制一部分数据,就可以使用一个包含blackhole表和过滤规则的本地分发主库,分发主库可以通过复制过滤不需要的日志。这可以避免在主库上进行不安全的日志选项设定,并且无须传输所有的数据到远程备库
为了利用访问局部性原理(locality of reference),并将需要读的工作集驻留在内存中,可以复制少量数据到备库中。如果每个备库只拥有主库的一部分数据,并且将读分配备库,就可以更好地利用备库的内存。并且每个备库也只有主库一部分的写入负载,这样主库的能力更强并能保证备库延迟。
这个方案优点类似水平数据划分,但它的优势在于主库包含了所有的数据集,这意味着无须为了一条写入查询去访问多个服务器。如果读操作无法在备库上找到数据,还可以通过主库来查询。即使不能从备库上读取所有数据,也可以移除大量的主库读负担。
最简单的方法是在主库上将数据划分到不同的数据库里,然后将每个数据库复制到不同的备库上。例如,若需要将公司的每一个部门的数据复制到不同的备库,可以创建名为sales、marketing、procurement等的数据库,每个备库通过选项replicate_wild_do_table选项来限制给定数据库的数据。下面是sales数据库的配置:
```c
replicate_wild_do_table=sales.%
```
也可以通过一台分发主库进行分发。举个例子,如果想通过一个很慢或者非常昂贵的网络,从一台负载很高的数据库上复制一部分数据,就可以使用一个包含blackhole表和过滤规则的本地分发主库,分发主库可以通过复制过滤不需要的日志。这可以避免在主库上进行不安全的日志选项设定,并且无须传输所有的数据到远程备库
分离功能。
许多应用都混合了在线事务处理(OLTP)和在线数据分析(OLAP)的查询。OLTP查询比较短并且是事务型的。OLAP查询则通常很大,也很慢,并且不要求绝对最新的数据。这两种查询给服务器带来的负担完全不同,因此它们需要不同的配置,甚至可能使用不同的存储引擎或者硬件。
一个常见的办法是将OLTP服务器的数据复制到专门为OLAP工作负载准备的备库上。这些备库可以有不同的硬件、配置、索引或者不同的存储引擎。如果决定在备库上执行OLAP查询,就可能需要忍受更大的复制延迟或者降低备库的服务质量。这意味着在一个非专用的备库上执行一些任务时,可能会导致不可接受的性能,例如执行一条长时间运行的查询。无须做一些特殊的配置,除了需要选择忽略主库上的一些数据,前提是能获得明显的提升。即使通过复制过滤器过滤掉一小部分的数据也会减少IO和缓存活动。
许多应用都混合了在线事务处理(OLTP)和在线数据分析(OLAP)的查询。OLTP查询比较短并且是事务型的。OLAP查询则通常很大,也很慢,并且不要求绝对最新的数据。这两种查询给服务器带来的负担完全不同,因此它们需要不同的配置,甚至可能使用不同的存储引擎或者硬件。
一个常见的办法是将OLTP服务器的数据复制到专门为OLAP工作负载准备的备库上。这些备库可以有不同的硬件、配置、索引或者不同的存储引擎。如果决定在备库上执行OLAP查询,就可能需要忍受更大的复制延迟或者降低备库的服务质量。这意味着在一个非专用的备库上执行一些任务时,可能会导致不可接受的性能,例如执行一条长时间运行的查询。无须做一些特殊的配置,除了需要选择忽略主库上的一些数据,前提是能获得明显的提升。即使通过复制过滤器过滤掉一小部分的数据也会减少IO和缓存活动。
数据归档。
可以在备库上实现数据归档,也就是说可以在备库上保留主库上删除过的数据,在主库上通过delete语句删除数据是确保delete语句不传递到备库就可以实现。有两种通常的办法:一种是在主库上选择性地禁止二进制日志,另一种是在备库上使用replicate_ignore_db规则(是的,两种方法都很危险)
第一种方法需要先将SQL_LOG_BIN设置为0,然后再进行数据清理。这种方法的好处是不需要在备库进行任何配置,由于SQL语句根本没有记录到二进制日志中,效率会稍微有所提升。最大缺点也正因为是没有将在主库的修改记录下来,因此无法使用二进制日志来进行审计或者做按时间点的数据恢复。另外还需要SUPER权限。
第二种办法是在清理数据之前对主库上特定的数据库使用USE语句。例如,可以创建一个名为purge的数据库,然后在备库的my.cnf文件里设置replicate_ignore_db=purge并重启服务器。备库将会忽略使用了use语句指定的数据库。这种方法没有第一种方法的缺点,但有另一个小小的缺点:备库需要去读取它不需要的事件。另外,也可能有人在purge数据库上执行非清理查询,从而导致备库无法重放该事件.
Percona Toolkit中的pt-arciver支持以上两种方式
可以在备库上实现数据归档,也就是说可以在备库上保留主库上删除过的数据,在主库上通过delete语句删除数据是确保delete语句不传递到备库就可以实现。有两种通常的办法:一种是在主库上选择性地禁止二进制日志,另一种是在备库上使用replicate_ignore_db规则(是的,两种方法都很危险)
第一种方法需要先将SQL_LOG_BIN设置为0,然后再进行数据清理。这种方法的好处是不需要在备库进行任何配置,由于SQL语句根本没有记录到二进制日志中,效率会稍微有所提升。最大缺点也正因为是没有将在主库的修改记录下来,因此无法使用二进制日志来进行审计或者做按时间点的数据恢复。另外还需要SUPER权限。
第二种办法是在清理数据之前对主库上特定的数据库使用USE语句。例如,可以创建一个名为purge的数据库,然后在备库的my.cnf文件里设置replicate_ignore_db=purge并重启服务器。备库将会忽略使用了use语句指定的数据库。这种方法没有第一种方法的缺点,但有另一个小小的缺点:备库需要去读取它不需要的事件。另外,也可能有人在purge数据库上执行非清理查询,从而导致备库无法重放该事件.
Percona Toolkit中的pt-arciver支持以上两种方式
将备库用作全文检索。
许多应用要求合并事务和全文检索。然而仅有MyISAM支持全文检索,但是MyISAM不支持事务(在MySQL5.6有一个实验室预览版本实现了InnoDB的全文检索,但尚未GA).一个普遍的做法是配置一台备库,将某些表设置为MyISAM存储引擎,然后创建全文索引并执行全文检索查询。这避免了在主库上同时使用事务型和非事务型存储引擎所带来的复制问题,减轻了主库维护全文索引的负担。
许多应用要求合并事务和全文检索。然而仅有MyISAM支持全文检索,但是MyISAM不支持事务(在MySQL5.6有一个实验室预览版本实现了InnoDB的全文检索,但尚未GA).一个普遍的做法是配置一台备库,将某些表设置为MyISAM存储引擎,然后创建全文索引并执行全文检索查询。这避免了在主库上同时使用事务型和非事务型存储引擎所带来的复制问题,减轻了主库维护全文索引的负担。
只读备库。
x许多机构选择将备库设置为只读,以防止在备库进行的无意识修改导致复制中断。可以通过设置read_only选项来实现。它会禁止大部分写操作,除了复制线程和拥有超级权限的用户以及临时表操作。只要不给也不应该普通用户超级权限,这应该是很完美的方法。
x许多机构选择将备库设置为只读,以防止在备库进行的无意识修改导致复制中断。可以通过设置read_only选项来实现。它会禁止大部分写操作,除了复制线程和拥有超级权限的用户以及临时表操作。只要不给也不应该普通用户超级权限,这应该是很完美的方法。
模拟多主库复制。
当前MySQL不支持多主库复制(一个备库拥有多个主库)。但是可以通过把一台备库轮流指向多台主库的方式来模拟这种结构。例如,可以先将备库指向主库A,运行片刻,再将其指向主库B并运行片刻,然后然后再次切换回主库A.这种办法的效果取决于数据以及两台主库导致备库所需完成的工作量。如果主库的负载很低,并且主库之间不会产生更新冲突,就会工作得很好。
需要做一些额外的工作来为每个主库跟踪二进制日志坐标。可能还需要保证备库的IO线程在每一次循环提取超过需要的数据,否则可能会因为每次循环反复地提取和抛弃大量数据导致主库地网络流量和开销明显增大。
还可以使用主-主(或者环形)复制结构以及使用blackhole存储引擎表地贝克来进行模拟,如图所示
在这种配置中,两台主库拥有自己的数据,但也包含了对方的表,但是对方的表使用blackhole存储引擎以避免在其中存储实际数据。备库和其中任意一个主库相连都可以,备库不适用blackhole存储引擎,因此其对两个主库而言都是有效的。
事实上并不一定需要主-主拓扑结构来是西安,可以简单地将server1复制到server2,再从server2复制到备库。如果在server2上为从server1复制的数据使用blackhole存储引擎,就不会包含任何server1的数据,如图所示.这些配置方法常常会碰到一些常见的问题,例如,更新冲突或建表时明确指定存储引擎。另外一个选择是使用Continuent的Tungsten Replicator
当前MySQL不支持多主库复制(一个备库拥有多个主库)。但是可以通过把一台备库轮流指向多台主库的方式来模拟这种结构。例如,可以先将备库指向主库A,运行片刻,再将其指向主库B并运行片刻,然后然后再次切换回主库A.这种办法的效果取决于数据以及两台主库导致备库所需完成的工作量。如果主库的负载很低,并且主库之间不会产生更新冲突,就会工作得很好。
需要做一些额外的工作来为每个主库跟踪二进制日志坐标。可能还需要保证备库的IO线程在每一次循环提取超过需要的数据,否则可能会因为每次循环反复地提取和抛弃大量数据导致主库地网络流量和开销明显增大。
还可以使用主-主(或者环形)复制结构以及使用blackhole存储引擎表地贝克来进行模拟,如图所示
在这种配置中,两台主库拥有自己的数据,但也包含了对方的表,但是对方的表使用blackhole存储引擎以避免在其中存储实际数据。备库和其中任意一个主库相连都可以,备库不适用blackhole存储引擎,因此其对两个主库而言都是有效的。
事实上并不一定需要主-主拓扑结构来是西安,可以简单地将server1复制到server2,再从server2复制到备库。如果在server2上为从server1复制的数据使用blackhole存储引擎,就不会包含任何server1的数据,如图所示.这些配置方法常常会碰到一些常见的问题,例如,更新冲突或建表时明确指定存储引擎。另外一个选择是使用Continuent的Tungsten Replicator
创建日志服务器。
使用MySQL复制的另一种用途就是创建没有数据的日志服务器,它唯一的目的就是更加容易重放并且/或者过滤二进制日志事件。它对崩溃后重启复制很有帮助。同时对基于时间点的恢复也很有帮助。假设有一组二进制日志或终极日志——可能从备份或者一台崩溃的服务器上获取——希望能够重放这些日志中的事件,可以通过mysqlbinlog工具从其中提取出事件,但更加方便和高效的方法是配置一个没有任何数据的MySQL实例并使其认为这些二进制日志是它拥有的。如果只是临时需要,可以获得一个MySQL沙箱脚本来创建日志服务器。因为无须执行二进制日志,日志服务器也就不需要任何数据。它的目的仅仅是将数据提供给别的服务器(但复制账户还是需要的)。
我们来看看该策略是如何工作的.假设日志被命名为somelog-bin.000001、somelog-bin.000002,等等,将这些日志放到日志服务器的日志文件夹中,假设为/var/log/mysql。然后在启动服务器前编辑my.cnf文件,如下所示:
```c
log_bin=/var/log/mysql/somelog-bin
log_bin_index=/var/log/mysql/somelog-bin.index
```
服务器不会自动发现日志文件,因此还需要更新日志的索引文件。下面这个命令可以在类UNIX系统上完成(明确地使用/bin/ls以避免启用通用别名,它们会为终端着色添加转义码):
```c
/bin/ls -1 /var/log/mysql/somelog-bin.[0-9]* > /var/log/mysql/somelog-bin.index
```
确保运行MySQL的账户能够读写日志索引文件。现在可以启动日志服务器并通过SHOW MASTER LOGS命令来确保其找到日志文件。
为什么使用日志服务器比用mysqlbinlog来实现恢复更好呢?有以下几个原因:
1.复制作为应用二进制日志的方法已经被大量的用户所测试,能够证明是可行的。mysqlbinlog并不能确保像复制那样工作,并且可能无法正确生成二进制日志中的数据更新
2.复制的速度更快,因为无须将语句从日志导出来并传送给MySQL
3.可以很容易观察到复制过程
4.能够更方便处理错误。例如,可以跳过执行失败的语句
5.更方便过滤复制事件
6.有时候mysqlbinlog会因为日志记录格式更改而无法读取二进制日志
使用MySQL复制的另一种用途就是创建没有数据的日志服务器,它唯一的目的就是更加容易重放并且/或者过滤二进制日志事件。它对崩溃后重启复制很有帮助。同时对基于时间点的恢复也很有帮助。假设有一组二进制日志或终极日志——可能从备份或者一台崩溃的服务器上获取——希望能够重放这些日志中的事件,可以通过mysqlbinlog工具从其中提取出事件,但更加方便和高效的方法是配置一个没有任何数据的MySQL实例并使其认为这些二进制日志是它拥有的。如果只是临时需要,可以获得一个MySQL沙箱脚本来创建日志服务器。因为无须执行二进制日志,日志服务器也就不需要任何数据。它的目的仅仅是将数据提供给别的服务器(但复制账户还是需要的)。
我们来看看该策略是如何工作的.假设日志被命名为somelog-bin.000001、somelog-bin.000002,等等,将这些日志放到日志服务器的日志文件夹中,假设为/var/log/mysql。然后在启动服务器前编辑my.cnf文件,如下所示:
```c
log_bin=/var/log/mysql/somelog-bin
log_bin_index=/var/log/mysql/somelog-bin.index
```
服务器不会自动发现日志文件,因此还需要更新日志的索引文件。下面这个命令可以在类UNIX系统上完成(明确地使用/bin/ls以避免启用通用别名,它们会为终端着色添加转义码):
```c
/bin/ls -1 /var/log/mysql/somelog-bin.[0-9]* > /var/log/mysql/somelog-bin.index
```
确保运行MySQL的账户能够读写日志索引文件。现在可以启动日志服务器并通过SHOW MASTER LOGS命令来确保其找到日志文件。
为什么使用日志服务器比用mysqlbinlog来实现恢复更好呢?有以下几个原因:
1.复制作为应用二进制日志的方法已经被大量的用户所测试,能够证明是可行的。mysqlbinlog并不能确保像复制那样工作,并且可能无法正确生成二进制日志中的数据更新
2.复制的速度更快,因为无须将语句从日志导出来并传送给MySQL
3.可以很容易观察到复制过程
4.能够更方便处理错误。例如,可以跳过执行失败的语句
5.更方便过滤复制事件
6.有时候mysqlbinlog会因为日志记录格式更改而无法读取二进制日志
复制和容量规划。
x写操作通常是复制的瓶颈,并且河南使用复制来扩展写操作。当计划为系统增加复制容量时,需要确保进行了正确的计算,否则很肉感一犯一些复制相关的错误。例如,假设工作负载为20%的写以及80%的读。为了计算简单,假设有以下前提:
1.读和写查询包含同样的工作量。
2.所有的服务器是等同的,每秒能进行1 000次查询
3.备库和主库有同样的性能特征
4.可以把所有的读操作转移到备库
如果当前有一个服务器能支持每秒1 000次查询,那么应该增加多少备库才能处理当前两倍的负载,并将所有的读查询分配给备库?
看上去应该增加两个备库并将1 600次读操作平分给它们。但是不要忘记,写入负载同样增加到了400次每秒,并且无法在主备服务器之间进行分摊。每个备库每秒必须处理400次写入,这意味着每个备库写入占了40%,只能每秒为600次查询提供服务。因此,需要三台而不是两台备库来处理双倍负载。如果负载再增加已被呢?将有每秒800次写入,这时候主库还能处理,但备库的写入同样也提升到80%,这样就需要16台备库来处理每秒3 200次读查询。并且如果再增加一点负载,主库也会无法承担。
这远远不是线性扩展,查询数量增加4倍,却需要增加17倍的服务器。这说明当为单台服务器增加备库时,将很快达到投入远高于回报的地步。这仅仅是基于上面的假设,还忽略了一些事情,例如单线程的基于语句的复制常常导致备库容量小于主库。真实的复制配置比我们的理论计算还要更差。
x写操作通常是复制的瓶颈,并且河南使用复制来扩展写操作。当计划为系统增加复制容量时,需要确保进行了正确的计算,否则很肉感一犯一些复制相关的错误。例如,假设工作负载为20%的写以及80%的读。为了计算简单,假设有以下前提:
1.读和写查询包含同样的工作量。
2.所有的服务器是等同的,每秒能进行1 000次查询
3.备库和主库有同样的性能特征
4.可以把所有的读操作转移到备库
如果当前有一个服务器能支持每秒1 000次查询,那么应该增加多少备库才能处理当前两倍的负载,并将所有的读查询分配给备库?
看上去应该增加两个备库并将1 600次读操作平分给它们。但是不要忘记,写入负载同样增加到了400次每秒,并且无法在主备服务器之间进行分摊。每个备库每秒必须处理400次写入,这意味着每个备库写入占了40%,只能每秒为600次查询提供服务。因此,需要三台而不是两台备库来处理双倍负载。如果负载再增加已被呢?将有每秒800次写入,这时候主库还能处理,但备库的写入同样也提升到80%,这样就需要16台备库来处理每秒3 200次读查询。并且如果再增加一点负载,主库也会无法承担。
这远远不是线性扩展,查询数量增加4倍,却需要增加17倍的服务器。这说明当为单台服务器增加备库时,将很快达到投入远高于回报的地步。这仅仅是基于上面的假设,还忽略了一些事情,例如单线程的基于语句的复制常常导致备库容量小于主库。真实的复制配置比我们的理论计算还要更差。
为什么复制无法扩展写操作。
糟糕的服务容量比例的根本原因是不能像分发读操作那样把写操作等同地分发到更多服务器上。换句话说,复制只能扩展读操作,无法扩展写操作。
你可能想知道到底有没有办法使用复制来增加写入能力。答案是否定的,根本不行。对数据进行分区是唯一可以扩展写入的方法。
一些人可能会想到使用主-主拓扑结构(主动-主动模式下的主-主复制)并为两个服务器执行写操作。这种配置比主备结构能支持稍微多一点的写入,因为可以再两台服务器之间共享串行化带来的开销。如果每台服务器上执行50%的写入,那复制的执行量也只有50%需要串行化。理论上讲,这比在一台机器上(主库)对100%的写入并发执行,而在另外一台机器上(备库)上对100%的写入做串行化更优。
这可能看起来很吸引人,然而这种配置还比不上单台服务器能支持的写入。一个有50%的写入被串行化的服务器性能比一台全部写入都并行化的服务器性能要低。这是这种策略不能扩展写入的原因。它只能在两台服务器间共享串行化写入的缺点。所以"链中最弱的一环"并不是那么弱,它只提供了比主动-被动复制稍微好点的性能。但是增加了很大的风险。通常不能带来任何好处
糟糕的服务容量比例的根本原因是不能像分发读操作那样把写操作等同地分发到更多服务器上。换句话说,复制只能扩展读操作,无法扩展写操作。
你可能想知道到底有没有办法使用复制来增加写入能力。答案是否定的,根本不行。对数据进行分区是唯一可以扩展写入的方法。
一些人可能会想到使用主-主拓扑结构(主动-主动模式下的主-主复制)并为两个服务器执行写操作。这种配置比主备结构能支持稍微多一点的写入,因为可以再两台服务器之间共享串行化带来的开销。如果每台服务器上执行50%的写入,那复制的执行量也只有50%需要串行化。理论上讲,这比在一台机器上(主库)对100%的写入并发执行,而在另外一台机器上(备库)上对100%的写入做串行化更优。
这可能看起来很吸引人,然而这种配置还比不上单台服务器能支持的写入。一个有50%的写入被串行化的服务器性能比一台全部写入都并行化的服务器性能要低。这是这种策略不能扩展写入的原因。它只能在两台服务器间共享串行化写入的缺点。所以"链中最弱的一环"并不是那么弱,它只提供了比主动-被动复制稍微好点的性能。但是增加了很大的风险。通常不能带来任何好处
备库什么时候开始延迟。
一个关于备库比较普遍的问题是如何预测备库会在何时跟不上主库。很难去描述备库使用的复制容量为5%与95%的区别,但是至少能够在接近饱和前预警并估计复制容量。首先应该古纳差复制延迟的尖刺。如果有复制延迟的曲线图,需要注意到图上的一些短暂的延迟骤升,这时候可能负载加大,备库短时间内无法跟上主库。当负载接近耗尽备库的容量时,会发现曲线上的凸起会更高更宽。前面曲线地上升角度不必那,但随后当备库在产生延迟后开始追赶主库时,将会产生一个平缓的斜坡。这些突起的出现和增长是一个警告信息,意味着已经接近容量限制。
为了预测在将来的某个时间点会发生什么,可以人为地制造延迟,然后看多久备库能赶上主库。目的时为了明确地曲线上的斜坡的陡度。如果讲备库停止一个小时,然后开启并在1小时内追赶上,说明正常情况下只消耗了一半的容量。也就是说,如果中午12:00停止备库复制,在1:00开启,并且在2:00追赶上,备库在一小时内完成了两个小时内所有的变更,说明复制可以在双倍速度下运行。
最后,如果使用得时Percona Server或者MariaDB,也可以直接获取复制的利用率。打开服务器变量userstat,然后执行如下语句:
```sql
mysql> SELECT * FROM INFORMATION_SCHEMA.USER_STATISTICS WHERE USER='#mysql_system#'\G
*************************** 1. row ***************************
USER:#mysql_system#
TOTAL_CONNECTIONS:1
CONCURRENT_CONNECTIONS:2
CONNECTED_TIME:46188
BUSY_TIME:719
ROWS_FETCHED:0
ROWS_UPDATED:1882292
SELECT_COMMANDS:0
UPDATE_COMMANDS:580431
OTHER_COMMANDS:338857
COMMIT_TRANSACTIONS:1016571
ROLLBACK_TRANSACTIONS:0
```
可以讲BUSY_TIME和CONNECTED_TIME的一半(因为备库有两个复制线程)做比较,来观察备库线程实际执行命令所花费的事件(如果复制线程总是在运行,你可以使用服务器的uptime来代替CONNECTED_TIME的一半)。在该例子中,备库大约使用了其3%的能力,这并不意味着它不会遇到偶然的延迟尖刺——如果主库运行了一个超过10分钟才完成的变更,可能延迟的事件和变更执行的事件是相同的——但这很好地暗示了备库能够很快从一个延迟尖刺中恢复
一个关于备库比较普遍的问题是如何预测备库会在何时跟不上主库。很难去描述备库使用的复制容量为5%与95%的区别,但是至少能够在接近饱和前预警并估计复制容量。首先应该古纳差复制延迟的尖刺。如果有复制延迟的曲线图,需要注意到图上的一些短暂的延迟骤升,这时候可能负载加大,备库短时间内无法跟上主库。当负载接近耗尽备库的容量时,会发现曲线上的凸起会更高更宽。前面曲线地上升角度不必那,但随后当备库在产生延迟后开始追赶主库时,将会产生一个平缓的斜坡。这些突起的出现和增长是一个警告信息,意味着已经接近容量限制。
为了预测在将来的某个时间点会发生什么,可以人为地制造延迟,然后看多久备库能赶上主库。目的时为了明确地曲线上的斜坡的陡度。如果讲备库停止一个小时,然后开启并在1小时内追赶上,说明正常情况下只消耗了一半的容量。也就是说,如果中午12:00停止备库复制,在1:00开启,并且在2:00追赶上,备库在一小时内完成了两个小时内所有的变更,说明复制可以在双倍速度下运行。
最后,如果使用得时Percona Server或者MariaDB,也可以直接获取复制的利用率。打开服务器变量userstat,然后执行如下语句:
```sql
mysql> SELECT * FROM INFORMATION_SCHEMA.USER_STATISTICS WHERE USER='#mysql_system#'\G
*************************** 1. row ***************************
USER:#mysql_system#
TOTAL_CONNECTIONS:1
CONCURRENT_CONNECTIONS:2
CONNECTED_TIME:46188
BUSY_TIME:719
ROWS_FETCHED:0
ROWS_UPDATED:1882292
SELECT_COMMANDS:0
UPDATE_COMMANDS:580431
OTHER_COMMANDS:338857
COMMIT_TRANSACTIONS:1016571
ROLLBACK_TRANSACTIONS:0
```
可以讲BUSY_TIME和CONNECTED_TIME的一半(因为备库有两个复制线程)做比较,来观察备库线程实际执行命令所花费的事件(如果复制线程总是在运行,你可以使用服务器的uptime来代替CONNECTED_TIME的一半)。在该例子中,备库大约使用了其3%的能力,这并不意味着它不会遇到偶然的延迟尖刺——如果主库运行了一个超过10分钟才完成的变更,可能延迟的事件和变更执行的事件是相同的——但这很好地暗示了备库能够很快从一个延迟尖刺中恢复
规划冗余容量。
在构建一个大型应用时,有意让服务器不被充分利用,,这应该是一种聪明并且划算的方式,尤其在使用复制的时候。有多余容量的服务器可以更好地处理负载尖峰,也有更多能力处理慢速查询和维护工作(如OPTIMIZE TABLE ),并且能够更好地跟上复制,试图同时向主-主拓扑结构的两个节点写入来减少复制问题通常是不划算的。分配给每台机器的读负载应该低于50%,否则,如果某台机器失效,就没有足够的容量了,如果两台服务器都能独立处理负载,就用不着担心复制的问题了。。构建冗余容量也是实现高可用性的最佳方式之一,当然还有别的方式,例如,当错误发生时让应用在降级模式下运行
在构建一个大型应用时,有意让服务器不被充分利用,,这应该是一种聪明并且划算的方式,尤其在使用复制的时候。有多余容量的服务器可以更好地处理负载尖峰,也有更多能力处理慢速查询和维护工作(如OPTIMIZE TABLE ),并且能够更好地跟上复制,试图同时向主-主拓扑结构的两个节点写入来减少复制问题通常是不划算的。分配给每台机器的读负载应该低于50%,否则,如果某台机器失效,就没有足够的容量了,如果两台服务器都能独立处理负载,就用不着担心复制的问题了。。构建冗余容量也是实现高可用性的最佳方式之一,当然还有别的方式,例如,当错误发生时让应用在降级模式下运行
复制管理和维护。
配置复制一般来说不会是需要经常做的工作,除非有很多服务器。但是一旦配置了复制,监控和管理复制拓扑应该称为一项日常工作,不管有多少服务器。这些工作应该尽量自动化,但不一定需要自己写工具来实现.,可以借助第三方的工具或插件。
配置复制一般来说不会是需要经常做的工作,除非有很多服务器。但是一旦配置了复制,监控和管理复制拓扑应该称为一项日常工作,不管有多少服务器。这些工作应该尽量自动化,但不一定需要自己写工具来实现.,可以借助第三方的工具或插件。
监控复制。
复制增加了MySQL监控的复杂性。尽管复制发生在主库和备库上,但大多数工作是在备库上完成的,这也正式最常出问题的地方。是否所有的备库都在工作?最慢的备库延迟是多大?MySQL本身提供了大量可以回答上述问题的信息,弹药实现自动化监控过程以及使复制更健壮还是需要用户做更多的工作。
在主库上,可以使用SHOW MASTER STATUS命令来查看当前主库的二进制日志位置和配置。还可以查看主库当前有哪些二进制日志在磁盘上的:
```sql
mysql>SHOW MASTER LOGS;
```
该命令用于给PURGE MASTER LOGS命令决定使用哪个参数。另外还可以通过SHOW BINLOG EVENTS来查看复制事件。例如,在运行前一个命令后,我们在另一个不曾使用过的服务器上创建一个表,因为知道这是唯一改变数据的语句,而且也知道语句在二进制日志中的偏移量是319,所以我们能看到如下内容:
复制增加了MySQL监控的复杂性。尽管复制发生在主库和备库上,但大多数工作是在备库上完成的,这也正式最常出问题的地方。是否所有的备库都在工作?最慢的备库延迟是多大?MySQL本身提供了大量可以回答上述问题的信息,弹药实现自动化监控过程以及使复制更健壮还是需要用户做更多的工作。
在主库上,可以使用SHOW MASTER STATUS命令来查看当前主库的二进制日志位置和配置。还可以查看主库当前有哪些二进制日志在磁盘上的:
```sql
mysql>SHOW MASTER LOGS;
```
该命令用于给PURGE MASTER LOGS命令决定使用哪个参数。另外还可以通过SHOW BINLOG EVENTS来查看复制事件。例如,在运行前一个命令后,我们在另一个不曾使用过的服务器上创建一个表,因为知道这是唯一改变数据的语句,而且也知道语句在二进制日志中的偏移量是319,所以我们能看到如下内容:
测量备库延迟。
一个比较普遍的问题是如何监控备库落后主库的延迟有多大。虽然SHOW SLAVE STATUS输出的Seconds_behind_master列理论上显示了备库的延时,但由于各种各样的原因,并不总是准确的:
1.备库Seconds_behind_master值是通过讲服务器当前的时间戳与二进制日志中的事件的时间戳相对比得到的,所以只有在执行事件时才能报告延迟。
2.如果备库复制线程没有运行,就会报延迟为NULL
3.一些错误(例如主备的max_allowed_packet不匹配,或者网络不稳定)可能中断复制并且/或者停止复制线程,但Seconds_behind_master将显示为0而不是显示错误
4.即使备库线程正在运行,备库有时候可能无法计算延时,如果发生这种情况,备库会报0或者NULL
5.一个大事务可能会导致延迟波动,例如,有一个事务更新数据长达一个小时,最后提交。这条更新将比它实际发生时间要晚一个小时才记录到二进制日志中。当备库执行这条语句时,会临时地报告备库延迟为一个小时,然后又很快变成0
6.如果分发主库落后了,并且其本身也有已经追赶上它的备库,备库的延迟将显示为0,而事实上和源主库之间是有延迟的。
解决这些问题的办法是忽略Seconds_behind_master的值,并使用一些可以直接观察和衡量的方式来监控备库延迟。最好的解决办法是使用heartbeat record,这是一个在主库上会每秒更新一次的时间戳。为了计算延时,可以直接用备库当前的时间戳减去心跳记录的值。这个方法能够解决刚刚我们提到的所有问题,另外一个额外的好处是我们还可以通过时间戳知道备库当前的复制状况。包含在Percona Toolkit里的pt-heartbeat脚本是"复制心跳"最流行的一种实现。心跳还有其他好处,记录在二进制日志中的心跳记录拥有多种用途,例如在一些很难解决的场景下可以用于灾难恢复。
刚刚所描述的几种延迟指标都不能表明备库需要多长时间才能赶上主库,这依赖于许多因素,例如备库的写入能力以及主库持续写入的次数。
一个比较普遍的问题是如何监控备库落后主库的延迟有多大。虽然SHOW SLAVE STATUS输出的Seconds_behind_master列理论上显示了备库的延时,但由于各种各样的原因,并不总是准确的:
1.备库Seconds_behind_master值是通过讲服务器当前的时间戳与二进制日志中的事件的时间戳相对比得到的,所以只有在执行事件时才能报告延迟。
2.如果备库复制线程没有运行,就会报延迟为NULL
3.一些错误(例如主备的max_allowed_packet不匹配,或者网络不稳定)可能中断复制并且/或者停止复制线程,但Seconds_behind_master将显示为0而不是显示错误
4.即使备库线程正在运行,备库有时候可能无法计算延时,如果发生这种情况,备库会报0或者NULL
5.一个大事务可能会导致延迟波动,例如,有一个事务更新数据长达一个小时,最后提交。这条更新将比它实际发生时间要晚一个小时才记录到二进制日志中。当备库执行这条语句时,会临时地报告备库延迟为一个小时,然后又很快变成0
6.如果分发主库落后了,并且其本身也有已经追赶上它的备库,备库的延迟将显示为0,而事实上和源主库之间是有延迟的。
解决这些问题的办法是忽略Seconds_behind_master的值,并使用一些可以直接观察和衡量的方式来监控备库延迟。最好的解决办法是使用heartbeat record,这是一个在主库上会每秒更新一次的时间戳。为了计算延时,可以直接用备库当前的时间戳减去心跳记录的值。这个方法能够解决刚刚我们提到的所有问题,另外一个额外的好处是我们还可以通过时间戳知道备库当前的复制状况。包含在Percona Toolkit里的pt-heartbeat脚本是"复制心跳"最流行的一种实现。心跳还有其他好处,记录在二进制日志中的心跳记录拥有多种用途,例如在一些很难解决的场景下可以用于灾难恢复。
刚刚所描述的几种延迟指标都不能表明备库需要多长时间才能赶上主库,这依赖于许多因素,例如备库的写入能力以及主库持续写入的次数。
确定主备是否一致。
在理想情况下,备库和主库的数据应该是完全一样的。但事实上备库可能发生错误并导致数据不一致。即使没有明显的错误,备库同样可能因为MySQL自身的特性导致数据不一致,例如MySQL的Bug、网络中断、服务器崩溃,非正常关闭或者其他一些错误。(如果你正在使用非事务型存储引擎,不首先调用STOP SLAVE就关闭服务器是很不妥当的)。
按照经验来看,主备一致应该是一种规范,而不是例外,也就是说,检查你的主备一致性应该是一个日常工作,特别是当使用备库来做备份时尤为重要,因为你肯定不希望从一个已经损坏的备库里获得备份数据。MySQL并没有内建的方法来比较一台服务器与别的服务器的数据是否相同。它提供了一些组建来为表和数据生成校验值,例如CHECKSUM TABLE。但当复制正在进行时,这种方法是不可行的。
Percona Toolkit里的pt-table-checksum能够解决上述几个问题。其主要特性是用于确认备库与主库的数据是否一致。工作方式是通过在主库执行INSERT ...SELECT查询。这些查询对数据进行校验并将结果插入到一个表中。这些语句通过复制传递到备库,并在备库执行一遍,然后可以比较主备上的结果是否一样。由于该方法是通过复制工作的。它能够给出一致的结果而无须同时把主备上的表都锁上。
通常情况下可以在主库上运行该工具,参数如下:
```c
$ pt-table-checksum --replicate=test.checksum <master_host>
```
该命令将检查所有的表,并将结果插入到test.checksum表中。当查询在备库执行完后,就可以简单地比较主备之间的不同了。pt-table-checksum能够发现服务器所有的备库,在每台备库上运行查询,并自动地输出结果。
在理想情况下,备库和主库的数据应该是完全一样的。但事实上备库可能发生错误并导致数据不一致。即使没有明显的错误,备库同样可能因为MySQL自身的特性导致数据不一致,例如MySQL的Bug、网络中断、服务器崩溃,非正常关闭或者其他一些错误。(如果你正在使用非事务型存储引擎,不首先调用STOP SLAVE就关闭服务器是很不妥当的)。
按照经验来看,主备一致应该是一种规范,而不是例外,也就是说,检查你的主备一致性应该是一个日常工作,特别是当使用备库来做备份时尤为重要,因为你肯定不希望从一个已经损坏的备库里获得备份数据。MySQL并没有内建的方法来比较一台服务器与别的服务器的数据是否相同。它提供了一些组建来为表和数据生成校验值,例如CHECKSUM TABLE。但当复制正在进行时,这种方法是不可行的。
Percona Toolkit里的pt-table-checksum能够解决上述几个问题。其主要特性是用于确认备库与主库的数据是否一致。工作方式是通过在主库执行INSERT ...SELECT查询。这些查询对数据进行校验并将结果插入到一个表中。这些语句通过复制传递到备库,并在备库执行一遍,然后可以比较主备上的结果是否一样。由于该方法是通过复制工作的。它能够给出一致的结果而无须同时把主备上的表都锁上。
通常情况下可以在主库上运行该工具,参数如下:
```c
$ pt-table-checksum --replicate=test.checksum <master_host>
```
该命令将检查所有的表,并将结果插入到test.checksum表中。当查询在备库执行完后,就可以简单地比较主备之间的不同了。pt-table-checksum能够发现服务器所有的备库,在每台备库上运行查询,并自动地输出结果。
从主库重新同步备库。
在职业生涯中,也许会不止一次需要去处理未被同步的备库。可能是使用校验工具发现了数据不一致,或是因为已经知道是备库忽略了某条查询或者有人在备库上修改了数据。传统的修复不一致的办法是关闭备库,然后重新从主库复制一份数据。当备库数据不一致的问题可能导致严重后果时,一旦发现就应该将备库停止并从生产环境移除,然后再从一个备份中克隆或恢复备库。
这种方法的缺点是不太方便,特别是数据量很大时。如果能够找出并修复不一致的数据,要比从其他服务器上重新克隆数据要有效得多。如果发现的不一致并不严重,就可以保持备库在线,并重新同步受影响的数据。最简单的办法是使用mysqldump转储受影响的数据并重新导入。在整个过程中,如果数据没有发生变化,这种方法会很好。你可以在主库上简单地锁住表然后进行转储,再等备库赶上主库,然后将数据导入到备库中。(需要等待备库赶上主库,这样就不至于为其他表引入新的不一致,例如那些可能通过和失去同步的表做join后进行数据更新的表)。
虽然这种方法再许多场景下是可行的。但在一个繁忙的服务器上有可能行不通。另外一个缺点是在备库上通过非复制的方式改变数据。通过复制改变备库数据(通过在主库上执行更新)通常是一种安全的技术,因为它避免了竞争条件和其他意料外的事情。如果表很大或者网络带宽首先,转储和重载数据的代价依然很高。当在一个有一百万行的表上只有一千行不同的数据呢?转储和重载表的数据是非常浪费资源的。
pt-table-sync是Percona Toolkit中的另外一个工具,可以解决该问题。该工具能够高效地查找并解决表之间的不同。它同样通过复制工作,在主库上执行查询,在备库上重新同步,这样就没有竞争条件。它是结合pt-table-checksum生成的checksum表来工作的。所以只能操作那些已知不同步的表的数据块。但该工具不是在所有场景下都有效。为了正确地同步主库和备库,该工具要求复制是正常地,否则就无法工作。pt-table-sync设计得很搞笑,但当数据量非常大时效率还是会很低。比较主库和备库上1TB的数据不可避免地会带来额外的工作。尽管如此,在那些合适的场景中,该工具依然能节约大量的时间和工作
在职业生涯中,也许会不止一次需要去处理未被同步的备库。可能是使用校验工具发现了数据不一致,或是因为已经知道是备库忽略了某条查询或者有人在备库上修改了数据。传统的修复不一致的办法是关闭备库,然后重新从主库复制一份数据。当备库数据不一致的问题可能导致严重后果时,一旦发现就应该将备库停止并从生产环境移除,然后再从一个备份中克隆或恢复备库。
这种方法的缺点是不太方便,特别是数据量很大时。如果能够找出并修复不一致的数据,要比从其他服务器上重新克隆数据要有效得多。如果发现的不一致并不严重,就可以保持备库在线,并重新同步受影响的数据。最简单的办法是使用mysqldump转储受影响的数据并重新导入。在整个过程中,如果数据没有发生变化,这种方法会很好。你可以在主库上简单地锁住表然后进行转储,再等备库赶上主库,然后将数据导入到备库中。(需要等待备库赶上主库,这样就不至于为其他表引入新的不一致,例如那些可能通过和失去同步的表做join后进行数据更新的表)。
虽然这种方法再许多场景下是可行的。但在一个繁忙的服务器上有可能行不通。另外一个缺点是在备库上通过非复制的方式改变数据。通过复制改变备库数据(通过在主库上执行更新)通常是一种安全的技术,因为它避免了竞争条件和其他意料外的事情。如果表很大或者网络带宽首先,转储和重载数据的代价依然很高。当在一个有一百万行的表上只有一千行不同的数据呢?转储和重载表的数据是非常浪费资源的。
pt-table-sync是Percona Toolkit中的另外一个工具,可以解决该问题。该工具能够高效地查找并解决表之间的不同。它同样通过复制工作,在主库上执行查询,在备库上重新同步,这样就没有竞争条件。它是结合pt-table-checksum生成的checksum表来工作的。所以只能操作那些已知不同步的表的数据块。但该工具不是在所有场景下都有效。为了正确地同步主库和备库,该工具要求复制是正常地,否则就无法工作。pt-table-sync设计得很搞笑,但当数据量非常大时效率还是会很低。比较主库和备库上1TB的数据不可避免地会带来额外的工作。尽管如此,在那些合适的场景中,该工具依然能节约大量的时间和工作
改变主库。
迟早会有把备库指向一个新的主库的需求。也许是为了更迭升级服务器,或者是主库出现问题时需要把一台备库转换成主库,或者只是希望重新分配容量。不管处于什么原因,都需要告诉其他的备库新主库的信息。如果这是计划内的操作,会比较容易(至少比紧急情况下要容易)。只需在备库简单地使用CHNAGE MASTER TO命令,并指定合适的值。大多数值都是可选的。只需要指定需要改变的项即可。备库将抛弃之前的配置和中继日志并从新的主库开始复制。同样新的参数会被更新到master.info文件中,这样就算重启,备库配置信息也不会丢失。
整个过程中最难的是获取新主库上合适的二进制日志文职,这样备库才可以从和老主库相同的逻辑位置开始复制。把备库提升为主库要更困难一点。有两种场景需要将备库替换为主库,一种是计划内的提升,一种是计划外的提升。
迟早会有把备库指向一个新的主库的需求。也许是为了更迭升级服务器,或者是主库出现问题时需要把一台备库转换成主库,或者只是希望重新分配容量。不管处于什么原因,都需要告诉其他的备库新主库的信息。如果这是计划内的操作,会比较容易(至少比紧急情况下要容易)。只需在备库简单地使用CHNAGE MASTER TO命令,并指定合适的值。大多数值都是可选的。只需要指定需要改变的项即可。备库将抛弃之前的配置和中继日志并从新的主库开始复制。同样新的参数会被更新到master.info文件中,这样就算重启,备库配置信息也不会丢失。
整个过程中最难的是获取新主库上合适的二进制日志文职,这样备库才可以从和老主库相同的逻辑位置开始复制。把备库提升为主库要更困难一点。有两种场景需要将备库替换为主库,一种是计划内的提升,一种是计划外的提升。
计划内的提升。
把备库提升为主库理论上是很简单的。简单来说,有以下步骤:
1.停止向老的主库ieru
2.让备库追赶上主库(可选的,会简化下面的步骤)
3.将一台备库配置为新的主库
4.将备库和写操作指向新的主库,然后开启主库的写入
但这其中还隐藏着很多细节。一些场景可能依赖于复制的拓扑结构。例如,主-主结构和主-备结构的配置就有所不同。
更深入一点,下面是大多数配置需要的步骤:
1.停止当前主库上的所有写操作。如果可以,最好能将所有的客户端程序关闭(除了复制连接)。为客户端程序建立一个"do not run"这样的类似标记可能会有所帮助。如果正在使用虚拟IP地址,也可以简单地关闭虚拟IP,然后断开所有地客户端连接以关闭其打开地事务
2.通过FLUSH TABLES WITH READ LOCK在主库上停止所有活跃的写入,这一步是可选的。也可以在主库上设置read_only选项。从这一刻开始,应该禁止向即将备替换的主库做任何写入。因为一旦它不是主库,写入就意味着数据丢失。注意,即使设置read_only也不会阻止当前已存在的事务继续提交。为了更好地保证这一点,可以"kill"所有打开的事务,这将会真正地结束所有写入
3.选择一个备库作为新的主库,并确保它已经完全跟上主库(例如,让他执行完所有从主库获得的中继日志)
4.确保新主库和旧主库的数据是已知的。可选
5.在新主库上执行STOP SLAVE
6.在新主库上执行CHANGE MASTER TO MASTER_HOST='',然后再执行RESET SLAVE,使其断开与老主库的连接,并丢弃master.info里记录的信息(如果连接信息记录在my.cnf里,会无法正确工作,这也是建议不要把复制连接信息写到配置文件里的原因之一)
7.执行SHOW MASTER STATUS记录新主库的二进制日志坐标
8.确保其他备库已经追赶上
9.关闭旧主库
10.在MySQL5.1及以上版本中,如果需要,激活新主库上事件
11.将客户端连接到新主库
12.在每台备库上执行CHANGE MASTER TO语句,使用之前通过SHOW MASTER STATUS获得的二进制日志坐标,来指向新的主库。
当将备库提升为主库时,要确保备库上任何特有的数据库、表和权限已经备移除。可能还需要修改备库特有的配置选项,例如innodb_flush_log_at_trx_commit选项,同样的,如果是把主库降级为备库,也要保证需要的配置。如果主备的配置相同,就不需要做任何改变。
把备库提升为主库理论上是很简单的。简单来说,有以下步骤:
1.停止向老的主库ieru
2.让备库追赶上主库(可选的,会简化下面的步骤)
3.将一台备库配置为新的主库
4.将备库和写操作指向新的主库,然后开启主库的写入
但这其中还隐藏着很多细节。一些场景可能依赖于复制的拓扑结构。例如,主-主结构和主-备结构的配置就有所不同。
更深入一点,下面是大多数配置需要的步骤:
1.停止当前主库上的所有写操作。如果可以,最好能将所有的客户端程序关闭(除了复制连接)。为客户端程序建立一个"do not run"这样的类似标记可能会有所帮助。如果正在使用虚拟IP地址,也可以简单地关闭虚拟IP,然后断开所有地客户端连接以关闭其打开地事务
2.通过FLUSH TABLES WITH READ LOCK在主库上停止所有活跃的写入,这一步是可选的。也可以在主库上设置read_only选项。从这一刻开始,应该禁止向即将备替换的主库做任何写入。因为一旦它不是主库,写入就意味着数据丢失。注意,即使设置read_only也不会阻止当前已存在的事务继续提交。为了更好地保证这一点,可以"kill"所有打开的事务,这将会真正地结束所有写入
3.选择一个备库作为新的主库,并确保它已经完全跟上主库(例如,让他执行完所有从主库获得的中继日志)
4.确保新主库和旧主库的数据是已知的。可选
5.在新主库上执行STOP SLAVE
6.在新主库上执行CHANGE MASTER TO MASTER_HOST='',然后再执行RESET SLAVE,使其断开与老主库的连接,并丢弃master.info里记录的信息(如果连接信息记录在my.cnf里,会无法正确工作,这也是建议不要把复制连接信息写到配置文件里的原因之一)
7.执行SHOW MASTER STATUS记录新主库的二进制日志坐标
8.确保其他备库已经追赶上
9.关闭旧主库
10.在MySQL5.1及以上版本中,如果需要,激活新主库上事件
11.将客户端连接到新主库
12.在每台备库上执行CHANGE MASTER TO语句,使用之前通过SHOW MASTER STATUS获得的二进制日志坐标,来指向新的主库。
当将备库提升为主库时,要确保备库上任何特有的数据库、表和权限已经备移除。可能还需要修改备库特有的配置选项,例如innodb_flush_log_at_trx_commit选项,同样的,如果是把主库降级为备库,也要保证需要的配置。如果主备的配置相同,就不需要做任何改变。
计划外的提升。
当主库崩溃时,需要提升一台备库来代替它,这个过程可能就不太容易。如果只有一台备库,可以直接使用这台备库。但如果有超过一台的备库,就需要做一些额外的工作。另外,还有潜在的丢失复制事件的问题。可能有主库上已经发生了修改还没有更新到它的任何一台备库上的情况。甚至还可能一条语句在主库上执行了回滚,但在备库上没有回滚,这样备库可能超过主库的逻辑复制位置(这是有可能的,即使MySQL在事务提交前并不记录任何事件。另外一种场景是主库崩溃后恢复,但没有设置innnodb_flush_log_at_trx_commit的值为1,所以可能会丢失一些更新)。如果能在某一点恢复主库的数据,也许就可以取得丢失的语句并手动执行它们。在以下的步骤中,需要确保在计算中使用Master_Log_File和Read_Master_Log_Pos的值。以下是对主备拓扑结构中的备库进行提升的过程:
1.确定哪台备库的数据最新。检查每台备库上的SHOW SLAVE STATUS命令的输出,选择其中Master_Log_File/read_Master_Log_Pos的值最新的那个。
2.让所有哦备库执行完所有其从崩溃前的旧主库那获得的中继日志。如果在未完成前修改备库的主库,它会抛弃剩下的日志事件,从而无法获知该备库在什么地方停止
3.执行前面的5~7步
4.比较每台备库和新主库上的Master_Log_File/Read_Master_Log_Pos的值
5.执行前面的10~12步
正如开始推荐的,假设已经在所有的备库上开启了log_bin和log_slave_updates,这样可以帮助你将所有的备库恢复到一个一致的时间点,如果没有开启这两个选项,则不能可靠地做到这一点。
当主库崩溃时,需要提升一台备库来代替它,这个过程可能就不太容易。如果只有一台备库,可以直接使用这台备库。但如果有超过一台的备库,就需要做一些额外的工作。另外,还有潜在的丢失复制事件的问题。可能有主库上已经发生了修改还没有更新到它的任何一台备库上的情况。甚至还可能一条语句在主库上执行了回滚,但在备库上没有回滚,这样备库可能超过主库的逻辑复制位置(这是有可能的,即使MySQL在事务提交前并不记录任何事件。另外一种场景是主库崩溃后恢复,但没有设置innnodb_flush_log_at_trx_commit的值为1,所以可能会丢失一些更新)。如果能在某一点恢复主库的数据,也许就可以取得丢失的语句并手动执行它们。在以下的步骤中,需要确保在计算中使用Master_Log_File和Read_Master_Log_Pos的值。以下是对主备拓扑结构中的备库进行提升的过程:
1.确定哪台备库的数据最新。检查每台备库上的SHOW SLAVE STATUS命令的输出,选择其中Master_Log_File/read_Master_Log_Pos的值最新的那个。
2.让所有哦备库执行完所有其从崩溃前的旧主库那获得的中继日志。如果在未完成前修改备库的主库,它会抛弃剩下的日志事件,从而无法获知该备库在什么地方停止
3.执行前面的5~7步
4.比较每台备库和新主库上的Master_Log_File/Read_Master_Log_Pos的值
5.执行前面的10~12步
正如开始推荐的,假设已经在所有的备库上开启了log_bin和log_slave_updates,这样可以帮助你将所有的备库恢复到一个一致的时间点,如果没有开启这两个选项,则不能可靠地做到这一点。
确定期望的日志位置。
如果有备库和新主库的位置不相同,则需要找到该备库最后一条执行的时间在新主库的二进制日志中相应的位置,然后再执行CHANGE MASTER TO.可以通过mysqlbinlog工具来找到备库执行的最后一条查询,然后在主库上找到同样的查询,进行简单的计算即可得到。
为了便于描述,假设每个日志时间有一个自增的数字ID,最新的备库,也就是新主库,在旧主库崩溃时获得了编号为100的事件,假设有另外两台备库:replica2和replica3。replica2已经获取了99号事件,replica3获取了98号事件。如果把两台备库都指向新主库的同一个二进制日志位置,它们将从101号事件开始复制,从而导致数据不同步。但只要新主库的二进制日志已经通过log_slave_updates打开,就可以在新主库的二进制日志中找到99号和100号日志,从而将备库恢复到一致的状态。由于服务器重启,不同的配置,日志轮转或者FLUSH LOGS命令,同一个事件在不同的服务器上可能有不同的偏移量。找到这些事件可能会耗时很长并且枯燥,但是通常没有难度。通过mysqlbinlog从二进制日志或中继日志中解析出每台备库上执行的最后一个事件,并同样使用该命令解析新主库上的二进制日志,找到相同的查询,mysqlbinlog会打印出该事件的偏移量,在CHANGE MASTER TO命令中使用这个值。(pt-heartbeat的心跳记录能够很好地帮助你好到正在查找的事件的大约位置)
更快的方法是把新主库和停止的备库上的字节偏移量相减,它显示了字节位置的差异。然后把这个值和新主库当前二进制日志的位置相减,就可以得到期望的查询的位置。只需要验证一下就可以据此启动备库。
假设server1是server2和server3的主库,其中服务器server1已经崩溃。根据SHOW SLAVE STATUS获得Master_Log_File/Read_Master_Log_Pos的值,server2已经执行完了server1上所有的二进制日志,但server3还不是最新的数据,如图显示了这个场景(日志事件和偏移量仅仅是为了举例).正如图所示。我们可以肯定server2已经执行完主库上的所有二进制日志,因为Master_Log_File和Read_Master_Log_Pos值和server1上最后的日志位置是相吻合的,因此我们可以将server2提升为新主库,并将server3设置为server2的备库。应该在server3上为需要执行的CHANGE MASTER TO语句赋予什么样样的参数呢?这里需要做一点点计算和调整。server3在偏移量1493停止,比server2执行的最后一条语句的偏移量1582小89字节。server2正在向偏移量为8167的二进制日志写入,8167-89=8078,因此理论上我们应该将server3指向server2的日志的偏移量为8078的位置。最好去确认下这个位置附近的日志事件,以确定在该位置上是否是正确的日志事件,因为可能有别的例外,例如有些更新可能只发生在server2上。假设我们光查到的事件是一样的,下面这条命令会将server3切换为server2的备库。
```sql
server2>CHANGE MASTER TO MASTER_HOST="server2",MASTER_LOG_FILE="mysql-bin.000009", MASTER_LOG_POS=8078;
```
如果服务器在它崩溃时已经执行完成并记录了超过一个事件,会怎样呢?因为server2仅仅读取并执行到了偏移量1582,你可能永远地失去了一个事件。但是如果老主库的磁盘没有损坏,仍然可以通过mysqlbinlog或者从日志服务器的二进制日志中找到丢失的事件。
如果需要从老朱库上恢复丢失的事件,建议在提升新主库之后且在允许客户端连接之前做这件事情,这样就无须再每台备库上都执行丢失的事件,只需要使用复制来完成。但如果崩溃的老主库完全不可用,就不得不等待,稍后再做这项工作。
上述流程中一个可调整的地方是使用可靠的方式来存储二进制日志,如SAN或分布式复制数据库设备(DRBD).即使主库完全失效,依然能够获得它的二进制日志。也可以设置一个日志服务器,把备库指向它,然后让所有备库赶上主库失效的点。这使得提升一个备库为新的主库没那么中国要。本质上这和计划中的提升是相同的。(当提升一台备库为主库时,千万不要将它鞥多服务器ID修改成源主库的服务器ID,否则将不能使用日志服务器从一个旧主库来重放日志事件。这也是确保服务器ID最好保持不变的原因之一)
如果有备库和新主库的位置不相同,则需要找到该备库最后一条执行的时间在新主库的二进制日志中相应的位置,然后再执行CHANGE MASTER TO.可以通过mysqlbinlog工具来找到备库执行的最后一条查询,然后在主库上找到同样的查询,进行简单的计算即可得到。
为了便于描述,假设每个日志时间有一个自增的数字ID,最新的备库,也就是新主库,在旧主库崩溃时获得了编号为100的事件,假设有另外两台备库:replica2和replica3。replica2已经获取了99号事件,replica3获取了98号事件。如果把两台备库都指向新主库的同一个二进制日志位置,它们将从101号事件开始复制,从而导致数据不同步。但只要新主库的二进制日志已经通过log_slave_updates打开,就可以在新主库的二进制日志中找到99号和100号日志,从而将备库恢复到一致的状态。由于服务器重启,不同的配置,日志轮转或者FLUSH LOGS命令,同一个事件在不同的服务器上可能有不同的偏移量。找到这些事件可能会耗时很长并且枯燥,但是通常没有难度。通过mysqlbinlog从二进制日志或中继日志中解析出每台备库上执行的最后一个事件,并同样使用该命令解析新主库上的二进制日志,找到相同的查询,mysqlbinlog会打印出该事件的偏移量,在CHANGE MASTER TO命令中使用这个值。(pt-heartbeat的心跳记录能够很好地帮助你好到正在查找的事件的大约位置)
更快的方法是把新主库和停止的备库上的字节偏移量相减,它显示了字节位置的差异。然后把这个值和新主库当前二进制日志的位置相减,就可以得到期望的查询的位置。只需要验证一下就可以据此启动备库。
假设server1是server2和server3的主库,其中服务器server1已经崩溃。根据SHOW SLAVE STATUS获得Master_Log_File/Read_Master_Log_Pos的值,server2已经执行完了server1上所有的二进制日志,但server3还不是最新的数据,如图显示了这个场景(日志事件和偏移量仅仅是为了举例).正如图所示。我们可以肯定server2已经执行完主库上的所有二进制日志,因为Master_Log_File和Read_Master_Log_Pos值和server1上最后的日志位置是相吻合的,因此我们可以将server2提升为新主库,并将server3设置为server2的备库。应该在server3上为需要执行的CHANGE MASTER TO语句赋予什么样样的参数呢?这里需要做一点点计算和调整。server3在偏移量1493停止,比server2执行的最后一条语句的偏移量1582小89字节。server2正在向偏移量为8167的二进制日志写入,8167-89=8078,因此理论上我们应该将server3指向server2的日志的偏移量为8078的位置。最好去确认下这个位置附近的日志事件,以确定在该位置上是否是正确的日志事件,因为可能有别的例外,例如有些更新可能只发生在server2上。假设我们光查到的事件是一样的,下面这条命令会将server3切换为server2的备库。
```sql
server2>CHANGE MASTER TO MASTER_HOST="server2",MASTER_LOG_FILE="mysql-bin.000009", MASTER_LOG_POS=8078;
```
如果服务器在它崩溃时已经执行完成并记录了超过一个事件,会怎样呢?因为server2仅仅读取并执行到了偏移量1582,你可能永远地失去了一个事件。但是如果老主库的磁盘没有损坏,仍然可以通过mysqlbinlog或者从日志服务器的二进制日志中找到丢失的事件。
如果需要从老朱库上恢复丢失的事件,建议在提升新主库之后且在允许客户端连接之前做这件事情,这样就无须再每台备库上都执行丢失的事件,只需要使用复制来完成。但如果崩溃的老主库完全不可用,就不得不等待,稍后再做这项工作。
上述流程中一个可调整的地方是使用可靠的方式来存储二进制日志,如SAN或分布式复制数据库设备(DRBD).即使主库完全失效,依然能够获得它的二进制日志。也可以设置一个日志服务器,把备库指向它,然后让所有备库赶上主库失效的点。这使得提升一个备库为新的主库没那么中国要。本质上这和计划中的提升是相同的。(当提升一台备库为主库时,千万不要将它鞥多服务器ID修改成源主库的服务器ID,否则将不能使用日志服务器从一个旧主库来重放日志事件。这也是确保服务器ID最好保持不变的原因之一)
在一个主-主配置中交换角色。
主-主复制拓扑结构的一个好处就是可以很容易地切换主动和被动的角色,因为其配置是对称的。当在主-主配置下切换角色时,必须确保任何时候只有一个服务器可以写入。如果两台服务器交叉写入,可能会导致写入冲突。换句话说,在切换角色后,原被动服务器不应该接收到主动服务器的任何二进制日志。可以通过确保原被动服务器的复制SQL线程在该服务器可写之前已经赶上主动服务器来避免。通过以下步骤切换服务器角色,可以避免更新冲突的危险:
1.停止主动服务器上的所有写入
2.在主动服务器上执行SET GLOBAL read_only=1,同时在配置文件里也设置以下read_only,防止重启后失效。但记住这不会阻止拥有超级权限的用户更改数据。如果想阻止所有人更改数据,可以执行FLUSH TABLES WITH READ LOCK。如果没有这么做,你必须kill所有的客户端连接以保证没有长时间运行的语句或者未提交的事务
3.在主动服务器上执行SHOW MASTER STATUS并记录二进制日志坐标
4.使用主动服务器上的二进制日志坐标在被动服务器上执行SELECT MASTER _POS_WAIT().该语句将阻塞住,直到复制跟上主动服务器
5.在被动服务器上执行SET GLOBAL read_only=0,这样就变换成主动服务器。
6.修改应用的配置,使其写入到新的主动服务器中
可能还需要做一些额外的工作,包括更改两台服务器的IP地址,这取决于应用的配置
主-主复制拓扑结构的一个好处就是可以很容易地切换主动和被动的角色,因为其配置是对称的。当在主-主配置下切换角色时,必须确保任何时候只有一个服务器可以写入。如果两台服务器交叉写入,可能会导致写入冲突。换句话说,在切换角色后,原被动服务器不应该接收到主动服务器的任何二进制日志。可以通过确保原被动服务器的复制SQL线程在该服务器可写之前已经赶上主动服务器来避免。通过以下步骤切换服务器角色,可以避免更新冲突的危险:
1.停止主动服务器上的所有写入
2.在主动服务器上执行SET GLOBAL read_only=1,同时在配置文件里也设置以下read_only,防止重启后失效。但记住这不会阻止拥有超级权限的用户更改数据。如果想阻止所有人更改数据,可以执行FLUSH TABLES WITH READ LOCK。如果没有这么做,你必须kill所有的客户端连接以保证没有长时间运行的语句或者未提交的事务
3.在主动服务器上执行SHOW MASTER STATUS并记录二进制日志坐标
4.使用主动服务器上的二进制日志坐标在被动服务器上执行SELECT MASTER _POS_WAIT().该语句将阻塞住,直到复制跟上主动服务器
5.在被动服务器上执行SET GLOBAL read_only=0,这样就变换成主动服务器。
6.修改应用的配置,使其写入到新的主动服务器中
可能还需要做一些额外的工作,包括更改两台服务器的IP地址,这取决于应用的配置
复制的问题和解决方案。
中断MySQL的复制并不是件难事,因为实现简单,配置相当容易,但也意味着有很多的方式导致复制停止,现如混乱并中断。
中断MySQL的复制并不是件难事,因为实现简单,配置相当容易,但也意味着有很多的方式导致复制停止,现如混乱并中断。
数据损坏或丢失的错误。
由于各种各样的原因,MySQL的复制并不能很好地从服务器崩溃、掉电、磁盘损坏、内存或网络错误中恢复。遇到这些问题时几乎可以肯定都需要从某个点开始重启复制。大部分由于非正常关机后导致的复制问题都是由于没有把数据及时地刷到磁盘。下面是意外关闭服务器时可能会碰到的情况.
1.主库意外关闭
如果没有设置主库的sync_binlog选项,就可能在崩溃前没有将最后的几个二进制日志事件刷新到磁盘中。备库IO线程因此也可能一直处于读不到尚未写入磁盘的事件的状态中。当主库重新启动时,备库将重连到主库并在此尝试去读该事件,但主库会告诉备库没有这个二进制日志偏移量。二进制日志转储线程通常很快,因此这种情况步经常发生。解决这个问题的方法是指定备库从下一个二进制日志的开头读日志。但是一些日志事件将永久地丢失,建议使用Percona Toolkit中的pt-table-checksum工具来检查主备一致性,以便于修复。可以通过在主库开启sync_binlog来避免事件丢失。即使开启了sync_binlog.MyISAM表的数据仍然可能在崩溃的时候损坏,对于InnoDB事务,如果innodb_flush_log_at_trx_commit没有设为1,也可能丢失数据(但数据不会损坏)
2.备库意外关闭
当备库在一次非计划中的关闭后重启时,会去读master.info文件以找到上次停止复制的位置。不幸的是,该文件并没有同步写到磁盘,文件中存储的信息可能是错误的。备库可能会尝试重新执行一些二进制日志事件,这可能会导致唯一索引错误。除非能确定备库在哪里停止(通常不太可能),否则唯一的办法就是忽略那些错误。Percona Toolkit中的pt-slave-restart工具可以帮助完成这一点。如果使用的都是InnoDB表,可以在重启观察MySQl错误日志。InnoDB在恢复过程中会打印出它的恢复点的二进制日志坐标。可以使用这个只来决定备库指向主库的偏移量。Percona Server提供了一个新的特性,可以在恢复的过程中自动将这些信息提取出来,并更新master.info文件,从根本上使得复制能够协调好备库上的事务。MySQL5.5也提供了一些选项来控制i如何将master.info和其他文件刷新到磁盘,这有助于减少这些问题。
除了由于MySQL非正常关闭导致的数据丢失外,磁盘上的二进制日志或终极日志文件损坏并不罕见,下面是一些更普遍的场景:
1.主库上的二进制日志损坏。
如果主库上的二进制日志损坏,除了忽略损坏的位置外你别无选择。可以在主库上执行FLUSH LOGS命令,这样主库会开始一个新的日志文件,然后将备库指向该文件的开始位置。也可以试着去发现损坏区域的结束位置。某些情况下可以通过SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1来忽略一个损坏的事件。如果有多个损坏的事件,就需要重复该步骤,直到跳过所有损坏的事件。但如果有太多的损坏事件,这么做可能就没有意义了。损坏的事件会阻止服务器找到下一个事件。这种情况下,可能不得不手动地去找到下一个完好地事件。
2.备库上的中继日志损坏
如果主库上的日志是完好的,就可以通过CHNAGE MASTER TO命令丢弃并重新获取损坏的事件。只需要将备库指向它当前正在复制的位置(Relay_Master_Log_File/Exec_Master_Log_Pos)这会导致备库丢弃所有在磁盘上的中继日志。就这一点而言,MySQL5.5做了一些改进,它能够在崩溃后自动重新获取中继日志。
3.二进制日志与InnoDB事务日志不同步
当主库崩溃时,InnoDB可能将一个事务标记为已提交,此时盖世五可能还没有记录到二进制日志中,除非是某个备库的中继日志已经保存,否则没有任何办法恢复丢弃的事务。在MySQL5.0版本可以设置sync_binlog选项来防止该问题,对于更早的MySQL4.1可以设置sync_binlog和safe_binlog选项
由于各种各样的原因,MySQL的复制并不能很好地从服务器崩溃、掉电、磁盘损坏、内存或网络错误中恢复。遇到这些问题时几乎可以肯定都需要从某个点开始重启复制。大部分由于非正常关机后导致的复制问题都是由于没有把数据及时地刷到磁盘。下面是意外关闭服务器时可能会碰到的情况.
1.主库意外关闭
如果没有设置主库的sync_binlog选项,就可能在崩溃前没有将最后的几个二进制日志事件刷新到磁盘中。备库IO线程因此也可能一直处于读不到尚未写入磁盘的事件的状态中。当主库重新启动时,备库将重连到主库并在此尝试去读该事件,但主库会告诉备库没有这个二进制日志偏移量。二进制日志转储线程通常很快,因此这种情况步经常发生。解决这个问题的方法是指定备库从下一个二进制日志的开头读日志。但是一些日志事件将永久地丢失,建议使用Percona Toolkit中的pt-table-checksum工具来检查主备一致性,以便于修复。可以通过在主库开启sync_binlog来避免事件丢失。即使开启了sync_binlog.MyISAM表的数据仍然可能在崩溃的时候损坏,对于InnoDB事务,如果innodb_flush_log_at_trx_commit没有设为1,也可能丢失数据(但数据不会损坏)
2.备库意外关闭
当备库在一次非计划中的关闭后重启时,会去读master.info文件以找到上次停止复制的位置。不幸的是,该文件并没有同步写到磁盘,文件中存储的信息可能是错误的。备库可能会尝试重新执行一些二进制日志事件,这可能会导致唯一索引错误。除非能确定备库在哪里停止(通常不太可能),否则唯一的办法就是忽略那些错误。Percona Toolkit中的pt-slave-restart工具可以帮助完成这一点。如果使用的都是InnoDB表,可以在重启观察MySQl错误日志。InnoDB在恢复过程中会打印出它的恢复点的二进制日志坐标。可以使用这个只来决定备库指向主库的偏移量。Percona Server提供了一个新的特性,可以在恢复的过程中自动将这些信息提取出来,并更新master.info文件,从根本上使得复制能够协调好备库上的事务。MySQL5.5也提供了一些选项来控制i如何将master.info和其他文件刷新到磁盘,这有助于减少这些问题。
除了由于MySQL非正常关闭导致的数据丢失外,磁盘上的二进制日志或终极日志文件损坏并不罕见,下面是一些更普遍的场景:
1.主库上的二进制日志损坏。
如果主库上的二进制日志损坏,除了忽略损坏的位置外你别无选择。可以在主库上执行FLUSH LOGS命令,这样主库会开始一个新的日志文件,然后将备库指向该文件的开始位置。也可以试着去发现损坏区域的结束位置。某些情况下可以通过SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1来忽略一个损坏的事件。如果有多个损坏的事件,就需要重复该步骤,直到跳过所有损坏的事件。但如果有太多的损坏事件,这么做可能就没有意义了。损坏的事件会阻止服务器找到下一个事件。这种情况下,可能不得不手动地去找到下一个完好地事件。
2.备库上的中继日志损坏
如果主库上的日志是完好的,就可以通过CHNAGE MASTER TO命令丢弃并重新获取损坏的事件。只需要将备库指向它当前正在复制的位置(Relay_Master_Log_File/Exec_Master_Log_Pos)这会导致备库丢弃所有在磁盘上的中继日志。就这一点而言,MySQL5.5做了一些改进,它能够在崩溃后自动重新获取中继日志。
3.二进制日志与InnoDB事务日志不同步
当主库崩溃时,InnoDB可能将一个事务标记为已提交,此时盖世五可能还没有记录到二进制日志中,除非是某个备库的中继日志已经保存,否则没有任何办法恢复丢弃的事务。在MySQL5.0版本可以设置sync_binlog选项来防止该问题,对于更早的MySQL4.1可以设置sync_binlog和safe_binlog选项
当一个二进制日志损坏时,能恢复多少数据取决于损坏的类型,有几种比较常见的类型:
1.数据改变,但事件仍是有效的SQL
不幸的是,MySQL甚至无法察觉这种损坏。因此最好还是经常检查备库的数据是否正确。在MySQL未来的版本中可能会被修复
2.数据改变并且事件是无效的SQL
这种情况可以通过mysqlbinlog提取出事件并看到一些错乱的数据,例如:
```sql
UPDATE tbl SET col ???????
```
可以通过增加偏移量的方式来尝试找到下一个事件,这样就可以只忽略这个损坏的事件。
3.数据一楼并且/或者事件的长度是错误的
这种情况下,mysqlbinlog可能会发生错误退出或者直接崩溃,因为它无法读取事件,并且找不到下一个事件的开始位置
4.某些事件已经损坏或被覆盖,或者偏移量已经改变并且下一个事件的起始偏移量也是错误的。
同样的,这种情况下mysqlbinlog也起不了多少作用
当损坏非常严重,通过mysqlbinlog已经无法获取日志事件时,就不得步进行一些十六进制的编辑或者通过一些繁琐的技术来找到日志事件的边界。这通常并不困难,因为有一些可辨识的标记会分割事件。如下例所示,首先使用mysqlbinlog找到样例日志的日志事件偏移量
```sql
$ mysqlbinlog mysql-bin.000113 | grep '^# at'
```
一个找到日志偏移量的比较简单的方法是比较以下string命令输出的偏移量:
```sql
$ strings -n 2 -t d mysql-bin.000113
```
mysqlbinlog默认在/var/lib/mysql目录下
有一些可辩别的模式可以帮助定位事件的开头,注意以'G结尾的字符串在日志事件开头的一个字节后的位置。它们是固定长度的事件头的一部分。这些值因服务器而异,因此结果也可能取决于解析的日志所在的服务器。简单地分析后应该能够从二进制日志中找到这些模式并找到下一个完整的日志事件偏移量。然后通过mysqlbinlog的--start-position选项来跳过损坏的事件,或者使用CHANGE MASTER TO命令的MASTER_LOG_POS参数。
1.数据改变,但事件仍是有效的SQL
不幸的是,MySQL甚至无法察觉这种损坏。因此最好还是经常检查备库的数据是否正确。在MySQL未来的版本中可能会被修复
2.数据改变并且事件是无效的SQL
这种情况可以通过mysqlbinlog提取出事件并看到一些错乱的数据,例如:
```sql
UPDATE tbl SET col ???????
```
可以通过增加偏移量的方式来尝试找到下一个事件,这样就可以只忽略这个损坏的事件。
3.数据一楼并且/或者事件的长度是错误的
这种情况下,mysqlbinlog可能会发生错误退出或者直接崩溃,因为它无法读取事件,并且找不到下一个事件的开始位置
4.某些事件已经损坏或被覆盖,或者偏移量已经改变并且下一个事件的起始偏移量也是错误的。
同样的,这种情况下mysqlbinlog也起不了多少作用
当损坏非常严重,通过mysqlbinlog已经无法获取日志事件时,就不得步进行一些十六进制的编辑或者通过一些繁琐的技术来找到日志事件的边界。这通常并不困难,因为有一些可辨识的标记会分割事件。如下例所示,首先使用mysqlbinlog找到样例日志的日志事件偏移量
```sql
$ mysqlbinlog mysql-bin.000113 | grep '^# at'
```
一个找到日志偏移量的比较简单的方法是比较以下string命令输出的偏移量:
```sql
$ strings -n 2 -t d mysql-bin.000113
```
mysqlbinlog默认在/var/lib/mysql目录下
有一些可辩别的模式可以帮助定位事件的开头,注意以'G结尾的字符串在日志事件开头的一个字节后的位置。它们是固定长度的事件头的一部分。这些值因服务器而异,因此结果也可能取决于解析的日志所在的服务器。简单地分析后应该能够从二进制日志中找到这些模式并找到下一个完整的日志事件偏移量。然后通过mysqlbinlog的--start-position选项来跳过损坏的事件,或者使用CHANGE MASTER TO命令的MASTER_LOG_POS参数。
使用非事务型表。
如果一切正常,基于语句的复制通常能够很好地处理非事务型表。但是当对非事务型表的更新发生错误时,例如查询在完成前被kill,就可能导致主库和备库的数据不一致。例如,假设更新一个MyISAM表的100行数据,若查询更新到了其中50条时有人kill该查询,会发生什么呢?一半的数据改变了,而另一半则没有,结果是复制必然不同步,因为该查询会在备库重放并更新完100行数据(MySQL随后会在主库上发现查询引起的错误,而备库上则没有报错,此后复制将会发生错误并中断)。
如果使用的是MyISAM表,在关闭MySQL之前需要确保已经运行了STOP SLAVE,否则服务器在关闭时会kill所有正在运行的查询(包括没有完成的更新)。事务型存储引擎则没有这个问题。如果使用的是事务型表,失败的更新会在主库上回滚并且不会记录到二进制日志中
如果一切正常,基于语句的复制通常能够很好地处理非事务型表。但是当对非事务型表的更新发生错误时,例如查询在完成前被kill,就可能导致主库和备库的数据不一致。例如,假设更新一个MyISAM表的100行数据,若查询更新到了其中50条时有人kill该查询,会发生什么呢?一半的数据改变了,而另一半则没有,结果是复制必然不同步,因为该查询会在备库重放并更新完100行数据(MySQL随后会在主库上发现查询引起的错误,而备库上则没有报错,此后复制将会发生错误并中断)。
如果使用的是MyISAM表,在关闭MySQL之前需要确保已经运行了STOP SLAVE,否则服务器在关闭时会kill所有正在运行的查询(包括没有完成的更新)。事务型存储引擎则没有这个问题。如果使用的是事务型表,失败的更新会在主库上回滚并且不会记录到二进制日志中
混合事务型和非事务型表。
如果使用的是事务型存储引擎,只有在事务提交后才会将查询记录到二进制日志中。因此如果事务回滚,MySQL就不会记录这条查询,也就不会在备库上重放。但是如果混合使用事务型和非事务型表,并且发生了一次回滚,MySQL能够回滚事务型表的更新,但非事务型表则被永久地更新了。只要不发生类似查询中途被kill这样的错误,这就不是问题:MySQL此时会记录该查询并记录一条ROLLBACK语句到日志中,结果是同样的语句也在备库中执行,所有的都很正常。这样效率会第一点,因为备库需要做一些工作并且最后再把它们丢弃掉。但理论上能够保证主备的数据一致。
目前看来一切很正常。但是如果备库发生死锁而主库没有也可能会导致问题。事务型表的更新会被回滚,而非事务型表则无法回滚,此时备库和主库的数据是不一致的。
防止该问题的唯一办法是避免混合使用事务型和非事务型表.如果遇到这个问题,唯一的解决办法是忽略错误,并重新同步相关的表。基于行的复制不会受到这个问题的影响。因为它记录的是数据的更改。而不是SQL语句。如果一条语句改变了一个MyISAM表和一个InnoDB表的某些行,然后主库发生了一次死锁,InnoDB表的更新会被回滚,而MyISAM表的更新仍会被记录到日志中并在备库重放。
如果使用的是事务型存储引擎,只有在事务提交后才会将查询记录到二进制日志中。因此如果事务回滚,MySQL就不会记录这条查询,也就不会在备库上重放。但是如果混合使用事务型和非事务型表,并且发生了一次回滚,MySQL能够回滚事务型表的更新,但非事务型表则被永久地更新了。只要不发生类似查询中途被kill这样的错误,这就不是问题:MySQL此时会记录该查询并记录一条ROLLBACK语句到日志中,结果是同样的语句也在备库中执行,所有的都很正常。这样效率会第一点,因为备库需要做一些工作并且最后再把它们丢弃掉。但理论上能够保证主备的数据一致。
目前看来一切很正常。但是如果备库发生死锁而主库没有也可能会导致问题。事务型表的更新会被回滚,而非事务型表则无法回滚,此时备库和主库的数据是不一致的。
防止该问题的唯一办法是避免混合使用事务型和非事务型表.如果遇到这个问题,唯一的解决办法是忽略错误,并重新同步相关的表。基于行的复制不会受到这个问题的影响。因为它记录的是数据的更改。而不是SQL语句。如果一条语句改变了一个MyISAM表和一个InnoDB表的某些行,然后主库发生了一次死锁,InnoDB表的更新会被回滚,而MyISAM表的更新仍会被记录到日志中并在备库重放。
不确定语句。
当使用基于语句的复制模式时,如果通过不确定的方式更改数据可能会导致主备不一致。例如,一条带LIMIT的UPDATE语句更改的数据取决于查找行的顺序,除非能保证主库和备库上的顺序相同。例如,若行更具主键排序,一条查询可能在主库和备库上更新不同的行,这些问题非常微妙并且很难注意到。所以一些人进制对那些更新数据的语句使用LIMIT.另外一种不确定的行为是在一个拥有多个唯一索引的表上使用REPLACE或者INSERT IGNORE语句——MySQL在主库和备库上可能会选择不同的索引。
另外还要注意那些设计INFORMATION_SCHEMA表的语句。它们很容易在主库和备库上产生不一致,其结果也会不同。最后需要注意许多系统变量,例如@@server_id和@@hostname,在MySQL5.1之前无法正确地复制,基于行地复制则没有上述限制
当使用基于语句的复制模式时,如果通过不确定的方式更改数据可能会导致主备不一致。例如,一条带LIMIT的UPDATE语句更改的数据取决于查找行的顺序,除非能保证主库和备库上的顺序相同。例如,若行更具主键排序,一条查询可能在主库和备库上更新不同的行,这些问题非常微妙并且很难注意到。所以一些人进制对那些更新数据的语句使用LIMIT.另外一种不确定的行为是在一个拥有多个唯一索引的表上使用REPLACE或者INSERT IGNORE语句——MySQL在主库和备库上可能会选择不同的索引。
另外还要注意那些设计INFORMATION_SCHEMA表的语句。它们很容易在主库和备库上产生不一致,其结果也会不同。最后需要注意许多系统变量,例如@@server_id和@@hostname,在MySQL5.1之前无法正确地复制,基于行地复制则没有上述限制
主库和备库使用不同的存储引擎。
在悲苦上使用不同的存储引擎,有时候可以带来好处。但是在一些场景下,当使用基于语句的复制方式时,如果备库使用了不同的存储引擎,则可能造成一条查询在主库和备库上的执行结果不同,例如不确定语句在主库使用不同的存储引擎更容易导致问题。如果发现主库和备库的某些表已经不同步,除了检查更新这些表的查询外,还需要检查两台服务器上使用的存储引擎是否相同。
在悲苦上使用不同的存储引擎,有时候可以带来好处。但是在一些场景下,当使用基于语句的复制方式时,如果备库使用了不同的存储引擎,则可能造成一条查询在主库和备库上的执行结果不同,例如不确定语句在主库使用不同的存储引擎更容易导致问题。如果发现主库和备库的某些表已经不同步,除了检查更新这些表的查询外,还需要检查两台服务器上使用的存储引擎是否相同。
备库发生数据改变。
基于语句的复制方式前提时确保备库和主库相同的数据,因此不应该允许对备库数据的任何更改(比较好的办法是设置read only选项)。假设有如下语句:
```sql
mysql>INSERT INTO table1 SELECT * FROM table2;
```
如果备库上table2的数据和主库上不同,该语句会导致table1的数据也会不一致。换句话说,数据不一致可能会在表之间传播。不仅仅是INSERT....SELECT查询,所有类型的查询都可能发生。有两种可能的结果:备库上发生重复索引键冲突错误或者根本不提示任何错误。如果能报告错误还好,起码能够提示你主备数据已经不一致。无法察觉的不一致可能会悄无声息地导致各种严重的问题。唯一解决的办法就是重新从主库同步数据
基于语句的复制方式前提时确保备库和主库相同的数据,因此不应该允许对备库数据的任何更改(比较好的办法是设置read only选项)。假设有如下语句:
```sql
mysql>INSERT INTO table1 SELECT * FROM table2;
```
如果备库上table2的数据和主库上不同,该语句会导致table1的数据也会不一致。换句话说,数据不一致可能会在表之间传播。不仅仅是INSERT....SELECT查询,所有类型的查询都可能发生。有两种可能的结果:备库上发生重复索引键冲突错误或者根本不提示任何错误。如果能报告错误还好,起码能够提示你主备数据已经不一致。无法察觉的不一致可能会悄无声息地导致各种严重的问题。唯一解决的办法就是重新从主库同步数据
不唯一的服务器ID。
这种问题更加难以捉摸。如果不小心为两台备库设置了相同的服务器ID,看起来似乎没有什么问题,但如果查看错误日志,或者使用innotop查看主库,可能会看到一些古怪的信息。在主库上,会发现两台备库中只有一台连接到主库(通常情况下所有的备库都会建立连接以等待随时进行复制)。在备库的错误日志中,则会发现反复的重连和连接断开信息,但不会提及被错误配置的服务器ID。
MySQL可能会缓慢地进行正确的复制,也可能无法进行正确复制,这取决于MySQL的版本,给定的备库可能会丢失二进制日志事件,或者重复执行事件,导致重复键错误(或者不可见的数据损坏)。也可能因为备库的互相竞争造成主库的负载升高。如果备库竞争非常激烈,回导致错误日志在很短的时间内急剧增大。唯一的解决办法是小心设置备库的服务器ID。一个比较号的办法是创建一个主库到备库的服务器ID映射表,这样就可以跟踪到备库的ID信息(也许你想把它保存在服务器中,这 不完全是玩笑,可以给ID添加一个唯一索引)。如果备库全在一个自网络内,可以将每台机器IP的后八位作为唯一ID
这种问题更加难以捉摸。如果不小心为两台备库设置了相同的服务器ID,看起来似乎没有什么问题,但如果查看错误日志,或者使用innotop查看主库,可能会看到一些古怪的信息。在主库上,会发现两台备库中只有一台连接到主库(通常情况下所有的备库都会建立连接以等待随时进行复制)。在备库的错误日志中,则会发现反复的重连和连接断开信息,但不会提及被错误配置的服务器ID。
MySQL可能会缓慢地进行正确的复制,也可能无法进行正确复制,这取决于MySQL的版本,给定的备库可能会丢失二进制日志事件,或者重复执行事件,导致重复键错误(或者不可见的数据损坏)。也可能因为备库的互相竞争造成主库的负载升高。如果备库竞争非常激烈,回导致错误日志在很短的时间内急剧增大。唯一的解决办法是小心设置备库的服务器ID。一个比较号的办法是创建一个主库到备库的服务器ID映射表,这样就可以跟踪到备库的ID信息(也许你想把它保存在服务器中,这 不完全是玩笑,可以给ID添加一个唯一索引)。如果备库全在一个自网络内,可以将每台机器IP的后八位作为唯一ID
未定义的服务器ID。
如果没有在my.cnf里面定义服务器ID,可以通过CHANGE MASTER TO 来设置备库,但却无法启动复制。
```c
mysql>START SLAVE;
ERROR 1200(HY000):The server is not configured as slave;fix in config file or with CHANGE MASTER TO
```
这个报错可能会让人困惑,因为刚刚执行CHNAGE MASTER TO 设置了备库,并且通过SHOW MASTER STATUS也确认了。执行SELECT @@server_id也可以获得一个值,但这只是默认值,必须为备库显式地设置服务器ID.
如果没有在my.cnf里面定义服务器ID,可以通过CHANGE MASTER TO 来设置备库,但却无法启动复制。
```c
mysql>START SLAVE;
ERROR 1200(HY000):The server is not configured as slave;fix in config file or with CHANGE MASTER TO
```
这个报错可能会让人困惑,因为刚刚执行CHNAGE MASTER TO 设置了备库,并且通过SHOW MASTER STATUS也确认了。执行SELECT @@server_id也可以获得一个值,但这只是默认值,必须为备库显式地设置服务器ID.
对未复制数据的依赖性。
如果在主库上有备库不存在的数据库或表,复制会很容易意外中断,反之亦然。假设主库上有一个备库不存在的数据库,命令为scratch。如果在主库上发生对该数据库中表的更新,备库会在尝试重放这些更新时中断。同样的,如果在主库上创建一个备库上已存在的表,复制也可能中断。
没有什么好的解决办法,唯一的办法就是避免在主库上创建备库上没有的表。这样的表是如何创建的呢?有很多可能的方式,其中一些可能更难防范。例如,假设先在备库上创建一个数据库scratch,该数据库在主库上不存在,然后因为某些原因切换了主备。当完成这些后,可能忘记了移除scratch数据库以及它的权限。这时候一些人就可以连接到该数据库并执行一些查询,或者一些定期的任务会发现这些表,并在每个表上执行OPTIMIZE TABLE命令。当提升备库未主库时,或者决定如何配置备库时,需要注意这一点。任何导致主备不同的行为都会产生潜在的问题。
如果在主库上有备库不存在的数据库或表,复制会很容易意外中断,反之亦然。假设主库上有一个备库不存在的数据库,命令为scratch。如果在主库上发生对该数据库中表的更新,备库会在尝试重放这些更新时中断。同样的,如果在主库上创建一个备库上已存在的表,复制也可能中断。
没有什么好的解决办法,唯一的办法就是避免在主库上创建备库上没有的表。这样的表是如何创建的呢?有很多可能的方式,其中一些可能更难防范。例如,假设先在备库上创建一个数据库scratch,该数据库在主库上不存在,然后因为某些原因切换了主备。当完成这些后,可能忘记了移除scratch数据库以及它的权限。这时候一些人就可以连接到该数据库并执行一些查询,或者一些定期的任务会发现这些表,并在每个表上执行OPTIMIZE TABLE命令。当提升备库未主库时,或者决定如何配置备库时,需要注意这一点。任何导致主备不同的行为都会产生潜在的问题。
丢失的临时表。
临时表在某些时候比较有用,但不幸的是,它与基于语句的复制是不相容的。如果备库崩溃或者正常关闭,任何复制线程拥有的临时表都会丢失。重启备库后,所有依赖于该临时表的语句都会失败。当基于语句进行复制时,在主库上并没有安全使用临时表的方法。许多人确实很喜欢临时表,所以很难说服他们,但这是不可否认的。(已经有人尝试各种方法来解决这个问题,但对于基于语句的复制并没有安全的临时表创建方法,起码一段时期时这样,不管你如何认为,起码已经证明了这是不可行的)。不管它们的存在多么短暂,都会使得备库的启动和停止以及崩溃恢复变得困难,即使是在一个事务内使用也一样。(如果在备库使用临时表可能问题会少些,但如果备库本身也是一个主库,问题依然存在)。
如果备库重启后复制因找不到临时表而停止,可能需要做以下一些事情:可以直接跳过错误,或者手动地创建一个名字和结构相同的表来代替消失的临时表。不管用什么办法,如果写入查询依赖于临时表,都可能造成数据不一致。避免使用临时表没有看起来那么难,临时表主要有两个比较有用的特性:
1.只对创建临时表的连接可见。所以不会和其他拥有相同名字临时表的链接起冲突
2.随者连接关闭而消失,所以无须显式地移除它们。
可以保留一个专用的数据库,在其中创建持久表,把它们作为伪临时表,以模拟这些特性。只需要为它们选择一个唯一的名字,还好这容易做到:简单将连接ID拼接到表名之后,例如,之前创建临时表的语句为:CREATE TEMPORARY TABLE top_user(...),现在则可以执行CREATE TABLE temp.top_users_1234(...),其中1234是函数CONECTION_ID()返回值。当应用不再使用伪临时表后,可以将其删除或使用一个清理线程来将其移除。表名中使用连接ID可以用于确定哪些表不再被使用——可以通过SHOW PROCESSLIST命令来获得活跃连接列表,并将其与表名中的连接ID相比较。使用实体表而非临时表还有别的好处。例如,能够帮助你更容易调试应用程序,因为可能通过别的连接来查看应用正在维护的数据。如果使用的是临时表,可能就没这么容易做到。到那时实体表可能会比临时表多一些开销,例如创建会更慢,因为这些表分配的.frm文件需要刷新到磁盘。可以通过进制sync_frm选项来加速,但这可能会导致潜在的风险。
如果确实需要使用临时表,也应该在关闭备库前确保Slave_open_temp_tables状态变量的值为0.如果不是0,在重启备库后就可能会出现问题。合适的流程是执行STOP SLAVE,检查变量,然后再关闭备库。如果在停止复制前检查变量,可能会发生竞争条件的风险
临时表在某些时候比较有用,但不幸的是,它与基于语句的复制是不相容的。如果备库崩溃或者正常关闭,任何复制线程拥有的临时表都会丢失。重启备库后,所有依赖于该临时表的语句都会失败。当基于语句进行复制时,在主库上并没有安全使用临时表的方法。许多人确实很喜欢临时表,所以很难说服他们,但这是不可否认的。(已经有人尝试各种方法来解决这个问题,但对于基于语句的复制并没有安全的临时表创建方法,起码一段时期时这样,不管你如何认为,起码已经证明了这是不可行的)。不管它们的存在多么短暂,都会使得备库的启动和停止以及崩溃恢复变得困难,即使是在一个事务内使用也一样。(如果在备库使用临时表可能问题会少些,但如果备库本身也是一个主库,问题依然存在)。
如果备库重启后复制因找不到临时表而停止,可能需要做以下一些事情:可以直接跳过错误,或者手动地创建一个名字和结构相同的表来代替消失的临时表。不管用什么办法,如果写入查询依赖于临时表,都可能造成数据不一致。避免使用临时表没有看起来那么难,临时表主要有两个比较有用的特性:
1.只对创建临时表的连接可见。所以不会和其他拥有相同名字临时表的链接起冲突
2.随者连接关闭而消失,所以无须显式地移除它们。
可以保留一个专用的数据库,在其中创建持久表,把它们作为伪临时表,以模拟这些特性。只需要为它们选择一个唯一的名字,还好这容易做到:简单将连接ID拼接到表名之后,例如,之前创建临时表的语句为:CREATE TEMPORARY TABLE top_user(...),现在则可以执行CREATE TABLE temp.top_users_1234(...),其中1234是函数CONECTION_ID()返回值。当应用不再使用伪临时表后,可以将其删除或使用一个清理线程来将其移除。表名中使用连接ID可以用于确定哪些表不再被使用——可以通过SHOW PROCESSLIST命令来获得活跃连接列表,并将其与表名中的连接ID相比较。使用实体表而非临时表还有别的好处。例如,能够帮助你更容易调试应用程序,因为可能通过别的连接来查看应用正在维护的数据。如果使用的是临时表,可能就没这么容易做到。到那时实体表可能会比临时表多一些开销,例如创建会更慢,因为这些表分配的.frm文件需要刷新到磁盘。可以通过进制sync_frm选项来加速,但这可能会导致潜在的风险。
如果确实需要使用临时表,也应该在关闭备库前确保Slave_open_temp_tables状态变量的值为0.如果不是0,在重启备库后就可能会出现问题。合适的流程是执行STOP SLAVE,检查变量,然后再关闭备库。如果在停止复制前检查变量,可能会发生竞争条件的风险
不复制所有的更新。
如果错误地使用SET SQL_LOG_BIN=0或者没有理解过滤规则,备库可能会丢失主库上已经发生的更新。有时候希望利用此特性来做归档,但常常会导致意外并出现不好的结果。例如假设设置了replicate_do_db规则,把sakila数据库的数据复制到某一台备库上,如果在主库上执行如下语句,会导致主备数据不一致:
```sql
mysql>USE test;
mysql>UPDATE sakila.actor ....
```
其他类型的语句甚至会因为没有复制依赖导致备库复制抛出错误而失败。
如果错误地使用SET SQL_LOG_BIN=0或者没有理解过滤规则,备库可能会丢失主库上已经发生的更新。有时候希望利用此特性来做归档,但常常会导致意外并出现不好的结果。例如假设设置了replicate_do_db规则,把sakila数据库的数据复制到某一台备库上,如果在主库上执行如下语句,会导致主备数据不一致:
```sql
mysql>USE test;
mysql>UPDATE sakila.actor ....
```
其他类型的语句甚至会因为没有复制依赖导致备库复制抛出错误而失败。
InnoDB加锁引起的锁争用。
正常情况下,InnoDB的读操作是非阻塞的,但在某些情况下需要加锁。特别是在使用基于语句的复制方式时,执行INSERT...SELECT操作会锁定源表上的所有行。MySQL需要加锁以确保该语句的执行结果在主库和备库上是一致的。实际上,加锁导致主库上的语句串行化,以确保和备库上执行的方式相符。
这种设计可能导致锁竞争、阻塞,以及锁等待超时等情况。一种缓解的办法就是避免让事务开启太久以减少阻塞。可以在主库上尽快地提交事务以释放锁。把大命令拆分成小命令,使其尽可能简短。这也是一种减少锁竞争的有效方法。即使有时很难做到,但也是值得的。
另一种办法是替换掉INSERT ... SELECT语句,在主库上先执行SELECT INTO OUTFILE,再执行LOAD DATA INFILE。这种方法根块,并且不需要加锁。这种方法很特殊,但有时还是有用的。最大的问题是为输出文件选择一个唯一的名字,并在完成后清理掉文件。可以通过之前讨论过的CONNECTION_ID()来保证文件名的唯一性,并且可以使用定时任务(UNIX的crontab, Windows平台的计划任务)在连接不再使用这些文件后进行自动清理。也可以尝试关闭上面的这种锁机制,而不是使用上面的变通方法。有一种方法可以做到,但在大多数场景下并不是好办法,备库可能会在不知不觉间就失去和主库的数据同步。这也会导致在做恢复时二进制日志变得毫无用处。但如果确实觉得这么做的利大于弊,可以使用下面的办法来关闭这种锁机制:
```c
# THIS IS NOT SAFE!
innodb_locks_unsafe_for_binlog =1
```
这使得查询的结果所依赖的数据不再加锁。如果第二条查询修改了数据并在第一条查询之前先提交。在主库和备库上执行这两条语句的结果可能不同。对于复制和基于时间点的恢复都是如此。为了了解锁定读取是如何防止混乱的,假设有两张表:一个没有数据,另一个只有一行数据,值为99.有两个事务更新数据。事务1将第二张表的数据插入到第一张表,事务2更新第二张表(源表),如图所示:
第二步非常重要,事务2尝试去更新源表,这需要在更新的行上加排他锁(写锁)。排他锁与其他锁是不相容的,包括事务1在行记录上加的共享锁。因此事务2需要等待直到事务1完成。事务按照其提交的顺序在二进制日志中记录,所以在备库重放这些事务时产生相同的结果。但从另一方面来说,如果事务1没有在读取的行上加共享锁,就无法保证了,如图显示了在没有锁的情况下的可能的事件序列。
如果没有加锁,记录在日志中的事务顺序在主备上可能会产生不同的结果。MySQL会先记录事务2,这会影响到事务1在备库上的结果,而主库上则不会发生,从而导致了主备的数据不一致。强烈建议在大多数情况下将innodb_locks_unsafe_for_binlog的值设置为0。基于行的复制由于记录了数据的变化而非语句,因此不会存在这个问题。
正常情况下,InnoDB的读操作是非阻塞的,但在某些情况下需要加锁。特别是在使用基于语句的复制方式时,执行INSERT...SELECT操作会锁定源表上的所有行。MySQL需要加锁以确保该语句的执行结果在主库和备库上是一致的。实际上,加锁导致主库上的语句串行化,以确保和备库上执行的方式相符。
这种设计可能导致锁竞争、阻塞,以及锁等待超时等情况。一种缓解的办法就是避免让事务开启太久以减少阻塞。可以在主库上尽快地提交事务以释放锁。把大命令拆分成小命令,使其尽可能简短。这也是一种减少锁竞争的有效方法。即使有时很难做到,但也是值得的。
另一种办法是替换掉INSERT ... SELECT语句,在主库上先执行SELECT INTO OUTFILE,再执行LOAD DATA INFILE。这种方法根块,并且不需要加锁。这种方法很特殊,但有时还是有用的。最大的问题是为输出文件选择一个唯一的名字,并在完成后清理掉文件。可以通过之前讨论过的CONNECTION_ID()来保证文件名的唯一性,并且可以使用定时任务(UNIX的crontab, Windows平台的计划任务)在连接不再使用这些文件后进行自动清理。也可以尝试关闭上面的这种锁机制,而不是使用上面的变通方法。有一种方法可以做到,但在大多数场景下并不是好办法,备库可能会在不知不觉间就失去和主库的数据同步。这也会导致在做恢复时二进制日志变得毫无用处。但如果确实觉得这么做的利大于弊,可以使用下面的办法来关闭这种锁机制:
```c
# THIS IS NOT SAFE!
innodb_locks_unsafe_for_binlog =1
```
这使得查询的结果所依赖的数据不再加锁。如果第二条查询修改了数据并在第一条查询之前先提交。在主库和备库上执行这两条语句的结果可能不同。对于复制和基于时间点的恢复都是如此。为了了解锁定读取是如何防止混乱的,假设有两张表:一个没有数据,另一个只有一行数据,值为99.有两个事务更新数据。事务1将第二张表的数据插入到第一张表,事务2更新第二张表(源表),如图所示:
第二步非常重要,事务2尝试去更新源表,这需要在更新的行上加排他锁(写锁)。排他锁与其他锁是不相容的,包括事务1在行记录上加的共享锁。因此事务2需要等待直到事务1完成。事务按照其提交的顺序在二进制日志中记录,所以在备库重放这些事务时产生相同的结果。但从另一方面来说,如果事务1没有在读取的行上加共享锁,就无法保证了,如图显示了在没有锁的情况下的可能的事件序列。
如果没有加锁,记录在日志中的事务顺序在主备上可能会产生不同的结果。MySQL会先记录事务2,这会影响到事务1在备库上的结果,而主库上则不会发生,从而导致了主备的数据不一致。强烈建议在大多数情况下将innodb_locks_unsafe_for_binlog的值设置为0。基于行的复制由于记录了数据的变化而非语句,因此不会存在这个问题。
子主题
在主-主复制结构总写入两台主库。
试图向两台主库写入并不是一个好主意,如果同时还希望安全地写入两台主库,会碰到很多问题,有些问题可以解决,有些则很难。一个专业人员可能需要经历大量的教训才能明白其中的不同。在MySQL5.0中,有两个变量可以用于帮助解决AUTO_INCREMENT自增主键冲突的问题:auto_increment_increment和auto_increment_offset。可以通过设置这两个变量来错开主库和备库生成的数字,这样可以避免自增列的冲突。但是这并不能解决所有由于同时写入两太主库所带来的问题;自增问题只是其中的一小部分。而且这种做法也带来了一些新的问题:
1.很难在复制拓扑间作故障转移
2.由于在数字之间出现间隙,会引起键空间的浪费
3.只有在使用了AUTO_INCREMENT主键的时候才有用。有时候使用AUTO_INCREMENT列作为主键并不总是好主意
你也可以自己来生成不冲突的主键值。一种办法时创建一个多个列的主键,第一列使用服务器ID值。这种办法很好,但却使得主键的值变得更大,会对InnoDB二级索引键值产生多重影响。也可以使用只有一列的主键,在主键的高字节位存储服务器ID。简单地左移法(除法)和加法就可以实现。例如,使用的是无符号BIGINT(64位)的高8位来保存服务器ID,可以按照如下方法在服务器15上插入值11:
```sql
mysql>INSERT INTO test(pk_col, ...) VALUE ((15 << 56) + 11, ....)
```
如果想把结果转换为二进制,并将其填充64位,其效果显而易见:
```sql
mysql>SELECT LPAD(CONV(pk_col, 10,2), 64 , '0') FROM test;
```
该方法的缺点是需要额外的方式来产生键值,因为AUTO_INCREMENT无法做到这一点。不要在INSERT语句中将常量15替换为@@server_id,因为这可能在备库产生不同的结果。还可以使用MD5()或者UUID()等函数来获取伪随机数,但这样做性能可能会很差,因为它们产生的值较大,并且本质上是随机的,这尤其会影响到InnoDB(除非是在应用中产生之,否则不要使用UUID(),因为基于语句的复制模式下UUID()不能正确复制)。
这个问题很难解决,通常推荐重构应用程序,以保证只有一个主库是可写的。谁能想得到呢?
试图向两台主库写入并不是一个好主意,如果同时还希望安全地写入两台主库,会碰到很多问题,有些问题可以解决,有些则很难。一个专业人员可能需要经历大量的教训才能明白其中的不同。在MySQL5.0中,有两个变量可以用于帮助解决AUTO_INCREMENT自增主键冲突的问题:auto_increment_increment和auto_increment_offset。可以通过设置这两个变量来错开主库和备库生成的数字,这样可以避免自增列的冲突。但是这并不能解决所有由于同时写入两太主库所带来的问题;自增问题只是其中的一小部分。而且这种做法也带来了一些新的问题:
1.很难在复制拓扑间作故障转移
2.由于在数字之间出现间隙,会引起键空间的浪费
3.只有在使用了AUTO_INCREMENT主键的时候才有用。有时候使用AUTO_INCREMENT列作为主键并不总是好主意
你也可以自己来生成不冲突的主键值。一种办法时创建一个多个列的主键,第一列使用服务器ID值。这种办法很好,但却使得主键的值变得更大,会对InnoDB二级索引键值产生多重影响。也可以使用只有一列的主键,在主键的高字节位存储服务器ID。简单地左移法(除法)和加法就可以实现。例如,使用的是无符号BIGINT(64位)的高8位来保存服务器ID,可以按照如下方法在服务器15上插入值11:
```sql
mysql>INSERT INTO test(pk_col, ...) VALUE ((15 << 56) + 11, ....)
```
如果想把结果转换为二进制,并将其填充64位,其效果显而易见:
```sql
mysql>SELECT LPAD(CONV(pk_col, 10,2), 64 , '0') FROM test;
```
该方法的缺点是需要额外的方式来产生键值,因为AUTO_INCREMENT无法做到这一点。不要在INSERT语句中将常量15替换为@@server_id,因为这可能在备库产生不同的结果。还可以使用MD5()或者UUID()等函数来获取伪随机数,但这样做性能可能会很差,因为它们产生的值较大,并且本质上是随机的,这尤其会影响到InnoDB(除非是在应用中产生之,否则不要使用UUID(),因为基于语句的复制模式下UUID()不能正确复制)。
这个问题很难解决,通常推荐重构应用程序,以保证只有一个主库是可写的。谁能想得到呢?
过大的复制延迟。
复制延迟是一个很普遍的问题。不管怎么样,最好在设计应用程序时能够让其容忍备库出现延迟,如果系统在备库出现延迟时就无法很好地工作,那么应用程序也许就不应该用到复制。但是也有一些办法可以让备库跟上主库。MySQL单线程复制的设计导致备库的效率相当低下。即使备库有很多磁盘、CPU或者内存,也会很容易落后于主库。因为备库的单线程通常只会有效地使用一个CPU和磁盘。而事实上,备库通常哦都会和主库使用相同配置的机器。
备库上的锁同样也是问题。其他在备库运行的查询可能会阻塞主复制线程,因为复制时单线程的,复制线程在等待时将无法做别的事情。复制一般有两种产生延迟的方式:突然产生延迟然后再跟上,或者稳定的延迟增大。前一种通常是由于一条运行很长的查询导致的,而后者即使在没有长时间运行的查询时也会出现。不幸的是,目前我们还没那么容易确定备库是否接近其容量上限。正如之前提到的。如果负载总是保持均匀的,备库在负载达到99%时和其负载10%的时候表现的性能相同,但一旦达到100%时就会突然产生延迟。但实际上负载不太可能很稳定,所以当备库接近写容量时,就可能在尖峰负载时看到复制延迟的增加。当备库无法跟上时,可以记录备库上的查询并使用一个日志分析工具找出哪里满了。不要依赖于自己的直觉,也不要基于查询在主库上的查询性能进行判断,因为主库和备库性能特征很不相同。最好的分析办法是暂时在备库上打开慢查询日志记录,然后使用pt-query-digest工具来分析。如果打开了log_slow_slave_statements选项,在标准的MySQL慢查询日志能够记录MySQL5.1及更新的版本中复制线程执行的语句,这样就可以找到在复制时哪些语句执行慢了。Percona Server和MariaDB允许开启或进制该选项而无须重启服务器。
除了购买更快的磁盘和CPU(固态硬盘能够提供极大的帮助),备库没有太多的调优空间。大部分选项都是禁止某些额外的工作以减少备库的负载。一个简单的办法是配置InnoDB,使其不要那么频繁地刷新磁盘,这样事务会提交得更快些。可以通过设置innodb_flush_log_at_trx_commit得值为2来上限。还可以在备库上禁止二进制日志记录,把innodb_locks_unsafe_for_binlog设置为1,并把MyISAM得delay_key_write设置为ALL.但是这些设置以牺牲安全换取速度。如果需要将备库提升为主库,记得把这些选项设置回安全的值
复制延迟是一个很普遍的问题。不管怎么样,最好在设计应用程序时能够让其容忍备库出现延迟,如果系统在备库出现延迟时就无法很好地工作,那么应用程序也许就不应该用到复制。但是也有一些办法可以让备库跟上主库。MySQL单线程复制的设计导致备库的效率相当低下。即使备库有很多磁盘、CPU或者内存,也会很容易落后于主库。因为备库的单线程通常只会有效地使用一个CPU和磁盘。而事实上,备库通常哦都会和主库使用相同配置的机器。
备库上的锁同样也是问题。其他在备库运行的查询可能会阻塞主复制线程,因为复制时单线程的,复制线程在等待时将无法做别的事情。复制一般有两种产生延迟的方式:突然产生延迟然后再跟上,或者稳定的延迟增大。前一种通常是由于一条运行很长的查询导致的,而后者即使在没有长时间运行的查询时也会出现。不幸的是,目前我们还没那么容易确定备库是否接近其容量上限。正如之前提到的。如果负载总是保持均匀的,备库在负载达到99%时和其负载10%的时候表现的性能相同,但一旦达到100%时就会突然产生延迟。但实际上负载不太可能很稳定,所以当备库接近写容量时,就可能在尖峰负载时看到复制延迟的增加。当备库无法跟上时,可以记录备库上的查询并使用一个日志分析工具找出哪里满了。不要依赖于自己的直觉,也不要基于查询在主库上的查询性能进行判断,因为主库和备库性能特征很不相同。最好的分析办法是暂时在备库上打开慢查询日志记录,然后使用pt-query-digest工具来分析。如果打开了log_slow_slave_statements选项,在标准的MySQL慢查询日志能够记录MySQL5.1及更新的版本中复制线程执行的语句,这样就可以找到在复制时哪些语句执行慢了。Percona Server和MariaDB允许开启或进制该选项而无须重启服务器。
除了购买更快的磁盘和CPU(固态硬盘能够提供极大的帮助),备库没有太多的调优空间。大部分选项都是禁止某些额外的工作以减少备库的负载。一个简单的办法是配置InnoDB,使其不要那么频繁地刷新磁盘,这样事务会提交得更快些。可以通过设置innodb_flush_log_at_trx_commit得值为2来上限。还可以在备库上禁止二进制日志记录,把innodb_locks_unsafe_for_binlog设置为1,并把MyISAM得delay_key_write设置为ALL.但是这些设置以牺牲安全换取速度。如果需要将备库提升为主库,记得把这些选项设置回安全的值
不要重复写操作中代价较高的部分。
重构应用程序并且/或者优化查询通常是最好的保持同步的办法。尝试去最小化系统中重复的工作。任何主库上昂贵的写操作都会在每一个备库上重放。如果可以把工作转移到备库,那么久只有一台备库需要执行,然后我们可以把写的结果回传到主库,例如通过执行LOAD DATA INFILE.
举个例子,假设有一个大表,需要汇总到一个小表中用于日常的工作:
```sql
mysql>REPLACE INTO main_db.summary_table(col1, col2, ...) SELECT col1, sum(col2, ...) FROM main_db.enormous_table GROUP BY col1;
```
如果在主库上执行查询,每个备库将同样需要执行庞大的GROUP BY 查询。当进行太多这样的操作时,备库将无法跟上。把这些工作转移到一些备库上也许会有帮助。在备库上创建一个特别保留的数据库,用于避免和从主库上复制的数据产生冲突。可以执行以下查询:
```sql
mysql>REPLACE INTO summary_db.summary_table(col1, col2, ...) SELECT col1, sum(col2,...) FROM main_db.enormous_table GROUP BY col1;
```
现在可以执行SELECT INTO OUTFILE,然后再执行LOAD DATA INFILE,将结果集加载到主库中。现在重复工作被简化为LOAD DATA INFILE操作。如果有N个备库,就节约了N-1次庞大的GROUP BY 操作。
该策略的问题是需要处理陈旧数据。有时候从备库读取的数据和写入主库的数据很难保持一致。如果难以在备库上读取数据,依然能够简化并节省备库工作。如果分离查询的REPLACE和SELECT部分,就可以把结果返回给应用程序,然后将其差人到主库中。首先,在主库执行如下查询:
```sql
mysql>SELECT col1, sum(col2,...) FROM main_db.enormous_table GROUP BY col1;
```
然后为结果集的每一行重复执行如下语句,将结果插入到汇总表中:
```sql
mysql>REPLACE INTO main_db.summary_table (col1, col2,...) VALUES(?,?,...)
```
这种方法再次避免了在备库上执行查询中的GROUP BY 部分。将SELECT和REPLACE分离后意味着查询的SELECT操作不会在每一台备库上重放。这种通用的策略——节约了备库上昂贵的写入操作部分——在很多情况下很有帮助:计算查询的结果代价很昂贵
重构应用程序并且/或者优化查询通常是最好的保持同步的办法。尝试去最小化系统中重复的工作。任何主库上昂贵的写操作都会在每一个备库上重放。如果可以把工作转移到备库,那么久只有一台备库需要执行,然后我们可以把写的结果回传到主库,例如通过执行LOAD DATA INFILE.
举个例子,假设有一个大表,需要汇总到一个小表中用于日常的工作:
```sql
mysql>REPLACE INTO main_db.summary_table(col1, col2, ...) SELECT col1, sum(col2, ...) FROM main_db.enormous_table GROUP BY col1;
```
如果在主库上执行查询,每个备库将同样需要执行庞大的GROUP BY 查询。当进行太多这样的操作时,备库将无法跟上。把这些工作转移到一些备库上也许会有帮助。在备库上创建一个特别保留的数据库,用于避免和从主库上复制的数据产生冲突。可以执行以下查询:
```sql
mysql>REPLACE INTO summary_db.summary_table(col1, col2, ...) SELECT col1, sum(col2,...) FROM main_db.enormous_table GROUP BY col1;
```
现在可以执行SELECT INTO OUTFILE,然后再执行LOAD DATA INFILE,将结果集加载到主库中。现在重复工作被简化为LOAD DATA INFILE操作。如果有N个备库,就节约了N-1次庞大的GROUP BY 操作。
该策略的问题是需要处理陈旧数据。有时候从备库读取的数据和写入主库的数据很难保持一致。如果难以在备库上读取数据,依然能够简化并节省备库工作。如果分离查询的REPLACE和SELECT部分,就可以把结果返回给应用程序,然后将其差人到主库中。首先,在主库执行如下查询:
```sql
mysql>SELECT col1, sum(col2,...) FROM main_db.enormous_table GROUP BY col1;
```
然后为结果集的每一行重复执行如下语句,将结果插入到汇总表中:
```sql
mysql>REPLACE INTO main_db.summary_table (col1, col2,...) VALUES(?,?,...)
```
这种方法再次避免了在备库上执行查询中的GROUP BY 部分。将SELECT和REPLACE分离后意味着查询的SELECT操作不会在每一台备库上重放。这种通用的策略——节约了备库上昂贵的写入操作部分——在很多情况下很有帮助:计算查询的结果代价很昂贵
在复制之外并行写入。
另一种避免备库严重延迟的办法是绕过复制。任何在主库上的写入操作必须在备库串行化。因此有理由认为"串行化写入"不能充分利用资源。所有写操作都应该从主库传递到备库码?如果把备库优先的串行写入容量留给哪些真正需要通过复制进行的写入?
这种考虑有助于对写入进行区分。特别是,如果能确定一些写入可以轻易地在复制之外执行,就可以并行化这些操作以利用备库的写入容量。一个很好的例子是之前讨论过的数据归档。OLTP归档需求通常是简单的单行操作。如果只是把不需要的记录从一个表转移到另一个表,就没有必要将这些写入复制到备库。可以禁止归档查询记录到二进制日志中,然后分别在主库和备库上单独执行这些归档查询。自己复制数据到另外一台服务器,而不是通过复制,这听起来有些疯狂,但却对一些应用有意义,特别是如果应用是某些表的唯一更新源。复制的瓶颈通常集中在小部分表上,如果能在复制之外单独处理这些表,就能够显著地加快复制。
另一种避免备库严重延迟的办法是绕过复制。任何在主库上的写入操作必须在备库串行化。因此有理由认为"串行化写入"不能充分利用资源。所有写操作都应该从主库传递到备库码?如果把备库优先的串行写入容量留给哪些真正需要通过复制进行的写入?
这种考虑有助于对写入进行区分。特别是,如果能确定一些写入可以轻易地在复制之外执行,就可以并行化这些操作以利用备库的写入容量。一个很好的例子是之前讨论过的数据归档。OLTP归档需求通常是简单的单行操作。如果只是把不需要的记录从一个表转移到另一个表,就没有必要将这些写入复制到备库。可以禁止归档查询记录到二进制日志中,然后分别在主库和备库上单独执行这些归档查询。自己复制数据到另外一台服务器,而不是通过复制,这听起来有些疯狂,但却对一些应用有意义,特别是如果应用是某些表的唯一更新源。复制的瓶颈通常集中在小部分表上,如果能在复制之外单独处理这些表,就能够显著地加快复制。
为复制线程预取缓存。
如果有正确的工作负载,就能通过预先将数据读入内存中,以受益于在备库上的并行IO所带来的好处。这种方式并不广为人知。大多数人不会使用,因为除非有正确的工作负载特性和硬件配置,否则可能没有任何用处。上面讨论的其他几种变通方式通常是更好的选择,并且有更多的方法来应用它们。但是我们直到也有小部分应用会受益于数据预取。
有两种可行的实现方法。一种是通过程序实现,略微比备库SQL线程提前读取中继日志,并将其转换为SELECT语句执行。这会使得服务器将数据从磁盘加载到内存中,这样当SQL线程执行到相应的语句时,就无须从磁盘读取数据。事实上,SELECT语句可以并行地执行,所以可以加速SQL线程的串行IO.当一条语句正在执行时,下一条语句需要的数据也正在从磁盘加载到内存中。
如果满足下面这些条件,预取可能会有效:
1.复制SQL线程是IO密集型的,但备库服务器并不是IO密集型的.一个完全的IO密集型服务器不会受益于预取,因为它没有多余的磁盘性能来提供预取
2.备库有多个硬盘驱动器,也许8个或者更多
3.使用的是InnoDB以前宁,并且工作集远不能完全加载到内存中
一个受益于预读取的例子是随机单行UPDTE语句,这些语句通常在主库上高并发执行。DELETE语句也可能受益于这种方法,但INSERT语句则不太可能会——尤其是当顺序插入时——因为前一次插入已经使索引"预热"了。如果表上有很多索引,同样无法预取所有将要被修改的数据。UPDATE语句可能需要更新所有索引,但SELECT语句通常只会读取主键和一个二级索引。UPDATE语句依然需要去读取其他索引的数据以进行更新。在多索引表上这种方法的效率会降低。这种技术并不是"银弹",有很多原因会导致其不能工作,甚至适得其反。只有在清楚硬件和操作系统的状况时才能尝试这种方法。我们直到有些人利用这种办法将复制速度提升了300%到400%,但也尝试过很多次,并发现这种办法常常无法工作。正确地设置参数非常重要,但并没有绝对正确的参数组合。
mk-slave-perfetch是Maatkit种的一款工具,该工具实现了提到的预取策略。mk-slave-prefetch本身有很多复杂的策略以保证其尽可能多的场景下工作,但缺点是它实在太复杂并且需要许多专业知识来使用,另一款工具是Anders Karlsson的slavereadahead工具。
另一种方法是在InnoDB内部实现的。它可以允许设置事务为特殊的模式,以允许InnoDB执行"假"更新。因此可以使用一个程序来执行这些加更新,这样复制线程就可以更快地执行真正的更新。Percona Server为一个非常流行的互联网网络应用单独开发了该功能。可以去检查一下此特性现在的状况。
如果正在考虑这项技术,可以从一个熟悉其工作原理及可用选项的专家那里获得很好的建议,这应该作为其他方法都不可行时最后的解决办法
如果有正确的工作负载,就能通过预先将数据读入内存中,以受益于在备库上的并行IO所带来的好处。这种方式并不广为人知。大多数人不会使用,因为除非有正确的工作负载特性和硬件配置,否则可能没有任何用处。上面讨论的其他几种变通方式通常是更好的选择,并且有更多的方法来应用它们。但是我们直到也有小部分应用会受益于数据预取。
有两种可行的实现方法。一种是通过程序实现,略微比备库SQL线程提前读取中继日志,并将其转换为SELECT语句执行。这会使得服务器将数据从磁盘加载到内存中,这样当SQL线程执行到相应的语句时,就无须从磁盘读取数据。事实上,SELECT语句可以并行地执行,所以可以加速SQL线程的串行IO.当一条语句正在执行时,下一条语句需要的数据也正在从磁盘加载到内存中。
如果满足下面这些条件,预取可能会有效:
1.复制SQL线程是IO密集型的,但备库服务器并不是IO密集型的.一个完全的IO密集型服务器不会受益于预取,因为它没有多余的磁盘性能来提供预取
2.备库有多个硬盘驱动器,也许8个或者更多
3.使用的是InnoDB以前宁,并且工作集远不能完全加载到内存中
一个受益于预读取的例子是随机单行UPDTE语句,这些语句通常在主库上高并发执行。DELETE语句也可能受益于这种方法,但INSERT语句则不太可能会——尤其是当顺序插入时——因为前一次插入已经使索引"预热"了。如果表上有很多索引,同样无法预取所有将要被修改的数据。UPDATE语句可能需要更新所有索引,但SELECT语句通常只会读取主键和一个二级索引。UPDATE语句依然需要去读取其他索引的数据以进行更新。在多索引表上这种方法的效率会降低。这种技术并不是"银弹",有很多原因会导致其不能工作,甚至适得其反。只有在清楚硬件和操作系统的状况时才能尝试这种方法。我们直到有些人利用这种办法将复制速度提升了300%到400%,但也尝试过很多次,并发现这种办法常常无法工作。正确地设置参数非常重要,但并没有绝对正确的参数组合。
mk-slave-perfetch是Maatkit种的一款工具,该工具实现了提到的预取策略。mk-slave-prefetch本身有很多复杂的策略以保证其尽可能多的场景下工作,但缺点是它实在太复杂并且需要许多专业知识来使用,另一款工具是Anders Karlsson的slavereadahead工具。
另一种方法是在InnoDB内部实现的。它可以允许设置事务为特殊的模式,以允许InnoDB执行"假"更新。因此可以使用一个程序来执行这些加更新,这样复制线程就可以更快地执行真正的更新。Percona Server为一个非常流行的互联网网络应用单独开发了该功能。可以去检查一下此特性现在的状况。
如果正在考虑这项技术,可以从一个熟悉其工作原理及可用选项的专家那里获得很好的建议,这应该作为其他方法都不可行时最后的解决办法
来自主库的过大的包。
另一个难以追踪的问题是主库的max_allowed_packet值和备库的不匹配。在这种情况下,主库可能会记录一个备库认为过大的包。当备库获取到该二进制日志事件时,可能会碰到各种各样的问题,包括无限报错和充实,或者中继日志损坏
另一个难以追踪的问题是主库的max_allowed_packet值和备库的不匹配。在这种情况下,主库可能会记录一个备库认为过大的包。当备库获取到该二进制日志事件时,可能会碰到各种各样的问题,包括无限报错和充实,或者中继日志损坏
受限制的复制带宽。
如果使用受限的带宽进行复制,可以开启备库上的slave_compressed_protocol选项。当备库连接主库时,会请求一个被压缩的连接——和MySQL客户端使用的压缩连接一样。使用的压缩引擎是zlib,测试表明它能将文本类型的数据压缩到大约其原始大小的三分之一。其代价是需要额外的CPU时间,包括在主库上压缩数据和在备库上解压数据。
如果主库和备库间的连接是慢速连接,可能需要将分发主库和备库分布在同一地点。这样就只有一台服务器通过慢速连接和主库相连,可以减少链路上的带宽负载以及主库的CPU负载
如果使用受限的带宽进行复制,可以开启备库上的slave_compressed_protocol选项。当备库连接主库时,会请求一个被压缩的连接——和MySQL客户端使用的压缩连接一样。使用的压缩引擎是zlib,测试表明它能将文本类型的数据压缩到大约其原始大小的三分之一。其代价是需要额外的CPU时间,包括在主库上压缩数据和在备库上解压数据。
如果主库和备库间的连接是慢速连接,可能需要将分发主库和备库分布在同一地点。这样就只有一台服务器通过慢速连接和主库相连,可以减少链路上的带宽负载以及主库的CPU负载
磁盘空间不足。
复制有可能因为二进制日志、中继日志或临时文件将磁盘撑满,特别是在主库上执行了LOAD DATA INFILE查询并在备库开启了log_slave_updates选项。延迟越严重,接收到但尚未执行的中继日志会占用越多的磁盘空间。可以通过监控磁盘并设置relay_log_space选项来避免这个问题
复制有可能因为二进制日志、中继日志或临时文件将磁盘撑满,特别是在主库上执行了LOAD DATA INFILE查询并在备库开启了log_slave_updates选项。延迟越严重,接收到但尚未执行的中继日志会占用越多的磁盘空间。可以通过监控磁盘并设置relay_log_space选项来避免这个问题
复制的局限性。
MySQL复制可能失败或者不同步,不管有没有报错,这是因为其内部的限制导致的。大量的SQL函数和编程实践不能被可靠地复制。很难确保应用代码里不会出现这样或那样的问题,特别是应用或者团队非常庞大的时候。另外一个问题是服务器的bug,虽然听起来很消极,但大多数MySQL的主版本都存在着历史遗留的复制Bug。特别是每个主版本的第一个版本。诸如存储过程这样的新特性常常会导致更多的问题。
MySQL复制非常复杂,应用程序越复杂,你就需要越小心。但是如果学会了如何使用,复制会工作得很好。
MySQL复制可能失败或者不同步,不管有没有报错,这是因为其内部的限制导致的。大量的SQL函数和编程实践不能被可靠地复制。很难确保应用代码里不会出现这样或那样的问题,特别是应用或者团队非常庞大的时候。另外一个问题是服务器的bug,虽然听起来很消极,但大多数MySQL的主版本都存在着历史遗留的复制Bug。特别是每个主版本的第一个版本。诸如存储过程这样的新特性常常会导致更多的问题。
MySQL复制非常复杂,应用程序越复杂,你就需要越小心。但是如果学会了如何使用,复制会工作得很好。
复制有多快。
关于复制的一个比较普遍的问题是复制到底有多快?简单来讲,它与MySQL从主库复制事件并在备库重放的速度一样快。如果网络很慢并且二进制日志事件很大,记录二进制日志和在备库上执行的延迟可能会非常明显。如果查询需要执行很长事件而网络很快。通常可以认为查询事件占据了更多的复制时间开销。
更完整的答案是计算每一步花费的时间,并找到应用种耗时最多的那一部分。一些人可能只关注主库上记录事件和将事件复制到中继日志的时间间隔。对于那些想要了解更多细节的人,我们可以做一个快速的实验。
前面详细地描述了复制的过程和Giuseppe Maxia提供的测量高精度复制速度的方法。我们创建了一个非确定性的用户自定义函数(UDF),以微妙精度返回系统时间:
```sql
mysql>SELECT NOW_USEC();
```
首先将NOW_USEC()函数的值插入到主库的一张表种,然后比较它在备库上的值,以此来测量复制的速度。为了测量延迟,我们在一台服务器上开启两个MySQL实例,以避免由于时钟引起的不精确。我们将其中一个实例配置为另一个的备库,然后在主库实例上执行如下语句:
```sql
mysql>CREATE TABLE test.lag_test( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, now_usec VARCHAR(26) NOT NULL);
mysql> INSERT INTO test.lag_test(now_usec) VALUES (NOW_USEC());
```
我们使用的是VARCHAR列,因为MySQL内建的时间类型只能精确到秒(尽管一些时间函数可以执行小于秒级别的计算),剩下的就是比较主备的差异。这里我们使用Federated表.在备库上执行:
```sql
mysql>CREATE TABLE test.master_varl (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, now_usec VARCHAR(26) NOT NULL) ENGINE=FEDERATED CONNECTION='mysql://user:pass@127.0.0.1/test/lag_test';
```
简单的关联和TIMESTAMPDIFF()函数可以微妙精度显示主库和备库上执行查询的延迟。
```sql
mysql>SELECT m.id, TIMESTAMPDIFF(FRAC_SECOND, m.now_usec,s.now_usec) AS usec_lag FROM test.lag_test as s INNER JOIN test.master_val AS m USING(id);
```
这里得到的usec_lag是476.我们使用Perl脚本向主库插入1 000行数据,每个插入间有10毫秒的延时,以避免主备实例竞争CPU是按。然后创建一个临时表来存储每个时间的延迟:
```sql
mysql>CREATE TABLE test.lag AS SELECT TIMESTAMPDIFF(FRAC_SECOND, m.now_usec, s.now_usec) AS lag FROM test.master_val AS m INNER JOIN test.lag_test AS s USING(id);
```
接着根据延迟时间分组,可以看到最常见的延迟时间是多少:
```sql
mysql>SELECT ROUND(lag/ 1000000.0, 4) * 1000 AS msec_lag COUNT(*) FROM lag GROUP BY msec_lag ORDER BY msec_lag;
```
结果显示大多数小查询在主库上的执行时间和备库上的执行时间间隔大多数小于0.3毫秒。复制过程种没有计算的部分是事件在主库上记录到二进制日志需要多长事件传递到备库。有必要知道这一点,因为备库越快接收到日志事件越好。如果备库已经接收到了事件,它就能在主库崩溃时提供一个拷贝。尽管测量结果没有精确地显示这部分需要多少事件,但理论上非常快(例如,仅仅受限于网络速度)。MySQL二进制日志转储线程并没有通过轮询的方式从主库请求事件,而是由主库来通知备库新的实践,因为前者低效且缓慢。从主库读取一个二进制日志事件是一个阻塞型网络调用,当主库记录事件后,马上就开始发送。因此可以说,只要复制线程被环形并且能够通过网络传输数据,事件就会很快到达备库
关于复制的一个比较普遍的问题是复制到底有多快?简单来讲,它与MySQL从主库复制事件并在备库重放的速度一样快。如果网络很慢并且二进制日志事件很大,记录二进制日志和在备库上执行的延迟可能会非常明显。如果查询需要执行很长事件而网络很快。通常可以认为查询事件占据了更多的复制时间开销。
更完整的答案是计算每一步花费的时间,并找到应用种耗时最多的那一部分。一些人可能只关注主库上记录事件和将事件复制到中继日志的时间间隔。对于那些想要了解更多细节的人,我们可以做一个快速的实验。
前面详细地描述了复制的过程和Giuseppe Maxia提供的测量高精度复制速度的方法。我们创建了一个非确定性的用户自定义函数(UDF),以微妙精度返回系统时间:
```sql
mysql>SELECT NOW_USEC();
```
首先将NOW_USEC()函数的值插入到主库的一张表种,然后比较它在备库上的值,以此来测量复制的速度。为了测量延迟,我们在一台服务器上开启两个MySQL实例,以避免由于时钟引起的不精确。我们将其中一个实例配置为另一个的备库,然后在主库实例上执行如下语句:
```sql
mysql>CREATE TABLE test.lag_test( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, now_usec VARCHAR(26) NOT NULL);
mysql> INSERT INTO test.lag_test(now_usec) VALUES (NOW_USEC());
```
我们使用的是VARCHAR列,因为MySQL内建的时间类型只能精确到秒(尽管一些时间函数可以执行小于秒级别的计算),剩下的就是比较主备的差异。这里我们使用Federated表.在备库上执行:
```sql
mysql>CREATE TABLE test.master_varl (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, now_usec VARCHAR(26) NOT NULL) ENGINE=FEDERATED CONNECTION='mysql://user:pass@127.0.0.1/test/lag_test';
```
简单的关联和TIMESTAMPDIFF()函数可以微妙精度显示主库和备库上执行查询的延迟。
```sql
mysql>SELECT m.id, TIMESTAMPDIFF(FRAC_SECOND, m.now_usec,s.now_usec) AS usec_lag FROM test.lag_test as s INNER JOIN test.master_val AS m USING(id);
```
这里得到的usec_lag是476.我们使用Perl脚本向主库插入1 000行数据,每个插入间有10毫秒的延时,以避免主备实例竞争CPU是按。然后创建一个临时表来存储每个时间的延迟:
```sql
mysql>CREATE TABLE test.lag AS SELECT TIMESTAMPDIFF(FRAC_SECOND, m.now_usec, s.now_usec) AS lag FROM test.master_val AS m INNER JOIN test.lag_test AS s USING(id);
```
接着根据延迟时间分组,可以看到最常见的延迟时间是多少:
```sql
mysql>SELECT ROUND(lag/ 1000000.0, 4) * 1000 AS msec_lag COUNT(*) FROM lag GROUP BY msec_lag ORDER BY msec_lag;
```
结果显示大多数小查询在主库上的执行时间和备库上的执行时间间隔大多数小于0.3毫秒。复制过程种没有计算的部分是事件在主库上记录到二进制日志需要多长事件传递到备库。有必要知道这一点,因为备库越快接收到日志事件越好。如果备库已经接收到了事件,它就能在主库崩溃时提供一个拷贝。尽管测量结果没有精确地显示这部分需要多少事件,但理论上非常快(例如,仅仅受限于网络速度)。MySQL二进制日志转储线程并没有通过轮询的方式从主库请求事件,而是由主库来通知备库新的实践,因为前者低效且缓慢。从主库读取一个二进制日志事件是一个阻塞型网络调用,当主库记录事件后,马上就开始发送。因此可以说,只要复制线程被环形并且能够通过网络传输数据,事件就会很快到达备库
MySQL复制的高级特性。
Oracle对MySQL5.5的复制有着明显的改进。更多的特性还在开发种,MySQL5.6将包含这些特性。一些改进使得复制更加强健,例如,增加了多线程(并行)复制以减少当前单线程复制的瓶颈。另外,还有一些改进增加了一些高级特性,使得复制更加灵活并可控制。
第一个是半同步复制,基于Google多年前所做的工作。这是自MySQL5.1引入行复制后最大的改进。它可以帮助你确保备库拥有主库数据的拷贝,减少了潜在的数据丢失危险。半同步复制在提交过程种增加了一个延迟:当提交事务时,在客户端接收到查询结束反馈前必须保证二进制日志已经传输到至少一台备库上。主库将事务提交到磁盘上之后会增加一些延迟。同样的,这也增加了客户端的延迟,因此其执行大量事务的速度不会比将这些事务传递给备库的速度更快。关于半同步,有一些普遍的误解,下面时它不会去做的:
1.在备库提示其已经受到事件前,会阻塞主库上的事务提交。事实上在主库上已经完成事务提交,只有通知客户端被延迟了
2.直到备库执行事务后,才不会阻塞客户端。备库在接收到事务后发送反馈而非完成事务后发送
3.半同步不总是能够工作。如果备库一直没有回应已收到事件,会超时并转化为正常的异步复制模式
尽管如此,这仍然是一个很好用的工具,有助于确保备库提供更好的冗余度和持久性。在性能方面,从客户端的角度来看,增加了事务提交的延时,延时的多少取决于网络传输,数据写入和刷新到备库磁盘的时间(如果开启了配置)以及备库反馈的网络时间。听起来似乎这是累加的,但测试表明这些几乎是不中国要的,也许延迟是由其他原因引起的。Giuseppe Maxia发现每次提交大约延时200微妙.对于小事务开销可能会比较明显,这也是预期中的。
事实上半同步复制在某些场景下确实能够提供足够的灵活性以改善性能,在主库关闭sync_binlog的情况下保证更加安全。写入远程的内存(一台备库反馈)比写入本地的磁盘(写入并刷新)要更快。Henrik Ingo运行了一些性能测试表明,使用半同步复制相比在主库上进行强持久化的性能有两倍的改善。在任何系统上都没有绝对的持久化)只有更加高的持久化层次——并且看起来半同步复制应该是一种比其他替代方案开销更小的系统数据持久化方法。
除了半同步复制,MySQL5.5还提供了复制心跳,保证备库一直与主库相联系,避免悄无声息地断开连接。如果出现断开的网络连接,备库会注意到丢失的心跳数据。当使用基于行的复制时,还提供了一种改进的能力来处理主库和备库上不同的数据类型。有几个选项可以用于配置复制元数据文件是如何刷新到磁盘以及在一次崩溃后如何处理中继日志,减少了备库崩溃恢复后出现问题的概率
Oracle对MySQL5.5的复制有着明显的改进。更多的特性还在开发种,MySQL5.6将包含这些特性。一些改进使得复制更加强健,例如,增加了多线程(并行)复制以减少当前单线程复制的瓶颈。另外,还有一些改进增加了一些高级特性,使得复制更加灵活并可控制。
第一个是半同步复制,基于Google多年前所做的工作。这是自MySQL5.1引入行复制后最大的改进。它可以帮助你确保备库拥有主库数据的拷贝,减少了潜在的数据丢失危险。半同步复制在提交过程种增加了一个延迟:当提交事务时,在客户端接收到查询结束反馈前必须保证二进制日志已经传输到至少一台备库上。主库将事务提交到磁盘上之后会增加一些延迟。同样的,这也增加了客户端的延迟,因此其执行大量事务的速度不会比将这些事务传递给备库的速度更快。关于半同步,有一些普遍的误解,下面时它不会去做的:
1.在备库提示其已经受到事件前,会阻塞主库上的事务提交。事实上在主库上已经完成事务提交,只有通知客户端被延迟了
2.直到备库执行事务后,才不会阻塞客户端。备库在接收到事务后发送反馈而非完成事务后发送
3.半同步不总是能够工作。如果备库一直没有回应已收到事件,会超时并转化为正常的异步复制模式
尽管如此,这仍然是一个很好用的工具,有助于确保备库提供更好的冗余度和持久性。在性能方面,从客户端的角度来看,增加了事务提交的延时,延时的多少取决于网络传输,数据写入和刷新到备库磁盘的时间(如果开启了配置)以及备库反馈的网络时间。听起来似乎这是累加的,但测试表明这些几乎是不中国要的,也许延迟是由其他原因引起的。Giuseppe Maxia发现每次提交大约延时200微妙.对于小事务开销可能会比较明显,这也是预期中的。
事实上半同步复制在某些场景下确实能够提供足够的灵活性以改善性能,在主库关闭sync_binlog的情况下保证更加安全。写入远程的内存(一台备库反馈)比写入本地的磁盘(写入并刷新)要更快。Henrik Ingo运行了一些性能测试表明,使用半同步复制相比在主库上进行强持久化的性能有两倍的改善。在任何系统上都没有绝对的持久化)只有更加高的持久化层次——并且看起来半同步复制应该是一种比其他替代方案开销更小的系统数据持久化方法。
除了半同步复制,MySQL5.5还提供了复制心跳,保证备库一直与主库相联系,避免悄无声息地断开连接。如果出现断开的网络连接,备库会注意到丢失的心跳数据。当使用基于行的复制时,还提供了一种改进的能力来处理主库和备库上不同的数据类型。有几个选项可以用于配置复制元数据文件是如何刷新到磁盘以及在一次崩溃后如何处理中继日志,减少了备库崩溃恢复后出现问题的概率
其他复制技术。
MySQL内建的复制并不是将数据从一台服务器复制到另外一台服务器的唯一办法,尽管大多时候是最好的办法。前面已经讨论了MySQL复制的一些扩展性技术,如Oracle GoldenGate,但对大多数工具我们都不熟悉,因此无法讨论太多。但是有两个我们需要指出来,第一个是Percona XtraDB Cluster的同步复制。另一个是Continuent的Tungsten Replicator.
Tungsten是一个用Java编写的开源的中间件复制产品。它的功能和Oracle GoldenGate类似,并且开起来在未来发布的版本中将逐步增加许多的特性。例如,在服务器间复制数据、自动数据分片、在备库并发执行更新(多线程复制)、当主库失败时提升备库、跨平台复制,以及多源复制(多个复制源到一个目标)。它是Tungsten数据库的clustering suite的开源版本。
Tungsten同样实现了多主库集群,可以把写入指向集群中任意一台服务器。这种架构实现通常都包含冲突发现与解决。这一点很难做到,并且不总是需要的。Tungsten的实现稍微做了点限制,不是所有的数据都能在所有的节点写入,每个节点被标记为记录系统,以接收特定的数据。例如,在西雅图的办公室可以拥有并写入它的数据,然后复制到休斯敦和巴尔的摩。在休斯敦和巴尔的摩本地可以实现低延迟读数据,但在这里Tungsten不允许写入数据,这样数据冲突就不存在了。当然休斯敦和巴尔的摩可以更新它们自己的数据,并被复制到其他地点。这种"记录系统"方案解决了人们需要在环形结构中频繁调整内建MySQL复制的问题。
Tungsten Replicator不仅仅时嵌入或管理MySQL复制,而是直接替代它。它通过读取主库的二进制日志来获得数据更新,那里正是内建MySQL复制工作结束的地方,然后由Tungsten Replicator接管。它读取二进制日志,并抽取事务,然后再备库执行它们。
该过程比MySQL复制本身有更丰富的功能集。实际上,Tungsten Replicator是第一个提供MySQL并行复制支持的。它声称能够提供最多三倍的复制速度改善,具体取决于负载特性,基于该架构以及对该产品的了解,这看起来是可信的。
一下是关于Tungsten Replicator中值得欣赏的部分:
1.它提供了内建的数据一致性检查
2.提供了插件特性,因此可以编写自己的函数。MySQL的复制源代码非常难以理解并且很难去修改。即使非常聪明的程序员再试图修改时,也会引入新的Bug,因而能有途径去修改复制而无须修改MySQL的复制代码,是非常理想的。
3.拥有全局事务ID,能够帮助你了解每个服务器相互之间的状态而无须去匹配二进制日志名和偏移量
4.它是一个高可用的解决方案,能够快速地将一台备库提升为主库
5.提供异构数据复制(例如,在MySQL和PostgreSQL之间或者MySQL和Oracle之间)
6.支持不同版本的MySQL复制,以防止MySQL复制不能反向兼容。这对某些升级的场景非常有用。当升级运行得不理想时,你可能无法设计一个可行的回滚方案,或者必须升级服务器到一个并不是你期望的版本
7.并行复制的设计非常适用于共享应用程序或多任务应用程序
8.Java应用能够明确地写入主库并从备库读取
9.得益于Giuseppe Maxia作为QA主管的大量工作,现在比以往更加简单并且更加容易配置和管理。
以下是它的一些缺点:
1.它比内建的MySQL复制更加复杂,有更多可变动的地方需要配置和管理,毕竟它是一个中间件
2.在你的应用栈中需要多学习和理解一个新的工具
3.它并不像内建的MySQL复制那样轻量级,并且没有同样的性能。使用Tungesten Replicator进行单线程复制比MySQL的单线程复制要慢
4.作为MySQL复制并没有经过广泛的测试和部署,所以Bug和问题的风险很高。
总而言之,Tungtsten是可用的,并且在积极的开发中,稳定地释放新的特性和功能,拥有一个可替代内建MySQL复制的选择,这非常棒,使得MySQL能够适用于更多的应用场景,并且足够灵活,能够满足内建的MySQL复制可能永远无法满足的需求
MySQL内建的复制并不是将数据从一台服务器复制到另外一台服务器的唯一办法,尽管大多时候是最好的办法。前面已经讨论了MySQL复制的一些扩展性技术,如Oracle GoldenGate,但对大多数工具我们都不熟悉,因此无法讨论太多。但是有两个我们需要指出来,第一个是Percona XtraDB Cluster的同步复制。另一个是Continuent的Tungsten Replicator.
Tungsten是一个用Java编写的开源的中间件复制产品。它的功能和Oracle GoldenGate类似,并且开起来在未来发布的版本中将逐步增加许多的特性。例如,在服务器间复制数据、自动数据分片、在备库并发执行更新(多线程复制)、当主库失败时提升备库、跨平台复制,以及多源复制(多个复制源到一个目标)。它是Tungsten数据库的clustering suite的开源版本。
Tungsten同样实现了多主库集群,可以把写入指向集群中任意一台服务器。这种架构实现通常都包含冲突发现与解决。这一点很难做到,并且不总是需要的。Tungsten的实现稍微做了点限制,不是所有的数据都能在所有的节点写入,每个节点被标记为记录系统,以接收特定的数据。例如,在西雅图的办公室可以拥有并写入它的数据,然后复制到休斯敦和巴尔的摩。在休斯敦和巴尔的摩本地可以实现低延迟读数据,但在这里Tungsten不允许写入数据,这样数据冲突就不存在了。当然休斯敦和巴尔的摩可以更新它们自己的数据,并被复制到其他地点。这种"记录系统"方案解决了人们需要在环形结构中频繁调整内建MySQL复制的问题。
Tungsten Replicator不仅仅时嵌入或管理MySQL复制,而是直接替代它。它通过读取主库的二进制日志来获得数据更新,那里正是内建MySQL复制工作结束的地方,然后由Tungsten Replicator接管。它读取二进制日志,并抽取事务,然后再备库执行它们。
该过程比MySQL复制本身有更丰富的功能集。实际上,Tungsten Replicator是第一个提供MySQL并行复制支持的。它声称能够提供最多三倍的复制速度改善,具体取决于负载特性,基于该架构以及对该产品的了解,这看起来是可信的。
一下是关于Tungsten Replicator中值得欣赏的部分:
1.它提供了内建的数据一致性检查
2.提供了插件特性,因此可以编写自己的函数。MySQL的复制源代码非常难以理解并且很难去修改。即使非常聪明的程序员再试图修改时,也会引入新的Bug,因而能有途径去修改复制而无须修改MySQL的复制代码,是非常理想的。
3.拥有全局事务ID,能够帮助你了解每个服务器相互之间的状态而无须去匹配二进制日志名和偏移量
4.它是一个高可用的解决方案,能够快速地将一台备库提升为主库
5.提供异构数据复制(例如,在MySQL和PostgreSQL之间或者MySQL和Oracle之间)
6.支持不同版本的MySQL复制,以防止MySQL复制不能反向兼容。这对某些升级的场景非常有用。当升级运行得不理想时,你可能无法设计一个可行的回滚方案,或者必须升级服务器到一个并不是你期望的版本
7.并行复制的设计非常适用于共享应用程序或多任务应用程序
8.Java应用能够明确地写入主库并从备库读取
9.得益于Giuseppe Maxia作为QA主管的大量工作,现在比以往更加简单并且更加容易配置和管理。
以下是它的一些缺点:
1.它比内建的MySQL复制更加复杂,有更多可变动的地方需要配置和管理,毕竟它是一个中间件
2.在你的应用栈中需要多学习和理解一个新的工具
3.它并不像内建的MySQL复制那样轻量级,并且没有同样的性能。使用Tungesten Replicator进行单线程复制比MySQL的单线程复制要慢
4.作为MySQL复制并没有经过广泛的测试和部署,所以Bug和问题的风险很高。
总而言之,Tungtsten是可用的,并且在积极的开发中,稳定地释放新的特性和功能,拥有一个可替代内建MySQL复制的选择,这非常棒,使得MySQL能够适用于更多的应用场景,并且足够灵活,能够满足内建的MySQL复制可能永远无法满足的需求
可扩展的MySQL
概述。
有些应用仅仅适用于一台或少数几台服务器,那么哪些可扩展性建议是和这些应用相关的呢?大多数人从不会维护超大规模的系统,并且通常也无法效仿在主流大公司所使用的策略。选择一个合适的策略能够大大地节约时间和金钱。
MySQL经常被批评很难进行扩展,有些情况下这种看法是正确的,但如果选择正确的架构并很好地实现,就能够非常好地扩展MySQL.但是扩展性并不是一个很好理解的主题。
有些应用仅仅适用于一台或少数几台服务器,那么哪些可扩展性建议是和这些应用相关的呢?大多数人从不会维护超大规模的系统,并且通常也无法效仿在主流大公司所使用的策略。选择一个合适的策略能够大大地节约时间和金钱。
MySQL经常被批评很难进行扩展,有些情况下这种看法是正确的,但如果选择正确的架构并很好地实现,就能够非常好地扩展MySQL.但是扩展性并不是一个很好理解的主题。
什么是可扩展性。
人们常常把诸如"可扩展性"、"高可用性"以及"性能"等术语在一些非正式的场合用作同一侧,但事实上它们是完全不同的。在前面我们将性能定义为响应时间。我们也可以很精确地定义可扩展性。简要地说,可扩展性表明了当需要增加资源以执行更多工作时系统能够获得划算地等同提升(equal bang for the buck)的能力。缺乏扩展能力的系统在达到收益递减的转折点后,将无法进一步增长。
容量是一个和可扩展性相关的概念。系统容量表示在一定时间内能够完成的工作量(从物理学来看,单位时间内做的功称为功率(power),而在计算机领域,"power"是一个被反复使用的术语,含义模糊,因此应避免使用它,但是关于容量的精确定义是系统的最大功率输出。)但容量必须是可以有效利用的。系统的最大吞吐量并不等同于容量。大多数基准测试能够衡量一个系统的最大吞吐量,但真实的系统一般不会使用到极限。如果达到最大吞吐量,则性能会下降,并且响应时间会变得不可接受地大且非常不稳定。我们将系统地真实容量定义为在保证可接受地性能地情况下能够达到地吞吐量。这就是为什么基准测试地结果通常不应该简化为一个单独地数字。
容量和可扩展性并不依赖于性能。以高速公路上的汽车来类比的话:
1.性能是汽车的时速
2.容量是车道数乘以最大安全时速
3.可扩展性就是在不减慢交通的情况下,能增加更多车和车道的程度
在这个类比中,可扩展性依赖于多个条件:换道设计得是否合理、路上有多少车抛锚或者发生事故,汽车行驶速度是否不同或者是否频繁变换车道——但一般来说和汽车得引擎是否强大无关。这并不是说性能不重要,性能确实重要,只是需要指出,即使系统性能不是很高也可以具备可扩展性。
从较高层次看,可扩展性就是能够通过增加资源来提升容量的能力。即使MySQL架构是可扩展的,但应用本身也可能无法扩展,如果很难增加容量,不管原因是什么,应用都是不可扩展的。之前我们从吞吐量方面来定义容量,但同样也需要从较高的层次来看代容量问题。从有利的角度来看,容量可以简单地认为是处理负载的能力,从不同的角度来考虑负载很有帮助。
1.数据量
应用所能累积的数据量是可扩展性最普遍的挑战,特别时对于现在的许多互联网应用而言,这些应用从不删除任何数据。例如社交网站,通常从不会删除老的消息或评论
2.用户量
即使每个用户只有少量的数据,但在累计到一定数量的用户后,数据量也会开始不成比例地增长且速度快过用户增长。更多的用户意味着要处理更多的事务,并且事务数可能和用户数不成比例。最后,大量用户(以及更多的数据)也意味着更多复杂的查询,特别是查询跟用户关系相关时(用户间的关联数可以用N x (N -1)来计算,这里的N表示用户数)
3.用户活跃度
不是所有的用户活跃度都相同,并且用户活跃度也不总是不变的。如果用户突然变得活跃,例如由于增加了一个吸引人的新特性,那么负载可能会明显提升。用户活跃度不仅仅指页面浏览数,即使同样的页面浏览数,如果网站的某个需要执行大量工作的部分变得流行,也可能导致更多的工作。另外,某些yoghurt也会比其他用户更活跃:它们可能比一般人拥有更多的朋友、消息、照片
4.相关数据集的大小
如果用户间存在关系,应用可能需要在整个相关联用户群体上执行查询和计算,这比处理一个一个的用户和用户数据要复杂得多。社交网站经常会遇到由那些人气很旺的用户组或朋友很多的用户所带来的挑战
人们常常把诸如"可扩展性"、"高可用性"以及"性能"等术语在一些非正式的场合用作同一侧,但事实上它们是完全不同的。在前面我们将性能定义为响应时间。我们也可以很精确地定义可扩展性。简要地说,可扩展性表明了当需要增加资源以执行更多工作时系统能够获得划算地等同提升(equal bang for the buck)的能力。缺乏扩展能力的系统在达到收益递减的转折点后,将无法进一步增长。
容量是一个和可扩展性相关的概念。系统容量表示在一定时间内能够完成的工作量(从物理学来看,单位时间内做的功称为功率(power),而在计算机领域,"power"是一个被反复使用的术语,含义模糊,因此应避免使用它,但是关于容量的精确定义是系统的最大功率输出。)但容量必须是可以有效利用的。系统的最大吞吐量并不等同于容量。大多数基准测试能够衡量一个系统的最大吞吐量,但真实的系统一般不会使用到极限。如果达到最大吞吐量,则性能会下降,并且响应时间会变得不可接受地大且非常不稳定。我们将系统地真实容量定义为在保证可接受地性能地情况下能够达到地吞吐量。这就是为什么基准测试地结果通常不应该简化为一个单独地数字。
容量和可扩展性并不依赖于性能。以高速公路上的汽车来类比的话:
1.性能是汽车的时速
2.容量是车道数乘以最大安全时速
3.可扩展性就是在不减慢交通的情况下,能增加更多车和车道的程度
在这个类比中,可扩展性依赖于多个条件:换道设计得是否合理、路上有多少车抛锚或者发生事故,汽车行驶速度是否不同或者是否频繁变换车道——但一般来说和汽车得引擎是否强大无关。这并不是说性能不重要,性能确实重要,只是需要指出,即使系统性能不是很高也可以具备可扩展性。
从较高层次看,可扩展性就是能够通过增加资源来提升容量的能力。即使MySQL架构是可扩展的,但应用本身也可能无法扩展,如果很难增加容量,不管原因是什么,应用都是不可扩展的。之前我们从吞吐量方面来定义容量,但同样也需要从较高的层次来看代容量问题。从有利的角度来看,容量可以简单地认为是处理负载的能力,从不同的角度来考虑负载很有帮助。
1.数据量
应用所能累积的数据量是可扩展性最普遍的挑战,特别时对于现在的许多互联网应用而言,这些应用从不删除任何数据。例如社交网站,通常从不会删除老的消息或评论
2.用户量
即使每个用户只有少量的数据,但在累计到一定数量的用户后,数据量也会开始不成比例地增长且速度快过用户增长。更多的用户意味着要处理更多的事务,并且事务数可能和用户数不成比例。最后,大量用户(以及更多的数据)也意味着更多复杂的查询,特别是查询跟用户关系相关时(用户间的关联数可以用N x (N -1)来计算,这里的N表示用户数)
3.用户活跃度
不是所有的用户活跃度都相同,并且用户活跃度也不总是不变的。如果用户突然变得活跃,例如由于增加了一个吸引人的新特性,那么负载可能会明显提升。用户活跃度不仅仅指页面浏览数,即使同样的页面浏览数,如果网站的某个需要执行大量工作的部分变得流行,也可能导致更多的工作。另外,某些yoghurt也会比其他用户更活跃:它们可能比一般人拥有更多的朋友、消息、照片
4.相关数据集的大小
如果用户间存在关系,应用可能需要在整个相关联用户群体上执行查询和计算,这比处理一个一个的用户和用户数据要复杂得多。社交网站经常会遇到由那些人气很旺的用户组或朋友很多的用户所带来的挑战
正式的可扩展性定义。
有必要探讨以下可扩展性在数学上的定义了,这有助于在更高层次的概念上清晰地理解可扩展性。如果没有这样的基础,就可能无法理解或精确地表达可扩展性。不过不用担心,这里不会涉及高等数学,即使不是数学天才,也能够很直观地理解它。关键是前面使用的短语:"划算的等同提升(equal bang for the buck)".另一种说法是,可扩展性是当增加资源以处理负载和增加容量时系统能够获得的投资产出率(ROI).假设有一个只有一台服务器的系统,并且能够测量它的最大容量,如图所示。下面用s表示服务器,c表示容量,假设我们现在增加一台服务器,系统的能力加倍,如图所示。这就是线性扩展。我们增加了一倍的服务器,结果增加了一倍的容量。大部分系统并不是线性扩展的,而是如图所示的扩展方式。大部分系统都只能以比线性扩展略低的扩展系数进行扩展,越高的扩展系数会导致越大的线性偏差。事实上,多数系统最终会达到一个最大吞吐量临界点,超过这个点后增加头伏反而会带来负回报——继续增加更多工作负载,实际上会降低系统的吞吐量(事实上,"投资产出率"也可以从金融投资的角度来考虑,将一个组件的容量升级到两倍所需要符出的常常不止是最初开销的两倍。虽然在现实世界里我们常常这么考虑,但在讨论中会将其忽略掉,因为它会使一个已经复杂的主题变得更加复杂)。这怎么可能呢?这些年产生了许多可扩展性模型,它们有着不同程度的良好表现和实用性。我们这里所讲的可扩展性模型是基于某些能够影响系统扩展的内在机制。这就是Neil J.Gunther博士提出的通用可扩展性定律(Universal Scalability Law, USL).Gunther博士将这些详尽地写到了他的书中,包括Guerrilla Capacity Planning(Springer).这里不会深入到背后的数学理论中。
简而言之,USL说的是线性扩展的偏差可通过两个因素来建立模型:无法并发执行的一部分工作,以及需要交互的另外一部分工作。为第一个因素建模就有了著名的Amdahl定律,它会导致吞吐量趋于平缓。如果部分任务无法并行,那么不管你如何分而治之,该任务至少需要串行部分的时间。
增加第二个因素——内部节点间或者进程间的通信——到Amdahl定律就得出了USL.这种通信的代价取决于通信信道的数量,而信道的数量将按照系统内工作者数量的二次方增长。因此最终开销比带来的收益增长得更快,这是产生扩展性倒退得原因。如图所示,阐明了目前讨论到的三个概念:线性扩展、Amdahl扩展以及USL扩展。大多数真实系统看起来更像USL取线。
USL可以应用于硬件和软件领域。对于硬件,横轴表示硬件的数量,例如服务器数量或CPU数量。每个硬件的工作量、数据大小以及查询的复杂度必须保持为常量(现实中很难精确定义硬件的可扩展性,因为当你改变你的系统中的服务器数量时很难保证哪些变量不变)。对于软件,横轴表示并发度,例如用户数或者线程数。每个并发的工作量必须保持为常量。有一点很重要,USL并不能完美地描述真实系统,它只是一个简化哦行。但这是一个很好的框架,可用于理解为什么系统增长无法带来等同的收益。它也揭示了一个构建高可扩展性系统的重要原则:在系统内尽量避免串行化和交互。
可以衡量一个系统并使用回归来确定串行和交互的量。你可以将它作为容量规划和性能预测评估的最优上限值。也可以检查系统是怎么偏离USL模型的,将其作为最差下限值以之处系统的哪一部分没有表现出它应有的性能。这两种情况下,USL给出了一个讨论可扩展性的参考。如果没有USL,那即使盯着系统看也无法直到期望的结果是什么。
另外一个理解可扩展性的框架是约束理论,它解释了如何通过减少依赖时间和统计变化(statistical variation)来改进系统的吞吐量和性能。这在Eliyahu M.Goldratt所撰写的The Goal(North River)一书中有描述,其中有一个关于管理制造业设备的眼神的比喻。尽管这看起来和数据库服务器没有什么关联,但其中包含的法则和排队理论以及其他运筹学方面是一样的
有必要探讨以下可扩展性在数学上的定义了,这有助于在更高层次的概念上清晰地理解可扩展性。如果没有这样的基础,就可能无法理解或精确地表达可扩展性。不过不用担心,这里不会涉及高等数学,即使不是数学天才,也能够很直观地理解它。关键是前面使用的短语:"划算的等同提升(equal bang for the buck)".另一种说法是,可扩展性是当增加资源以处理负载和增加容量时系统能够获得的投资产出率(ROI).假设有一个只有一台服务器的系统,并且能够测量它的最大容量,如图所示。下面用s表示服务器,c表示容量,假设我们现在增加一台服务器,系统的能力加倍,如图所示。这就是线性扩展。我们增加了一倍的服务器,结果增加了一倍的容量。大部分系统并不是线性扩展的,而是如图所示的扩展方式。大部分系统都只能以比线性扩展略低的扩展系数进行扩展,越高的扩展系数会导致越大的线性偏差。事实上,多数系统最终会达到一个最大吞吐量临界点,超过这个点后增加头伏反而会带来负回报——继续增加更多工作负载,实际上会降低系统的吞吐量(事实上,"投资产出率"也可以从金融投资的角度来考虑,将一个组件的容量升级到两倍所需要符出的常常不止是最初开销的两倍。虽然在现实世界里我们常常这么考虑,但在讨论中会将其忽略掉,因为它会使一个已经复杂的主题变得更加复杂)。这怎么可能呢?这些年产生了许多可扩展性模型,它们有着不同程度的良好表现和实用性。我们这里所讲的可扩展性模型是基于某些能够影响系统扩展的内在机制。这就是Neil J.Gunther博士提出的通用可扩展性定律(Universal Scalability Law, USL).Gunther博士将这些详尽地写到了他的书中,包括Guerrilla Capacity Planning(Springer).这里不会深入到背后的数学理论中。
简而言之,USL说的是线性扩展的偏差可通过两个因素来建立模型:无法并发执行的一部分工作,以及需要交互的另外一部分工作。为第一个因素建模就有了著名的Amdahl定律,它会导致吞吐量趋于平缓。如果部分任务无法并行,那么不管你如何分而治之,该任务至少需要串行部分的时间。
增加第二个因素——内部节点间或者进程间的通信——到Amdahl定律就得出了USL.这种通信的代价取决于通信信道的数量,而信道的数量将按照系统内工作者数量的二次方增长。因此最终开销比带来的收益增长得更快,这是产生扩展性倒退得原因。如图所示,阐明了目前讨论到的三个概念:线性扩展、Amdahl扩展以及USL扩展。大多数真实系统看起来更像USL取线。
USL可以应用于硬件和软件领域。对于硬件,横轴表示硬件的数量,例如服务器数量或CPU数量。每个硬件的工作量、数据大小以及查询的复杂度必须保持为常量(现实中很难精确定义硬件的可扩展性,因为当你改变你的系统中的服务器数量时很难保证哪些变量不变)。对于软件,横轴表示并发度,例如用户数或者线程数。每个并发的工作量必须保持为常量。有一点很重要,USL并不能完美地描述真实系统,它只是一个简化哦行。但这是一个很好的框架,可用于理解为什么系统增长无法带来等同的收益。它也揭示了一个构建高可扩展性系统的重要原则:在系统内尽量避免串行化和交互。
可以衡量一个系统并使用回归来确定串行和交互的量。你可以将它作为容量规划和性能预测评估的最优上限值。也可以检查系统是怎么偏离USL模型的,将其作为最差下限值以之处系统的哪一部分没有表现出它应有的性能。这两种情况下,USL给出了一个讨论可扩展性的参考。如果没有USL,那即使盯着系统看也无法直到期望的结果是什么。
另外一个理解可扩展性的框架是约束理论,它解释了如何通过减少依赖时间和统计变化(statistical variation)来改进系统的吞吐量和性能。这在Eliyahu M.Goldratt所撰写的The Goal(North River)一书中有描述,其中有一个关于管理制造业设备的眼神的比喻。尽管这看起来和数据库服务器没有什么关联,但其中包含的法则和排队理论以及其他运筹学方面是一样的
一个线性扩展的系统能由两台服务器获得两倍容量
一个非线性扩展的系统
线性扩展、Amadhl扩展以及USL扩展定律
扩展模型不是最终定论。
虽然有许多理论,但在现实中能做到何种程度呢?正如牛顿定律被证明只有远低于光速时才合理,哪些"扩展性定律"也只是在某些场景下才能很好地工作的简化模型。有一种说法认为所有的模型都是错误的,但有一些模型还是有用的,特别是USL能够帮助理解一些导致扩展性差的因素。
当工作负载和其所运行的系统存在微妙的关系时,USL理论可能失效。例如,一个USL无法很好建模的常见情况是:当集群的总内存由于数据集大小而发生改变时,也会导致系统的行为发生变化。USL不允许比线性更好的可扩展性,但现实中可能会发生这样的事情:增加系统的资源后,原来一部分IO密集型的工作变成了纯内存工作,因此获得了超过线性的性能扩展。
还有一些情况,USL无法很好地描述系统行为。当系统或数据集大小改变时算法的复杂度可能改变,类似这样的情况可能就无法建立模型(USL由O(1)复杂度和O(N^2)复杂度两部分构成,那么对于诸如O(logN)或者O(NlogN)这样复杂度的部分呢?)根据一些思考和实际经验,我们可以将USL扩展以覆盖这些比较普遍的场景中的一部分。但这会将一个简单并且有用的模型变得复杂并难以使用。事实上,它在很多情况下都是很好地,足以为你所能想象到的系统行为建立模型,这也是为什么我们发现它是在正确性和有效性之间的一个很好的妥协。
简单地说:有保留地使用模型,并且在使用中验证你的发现
虽然有许多理论,但在现实中能做到何种程度呢?正如牛顿定律被证明只有远低于光速时才合理,哪些"扩展性定律"也只是在某些场景下才能很好地工作的简化模型。有一种说法认为所有的模型都是错误的,但有一些模型还是有用的,特别是USL能够帮助理解一些导致扩展性差的因素。
当工作负载和其所运行的系统存在微妙的关系时,USL理论可能失效。例如,一个USL无法很好建模的常见情况是:当集群的总内存由于数据集大小而发生改变时,也会导致系统的行为发生变化。USL不允许比线性更好的可扩展性,但现实中可能会发生这样的事情:增加系统的资源后,原来一部分IO密集型的工作变成了纯内存工作,因此获得了超过线性的性能扩展。
还有一些情况,USL无法很好地描述系统行为。当系统或数据集大小改变时算法的复杂度可能改变,类似这样的情况可能就无法建立模型(USL由O(1)复杂度和O(N^2)复杂度两部分构成,那么对于诸如O(logN)或者O(NlogN)这样复杂度的部分呢?)根据一些思考和实际经验,我们可以将USL扩展以覆盖这些比较普遍的场景中的一部分。但这会将一个简单并且有用的模型变得复杂并难以使用。事实上,它在很多情况下都是很好地,足以为你所能想象到的系统行为建立模型,这也是为什么我们发现它是在正确性和有效性之间的一个很好的妥协。
简单地说:有保留地使用模型,并且在使用中验证你的发现
扩展性MySQL。
如果将应用所有的数据简单地放到单个MySQL服务器实例上,则无法很好地扩展,迟早会碰到性能瓶颈。对于许多类型的应用,传统的解决办法是购买更多强悍的机器,也就是常说的"垂直扩展"或者"向上扩展"。另外一个与之相反的方法是将任务分配到多台计算机上,这通常被称为"水平扩展"或者"向外扩展"。接下来将讨论如何联合使用向上扩展和向外扩展的解决方案,以及如何使用集群方案来进行扩展。最后,大部分应用还会有一些很少或者从不需要的数据,这些数据可以被清理或归档。
如果将应用所有的数据简单地放到单个MySQL服务器实例上,则无法很好地扩展,迟早会碰到性能瓶颈。对于许多类型的应用,传统的解决办法是购买更多强悍的机器,也就是常说的"垂直扩展"或者"向上扩展"。另外一个与之相反的方法是将任务分配到多台计算机上,这通常被称为"水平扩展"或者"向外扩展"。接下来将讨论如何联合使用向上扩展和向外扩展的解决方案,以及如何使用集群方案来进行扩展。最后,大部分应用还会有一些很少或者从不需要的数据,这些数据可以被清理或归档。
规划可扩展性。
人们通常只有在无法满足增加的负载时才会考虑到可扩展性,具体表现为工作负载从CPU密集型变成IO密集型,并发查询的竞争,以及不断增大的延迟。主要原因是查询的复杂度增加或者内存中驻留着一部分不再使用的数据或者索引。你可能看到一部分类型的查询发生改变,例如大的查询或者复杂查询常常比哪些小的查询更影响系统。如果是可扩展的应用,则可以简单地增加更多的服务器来分担负载,这样就没有性能问题了。但如果不是可扩展的,你会发现自己将遭遇到无穷无尽的问题。可以通过规划可扩展性来避免这个问题。
规划可扩展性最困难的部分是估算需要承担的负载到底有多少。这个值不一定非常精确,但必须在一定的数量级范围内。如果估计过高,会浪费开发资源。但如果低估了,则难以应付可能的负载。
另外还需要大致正确地估计日程表——也就是说,需要知道底线在哪里。对于一些应用,一个简单的原型可以很好地工作几个月,从而有事件去筹资建立一个更加扩展的架构。对于其他的一些应用,你可能需要当前的架构能够为未来两年提供足够的容量。
以下的问题可以帮助规划可扩展性:
1.应用的功能完成了多少?许多建议的可扩展性解决方案可能会导致实现某些功能变得更加困难。如果应用的某些核心功能还没有开始实现,就很难看出如何在一个可扩展的应用中实现它们。同样地,在知道这些特性如何真正地工作之前也很难决定,使用哪一种可扩展性解决方案
2.预期的最大负载是多少?应用应当在最大负载下也可以正常工作。如果你的网络和Yahoo!News或者Slashdot的首页一样,会发生什么呢?即使不是很热门的网站,也同样有最高负载。例如,对于一个在线零售商,假日期间——尤其是在圣诞前的几个星期——通常是负载达到巅峰的时候。在美国,情人节和母亲节前的周末对于在线花店来说也是负载高峰期
3.如果依赖系统的每个部分来分担负载,在某个部分失效时会发生什么呢?例如,如果依赖备库来分担读负载,当其中一个失效时,是否还能正常处理请求?是否需要禁用一些功能?可以预先准备一些空闲容量来防范这种问题
人们通常只有在无法满足增加的负载时才会考虑到可扩展性,具体表现为工作负载从CPU密集型变成IO密集型,并发查询的竞争,以及不断增大的延迟。主要原因是查询的复杂度增加或者内存中驻留着一部分不再使用的数据或者索引。你可能看到一部分类型的查询发生改变,例如大的查询或者复杂查询常常比哪些小的查询更影响系统。如果是可扩展的应用,则可以简单地增加更多的服务器来分担负载,这样就没有性能问题了。但如果不是可扩展的,你会发现自己将遭遇到无穷无尽的问题。可以通过规划可扩展性来避免这个问题。
规划可扩展性最困难的部分是估算需要承担的负载到底有多少。这个值不一定非常精确,但必须在一定的数量级范围内。如果估计过高,会浪费开发资源。但如果低估了,则难以应付可能的负载。
另外还需要大致正确地估计日程表——也就是说,需要知道底线在哪里。对于一些应用,一个简单的原型可以很好地工作几个月,从而有事件去筹资建立一个更加扩展的架构。对于其他的一些应用,你可能需要当前的架构能够为未来两年提供足够的容量。
以下的问题可以帮助规划可扩展性:
1.应用的功能完成了多少?许多建议的可扩展性解决方案可能会导致实现某些功能变得更加困难。如果应用的某些核心功能还没有开始实现,就很难看出如何在一个可扩展的应用中实现它们。同样地,在知道这些特性如何真正地工作之前也很难决定,使用哪一种可扩展性解决方案
2.预期的最大负载是多少?应用应当在最大负载下也可以正常工作。如果你的网络和Yahoo!News或者Slashdot的首页一样,会发生什么呢?即使不是很热门的网站,也同样有最高负载。例如,对于一个在线零售商,假日期间——尤其是在圣诞前的几个星期——通常是负载达到巅峰的时候。在美国,情人节和母亲节前的周末对于在线花店来说也是负载高峰期
3.如果依赖系统的每个部分来分担负载,在某个部分失效时会发生什么呢?例如,如果依赖备库来分担读负载,当其中一个失效时,是否还能正常处理请求?是否需要禁用一些功能?可以预先准备一些空闲容量来防范这种问题
为扩展赢得时间。
在理想情况下,应该时计划先行,拥有足够的开发者、有花不完的预算,等等,但现实中这些情况会很复杂,在扩展应用时常常需要做一些妥协,特别是需要把对系统的改动推迟一段时间再执行。在深入MySQL扩展的细节前,以下是一些可以做的准备工作:
1.优化性能
很多时候可以通过一个简单的改动来获得明显的性能提升,例如为表建立正确的索引或从MyISAM切换到InnoDB存储引擎。如果遇到了性能限制,可以打开查询日志进行分析。在修复了大多数主要的问题后,会到达一个收益递减点,这时候提升性能会变得越来越困难。每个新的优化都可能耗费更多的经历但只有很小的提升,并会使应用更加复杂
2.购买性能更强的硬件
升级或增加服务器在某些场景下行之有效,特别使对于处于软件生命周期早期的应用,购买更多的服务器或者增加内存通常是个好办法。另一个选择是尽量在一台服务器上运行应用程序。比起修改应用的涉及,购买更多的硬件可能是更实际办法,特别是时间紧急并且缺乏开发者的时候。
如果应用很小或者被涉及为便于利用更多的硬件,那么购买更多的硬件应该是行之有效的办法。对于新应用这是很普遍的,因为它们通常很小或者涉及合理。但对于大型的旧应用,购买更多硬件可能没什么效果,或者代价太高。服务器从1台增加到3台或许算不了什么,但从100台增加到300台就是另外一回事儿了——代价非常昂贵。如果是这样,花一些时间和经历来尽可能地提升现有系统的性能就很划算
在理想情况下,应该时计划先行,拥有足够的开发者、有花不完的预算,等等,但现实中这些情况会很复杂,在扩展应用时常常需要做一些妥协,特别是需要把对系统的改动推迟一段时间再执行。在深入MySQL扩展的细节前,以下是一些可以做的准备工作:
1.优化性能
很多时候可以通过一个简单的改动来获得明显的性能提升,例如为表建立正确的索引或从MyISAM切换到InnoDB存储引擎。如果遇到了性能限制,可以打开查询日志进行分析。在修复了大多数主要的问题后,会到达一个收益递减点,这时候提升性能会变得越来越困难。每个新的优化都可能耗费更多的经历但只有很小的提升,并会使应用更加复杂
2.购买性能更强的硬件
升级或增加服务器在某些场景下行之有效,特别使对于处于软件生命周期早期的应用,购买更多的服务器或者增加内存通常是个好办法。另一个选择是尽量在一台服务器上运行应用程序。比起修改应用的涉及,购买更多的硬件可能是更实际办法,特别是时间紧急并且缺乏开发者的时候。
如果应用很小或者被涉及为便于利用更多的硬件,那么购买更多的硬件应该是行之有效的办法。对于新应用这是很普遍的,因为它们通常很小或者涉及合理。但对于大型的旧应用,购买更多硬件可能没什么效果,或者代价太高。服务器从1台增加到3台或许算不了什么,但从100台增加到300台就是另外一回事儿了——代价非常昂贵。如果是这样,花一些时间和经历来尽可能地提升现有系统的性能就很划算
向上扩展。
向上扩展(有时也称为垂直扩展)意味着购买更多性能强悍的硬件,对很多应用来说这是唯一需要做的事情。这种策略有很多好处。例如,单台服务器比多台服务器更加容易维护和开发,能显著节约开销。在单台服务器上备份和恢复应用很简单,因为无须关注以执行或者哪个数据集是权威的。当然还有一些别的原因。从复杂性的成本来说,向上扩展比向外扩展更简单。
向上扩展的空间起始也很大。拥有0.5TB内存、32核(或者更多)CPU以及更强悍IO性能的(例如PCIe卡的flash存储)商用服务器现在很容易获得。优秀的应用核数据库涉及,以及很好的性能优化技能,可以帮助你在这样的服务器上建立一个MySQL大型应用。
在现代硬件上MySQL能扩展到多大的规模呢?尽管可以在非常强大的服务器上运行,但和大多数数据库服务器一样,在增加硬件资源的情况下MySQL也无法很好地扩展(非常奇怪!)。为了更好地在大型服务器上运行MySQL,一定要尽量选择最新的版本,由于内部可扩展性问题,MySQL5.0和5.1在大型硬件里的表现并不理想。建议使用5.5或者更新的版本,或者Percona Server5.1及后续版本。即便如此,当前合理的"收益递减点"的机器配置大约是256GB RAM,32核CPU以及一个PCIe flash驱动器,如果继续提升硬件的配置,MySQL的性能虽然还能有所提升,但性价比就会降低,实际上,在更强大的系统上,也可以通过运行多个小的MySQL实例来替代单个大实例,这样可以获得更好的性能。当然,机器配置的变化速度非常快,这个建议也许很快就会过时了。
向上扩展的策略能够顶一段时间,实际很多应用是不会达到天花板的。但是如果应用变得非常大,向上扩展可能就没有办法了。第一个原因是前,无论服务器上运行什么样的软件,从某种程度来看,向上扩展都是个糟糕的财务决策,当超过硬件能够提供的最优性价比时,就会需要非同寻常的特殊配置的硬件,这样的硬件往往非常昂贵。这意味着能向上扩展到什么地步是有实际的限制的。如果使用了复制,那么当主库升级到高端硬件后,一般是不太可能配置出一台能够跟上主库的强大备库的。一个高负载的主库通常可以承担比拥有同样配置的备库更多的工作因为备库的复制线程无法高效地利用多核CPU和磁盘资源.
最后,向上扩展不是无限制的,即使最强大的计算机也有限制。单服务器应用通常会首先达到读限制,特别是执行复杂的读查询时。类似这样的查询在MySQL内部是单线程的新,如图所示,因此只能使用一个CPU,这种情况下花钱也无法提升性能。即使购买最快地CPU也仅仅会是商用CPU的几倍速度。增加更多的CPU或CPU和数并不能使得慢查询执行得更快。当数据变得庞大以至于无法有效缓存时,内存也会称为瓶颈,这通常表现为很高的磁盘使用率,而磁盘是现在计算机中最慢的部分。
无法使用向上扩展最明显的场景是云计算。在大多数公有云中都无法获得性能非常强的服务器,如果应用肯定会变得非常庞大,就不能选择向上扩展的方式。
因此,建议,如果系统确实有可能碰到可扩展性的天花板,并且会导致严重的业务问题,那就不要无限制地做向上扩展的规划。如果你知道应用会变得很庞大,在实现另外一种解决方案前,短期内购买更优的服务器是可以的。但是最终还是需要向外扩展。
向上扩展(有时也称为垂直扩展)意味着购买更多性能强悍的硬件,对很多应用来说这是唯一需要做的事情。这种策略有很多好处。例如,单台服务器比多台服务器更加容易维护和开发,能显著节约开销。在单台服务器上备份和恢复应用很简单,因为无须关注以执行或者哪个数据集是权威的。当然还有一些别的原因。从复杂性的成本来说,向上扩展比向外扩展更简单。
向上扩展的空间起始也很大。拥有0.5TB内存、32核(或者更多)CPU以及更强悍IO性能的(例如PCIe卡的flash存储)商用服务器现在很容易获得。优秀的应用核数据库涉及,以及很好的性能优化技能,可以帮助你在这样的服务器上建立一个MySQL大型应用。
在现代硬件上MySQL能扩展到多大的规模呢?尽管可以在非常强大的服务器上运行,但和大多数数据库服务器一样,在增加硬件资源的情况下MySQL也无法很好地扩展(非常奇怪!)。为了更好地在大型服务器上运行MySQL,一定要尽量选择最新的版本,由于内部可扩展性问题,MySQL5.0和5.1在大型硬件里的表现并不理想。建议使用5.5或者更新的版本,或者Percona Server5.1及后续版本。即便如此,当前合理的"收益递减点"的机器配置大约是256GB RAM,32核CPU以及一个PCIe flash驱动器,如果继续提升硬件的配置,MySQL的性能虽然还能有所提升,但性价比就会降低,实际上,在更强大的系统上,也可以通过运行多个小的MySQL实例来替代单个大实例,这样可以获得更好的性能。当然,机器配置的变化速度非常快,这个建议也许很快就会过时了。
向上扩展的策略能够顶一段时间,实际很多应用是不会达到天花板的。但是如果应用变得非常大,向上扩展可能就没有办法了。第一个原因是前,无论服务器上运行什么样的软件,从某种程度来看,向上扩展都是个糟糕的财务决策,当超过硬件能够提供的最优性价比时,就会需要非同寻常的特殊配置的硬件,这样的硬件往往非常昂贵。这意味着能向上扩展到什么地步是有实际的限制的。如果使用了复制,那么当主库升级到高端硬件后,一般是不太可能配置出一台能够跟上主库的强大备库的。一个高负载的主库通常可以承担比拥有同样配置的备库更多的工作因为备库的复制线程无法高效地利用多核CPU和磁盘资源.
最后,向上扩展不是无限制的,即使最强大的计算机也有限制。单服务器应用通常会首先达到读限制,特别是执行复杂的读查询时。类似这样的查询在MySQL内部是单线程的新,如图所示,因此只能使用一个CPU,这种情况下花钱也无法提升性能。即使购买最快地CPU也仅仅会是商用CPU的几倍速度。增加更多的CPU或CPU和数并不能使得慢查询执行得更快。当数据变得庞大以至于无法有效缓存时,内存也会称为瓶颈,这通常表现为很高的磁盘使用率,而磁盘是现在计算机中最慢的部分。
无法使用向上扩展最明显的场景是云计算。在大多数公有云中都无法获得性能非常强的服务器,如果应用肯定会变得非常庞大,就不能选择向上扩展的方式。
因此,建议,如果系统确实有可能碰到可扩展性的天花板,并且会导致严重的业务问题,那就不要无限制地做向上扩展的规划。如果你知道应用会变得很庞大,在实现另外一种解决方案前,短期内购买更优的服务器是可以的。但是最终还是需要向外扩展。
向外扩展。
可以把向外扩展(有时也称为横向扩展或水平扩展)策略划分为三个部分:复制、拆分以及数据分片(sharding).最简单也最常见的向外扩展的方法是通过复制将数据分发到多个服务器上,然后将备库用于读查询。这种技术对于以读为主的应用很有效。它也有一些缺点,例如重复缓存,但如果数据规模有限这就不是问题。另外一个比较常见的向外扩展是工作负载分布到多个"节点"。具体如何分布工作负载是一个复杂的话题。许多大型的MySQL应用不能自动分布负载,就算有也没有做到完全的自动化。后面会讨论一些可能的分布负载的方案,并探讨它们的优点和缺点。
在MySQL架构中,一个节点(node)就是一个功能部件。如果没有规划冗余和高可用性。那么一个节点可能就是一台服务器。如果设计的是能够故障转移的冗余系统,那么一个节点通常可能是下面的某一种:
1.一个主——主复制双机结构,拥有一个主动服务器和被动服务器
2.一个主库和多个备库
3.一个主动服务器,并使用分布式复制块设备(DRBD)作为备用服务器
4.一个基于存储区域网络(SAN)的"集群"
大多数情况下,一个节点内的所有服务器应该拥有相同的数据。我们倾向把主——主复制架构作为两台服务器的主动——被动节点
可以把向外扩展(有时也称为横向扩展或水平扩展)策略划分为三个部分:复制、拆分以及数据分片(sharding).最简单也最常见的向外扩展的方法是通过复制将数据分发到多个服务器上,然后将备库用于读查询。这种技术对于以读为主的应用很有效。它也有一些缺点,例如重复缓存,但如果数据规模有限这就不是问题。另外一个比较常见的向外扩展是工作负载分布到多个"节点"。具体如何分布工作负载是一个复杂的话题。许多大型的MySQL应用不能自动分布负载,就算有也没有做到完全的自动化。后面会讨论一些可能的分布负载的方案,并探讨它们的优点和缺点。
在MySQL架构中,一个节点(node)就是一个功能部件。如果没有规划冗余和高可用性。那么一个节点可能就是一台服务器。如果设计的是能够故障转移的冗余系统,那么一个节点通常可能是下面的某一种:
1.一个主——主复制双机结构,拥有一个主动服务器和被动服务器
2.一个主库和多个备库
3.一个主动服务器,并使用分布式复制块设备(DRBD)作为备用服务器
4.一个基于存储区域网络(SAN)的"集群"
大多数情况下,一个节点内的所有服务器应该拥有相同的数据。我们倾向把主——主复制架构作为两台服务器的主动——被动节点
1.按功能拆分。
按功能拆分,或者说按职责拆分,意味着不同的节点执行不同的任务。我们之前已经提到了一些类似的实现,。按功能拆分采取的策略比这些更进一步,将独立的服务器或节点分配给不同的应用,这样每个节点只包含它的特定应用所需要的数据。
这里我们显式地使用了"应用"一词。所指的并不是一个单独的计算机程序,而是相关的一系列程序,这些程序可以很容易地批彼此分离,没有关联。例如如果有一个网站,各个部分无须共享数据,那么可以按照网站的功能进行划分。门户网站常常把不同的栏目放在一起;在门户网站,可以浏览网站新闻、论坛,寻求支持和访问知识库等,等等。这些不同功能区域的数据可以放到专用的MySQL服务器中,如图所示。
如果应用很庞大,每个功能区域还可以拥有其专用的Web服务器,但没有专用的数据库服务器这么常见。另一个可能的按功能划分方法是对单个服务器的数据进行划分,并确保划分的表集合之间不会执行关联操作。当必须执行关联操作时,如果对性能要求不高,可以在应用中做关联。虽然有一些变通的方法,但它们有一个共同点,就是每种类型的数据只能在单个节点上找到。这并不是一种通用的分布数据方法,因为很难做到高效,并且相比其他方案没有任何优势。
归根结底,还是不能通过功能划分来无限地进行扩展,因为如果一个功能区域被捆绑到单个MySQL节点,就只能进行垂直扩展。其中的一个应用或者功能区域最终增长到非常庞大时,都会迫使你去寻求一个不同的策略。如果进行了太多的功能划分,以后就很难采用更具扩展性的设计了
按功能拆分,或者说按职责拆分,意味着不同的节点执行不同的任务。我们之前已经提到了一些类似的实现,。按功能拆分采取的策略比这些更进一步,将独立的服务器或节点分配给不同的应用,这样每个节点只包含它的特定应用所需要的数据。
这里我们显式地使用了"应用"一词。所指的并不是一个单独的计算机程序,而是相关的一系列程序,这些程序可以很容易地批彼此分离,没有关联。例如如果有一个网站,各个部分无须共享数据,那么可以按照网站的功能进行划分。门户网站常常把不同的栏目放在一起;在门户网站,可以浏览网站新闻、论坛,寻求支持和访问知识库等,等等。这些不同功能区域的数据可以放到专用的MySQL服务器中,如图所示。
如果应用很庞大,每个功能区域还可以拥有其专用的Web服务器,但没有专用的数据库服务器这么常见。另一个可能的按功能划分方法是对单个服务器的数据进行划分,并确保划分的表集合之间不会执行关联操作。当必须执行关联操作时,如果对性能要求不高,可以在应用中做关联。虽然有一些变通的方法,但它们有一个共同点,就是每种类型的数据只能在单个节点上找到。这并不是一种通用的分布数据方法,因为很难做到高效,并且相比其他方案没有任何优势。
归根结底,还是不能通过功能划分来无限地进行扩展,因为如果一个功能区域被捆绑到单个MySQL节点,就只能进行垂直扩展。其中的一个应用或者功能区域最终增长到非常庞大时,都会迫使你去寻求一个不同的策略。如果进行了太多的功能划分,以后就很难采用更具扩展性的设计了
2.数据分片。
在目前用于扩展大型MySQL应用的方案中,数据分片(分片也被称为"分裂"、"分区",但是我们使用"分片"以避免混淆。谷歌将它称为"分片",如果谷歌觉得这样称呼合适,我们采取这种称呼也就合适了)是最通用且最成功的方法。它把数据分割成一小片,或者说一块,然后存储到不同的节点中。数据分片在和某些类型的按功能划分联合使用时非常有用。大多数分片系统也有一些"全局的"数据不会被分片(例如城市列表或者登录数据)。全局数据一般存储在单个节点上,并且通常保存在类似memcached这样的缓存里。
事实上,大多数应用只会对需要的数据做分片——通常那些将会增长得非常庞大的数据。假设正在构建的博客服务,预计会有1000万用户,这时候就无须对注册用户进行分片,因为完全可以将所有的用户(或者其中的活跃用户)放到内存中。加入用户数达到5亿,那么就可能需要对用户数据分片。用户产生的内容,例如发表的文章和评论,几乎肯定需要进行数据分片,因为这些数据非常庞大,并且还会越来越多。
大型应用可能有多个逻辑数据集,并且处理方式也可以各不相同。可以将它们存储到不同的服务器组上,但这并不是必需的。还可以以多种方式对数据进行分片,这取决于如何使用它们。
分片技术和大多数应用的最初设计有着显著的差异,并且很难将应用从单一数据存储转换为分片架构。如果在应用设计储器就已经预计到分片,那实现起来就容易得多。许多一开始没有建立分片架构的应用都会碰到规模扩大的情形。例如,可以使用复制来扩展博客服务的读查询,直到它不再奏效。然后可以把服务器划分为三个部分:用户信息、文章,以及评论。可以将这些数据放到不同的服务器上(按功能划分),也许可以使用面向服务的架构,并在应用层执行联合查询,如图显示了从单台服务器到按功能划分的演变。
最后可以通过用户ID来对文章和评论进行分片,而将用户信息保留在单个节点上,如果为全局节点配置一个主——备结构并为分片节点使用主——主结构,最终的数据可能如图所示。如果事先直到应用会扩大到很大的规模,并且清楚按功能划分的局限性,就可以台哦过中间步骤,直接从单个节点升级为分片数据存储。事实上,这种前瞻性可以帮你避免由于粗糙的分片方案带来的挑战。采用分片的应用常会有一个数据库访问抽象层,用以降低应用和分片数据存储之前通信的复杂度,但无法完全隐藏分片。因为相比数据存储,应用通常更了解跟查询相关的一些信息。太多的抽象会导致低效率,例如查询所有的节点,可实际上需要的数据只在单一节点上。分片数据存储看起来像是优雅的解决方案,单很难实现。那为什么要选择这个架构呢?答案很简单:如果想扩展写容量,就必需切分数据。如果只有单台主库,那么不管有多少备库,写容量都是无法扩展的。对于上述缺点而言,数据分片是我们首选的解决方案
在目前用于扩展大型MySQL应用的方案中,数据分片(分片也被称为"分裂"、"分区",但是我们使用"分片"以避免混淆。谷歌将它称为"分片",如果谷歌觉得这样称呼合适,我们采取这种称呼也就合适了)是最通用且最成功的方法。它把数据分割成一小片,或者说一块,然后存储到不同的节点中。数据分片在和某些类型的按功能划分联合使用时非常有用。大多数分片系统也有一些"全局的"数据不会被分片(例如城市列表或者登录数据)。全局数据一般存储在单个节点上,并且通常保存在类似memcached这样的缓存里。
事实上,大多数应用只会对需要的数据做分片——通常那些将会增长得非常庞大的数据。假设正在构建的博客服务,预计会有1000万用户,这时候就无须对注册用户进行分片,因为完全可以将所有的用户(或者其中的活跃用户)放到内存中。加入用户数达到5亿,那么就可能需要对用户数据分片。用户产生的内容,例如发表的文章和评论,几乎肯定需要进行数据分片,因为这些数据非常庞大,并且还会越来越多。
大型应用可能有多个逻辑数据集,并且处理方式也可以各不相同。可以将它们存储到不同的服务器组上,但这并不是必需的。还可以以多种方式对数据进行分片,这取决于如何使用它们。
分片技术和大多数应用的最初设计有着显著的差异,并且很难将应用从单一数据存储转换为分片架构。如果在应用设计储器就已经预计到分片,那实现起来就容易得多。许多一开始没有建立分片架构的应用都会碰到规模扩大的情形。例如,可以使用复制来扩展博客服务的读查询,直到它不再奏效。然后可以把服务器划分为三个部分:用户信息、文章,以及评论。可以将这些数据放到不同的服务器上(按功能划分),也许可以使用面向服务的架构,并在应用层执行联合查询,如图显示了从单台服务器到按功能划分的演变。
最后可以通过用户ID来对文章和评论进行分片,而将用户信息保留在单个节点上,如果为全局节点配置一个主——备结构并为分片节点使用主——主结构,最终的数据可能如图所示。如果事先直到应用会扩大到很大的规模,并且清楚按功能划分的局限性,就可以台哦过中间步骤,直接从单个节点升级为分片数据存储。事实上,这种前瞻性可以帮你避免由于粗糙的分片方案带来的挑战。采用分片的应用常会有一个数据库访问抽象层,用以降低应用和分片数据存储之前通信的复杂度,但无法完全隐藏分片。因为相比数据存储,应用通常更了解跟查询相关的一些信息。太多的抽象会导致低效率,例如查询所有的节点,可实际上需要的数据只在单一节点上。分片数据存储看起来像是优雅的解决方案,单很难实现。那为什么要选择这个架构呢?答案很简单:如果想扩展写容量,就必需切分数据。如果只有单台主库,那么不管有多少备库,写容量都是无法扩展的。对于上述缺点而言,数据分片是我们首选的解决方案
分片?还是不分片?
这是一个问题,对吧?答案很简单:如非必要,尽量不分片。首先看是否能通过性能调优或者更好的应用或数据库设计来推迟分片。如果能足够长时间地推迟分片,也许可以直接购买更大地服务器,升级MySQL到性能更优地版本,然后继续使用单台服务器,也可以增加或减少复制。
简单地说,对单台服务器而言,数据大小或写负载变得太大时,分片将是不可避免的。如果不分片,而是尽可能地优化应用,系统能扩展到什么程度呢?答案可能会让你惊讶。有些非常受欢迎的应用,你可能以为从一开始就分片了,但实际上直到已经值数十亿美元并且流量及其巨大也还没有采用分片的设计。分片不是城里唯一的游戏,在没有必要的情况下采用分片的架构来构建应用会步履维艰
这是一个问题,对吧?答案很简单:如非必要,尽量不分片。首先看是否能通过性能调优或者更好的应用或数据库设计来推迟分片。如果能足够长时间地推迟分片,也许可以直接购买更大地服务器,升级MySQL到性能更优地版本,然后继续使用单台服务器,也可以增加或减少复制。
简单地说,对单台服务器而言,数据大小或写负载变得太大时,分片将是不可避免的。如果不分片,而是尽可能地优化应用,系统能扩展到什么程度呢?答案可能会让你惊讶。有些非常受欢迎的应用,你可能以为从一开始就分片了,但实际上直到已经值数十亿美元并且流量及其巨大也还没有采用分片的设计。分片不是城里唯一的游戏,在没有必要的情况下采用分片的架构来构建应用会步履维艰
3.选择分区键(partitioning key)
数据分片最大的挑战是查找和获取数据:如何查找数据取决于如何进行分片。有很多方法,其中有一些方法会比另外一些更好。我们的目标是对那些最重要并且频繁查询的数据减少分片(记住,可扩展性法则的其中一条就是要避免不同节点间的交互)。这其中最重要的是如何为数据选择一个或多个分区键。分区键决定了每一行分配到哪一个分片中。如果直到一个对象的分区键,就可以回答如下两个问题:
1.应该在哪里存储数据?
2.应该从哪里取到希望得到的数据?
后面讲展示多个选择和使用分区键的方法。先看一个例子。假设像MySQL NDB Cluster那样来操作,并对每个表的主键使用哈希来将数据分割到各个分片中。这是一种非常简单的实现,但可扩展性不好,因为可能需要频繁检查所有的分片来获得需要的数据。例如,如果想查看user3的博客文章,可以从哪里找到呢?由于使用主键值而非用户名进行分割,博客文章可能均匀分散在所有的数据分片中。使用主键值哈希简化了判断数据存储在何处的操作,但却可能增加获取数据的难度,具体取决于需要什么数据以及是否知道主键。跨多个分片的查询比单个分片上的查询性能要差,但只要不涉及太多的分片,也不会太糟糕。最糟糕的情况是不知道需要的数据存储在哪里,这时候就需要扫描所有分片。
一个好的分区键常常是数据库中一个非常重要的实体的主键。这些键值决定了分片单元。例如,如果通过用户ID或客户端ID来分割数据,分片单元就是用户或者客户端。确定分区键一个比较好的办法是用实体——关系图,或一个等效的能显示所有实体及其关系的工具来展示数据模型。尽量把相关联的实体靠的更近。这样可以很直观地找出候选分区键。当然不要仅仅看图,同样地也要考虑应用的查询。即使两个实体在某些方面是相关联的,但如果很少或几乎不对其做关联操作,也可以打断这种联系来实现分片。
某些数据模型比其他的更容易进行分片,具体取决于实体——关系图中的关联性程度。如图的左边展示了一个易于分片的数据模型,右边的那个则很难分片。
左边的数据模型比较容易分片,因为与之相连的子图中大多数节点只有一个连接,很容易切断子图之间的联系。右边的数据模型则很难分片,因为它没有类似的子图,幸好大多数数据模型更像左边的图。
选择分区键的时候,尽可能选择那些能够避免跨分片查询的,但同时也要让分片足够小,以免过大的数据片导致问题。如果可能,应该期望分片尽可能同样小,这样在为不同数量的分片进行分组时能够很容易平衡。例如,如果应用只在美国使用,并且希望将数据分割为20个分片,则可能不应该按照州来划分,因为加利福尼亚的人口非常多。但可以按照县或者电话区号来划分,因为尽管并不是均匀的,但足以选择20个集合以粗略地表示等同的密集程度,并且基本上避免跨分片查询。
数据分片最大的挑战是查找和获取数据:如何查找数据取决于如何进行分片。有很多方法,其中有一些方法会比另外一些更好。我们的目标是对那些最重要并且频繁查询的数据减少分片(记住,可扩展性法则的其中一条就是要避免不同节点间的交互)。这其中最重要的是如何为数据选择一个或多个分区键。分区键决定了每一行分配到哪一个分片中。如果直到一个对象的分区键,就可以回答如下两个问题:
1.应该在哪里存储数据?
2.应该从哪里取到希望得到的数据?
后面讲展示多个选择和使用分区键的方法。先看一个例子。假设像MySQL NDB Cluster那样来操作,并对每个表的主键使用哈希来将数据分割到各个分片中。这是一种非常简单的实现,但可扩展性不好,因为可能需要频繁检查所有的分片来获得需要的数据。例如,如果想查看user3的博客文章,可以从哪里找到呢?由于使用主键值而非用户名进行分割,博客文章可能均匀分散在所有的数据分片中。使用主键值哈希简化了判断数据存储在何处的操作,但却可能增加获取数据的难度,具体取决于需要什么数据以及是否知道主键。跨多个分片的查询比单个分片上的查询性能要差,但只要不涉及太多的分片,也不会太糟糕。最糟糕的情况是不知道需要的数据存储在哪里,这时候就需要扫描所有分片。
一个好的分区键常常是数据库中一个非常重要的实体的主键。这些键值决定了分片单元。例如,如果通过用户ID或客户端ID来分割数据,分片单元就是用户或者客户端。确定分区键一个比较好的办法是用实体——关系图,或一个等效的能显示所有实体及其关系的工具来展示数据模型。尽量把相关联的实体靠的更近。这样可以很直观地找出候选分区键。当然不要仅仅看图,同样地也要考虑应用的查询。即使两个实体在某些方面是相关联的,但如果很少或几乎不对其做关联操作,也可以打断这种联系来实现分片。
某些数据模型比其他的更容易进行分片,具体取决于实体——关系图中的关联性程度。如图的左边展示了一个易于分片的数据模型,右边的那个则很难分片。
左边的数据模型比较容易分片,因为与之相连的子图中大多数节点只有一个连接,很容易切断子图之间的联系。右边的数据模型则很难分片,因为它没有类似的子图,幸好大多数数据模型更像左边的图。
选择分区键的时候,尽可能选择那些能够避免跨分片查询的,但同时也要让分片足够小,以免过大的数据片导致问题。如果可能,应该期望分片尽可能同样小,这样在为不同数量的分片进行分组时能够很容易平衡。例如,如果应用只在美国使用,并且希望将数据分割为20个分片,则可能不应该按照州来划分,因为加利福尼亚的人口非常多。但可以按照县或者电话区号来划分,因为尽管并不是均匀的,但足以选择20个集合以粗略地表示等同的密集程度,并且基本上避免跨分片查询。
4.多个分区键。
复杂的数据模型会使苏剧分片更加困难。许多应用拥有多个分区键,特别是存在两个或更多个"维度"的时候。换句话说,应用需要从不同的角度看到有效且连贯的数据视图。这意味着某些数据在系统内至少需要存储两份。例如,需要将博客应用的数据按照用户ID和文章ID进行分片,因为这两者都是应用查询数据时使用比较普遍的方式。试想一下这种情形:频繁地读取某个用户的所有文章,以及某个文章的所有评论。如果按用户分片就无法找到某篇文章的所有评论,而按文章分片则无法找到某个用户的所有文章。如果希望这两个查询都落到同一个分片上,就需要从两个维度进行分片。
需要多个分区键并不意味着需要去设计两个完全冗余的数据存储。我们来看看另一个例子:一个社交网站下的读书俱乐部站点,该站点的所有用户都可以对书进行评论。该网站可以显示所有书籍的所有评论,也能显示某个用户已经读过或评论过的所有书籍。假设为用户数据和书籍数据都设计了分片数据存储。而评论同时拥有用户ID和评论ID。这样就跨越了两个分片的边界。实际上却无须冗余存储两份评论数据,替代方案时,将评论和用户数据一起存储,然后把每个评论的标题和ID与书籍数据存储在一起。这样在渲染大多数关于某本书的评论的视图时无须同时访问用户和书籍数据存储,如果需要显示完整的评论内容,可以从用户数据存储中获得。
复杂的数据模型会使苏剧分片更加困难。许多应用拥有多个分区键,特别是存在两个或更多个"维度"的时候。换句话说,应用需要从不同的角度看到有效且连贯的数据视图。这意味着某些数据在系统内至少需要存储两份。例如,需要将博客应用的数据按照用户ID和文章ID进行分片,因为这两者都是应用查询数据时使用比较普遍的方式。试想一下这种情形:频繁地读取某个用户的所有文章,以及某个文章的所有评论。如果按用户分片就无法找到某篇文章的所有评论,而按文章分片则无法找到某个用户的所有文章。如果希望这两个查询都落到同一个分片上,就需要从两个维度进行分片。
需要多个分区键并不意味着需要去设计两个完全冗余的数据存储。我们来看看另一个例子:一个社交网站下的读书俱乐部站点,该站点的所有用户都可以对书进行评论。该网站可以显示所有书籍的所有评论,也能显示某个用户已经读过或评论过的所有书籍。假设为用户数据和书籍数据都设计了分片数据存储。而评论同时拥有用户ID和评论ID。这样就跨越了两个分片的边界。实际上却无须冗余存储两份评论数据,替代方案时,将评论和用户数据一起存储,然后把每个评论的标题和ID与书籍数据存储在一起。这样在渲染大多数关于某本书的评论的视图时无须同时访问用户和书籍数据存储,如果需要显示完整的评论内容,可以从用户数据存储中获得。
5.跨分片查询。
大多数分片应用多少都有一些查询需要对多个分片的数据进行聚合或关联操作,例如,一个读书俱乐部网站要显示最受欢迎或最活跃的用户,就必须访问每一个分片。如何让这类查询很好地执行,是实现数据分片的架构中最困难的部分。虽然从应用的角度来看,这是一条查询,但实际上需要拆分成多条并行执行的查询,每个分片上执行一条。一个设计良好的数据库抽象层能够减轻这个问题,但类似的查询仍然会比分片内查询要慢并且更加昂贵,所以通常会更加依赖缓存。一些语言,如PHP,对并行执行多条查询的支持不够好。普遍的做法是使用C或Java编写一个辅助应用来执行查询并聚合结果集。PHP应用只需要查询该辅助应用即可,例如Web服务或者类似Gearman的工作者服务。
跨分片查询也可以借助汇总表来执行。可以遍历所有分片来生成汇总表并将结果在每个分片上冗余存储。如果在每个分片上存储重复数据太过浪费,也可以把汇总表放到另外一个数据存储中,这样就只需要存储一份了。未分片的数据通常存储在全局节点中,可以使用缓存来分担负载。如果数据的均衡分布非常重要,或者没有很好的分区键,一些应用会采用随机分片的方式。分布式检索应用就是个很好的例子。这种场景下,跨分片查询和聚合查询非常常见。跨分片查询并不是数据分片面临的唯一难题。维护数据一致性同样困难。外键无法在分片间工作,因此需要由应用来检查参照一致性,或者只在分片内使用外键,因为分片内的内部一致性可能是最重要的。还可以使用XA事务,但由于开销太大,现实中使用很少。还可以设计一些定期执行的清理过程。例如,如果一个用户的读书俱乐部账号到期,并不需要立刻将其移除。可以写一个定期任务将用户评论从每个书籍分片中移除,也可以写一个检查脚本周期性运行以确保分片间的数据一致性
大多数分片应用多少都有一些查询需要对多个分片的数据进行聚合或关联操作,例如,一个读书俱乐部网站要显示最受欢迎或最活跃的用户,就必须访问每一个分片。如何让这类查询很好地执行,是实现数据分片的架构中最困难的部分。虽然从应用的角度来看,这是一条查询,但实际上需要拆分成多条并行执行的查询,每个分片上执行一条。一个设计良好的数据库抽象层能够减轻这个问题,但类似的查询仍然会比分片内查询要慢并且更加昂贵,所以通常会更加依赖缓存。一些语言,如PHP,对并行执行多条查询的支持不够好。普遍的做法是使用C或Java编写一个辅助应用来执行查询并聚合结果集。PHP应用只需要查询该辅助应用即可,例如Web服务或者类似Gearman的工作者服务。
跨分片查询也可以借助汇总表来执行。可以遍历所有分片来生成汇总表并将结果在每个分片上冗余存储。如果在每个分片上存储重复数据太过浪费,也可以把汇总表放到另外一个数据存储中,这样就只需要存储一份了。未分片的数据通常存储在全局节点中,可以使用缓存来分担负载。如果数据的均衡分布非常重要,或者没有很好的分区键,一些应用会采用随机分片的方式。分布式检索应用就是个很好的例子。这种场景下,跨分片查询和聚合查询非常常见。跨分片查询并不是数据分片面临的唯一难题。维护数据一致性同样困难。外键无法在分片间工作,因此需要由应用来检查参照一致性,或者只在分片内使用外键,因为分片内的内部一致性可能是最重要的。还可以使用XA事务,但由于开销太大,现实中使用很少。还可以设计一些定期执行的清理过程。例如,如果一个用户的读书俱乐部账号到期,并不需要立刻将其移除。可以写一个定期任务将用户评论从每个书籍分片中移除,也可以写一个检查脚本周期性运行以确保分片间的数据一致性
6.分配数据、分片和节点。
分片和节点不一定是一对一的关系,应该尽可能地让分片地大小比节点容量小很多,这样就可以在单个节点上存储多个分片。保持分片足够小更容易管理。这将使数据地备份和恢复更加容易,如果表很小,那么像更改表结构这样的操作会更加容易。例如,假设有一个100GB的表,你可以直接存储,也可以将其划分为100个1GB的分片,并存储在单个节点上。现在假如要向表上增加一个索引,在单个100GB的表上的执行时间会比100个1GB分片上执行的总时间更长,因为1GB的分片更容易全部加载到内存中,并且在执行ALTER TABLE时还会导致数据不可用,阻塞1GB的数据比阻塞100GB的数据要好得多。
小一点的分片也便于转移。这有助于重新分配容量,平衡各个节点的分片。转移分片的效率一般都不高。通常需要先将受影响的分片设置为只读模式(也是需要在应用中构建的特性),提取数据,然后转移到另外一个节点。这包括使用mysqldump获取数据然后使用mysql命令将其重新导入。如果使用的是Percona Server,可以通过XtraBackup在服务器间转移文件,这比转储和重新载入要高效得多。
除了在节点间移动分片,你可能还需要考虑在分片间移动数据,并尽量不中断整个应用提供服务。如果分片太大,就很难通过移动整个分片来平衡容量,这时候可能需要将一部分数据(例如一个用户)转移到其他分片。分片间转移数据比转移分片要更复杂,应该尽量避免这么做。这也是我们建议设置分片大小尽量易于管理的原因之一。分片的相对大小取决于应用的需求。简单地说,我们说的"易于管理的大小"是指保持表足够小,以便能在5或10分钟内提供日常的维护工作,例如ALTER TABLE、CHECK TABLE或者OPTIMIZE TABLE.
如果将分片设置得太小,会产生太多得表,这可能引发文件系统或MySQL内部结构得问题。另外太小的分片还会导致跨分片查询增多。
分片和节点不一定是一对一的关系,应该尽可能地让分片地大小比节点容量小很多,这样就可以在单个节点上存储多个分片。保持分片足够小更容易管理。这将使数据地备份和恢复更加容易,如果表很小,那么像更改表结构这样的操作会更加容易。例如,假设有一个100GB的表,你可以直接存储,也可以将其划分为100个1GB的分片,并存储在单个节点上。现在假如要向表上增加一个索引,在单个100GB的表上的执行时间会比100个1GB分片上执行的总时间更长,因为1GB的分片更容易全部加载到内存中,并且在执行ALTER TABLE时还会导致数据不可用,阻塞1GB的数据比阻塞100GB的数据要好得多。
小一点的分片也便于转移。这有助于重新分配容量,平衡各个节点的分片。转移分片的效率一般都不高。通常需要先将受影响的分片设置为只读模式(也是需要在应用中构建的特性),提取数据,然后转移到另外一个节点。这包括使用mysqldump获取数据然后使用mysql命令将其重新导入。如果使用的是Percona Server,可以通过XtraBackup在服务器间转移文件,这比转储和重新载入要高效得多。
除了在节点间移动分片,你可能还需要考虑在分片间移动数据,并尽量不中断整个应用提供服务。如果分片太大,就很难通过移动整个分片来平衡容量,这时候可能需要将一部分数据(例如一个用户)转移到其他分片。分片间转移数据比转移分片要更复杂,应该尽量避免这么做。这也是我们建议设置分片大小尽量易于管理的原因之一。分片的相对大小取决于应用的需求。简单地说,我们说的"易于管理的大小"是指保持表足够小,以便能在5或10分钟内提供日常的维护工作,例如ALTER TABLE、CHECK TABLE或者OPTIMIZE TABLE.
如果将分片设置得太小,会产生太多得表,这可能引发文件系统或MySQL内部结构得问题。另外太小的分片还会导致跨分片查询增多。
7.在节点上部署分片。
需要确定如何在节点上部署数据分片。以下是一些常用的办法:
1.每个分片使用单一数据库,并且数据库名要相同。典型的应用场景是需要为每个分片都能镜像到原应用的结构。这在部署多个应用实例,并且每个实例对应一个分片时很有用
2.将多个分片的表放到一个数据库中,在每个表名上包含分片号(例如bookclub.comments_23),这种配置下,单个数据库可以支持多个数据分片。
3.为每个分片使用一个数据库,并在数据库中包含所有应用需要的表。在数据库中包含分片号(例如表名可能是bookclub_23.comments或者bookclub_23.users等),但表名不包括分片号。当应用连接到单个数据库并且不在查询中指定数据库名时,这种做法很常见。其优点是无须为每个分片专门编写查询,也便于对只使用单个数据库的应用进行分片
4.每个分片使用一个数据库,并在数据库名和表名中包含分片号(例如表名可以是bookclub_23.comments_23)
5.在每个节点上运行多个MySQL实例,每个实例上有一个或多个分片,可以使用上面提到的方式的任意组合来安排分片。
如果在标命中包含了分片号,就需要在查询模板里插入分片号。常用的方法是在查询中使用特殊的"神奇的"占位符,例如sprintf()这样的格式化函数中的%s,或者使用变量做字符串插值。以下是在PHP中创建查询模板的方法:
```php
$sql = "SELECT book_id, book_title FROM bookclub_%d.comments_%d .....";
$res=mysql_query(sprintf($sql, $shardno, $shardno), $conn);
```
也可以就使用字符串插值的方法:
```php
$sql="SELECT book_id, book_title FROM bookclub_$shardno.comments_$shardno ...";
$res = mysql_query($sql, $conn);
```
这在新应用中很容易实现,但对于已有的应用则优点困难。构建新应用时,查询模板并不是问题,我们倾向于使用每个分片一个数据库的方式,并把分片号写到数据库名和表名中。这会增加ALTER TABLE这类操作的复杂度,但也有如下一些优点:
1.如果分片全部在一个数据库中,转移分片会比较容易
2.因为数据库本身是文件系统中的一个目录,所以可以很方便地管理一个分片地文件。
3.如果分片互不关联,则很容易查看分片的大小
4.全局唯一表名可避免误操作。如果表名每个地方都相同,很容易因为连接到错误的节点而查询了错误的分片,或者是将一个分片的数据误导入另外一个分片的表中.
你可能想知道应用的数据是否具有某种"分片亲和性"。也许将某些分片放在一起(在同一台服务器,同一个子网,同一个数据中心,或者同一个交换网络中)可以利用数据访问模式的相关性,能够带来些好处。例如,可以按照用户进行分片,然后将同一个国家的用户放到同一个节点的分片上。
为已有的应用增加分片支持的结果往往是一个节点对应一个分片。这种简化的设计可以减少对应用查询的修改。分片对应用而言通常是一种颠覆性的改变,所以应尽可能简化它。如果在分片后,每个节点看起来就像是整个应用数据的缩略图,就无须改变大多数查询或者担心查询是否传递到期望的节点
需要确定如何在节点上部署数据分片。以下是一些常用的办法:
1.每个分片使用单一数据库,并且数据库名要相同。典型的应用场景是需要为每个分片都能镜像到原应用的结构。这在部署多个应用实例,并且每个实例对应一个分片时很有用
2.将多个分片的表放到一个数据库中,在每个表名上包含分片号(例如bookclub.comments_23),这种配置下,单个数据库可以支持多个数据分片。
3.为每个分片使用一个数据库,并在数据库中包含所有应用需要的表。在数据库中包含分片号(例如表名可能是bookclub_23.comments或者bookclub_23.users等),但表名不包括分片号。当应用连接到单个数据库并且不在查询中指定数据库名时,这种做法很常见。其优点是无须为每个分片专门编写查询,也便于对只使用单个数据库的应用进行分片
4.每个分片使用一个数据库,并在数据库名和表名中包含分片号(例如表名可以是bookclub_23.comments_23)
5.在每个节点上运行多个MySQL实例,每个实例上有一个或多个分片,可以使用上面提到的方式的任意组合来安排分片。
如果在标命中包含了分片号,就需要在查询模板里插入分片号。常用的方法是在查询中使用特殊的"神奇的"占位符,例如sprintf()这样的格式化函数中的%s,或者使用变量做字符串插值。以下是在PHP中创建查询模板的方法:
```php
$sql = "SELECT book_id, book_title FROM bookclub_%d.comments_%d .....";
$res=mysql_query(sprintf($sql, $shardno, $shardno), $conn);
```
也可以就使用字符串插值的方法:
```php
$sql="SELECT book_id, book_title FROM bookclub_$shardno.comments_$shardno ...";
$res = mysql_query($sql, $conn);
```
这在新应用中很容易实现,但对于已有的应用则优点困难。构建新应用时,查询模板并不是问题,我们倾向于使用每个分片一个数据库的方式,并把分片号写到数据库名和表名中。这会增加ALTER TABLE这类操作的复杂度,但也有如下一些优点:
1.如果分片全部在一个数据库中,转移分片会比较容易
2.因为数据库本身是文件系统中的一个目录,所以可以很方便地管理一个分片地文件。
3.如果分片互不关联,则很容易查看分片的大小
4.全局唯一表名可避免误操作。如果表名每个地方都相同,很容易因为连接到错误的节点而查询了错误的分片,或者是将一个分片的数据误导入另外一个分片的表中.
你可能想知道应用的数据是否具有某种"分片亲和性"。也许将某些分片放在一起(在同一台服务器,同一个子网,同一个数据中心,或者同一个交换网络中)可以利用数据访问模式的相关性,能够带来些好处。例如,可以按照用户进行分片,然后将同一个国家的用户放到同一个节点的分片上。
为已有的应用增加分片支持的结果往往是一个节点对应一个分片。这种简化的设计可以减少对应用查询的修改。分片对应用而言通常是一种颠覆性的改变,所以应尽可能简化它。如果在分片后,每个节点看起来就像是整个应用数据的缩略图,就无须改变大多数查询或者担心查询是否传递到期望的节点
8.固定分配。
将数据分配到分片中有两种主要的方法:固定分片和动态分配。两种方法都需要一个分区函数,使用行的分区键值作为输入,返回存储该行的分片(这里的"函数"使用了其数学含义,表示从输入(域)到输出(区间)的映射。如你所见,可以用很多方式来创建类似的函数,包括在数据中使用查找表)。固定分配使用的分区函数仅仅依赖于分区键的值。哈希函数和取模运算就是很好的例子。这些函数按照每个分区键的值将数据分散到一定数量的"桶"中。
假设有100个桶,你希望弄清楚用户111该放到哪个桶里。如果使用的是对数字取模的方式,答案很简单:111对100取模的值为11,所以应该将其放到第11个分片中。而入股哦使用CRC32()函数来做哈希,答案是81.
```sql
mysql> SELECT CRC32(111)%100;
```
固定分配的主要优点是简单,开销低,甚至可以在应用中直接硬编码。但固定分配也有如下缺点:
1.如果分片很大并且数量不多,就很难平衡不同分片间的负载
2.固定分片的方式无法自定义数据放到哪个分片上,这一点对于那些在分片间负载不均衡的应用来说尤其重要。一些数据可能比其他的更加活跃,入股哦这些热点数据都分配到同一个分片中,固定分配的方式就无法通过热点数据转移的方式来平衡负载。(如果每个分片的数据量切分得比较小,这个问题就没那么严重,根据大数定律,这样做会更容易将热点数据平均分配到不同分片)
3.修改分片策略通常比较困难,因为需要重新分配已有的数据。例如,如果通过模10的哈希函数来进行分片,就会有10个分片。如果应用增长使得分片变大,如果要拆成20个分片,就需要对所有数据重新哈希,这回导致更新大量数据,并在分片间转移数据
正是由于这些限制,我们倾向于为新应用选择动态分配的方式。但如果是为已有的应用做分片,使用固定分配策略可能会更容易些,因为它更简单。也就是说,大多数使用固定分配的应用最后迟早要使用动态分配策略
将数据分配到分片中有两种主要的方法:固定分片和动态分配。两种方法都需要一个分区函数,使用行的分区键值作为输入,返回存储该行的分片(这里的"函数"使用了其数学含义,表示从输入(域)到输出(区间)的映射。如你所见,可以用很多方式来创建类似的函数,包括在数据中使用查找表)。固定分配使用的分区函数仅仅依赖于分区键的值。哈希函数和取模运算就是很好的例子。这些函数按照每个分区键的值将数据分散到一定数量的"桶"中。
假设有100个桶,你希望弄清楚用户111该放到哪个桶里。如果使用的是对数字取模的方式,答案很简单:111对100取模的值为11,所以应该将其放到第11个分片中。而入股哦使用CRC32()函数来做哈希,答案是81.
```sql
mysql> SELECT CRC32(111)%100;
```
固定分配的主要优点是简单,开销低,甚至可以在应用中直接硬编码。但固定分配也有如下缺点:
1.如果分片很大并且数量不多,就很难平衡不同分片间的负载
2.固定分片的方式无法自定义数据放到哪个分片上,这一点对于那些在分片间负载不均衡的应用来说尤其重要。一些数据可能比其他的更加活跃,入股哦这些热点数据都分配到同一个分片中,固定分配的方式就无法通过热点数据转移的方式来平衡负载。(如果每个分片的数据量切分得比较小,这个问题就没那么严重,根据大数定律,这样做会更容易将热点数据平均分配到不同分片)
3.修改分片策略通常比较困难,因为需要重新分配已有的数据。例如,如果通过模10的哈希函数来进行分片,就会有10个分片。如果应用增长使得分片变大,如果要拆成20个分片,就需要对所有数据重新哈希,这回导致更新大量数据,并在分片间转移数据
正是由于这些限制,我们倾向于为新应用选择动态分配的方式。但如果是为已有的应用做分片,使用固定分配策略可能会更容易些,因为它更简单。也就是说,大多数使用固定分配的应用最后迟早要使用动态分配策略
9.动态分配。
另外一个选择使用动态分配,将每个数据单元映射到一个分片。假设一个有两列的表,包括用户ID和分片ID.
```sql
mysql>CREATE TABLE user_to_shard(user_id INT NOT NULL, shard_id INT NOT NULL, PRIMARY KEY(user_id));
```
这个表本身就是分区函数。给定分区键(用户ID)的值就可以获得分片号。如果该行不存在,就从目标分片中找到并将其加入到表中。也可以推迟更新——这就是动态分配的含义。动态分配增加了分区函数的开销,因为需要额外调用一次外部资源,例如目录服务器(存储映射关系的数据存储节点)。出于效率方面的考虑,这种架构常常需要更多的分层。例如,可以使用一个分布式缓存系统将目录服务器的数据加载到内存中,因为这些数据平时改动很小。或者更普遍地,你可以直接向USERS表中增加一个shard_id列用于存储分片号。
动态分配的最大好处是可以对数据存储位置做细粒度的控制。这使得均衡分配数据到分片更加容易,并可提供适应未知改变的灵活性。动态映射可以在简单的键——分片(key-to-shard)映射的基础上建立多层次的分片策略。例如,可以建立一个双重映射,将每个分片单元指定到一个分组中(例如,读书俱乐部的用户组),然后尽可能将这些组保持在同一个分片中。这样可以利用分片亲和性,避免跨分片查询。
如果使用动态分配策略,可以生成不均衡的分片。如果服务器能力不相同,或者希望将其中一些分片用于特定目的(例如归档数据),这可能会有用。如果能够做到随时重新平衡分片,也可以为分片和节点间维持一一对应的映射关系,这不会浪费容量。也有些人喜欢简单的每个节点一个分片的方式。(但是请记住,保持分片尽可能小是有好处的。)
动态分配以及灵活地利用分片亲和性有助于减轻规模扩大而带来的跨分片查询。假设一个跨分片查询涉及四个节点,当使用固定分配时,任何给定的查询可能需要访问所有分片,但考虑一些当数据存储到400个分片时会发生什么?固定分配策略需要访问400个分片,而动态分配方式依然只需要访问3个。
动态分配可以让分片策略根据需要变得很复杂。固定分配则没有这么多选择。
另外一个选择使用动态分配,将每个数据单元映射到一个分片。假设一个有两列的表,包括用户ID和分片ID.
```sql
mysql>CREATE TABLE user_to_shard(user_id INT NOT NULL, shard_id INT NOT NULL, PRIMARY KEY(user_id));
```
这个表本身就是分区函数。给定分区键(用户ID)的值就可以获得分片号。如果该行不存在,就从目标分片中找到并将其加入到表中。也可以推迟更新——这就是动态分配的含义。动态分配增加了分区函数的开销,因为需要额外调用一次外部资源,例如目录服务器(存储映射关系的数据存储节点)。出于效率方面的考虑,这种架构常常需要更多的分层。例如,可以使用一个分布式缓存系统将目录服务器的数据加载到内存中,因为这些数据平时改动很小。或者更普遍地,你可以直接向USERS表中增加一个shard_id列用于存储分片号。
动态分配的最大好处是可以对数据存储位置做细粒度的控制。这使得均衡分配数据到分片更加容易,并可提供适应未知改变的灵活性。动态映射可以在简单的键——分片(key-to-shard)映射的基础上建立多层次的分片策略。例如,可以建立一个双重映射,将每个分片单元指定到一个分组中(例如,读书俱乐部的用户组),然后尽可能将这些组保持在同一个分片中。这样可以利用分片亲和性,避免跨分片查询。
如果使用动态分配策略,可以生成不均衡的分片。如果服务器能力不相同,或者希望将其中一些分片用于特定目的(例如归档数据),这可能会有用。如果能够做到随时重新平衡分片,也可以为分片和节点间维持一一对应的映射关系,这不会浪费容量。也有些人喜欢简单的每个节点一个分片的方式。(但是请记住,保持分片尽可能小是有好处的。)
动态分配以及灵活地利用分片亲和性有助于减轻规模扩大而带来的跨分片查询。假设一个跨分片查询涉及四个节点,当使用固定分配时,任何给定的查询可能需要访问所有分片,但考虑一些当数据存储到400个分片时会发生什么?固定分配策略需要访问400个分片,而动态分配方式依然只需要访问3个。
动态分配可以让分片策略根据需要变得很复杂。固定分配则没有这么多选择。
10.混合动态分配和固定分配。
可以混合使用固定分配和动态分配。这种方法通常很有用。有时候甚至必需要混合使用。目录映射不太大时,动态分配可以很好胜任。但如果分片单元太多,效果就会变差。以一个存储网站链接的系统为例。这样一个站点需要存储数百亿的行,所使用的分区键时源地址和目的地址URL的组合.(这两个URL的任意一个都可能有好几亿的链接,因此,单独一个URL并不适合做分区键)。但是在映射表中存储所有的源地址和目的地址URL组合并不合理,因为数据量太大了,每个URL都需要很多存储空间。
一个解决方案是将URL相连并将其哈希到固定数目的桶中,然后把桶动态地映射到分片上。如果桶地数目足够大——例如100万个——你就能把大多数数据分配到每个分片上,获得动态分配的大部分好处,而无须使用庞大的映射表
可以混合使用固定分配和动态分配。这种方法通常很有用。有时候甚至必需要混合使用。目录映射不太大时,动态分配可以很好胜任。但如果分片单元太多,效果就会变差。以一个存储网站链接的系统为例。这样一个站点需要存储数百亿的行,所使用的分区键时源地址和目的地址URL的组合.(这两个URL的任意一个都可能有好几亿的链接,因此,单独一个URL并不适合做分区键)。但是在映射表中存储所有的源地址和目的地址URL组合并不合理,因为数据量太大了,每个URL都需要很多存储空间。
一个解决方案是将URL相连并将其哈希到固定数目的桶中,然后把桶动态地映射到分片上。如果桶地数目足够大——例如100万个——你就能把大多数数据分配到每个分片上,获得动态分配的大部分好处,而无须使用庞大的映射表
11.显式分配。
第三种分配策略是在应用插入新的数据行时,显式地选择目标分片。这种策略在已有的数据上很难做到。所以在为应用增加分片时很少使用,但在某些情况下还是有用的。这个方法使把数据分片好编码到ID中 ,这和之前提到的避免主——主复制主键冲突策略比较相似。例如,假设应用要创建一个用户3,将其分配到第11个分片中,并使用BIGINT列的高八位来保存分片号。这样最终的ID就是(11<<56) +3,即792633534417207299。应用可以很方便地从中抽取用户ID和分片好,如下例所示:
```sql
mysql> SELECT (792633534417207299 >> 56) AS shard_id, 792633534417207299 & ~(11 << 56) AS user_id;
```
现在假设要为该用户创建一条评论,并存储在同一个分片中。应用可以为该用户分配一个评论ID5,然后以同样地方式组合5和分片号11.这种方法地好处使每个对象的ID同时包含了分区键,而其他方法需要一次关联或查找来确定分区键。如果要从数据库中检索某个特定的评论,无须知道哪个用户拥有它,对象ID会告诉你到哪里去着。如果对象使通过用户ID动态分片的,就得先找到该评论的用户,然后通过目录服务器找到对应的数据分片。
另一个解决方案使将分区键存储在一个单独的列里。例如,你可能从不会单独引用评论5,但是评论5属于用户3.这种方式可能会让一些人高兴,因为这不未被第一范式;然而额外的列会增加开销、编码,以及其他不便之处。(这也是我们将两值存在单独一列的优点之一)。
显式分配的缺点使分片方式是固定的,很难做到分片间的负载均衡。但结合固定分配和动态分配,该方法就能够很好地工作。不再像之前那样哈希到固定数目的桶里并将其映射到节点,而是将桶作为对象的一部分进行编码。这样应用就能够控制数据的存储未知,因此可以将相关联的数据一起放到同样的分片中。
BoardRead使用了该技术的一个变种:它把分区键编码到Sphinx的文档ID内。这使得在分片数据存储中查找每个查询结果的关联数据变得容易。我们讨论了混合分配方式,因为在某些场景下它是有用的。但正常情况下并不推荐这样用。倾向于尽可能使用动态分配,避免显式分配。
第三种分配策略是在应用插入新的数据行时,显式地选择目标分片。这种策略在已有的数据上很难做到。所以在为应用增加分片时很少使用,但在某些情况下还是有用的。这个方法使把数据分片好编码到ID中 ,这和之前提到的避免主——主复制主键冲突策略比较相似。例如,假设应用要创建一个用户3,将其分配到第11个分片中,并使用BIGINT列的高八位来保存分片号。这样最终的ID就是(11<<56) +3,即792633534417207299。应用可以很方便地从中抽取用户ID和分片好,如下例所示:
```sql
mysql> SELECT (792633534417207299 >> 56) AS shard_id, 792633534417207299 & ~(11 << 56) AS user_id;
```
现在假设要为该用户创建一条评论,并存储在同一个分片中。应用可以为该用户分配一个评论ID5,然后以同样地方式组合5和分片号11.这种方法地好处使每个对象的ID同时包含了分区键,而其他方法需要一次关联或查找来确定分区键。如果要从数据库中检索某个特定的评论,无须知道哪个用户拥有它,对象ID会告诉你到哪里去着。如果对象使通过用户ID动态分片的,就得先找到该评论的用户,然后通过目录服务器找到对应的数据分片。
另一个解决方案使将分区键存储在一个单独的列里。例如,你可能从不会单独引用评论5,但是评论5属于用户3.这种方式可能会让一些人高兴,因为这不未被第一范式;然而额外的列会增加开销、编码,以及其他不便之处。(这也是我们将两值存在单独一列的优点之一)。
显式分配的缺点使分片方式是固定的,很难做到分片间的负载均衡。但结合固定分配和动态分配,该方法就能够很好地工作。不再像之前那样哈希到固定数目的桶里并将其映射到节点,而是将桶作为对象的一部分进行编码。这样应用就能够控制数据的存储未知,因此可以将相关联的数据一起放到同样的分片中。
BoardRead使用了该技术的一个变种:它把分区键编码到Sphinx的文档ID内。这使得在分片数据存储中查找每个查询结果的关联数据变得容易。我们讨论了混合分配方式,因为在某些场景下它是有用的。但正常情况下并不推荐这样用。倾向于尽可能使用动态分配,避免显式分配。
12.重新均衡分片数据。
如有必要,可以通过在分片间移动数据来达到负载均衡。举个例子,许多读者可能听一些大型图片分享网站或流行社区网站的开发者提到过用于分片间移动用户数据的工具。在分片间移动数据的好处很明显。例如,当需要升级硬件时,可以将用户数据从旧分片转移到新分片上,而无须暂停整个分片的服务或将其设置为只读。然而,我们也应该尽量避免重新均衡分片数据,因为这可能会影响用户使用。在分片间转移数据也使得为应用增加新特性更加困难,因为新特性可能还需要包含针对重新均衡脚本的升级。如果分片足够小,就无须这么做;也可以经常移动整个分片来重新均衡负载,这比移动分片中的部分数据要容易得多(并且以每行数据开销来衡量的话,更有效率)。
一个较好的策略是使用动态分片策略,并将新数据随机分配到分片中。当一个分片快满时,可以设置一个标志位,告诉应用不要再往这里放数据了。如果未来需要向分片中放入更多数据,可以直接把标记位清除。假设安装了一个新的MySQL节点,上面有100个分片。先将它们的标记设置为1,这样应用就知道它们正准备接受新数据。一旦它们的数据足够多时(例如,每个分片10 000个用户),就把标记位设置为0.之后,如果节点因为大量废弃账号导致负载不足,可以冲洗你打开一些分片向其中增加新用户。
如果升级应用并且增加的新特性会导致每个分片的查询负载升高,或者只是算错了负载,可以把一些分片移到新节点来减轻负载。缺点时操作期间整个分片会变成只读或者处于离线状态。这需要根据实际情况来看看是否能接受。
另外一种使用得较多的策略时为每个分片设置两台备库,每个备库都有该分片的完整数据,然后每个备库负责其中一半的数据,并完全停止在主库上查询。这样每个备库都有有一半它不会用到的数据;我们可以使用一些工具,例如Percona Toolkit的pt-archiver,在后台运行,移除那些不再需要的数据。这种办法很简单并且几乎不需要停机。
如有必要,可以通过在分片间移动数据来达到负载均衡。举个例子,许多读者可能听一些大型图片分享网站或流行社区网站的开发者提到过用于分片间移动用户数据的工具。在分片间移动数据的好处很明显。例如,当需要升级硬件时,可以将用户数据从旧分片转移到新分片上,而无须暂停整个分片的服务或将其设置为只读。然而,我们也应该尽量避免重新均衡分片数据,因为这可能会影响用户使用。在分片间转移数据也使得为应用增加新特性更加困难,因为新特性可能还需要包含针对重新均衡脚本的升级。如果分片足够小,就无须这么做;也可以经常移动整个分片来重新均衡负载,这比移动分片中的部分数据要容易得多(并且以每行数据开销来衡量的话,更有效率)。
一个较好的策略是使用动态分片策略,并将新数据随机分配到分片中。当一个分片快满时,可以设置一个标志位,告诉应用不要再往这里放数据了。如果未来需要向分片中放入更多数据,可以直接把标记位清除。假设安装了一个新的MySQL节点,上面有100个分片。先将它们的标记设置为1,这样应用就知道它们正准备接受新数据。一旦它们的数据足够多时(例如,每个分片10 000个用户),就把标记位设置为0.之后,如果节点因为大量废弃账号导致负载不足,可以冲洗你打开一些分片向其中增加新用户。
如果升级应用并且增加的新特性会导致每个分片的查询负载升高,或者只是算错了负载,可以把一些分片移到新节点来减轻负载。缺点时操作期间整个分片会变成只读或者处于离线状态。这需要根据实际情况来看看是否能接受。
另外一种使用得较多的策略时为每个分片设置两台备库,每个备库都有该分片的完整数据,然后每个备库负责其中一半的数据,并完全停止在主库上查询。这样每个备库都有有一半它不会用到的数据;我们可以使用一些工具,例如Percona Toolkit的pt-archiver,在后台运行,移除那些不再需要的数据。这种办法很简单并且几乎不需要停机。
13.生成全局唯一ID.
当希望把一个现有系统转换为分片数据存储时,经常会需要在多台及其上生成全局唯一ID.单一数据存储时通常可以使用AUTO_INCREMENT列来获取唯一ID.但涉及多台服务器时就不奏效了。以下几种方法可以解决这个问题:
1.使用auto_increment_increment和auto_increment_offset
这两个服务器变量可以让MySQL以期望的值和偏移量来增加AUTO_INCREMENT列的值。举一个最简单的场景,只有两台服务器,可以配置这两台服务器自增幅度为2,其中一台的偏移量为1,另外一台为2(两个都不可以设置为0).这样一台服务器总是包含偶数,另外一台则总是包含奇数。这种设置可以配置到服务器的每一个表里。这种方法简单,并且不依赖于某个节点,因此是生成唯一ID的比较普遍的方法。 但这需要非常仔细地配置服务器。很容易因为配置错误生成重复数字,特别是当增加服务器需要改变其角色,或进行灾难恢复时。
2.全局节点中创建表
在一个全局数据库节点中创建一个包含AUTO_INCREMENT列的表,应用可以通过这个表来生成唯一数字。
3.使用memcached
在memcached的API中有一个incr()函数,可以自动增长一个数字并返回结果
4.批量分配数字。
应用可以从一个全局节点中请求一批数字,用完后再申请
5.使用复合值
可以使用一个复合值来做唯一ID,例如分片号和自增数的组合
6.使用GUID值
可以使用UUID()函数来生成全局唯一值。注意,尽管这个函数在基于语句的复制时不能正确复制,但是可以先获得这个值,再存放到应用的内存中,然后作为数字在查询中使用。GUID的值很大并且不连续,因此不适合做InnoDB表的主键。在5.1及更新的版本中还有一个函数UUID_SHORT()能够生成连续的值,并使用64位代替了之前的128位
如果使用全局分配器来产生唯一ID,要注意避免单点争用成为应用的性能瓶颈。虽然memcached()方法执行速度快(每秒数万个),但不具备持久性。每次重启memcached服务都需要重新初始化缓存里的值。由于需要首先找到所有的分片中的最大值,因此这一过程非常缓慢并且难以实现原子性
当希望把一个现有系统转换为分片数据存储时,经常会需要在多台及其上生成全局唯一ID.单一数据存储时通常可以使用AUTO_INCREMENT列来获取唯一ID.但涉及多台服务器时就不奏效了。以下几种方法可以解决这个问题:
1.使用auto_increment_increment和auto_increment_offset
这两个服务器变量可以让MySQL以期望的值和偏移量来增加AUTO_INCREMENT列的值。举一个最简单的场景,只有两台服务器,可以配置这两台服务器自增幅度为2,其中一台的偏移量为1,另外一台为2(两个都不可以设置为0).这样一台服务器总是包含偶数,另外一台则总是包含奇数。这种设置可以配置到服务器的每一个表里。这种方法简单,并且不依赖于某个节点,因此是生成唯一ID的比较普遍的方法。 但这需要非常仔细地配置服务器。很容易因为配置错误生成重复数字,特别是当增加服务器需要改变其角色,或进行灾难恢复时。
2.全局节点中创建表
在一个全局数据库节点中创建一个包含AUTO_INCREMENT列的表,应用可以通过这个表来生成唯一数字。
3.使用memcached
在memcached的API中有一个incr()函数,可以自动增长一个数字并返回结果
4.批量分配数字。
应用可以从一个全局节点中请求一批数字,用完后再申请
5.使用复合值
可以使用一个复合值来做唯一ID,例如分片号和自增数的组合
6.使用GUID值
可以使用UUID()函数来生成全局唯一值。注意,尽管这个函数在基于语句的复制时不能正确复制,但是可以先获得这个值,再存放到应用的内存中,然后作为数字在查询中使用。GUID的值很大并且不连续,因此不适合做InnoDB表的主键。在5.1及更新的版本中还有一个函数UUID_SHORT()能够生成连续的值,并使用64位代替了之前的128位
如果使用全局分配器来产生唯一ID,要注意避免单点争用成为应用的性能瓶颈。虽然memcached()方法执行速度快(每秒数万个),但不具备持久性。每次重启memcached服务都需要重新初始化缓存里的值。由于需要首先找到所有的分片中的最大值,因此这一过程非常缓慢并且难以实现原子性
14.分片工具。
在设计数据分片应用时,首先要做的事情是编写能够查询多个数据源的代码。如果没有任何抽象层,直接让应用访问多个数据源,那绝对是一个很差的设计,因为这会增加大量的编码复杂性。最好的办法是将数据源隐藏在抽象层中,这个抽象层主要完成以下任务:
1.连接到正确的分片并执行查询
2.分布式一致性校验
3.跨分片结果集聚合
4.跨分片关联操作
5.锁和事务管理
6.创建新的数据分片(或者至少在运行时找到新分片)并重新平衡分片(如果有时间实现)
你可能不需要从头开始构建分片结构。有一些工具和系统可以提供一些必要的功能或专门设计用来实现分片架构。
Hibernate Shards 是一个支持分片的数据库抽象层,基于Java语言的开源的Hibernate ORM库扩展,由谷歌提供。它在Hibernate Core接口上提供了分片感知功能,所以应用无须专门为分片设计;事实上,应用甚至无须知道它正在使用分片。Hibernate Shards 通过固定分配策略向分片分配数据。另外一个基于Java的分片系统是HiveDB.
如果使用的是PHP语言,可以使用Justin Swanhart提供的Shard-Query系统,它可以自动分解查询,并发执行,并合并结果集。另外一些有同样用途的商用系统有ScaleBase、ScalArc,以及dbShards.
Sphinx是一个全文检索引擎,虽然不是分片数据存储和检索系统,但对于一些跨分片数据存储的查询依然有用。Sphinx可以并行查询远程系统并聚合结果集。
在设计数据分片应用时,首先要做的事情是编写能够查询多个数据源的代码。如果没有任何抽象层,直接让应用访问多个数据源,那绝对是一个很差的设计,因为这会增加大量的编码复杂性。最好的办法是将数据源隐藏在抽象层中,这个抽象层主要完成以下任务:
1.连接到正确的分片并执行查询
2.分布式一致性校验
3.跨分片结果集聚合
4.跨分片关联操作
5.锁和事务管理
6.创建新的数据分片(或者至少在运行时找到新分片)并重新平衡分片(如果有时间实现)
你可能不需要从头开始构建分片结构。有一些工具和系统可以提供一些必要的功能或专门设计用来实现分片架构。
Hibernate Shards 是一个支持分片的数据库抽象层,基于Java语言的开源的Hibernate ORM库扩展,由谷歌提供。它在Hibernate Core接口上提供了分片感知功能,所以应用无须专门为分片设计;事实上,应用甚至无须知道它正在使用分片。Hibernate Shards 通过固定分配策略向分片分配数据。另外一个基于Java的分片系统是HiveDB.
如果使用的是PHP语言,可以使用Justin Swanhart提供的Shard-Query系统,它可以自动分解查询,并发执行,并合并结果集。另外一些有同样用途的商用系统有ScaleBase、ScalArc,以及dbShards.
Sphinx是一个全文检索引擎,虽然不是分片数据存储和检索系统,但对于一些跨分片数据存储的查询依然有用。Sphinx可以并行查询远程系统并聚合结果集。
通过多实例扩展。
一个分片较多的架构可能会更有效地利用硬件。研究和经验表名MySQL并不能完全发挥现代硬件的性能。当扩展到超过24个CPU核心时,MySQL的性能开始趋于平缓,不再上升。当内存超过128GB时也同样如此,MySQL甚至不能完全发挥诸如Virident或Fusion-io卡这样的高端PCIe flash设备的IO性能。
不要在一台性能强悍的机器上只运行一个服务器实例,我们还有别的选择。你可以让数据分片足够小,以使每台机器上都能放置多个分片(这也是一直提倡的),每台服务器上运行多个实例,然后划分服务器的硬件资源,将其分配给每个实例。这样做尽管比较繁琐,但确实有效。这是一种向上扩展和向外扩展的组合方案。也可以用其他方法来实现——不一定需要分派你——但分片对于在大型服务器上的联合扩展具有天然的适应性。
一些人倾向于通过虚拟化技术来实现合并扩展,这有它的好处。但虚拟化技术本身有很大的性能损耗。具体损耗多少取决于具体的技术,但通常都比较明显,尤其是IO非常快的时候损坏会非常惊人。另一种选择是运行多个MySQL实例,每个实例监听不同的网络端口,或绑定到不同的IP地址。
我们已经在一台性能强悍的硬件上获得了10倍或15倍的合并系数。你需要平衡管理复杂度代价和更优性能的收益,以决定哪种方法是最优的。这时候网络可能会成为瓶颈——这个问题大多数MySQL用户都不会遇到。可以通过使用多块网卡并进行绑定来解决这个问题。但Linux内核可能会不理想,这取决于内核版本,因为老的内核对每个绑定设备的网络中断只能使用一个CPU。因此不要把太多的连接绑定到很少的虚拟设备上,否则会遇到内核层的网络瓶颈。新的内核在这一方面会有所改善,所以需要检查你的系统版本,以确定该怎么做。
另一个方法是将每个MySQL实例绑定到待定的CPU核心上。这有两点好处:第一,由于MySQL内部的可扩展性限制,当核心数较少时,能够在每个核心上获得更好的性能;第二,当实例在多个核心上运行线程时,由于需要在多核心上同步共享数据,因为会有一些额外的开销。这可以避免硬件本身的可扩展性限制。限制MySQL到少数几个核心能够帮助减少CPU核心之间的交互。注意到反复出现的问题了没?将进程绑定到具有相同物理套接字的和欣赏可以获得最优的效果.
一个分片较多的架构可能会更有效地利用硬件。研究和经验表名MySQL并不能完全发挥现代硬件的性能。当扩展到超过24个CPU核心时,MySQL的性能开始趋于平缓,不再上升。当内存超过128GB时也同样如此,MySQL甚至不能完全发挥诸如Virident或Fusion-io卡这样的高端PCIe flash设备的IO性能。
不要在一台性能强悍的机器上只运行一个服务器实例,我们还有别的选择。你可以让数据分片足够小,以使每台机器上都能放置多个分片(这也是一直提倡的),每台服务器上运行多个实例,然后划分服务器的硬件资源,将其分配给每个实例。这样做尽管比较繁琐,但确实有效。这是一种向上扩展和向外扩展的组合方案。也可以用其他方法来实现——不一定需要分派你——但分片对于在大型服务器上的联合扩展具有天然的适应性。
一些人倾向于通过虚拟化技术来实现合并扩展,这有它的好处。但虚拟化技术本身有很大的性能损耗。具体损耗多少取决于具体的技术,但通常都比较明显,尤其是IO非常快的时候损坏会非常惊人。另一种选择是运行多个MySQL实例,每个实例监听不同的网络端口,或绑定到不同的IP地址。
我们已经在一台性能强悍的硬件上获得了10倍或15倍的合并系数。你需要平衡管理复杂度代价和更优性能的收益,以决定哪种方法是最优的。这时候网络可能会成为瓶颈——这个问题大多数MySQL用户都不会遇到。可以通过使用多块网卡并进行绑定来解决这个问题。但Linux内核可能会不理想,这取决于内核版本,因为老的内核对每个绑定设备的网络中断只能使用一个CPU。因此不要把太多的连接绑定到很少的虚拟设备上,否则会遇到内核层的网络瓶颈。新的内核在这一方面会有所改善,所以需要检查你的系统版本,以确定该怎么做。
另一个方法是将每个MySQL实例绑定到待定的CPU核心上。这有两点好处:第一,由于MySQL内部的可扩展性限制,当核心数较少时,能够在每个核心上获得更好的性能;第二,当实例在多个核心上运行线程时,由于需要在多核心上同步共享数据,因为会有一些额外的开销。这可以避免硬件本身的可扩展性限制。限制MySQL到少数几个核心能够帮助减少CPU核心之间的交互。注意到反复出现的问题了没?将进程绑定到具有相同物理套接字的和欣赏可以获得最优的效果.
通过集群扩展。
理想的扩展方案时单一逻辑数据库能够存储尽可能多的数据,处理尽可能多的查询,并如期望的那样增长。许多人的第一想法就是建立一个"集群"或者"网格"来无缝处理这些事情,这样应用就无须去做太多工作,也不需要知道数据到底存在哪台服务器上。随者云计算的流行,自动扩展——根据负载或数据大小变化动态地在集群中增加/移除服务器——变得越来越有趣。网上出现了许多被称为NoSQL的技术。许多NoSQL的支持者发表了一些奇怪且未经证实的观点,例如"关系型模型无法进行扩展",或者"SQL无法扩展"。随者新概念的出现,也出现了一些新的术语。最近谁没有听说过最终一致性、BASE、矢量时钟,或者CAP理论呢?
但随者时间推移,理性开始主键回归。经验表名许多NoSQL数据库太过于简单,并且无法完成很多工作。同时一些基于SQL的技术开始出现——例如451集团(451 Group)的Matt Aslett所提到的NewSQL数据库。SQL和NewSQL到底有什么区别呢?NewSQL数据库中的SQL及相关技术都不应该成为问题。而可扩展性问题在关系型数据库中是一个是线上的难题,但新的实现正表现出越来越好的结果。所有的旧事物都变成新的了嘛?是,但也不是。许多关系型数据库集群的高性能涉及正在倍构建到系统的更底层,在NoSQL数据库中,特别是使用键——值存储时,这一点很明显。例如NDB Cluster并不是一个SQL数据库;它是一个可扩展的数据库,使用其原生API来控制,通常是使用NoSQL,但也可以通过在前端使用MySQL存储引擎来支持SQL。它是一个完全分布式、非共享高性能、自动分片并且不存在单点故障的事务型数据库服务器。最近几年正变得更强大、更复杂,用途也更广泛。同时,NoSQL数据库也逐渐看起来越来越像关系型数据库。有些甚至还开发了类SQL查询语言。未来典型的集群数据库可能更像是SQL和NoSQL的混合体,有多种存取机制来满足不同的使用需求。所以,我们可以从NoSQL中汲取优点,但SQL仍然会保留在集群数据库中。
和MySQL结合在一起的集群或分布式数据库技术大致包括:NDB Cluster、Clustrix、Percona XtraDB Cluster、Galera、Schooner Active Cluster、Continuent Tungsten、ScaleBase、ScaleArc、dbShards、Xeround、Akiban、VoltDB,以及GenieDB。这或多或少以MySQL为基础,或通过MySQL进行控制,或是和MySQL相关。
在开始前,需要指出,可扩展性、高可用性、事务性等是数据库系统的不同特性。许多人会感到困惑并将这些当作是相同的东西,但事实上不是。可扩展的数据库并不一定非常优秀,除非它能保证高性能,谁愿意牺牲高可用性来进行扩展呢?这些特性的组合堪称数据库的必杀技,但这很难实现。最后,除NDB Cluster外,大多数NewSQL集群产品都是比较新的事物。其他产品还没有看到足够多的生产环境部署以完全获知其优点和限制。尽管它们提到了MySQL协议或其他与MySQL相关的地方,但它们毕竟不是MySQL,所以这里仅仅稍微提一下,然后需要自己来判断它们是否适用。
理想的扩展方案时单一逻辑数据库能够存储尽可能多的数据,处理尽可能多的查询,并如期望的那样增长。许多人的第一想法就是建立一个"集群"或者"网格"来无缝处理这些事情,这样应用就无须去做太多工作,也不需要知道数据到底存在哪台服务器上。随者云计算的流行,自动扩展——根据负载或数据大小变化动态地在集群中增加/移除服务器——变得越来越有趣。网上出现了许多被称为NoSQL的技术。许多NoSQL的支持者发表了一些奇怪且未经证实的观点,例如"关系型模型无法进行扩展",或者"SQL无法扩展"。随者新概念的出现,也出现了一些新的术语。最近谁没有听说过最终一致性、BASE、矢量时钟,或者CAP理论呢?
但随者时间推移,理性开始主键回归。经验表名许多NoSQL数据库太过于简单,并且无法完成很多工作。同时一些基于SQL的技术开始出现——例如451集团(451 Group)的Matt Aslett所提到的NewSQL数据库。SQL和NewSQL到底有什么区别呢?NewSQL数据库中的SQL及相关技术都不应该成为问题。而可扩展性问题在关系型数据库中是一个是线上的难题,但新的实现正表现出越来越好的结果。所有的旧事物都变成新的了嘛?是,但也不是。许多关系型数据库集群的高性能涉及正在倍构建到系统的更底层,在NoSQL数据库中,特别是使用键——值存储时,这一点很明显。例如NDB Cluster并不是一个SQL数据库;它是一个可扩展的数据库,使用其原生API来控制,通常是使用NoSQL,但也可以通过在前端使用MySQL存储引擎来支持SQL。它是一个完全分布式、非共享高性能、自动分片并且不存在单点故障的事务型数据库服务器。最近几年正变得更强大、更复杂,用途也更广泛。同时,NoSQL数据库也逐渐看起来越来越像关系型数据库。有些甚至还开发了类SQL查询语言。未来典型的集群数据库可能更像是SQL和NoSQL的混合体,有多种存取机制来满足不同的使用需求。所以,我们可以从NoSQL中汲取优点,但SQL仍然会保留在集群数据库中。
和MySQL结合在一起的集群或分布式数据库技术大致包括:NDB Cluster、Clustrix、Percona XtraDB Cluster、Galera、Schooner Active Cluster、Continuent Tungsten、ScaleBase、ScaleArc、dbShards、Xeround、Akiban、VoltDB,以及GenieDB。这或多或少以MySQL为基础,或通过MySQL进行控制,或是和MySQL相关。
在开始前,需要指出,可扩展性、高可用性、事务性等是数据库系统的不同特性。许多人会感到困惑并将这些当作是相同的东西,但事实上不是。可扩展的数据库并不一定非常优秀,除非它能保证高性能,谁愿意牺牲高可用性来进行扩展呢?这些特性的组合堪称数据库的必杀技,但这很难实现。最后,除NDB Cluster外,大多数NewSQL集群产品都是比较新的事物。其他产品还没有看到足够多的生产环境部署以完全获知其优点和限制。尽管它们提到了MySQL协议或其他与MySQL相关的地方,但它们毕竟不是MySQL,所以这里仅仅稍微提一下,然后需要自己来判断它们是否适用。
1.MySQL Cluster(NDB Cluster)
MySQL Cluster是两项技术的结合:NDB数据库,以及作为SQL前端的MySQL存储引擎。NDB是一个分布式、具备容错性、非共享的数据库,提供同步复制以及节点间的数据自动分片。NDB Cluster存储引擎将SQL转换为NDB API调用,但遇到NDB不支持的操作时,就会在MySQL服务器上执行(NDB是一个键——值数据存储,无法执行类似连接或者聚合的操作)。NDB是一个非常复杂的数据库,和MySQL几乎完全不同。在适用NDB时甚至可以不需要MySQL:你可以把它作为一个独立的键——值数据库服务器。它的两点包括非常高的写入和按键查询吞吐量。NDB可以基于键的哈希自动决定哪个节点应该存储给定的数据。当通过MySQL来控制NDB时,行的主键就是键,其他的列是值。
因为它基于一些新的技术,并且集群具有容错性和分布式特性,所以管理NDB需要非常专业和特殊的技能。有许多动态变化的部分,还有类似升级集群或增加节点的操作必需正确执行以防止意外的问题。NDB是一项开源技术,但也可以从Oracle购买商业支持。商业支持中包括能够获得专门的集群管理产品Cluster Manager,可以自动执行一些枯燥且棘手的任务(Severalnines同样提供了一个集群管理产品)
MySQL Cluster正在迅速地增加越来越多地特性和功能。例如在最近的版本中,它开始支持更多类型的集群变更而无须停机操作,并且能够在数据存储的节点上执行一些特定类型的查询,以减少数据传递给MySQL层并在其中执行查询的必要性。(这个特性已由关联下推(push-down join)更名为自适应查询本地化(adaptive query localization)。)
NDB曾经相对其他MySQL存储引擎具有完全不同的性能特性,但最近的版本更加通用化了。它正在成为越来越多应用的更好的解决方案,包括游戏和移动应用。我们必须强调,NDB是一项重要的技术,能够支持全球最大的关键应用,这些应用处于极高的负载下,具有非常严苛的延迟要求以及不间断要求。举个例子,世界上任何一个通过移动电话网络呼叫的电话使用的就是NDB,并且不是临时方案——对于许多移动电话提供商而言,它是一个主要的并且非常重要的数据库。NDB需要一个快速且可靠的网络来连接节点。为了获得最好的性能,最好使用特定的高速连接设备。由于大多数情况下需要内存操作,因此服务器间需要大量的内存。那么它有什么缺点呢?复杂查询现在支持得不是很好,例如那些有很多关联和聚合得查询。所以不要指望用它来做数据仓库。NDB是一个事务型系统,但不支持MVCC,所以读操作也需要加锁,也不做任何得死锁检测。如果发生死锁,NDB就以超市返回的方式来解决。还有很多你应该知道的要点和警告,可以专门写一本书了,最好的办法就是阅读手册
MySQL Cluster是两项技术的结合:NDB数据库,以及作为SQL前端的MySQL存储引擎。NDB是一个分布式、具备容错性、非共享的数据库,提供同步复制以及节点间的数据自动分片。NDB Cluster存储引擎将SQL转换为NDB API调用,但遇到NDB不支持的操作时,就会在MySQL服务器上执行(NDB是一个键——值数据存储,无法执行类似连接或者聚合的操作)。NDB是一个非常复杂的数据库,和MySQL几乎完全不同。在适用NDB时甚至可以不需要MySQL:你可以把它作为一个独立的键——值数据库服务器。它的两点包括非常高的写入和按键查询吞吐量。NDB可以基于键的哈希自动决定哪个节点应该存储给定的数据。当通过MySQL来控制NDB时,行的主键就是键,其他的列是值。
因为它基于一些新的技术,并且集群具有容错性和分布式特性,所以管理NDB需要非常专业和特殊的技能。有许多动态变化的部分,还有类似升级集群或增加节点的操作必需正确执行以防止意外的问题。NDB是一项开源技术,但也可以从Oracle购买商业支持。商业支持中包括能够获得专门的集群管理产品Cluster Manager,可以自动执行一些枯燥且棘手的任务(Severalnines同样提供了一个集群管理产品)
MySQL Cluster正在迅速地增加越来越多地特性和功能。例如在最近的版本中,它开始支持更多类型的集群变更而无须停机操作,并且能够在数据存储的节点上执行一些特定类型的查询,以减少数据传递给MySQL层并在其中执行查询的必要性。(这个特性已由关联下推(push-down join)更名为自适应查询本地化(adaptive query localization)。)
NDB曾经相对其他MySQL存储引擎具有完全不同的性能特性,但最近的版本更加通用化了。它正在成为越来越多应用的更好的解决方案,包括游戏和移动应用。我们必须强调,NDB是一项重要的技术,能够支持全球最大的关键应用,这些应用处于极高的负载下,具有非常严苛的延迟要求以及不间断要求。举个例子,世界上任何一个通过移动电话网络呼叫的电话使用的就是NDB,并且不是临时方案——对于许多移动电话提供商而言,它是一个主要的并且非常重要的数据库。NDB需要一个快速且可靠的网络来连接节点。为了获得最好的性能,最好使用特定的高速连接设备。由于大多数情况下需要内存操作,因此服务器间需要大量的内存。那么它有什么缺点呢?复杂查询现在支持得不是很好,例如那些有很多关联和聚合得查询。所以不要指望用它来做数据仓库。NDB是一个事务型系统,但不支持MVCC,所以读操作也需要加锁,也不做任何得死锁检测。如果发生死锁,NDB就以超市返回的方式来解决。还有很多你应该知道的要点和警告,可以专门写一本书了,最好的办法就是阅读手册
2.Clustrix
Clustrix是一个分布式数据库,支持MySQL协议,所以它可以直接替代MySQL。除了协议外,它是一个全新的技术,并非建立在MySQL的基础之上,它是一个完全支持ACID,支持MVCC的事务型SQL数据库,主要用于OLTP负载场景,Clustrix在节点间进行数据分片以满足容错性,并对查询进行分发,在节点上并发执行,而不是将所有节点上取得的数据集中起来执行。集群可以在线扩展节点来处理更多的数据或负载。在某些方面Clustrix和MySQL Cluster很像;关键的不同点是,Clustrix是完全分布式执行并且缺少顶层的"代理"或者集群前端的查询协调器(query coordinator)。Clustrix本身能够理解MySQL协议,所以无须MySQL来进行协议转换。相比较而言,MySQL cluster是由三个部分组成的:MySQL、NDB集群存储引擎,以及NDB
实验评估和性能测试表明,Clustrix能够提供高性能和可扩展性。Clustrix看起来是一项比较有前景的技术
Clustrix是一个分布式数据库,支持MySQL协议,所以它可以直接替代MySQL。除了协议外,它是一个全新的技术,并非建立在MySQL的基础之上,它是一个完全支持ACID,支持MVCC的事务型SQL数据库,主要用于OLTP负载场景,Clustrix在节点间进行数据分片以满足容错性,并对查询进行分发,在节点上并发执行,而不是将所有节点上取得的数据集中起来执行。集群可以在线扩展节点来处理更多的数据或负载。在某些方面Clustrix和MySQL Cluster很像;关键的不同点是,Clustrix是完全分布式执行并且缺少顶层的"代理"或者集群前端的查询协调器(query coordinator)。Clustrix本身能够理解MySQL协议,所以无须MySQL来进行协议转换。相比较而言,MySQL cluster是由三个部分组成的:MySQL、NDB集群存储引擎,以及NDB
实验评估和性能测试表明,Clustrix能够提供高性能和可扩展性。Clustrix看起来是一项比较有前景的技术
3.ScaleBase.
ScaleBase是一个软件代理,处于应用和多个后端MySQL服务器之间。它会把发起的查询进行分裂,并将其分发到后端服务器并发执行,然后汇集结果返回给应用。
ScaleBase是一个软件代理,处于应用和多个后端MySQL服务器之间。它会把发起的查询进行分裂,并将其分发到后端服务器并发执行,然后汇集结果返回给应用。
4.GenieDB
GenieDB最开始用于地理上分布部署的NoSQL文档存储。现在它也有一个SQL层,可以通过MySQL存储引擎进行控制。它包含了很多技术,包括本地内存缓存、消息层,以及持久化磁盘数据存储。将这些技术汇集在一起,就可以使用松散的最终一致性,让应用在本地快速执行查询,或是通过分布式集群(会增加网络延迟)来保证最新的数据视图。
通过存储引擎实现的MySQL兼容层不能提供100%的MySQL特性,但对于支持类似Joomla!WordPress,以及Drupal这样的应用已经够用了。MySQL存储引擎的用处主要是使GenieDB能够结合存储引擎获得对ACID的支持,例如InnoDB。GenieDB本身并不是ACID数据库
GenieDB最开始用于地理上分布部署的NoSQL文档存储。现在它也有一个SQL层,可以通过MySQL存储引擎进行控制。它包含了很多技术,包括本地内存缓存、消息层,以及持久化磁盘数据存储。将这些技术汇集在一起,就可以使用松散的最终一致性,让应用在本地快速执行查询,或是通过分布式集群(会增加网络延迟)来保证最新的数据视图。
通过存储引擎实现的MySQL兼容层不能提供100%的MySQL特性,但对于支持类似Joomla!WordPress,以及Drupal这样的应用已经够用了。MySQL存储引擎的用处主要是使GenieDB能够结合存储引擎获得对ACID的支持,例如InnoDB。GenieDB本身并不是ACID数据库
5.Akiban
对Akiban最好的描述应该使查询加速器。它通过存储物理数据来匹配查询模式,使得低开销的跨表关联操作成为可能。尽管类似反范式话(denormalization),但数据层并不是冗余的,所以这和预先计算关联并存储结果的方式是不同的,关联表中元组是互相交错的,所以能够按照关联顺序进行顺序扫描。这就要求管理员确定查询模式能够从所谓的"表组(table grouping)"技术中受益,并需要为查询优化涉及表组。目前建议的系统架构是将Akiban配置为MySQL主库的备库,并用它来为可能较满的查询提供服务。加速系数是一到两个数量级。但是还没有看到生产环境部署或者相关的实验评估
对Akiban最好的描述应该使查询加速器。它通过存储物理数据来匹配查询模式,使得低开销的跨表关联操作成为可能。尽管类似反范式话(denormalization),但数据层并不是冗余的,所以这和预先计算关联并存储结果的方式是不同的,关联表中元组是互相交错的,所以能够按照关联顺序进行顺序扫描。这就要求管理员确定查询模式能够从所谓的"表组(table grouping)"技术中受益,并需要为查询优化涉及表组。目前建议的系统架构是将Akiban配置为MySQL主库的备库,并用它来为可能较满的查询提供服务。加速系数是一到两个数量级。但是还没有看到生产环境部署或者相关的实验评估
向内扩展.
处理不断增长的数据和负载最简单的办法是对不再需要的数据进行归档和清理,这种操作可能会带来显著的成效,具体取决于工作负载和数据特性。这种做法并不用来代替其他策略,但可以作为争取时间的短期策略,也可以作为处理大数据量的长期计划之一。在设计归档和清理策略时需要考虑到如下几点:
1.对应用的影响
一个设计良好的归档系统能够在不影响事务处理的情况下,从一个高负载的OLTP服务器上移除数据。这里的关键是能高效地找到要删除的行,然后一小块一小块地移除。通常需要平衡一次归档的行数和事务的大小,以找到一个锁竞争和事务负载量的平衡。还需要设计归档任务在必要的时候让步于事务处理
2.要归档的行
当知道某些数据不再使用后,就可以立刻清理或归档它们。也可以设计应用去归档那些几乎不怎么使用的数据。可以把归档的数据置于核心表父进附近通过视图来访问,或完全转移到别的服务器上
3.维护数据一致性
当数据间存在联系时,会导致归档和清理工作更加复杂。一个设计良好的归档任务能够保证数据的逻辑一致性.或至少在应用需要时能够保证一致,而无须在大量事务中包含多个表。当表之间存在关系时,哪个表首先归档是个问题。在归档时需要考虑孤立行的影响。可以选择违背外键约束(可以通过执行SET FOREIGN_KEY_CHECKS=0禁止InnoDB的外键约束)或暂时把"悬空指针"(dangling pointer)记录放到一边。如果应用层认为这些相关联的表具有层次关系,那么归档的顺序也应该和它一样。例如,如果应用总是先检查订单再检查发货单,就先归档订单。应用应该看不到孤立的发货单,因此接下来就可以将返货但归档。
4.避免数据丢失
如果是再服务器间归档,归档期间可能就无法做分布式事务,也有可能将数据归档到MyISAM或其他非事务型的存储引擎中。因此,为了避免数据丢失,再从源表中删除时,要保证已经在目标机器上保存。将归档数据单独写到一个文件里也是个好主意。可以将归档任务设计为能够随时关闭或重启,并且不会引起不一致或索引冲突之类的错误。
5.解除归档(unarchiving)
可以通过一些解除归档策略来减少归档的数据量。它可以帮助你归档那些不确定是否需要的数据,并在以后可以通过选项进行回退。如果可以设置一些检查点让系统来检查是否有需要归档的数据,那么这应该时一个很容易实现的策略。例如,要对不活跃的用户进行归档,检查点就可以设置在登录验证时。如果因为用户不存在导致登录失败,可以去检查归档数据中是否存在该用户,如果有,则从中取出来并完成登录。
Percona Toolkit包含的工具pt-archiver能够帮助你有效地归档和清理MySQL表,但不提供解除归档功能
处理不断增长的数据和负载最简单的办法是对不再需要的数据进行归档和清理,这种操作可能会带来显著的成效,具体取决于工作负载和数据特性。这种做法并不用来代替其他策略,但可以作为争取时间的短期策略,也可以作为处理大数据量的长期计划之一。在设计归档和清理策略时需要考虑到如下几点:
1.对应用的影响
一个设计良好的归档系统能够在不影响事务处理的情况下,从一个高负载的OLTP服务器上移除数据。这里的关键是能高效地找到要删除的行,然后一小块一小块地移除。通常需要平衡一次归档的行数和事务的大小,以找到一个锁竞争和事务负载量的平衡。还需要设计归档任务在必要的时候让步于事务处理
2.要归档的行
当知道某些数据不再使用后,就可以立刻清理或归档它们。也可以设计应用去归档那些几乎不怎么使用的数据。可以把归档的数据置于核心表父进附近通过视图来访问,或完全转移到别的服务器上
3.维护数据一致性
当数据间存在联系时,会导致归档和清理工作更加复杂。一个设计良好的归档任务能够保证数据的逻辑一致性.或至少在应用需要时能够保证一致,而无须在大量事务中包含多个表。当表之间存在关系时,哪个表首先归档是个问题。在归档时需要考虑孤立行的影响。可以选择违背外键约束(可以通过执行SET FOREIGN_KEY_CHECKS=0禁止InnoDB的外键约束)或暂时把"悬空指针"(dangling pointer)记录放到一边。如果应用层认为这些相关联的表具有层次关系,那么归档的顺序也应该和它一样。例如,如果应用总是先检查订单再检查发货单,就先归档订单。应用应该看不到孤立的发货单,因此接下来就可以将返货但归档。
4.避免数据丢失
如果是再服务器间归档,归档期间可能就无法做分布式事务,也有可能将数据归档到MyISAM或其他非事务型的存储引擎中。因此,为了避免数据丢失,再从源表中删除时,要保证已经在目标机器上保存。将归档数据单独写到一个文件里也是个好主意。可以将归档任务设计为能够随时关闭或重启,并且不会引起不一致或索引冲突之类的错误。
5.解除归档(unarchiving)
可以通过一些解除归档策略来减少归档的数据量。它可以帮助你归档那些不确定是否需要的数据,并在以后可以通过选项进行回退。如果可以设置一些检查点让系统来检查是否有需要归档的数据,那么这应该时一个很容易实现的策略。例如,要对不活跃的用户进行归档,检查点就可以设置在登录验证时。如果因为用户不存在导致登录失败,可以去检查归档数据中是否存在该用户,如果有,则从中取出来并完成登录。
Percona Toolkit包含的工具pt-archiver能够帮助你有效地归档和清理MySQL表,但不提供解除归档功能
保持活跃数据独立。
即使并不真的把老数据转移到别的服务器,许多应用也能受益于活跃书数据和非活跃数据的隔离。这有助于高效利用缓存,并为活跃和不活跃的数据使用不同的硬件或应用架构.下面列举了几种做法:
1.将表划分为几个部分
分表是一个比较明智的办法,特别是整张表无法完全加载到内存时。例如,可以把users表划分为active_users和inactive_users表。你可能认为这并不需要,因为数据库本身只缓存"热"数据,但事实上这取决于存储引擎。如果用的是InnoDB,每次缓存一页,而一页能存储100个用户,但只有10%是活跃的,那么这时候InnoDB可能认为所有的页都是"热"的——因此每个"热"页的90%将被浪费掉。将其拆分成两个表可以明显改善内存利用率
2.MySQL分区
MySQL5.1本身提供了对表进行分区的功能,能够帮助把最近的数据留在内存中
3.基于时间的数据分区。
如果应用不断有心数据进来,一般心数据总是比旧数据更加活跃。例如,我们知道博客服务的流量大多是最近七天发表的文章和评论。更新的大部分是相同的数据集。因此这些数据被完整地保留在内存中,使用复制来保证在主库失效时有一份可用的备份。其他数据则完全可以放到别的地方去。我们也看到过这样一种设计,在两个节点的分片上存储用户数据,心数据总是进入"活跃节点",该节点使用更大的内存和快速硬盘。另外一个节点存储旧数据,使用非常大(但比较慢)的硬盘。应用假设不会太需要旧数据。对于很多应用而言这是合理的假设,依靠10%的最新数据能够满足90%或更多的请求。可以通过动态分片来轻松实现这种策略。例如,分片目录可能定义如下:
```sql
CREATE TABLE users (
user_id int unsigned not null,
shard_new int unsigned not null,
shard_archive int unsigned not null,
archive_timestamp timestamp,
PRIMARY KEY (user_id)
);
```
通过一个归档脚本将旧数据从活跃节点转移到归档节点,当移动用户数据到归档节点时,更新archive_timestamp列的值。shard_new和shard_archive列记录存储数据的分片号
即使并不真的把老数据转移到别的服务器,许多应用也能受益于活跃书数据和非活跃数据的隔离。这有助于高效利用缓存,并为活跃和不活跃的数据使用不同的硬件或应用架构.下面列举了几种做法:
1.将表划分为几个部分
分表是一个比较明智的办法,特别是整张表无法完全加载到内存时。例如,可以把users表划分为active_users和inactive_users表。你可能认为这并不需要,因为数据库本身只缓存"热"数据,但事实上这取决于存储引擎。如果用的是InnoDB,每次缓存一页,而一页能存储100个用户,但只有10%是活跃的,那么这时候InnoDB可能认为所有的页都是"热"的——因此每个"热"页的90%将被浪费掉。将其拆分成两个表可以明显改善内存利用率
2.MySQL分区
MySQL5.1本身提供了对表进行分区的功能,能够帮助把最近的数据留在内存中
3.基于时间的数据分区。
如果应用不断有心数据进来,一般心数据总是比旧数据更加活跃。例如,我们知道博客服务的流量大多是最近七天发表的文章和评论。更新的大部分是相同的数据集。因此这些数据被完整地保留在内存中,使用复制来保证在主库失效时有一份可用的备份。其他数据则完全可以放到别的地方去。我们也看到过这样一种设计,在两个节点的分片上存储用户数据,心数据总是进入"活跃节点",该节点使用更大的内存和快速硬盘。另外一个节点存储旧数据,使用非常大(但比较慢)的硬盘。应用假设不会太需要旧数据。对于很多应用而言这是合理的假设,依靠10%的最新数据能够满足90%或更多的请求。可以通过动态分片来轻松实现这种策略。例如,分片目录可能定义如下:
```sql
CREATE TABLE users (
user_id int unsigned not null,
shard_new int unsigned not null,
shard_archive int unsigned not null,
archive_timestamp timestamp,
PRIMARY KEY (user_id)
);
```
通过一个归档脚本将旧数据从活跃节点转移到归档节点,当移动用户数据到归档节点时,更新archive_timestamp列的值。shard_new和shard_archive列记录存储数据的分片号
负载均衡。
负载均衡的基本思路很简单:在一个服务器集群中尽可能地平均负载量。通常的做法是在服务器前端设置一个负载均衡器(一般是专门的硬件设备)。然后负载均衡器将请求的连接路由到最空闲的可用服务器。如图显示了一个典型的大型网站负载均衡设置,其中一个负载均衡器用于HTTP流量,另一个用于MySQL访问。负载均衡有五个常见目的。
1.可扩展性
负载均衡对某些扩展策略有所帮助,例如读写分离时从备库读数据
2.高效性
负载均衡有助于更有效地使用资源,因为它能够控制请求被路由到何处。如果服务器器处理能力各不相同,这就尤为重要:你可以把更多的工作分配给性能更好的机器
3.可用性
一个灵活的负载均衡解决方案能够使用时刻保持可用的服务器
4.透明性
客户端无须知道是否存在负载均衡设置,也不需要关心在负载均衡器的背后有多少机器,它们的名字是什么。负载均衡器给客户端看到的只是一个虚拟的服务器
5.一致性
如果应用是有状态的(数据库事务、网站会话等),那么负载均衡器就应该将相关的查询指向同一个服务器,以防止状态丢失。应用无须跟踪到底连接的是哪个服务器。
在与MySQL相关的领域里,负载均衡架构通常和数据分片及复制紧密相关。你可以把负载均衡和高可用性结合在一起,部署到应用的任一层次上。例如,可以在MySQL Cluster集群的多个SQl节点上做负载均衡,也可以在多个数据中心间作负载均衡,其中每个数据中心又可以使用数据分片架构,每个节点实际上是拥有多个备库的主——主复制对结构,这里又可以作负载均衡。对于高可用性策略也同样如此:在一个架构里可以配置多层的故障转移机制。负载均衡有许多微妙之处,举个例子,其中一个挑战就是管理读/写策略。有些负载均衡技术本身能够实现这一点,但其他的则需要应用自己知道哪些节点是可读的或可写的。在决定如何实现负载均衡时,应该考虑到这些因素。有许多负载均衡解决方案可以使用,从诸如Wackamole这样基于端点的(peer-based)实现,到DNS、LVS(Linux Virtual Server)硬件负载均衡器、TCP代理、MySQL Proxy,以及在应用中管理负载均衡。最普遍的策略是使用硬件负载均衡器,大多是使用HAProxy,它看起来很流行并且工作得很好。还有一些人使用TCP代理,例如Pen.但MySQL Proxy用得并不多
负载均衡的基本思路很简单:在一个服务器集群中尽可能地平均负载量。通常的做法是在服务器前端设置一个负载均衡器(一般是专门的硬件设备)。然后负载均衡器将请求的连接路由到最空闲的可用服务器。如图显示了一个典型的大型网站负载均衡设置,其中一个负载均衡器用于HTTP流量,另一个用于MySQL访问。负载均衡有五个常见目的。
1.可扩展性
负载均衡对某些扩展策略有所帮助,例如读写分离时从备库读数据
2.高效性
负载均衡有助于更有效地使用资源,因为它能够控制请求被路由到何处。如果服务器器处理能力各不相同,这就尤为重要:你可以把更多的工作分配给性能更好的机器
3.可用性
一个灵活的负载均衡解决方案能够使用时刻保持可用的服务器
4.透明性
客户端无须知道是否存在负载均衡设置,也不需要关心在负载均衡器的背后有多少机器,它们的名字是什么。负载均衡器给客户端看到的只是一个虚拟的服务器
5.一致性
如果应用是有状态的(数据库事务、网站会话等),那么负载均衡器就应该将相关的查询指向同一个服务器,以防止状态丢失。应用无须跟踪到底连接的是哪个服务器。
在与MySQL相关的领域里,负载均衡架构通常和数据分片及复制紧密相关。你可以把负载均衡和高可用性结合在一起,部署到应用的任一层次上。例如,可以在MySQL Cluster集群的多个SQl节点上做负载均衡,也可以在多个数据中心间作负载均衡,其中每个数据中心又可以使用数据分片架构,每个节点实际上是拥有多个备库的主——主复制对结构,这里又可以作负载均衡。对于高可用性策略也同样如此:在一个架构里可以配置多层的故障转移机制。负载均衡有许多微妙之处,举个例子,其中一个挑战就是管理读/写策略。有些负载均衡技术本身能够实现这一点,但其他的则需要应用自己知道哪些节点是可读的或可写的。在决定如何实现负载均衡时,应该考虑到这些因素。有许多负载均衡解决方案可以使用,从诸如Wackamole这样基于端点的(peer-based)实现,到DNS、LVS(Linux Virtual Server)硬件负载均衡器、TCP代理、MySQL Proxy,以及在应用中管理负载均衡。最普遍的策略是使用硬件负载均衡器,大多是使用HAProxy,它看起来很流行并且工作得很好。还有一些人使用TCP代理,例如Pen.但MySQL Proxy用得并不多
直接连接。
有些人认为负载均衡就是配置在应用和MySQL服务器之间的东西。但这并不是唯一的负载均衡方法。你可以在保持应用和MySQL连接的情况下使用负载均衡。事实上,集中化的负载均衡系统只有在存在一个对等置换的服务器池时才能很好工作。如果应用需要做一些决策,例如在悲苦上执行读操作是否安全,就需要直接连接到服务器。除了可能出现的一些特定逻辑,应用为负载均衡做决策是非常高效的。例如,如果有两个完全相同的备库,你可以使用其中的一个来处理特定分片的数据查询,另一个处理其他的查询。这样能够有效利用备库的内存,因为每个备库只会缓存一部分数据。如果其中一个备库失效,另外一个备库拥有所有的数据,仍然能提供服务。
有些人认为负载均衡就是配置在应用和MySQL服务器之间的东西。但这并不是唯一的负载均衡方法。你可以在保持应用和MySQL连接的情况下使用负载均衡。事实上,集中化的负载均衡系统只有在存在一个对等置换的服务器池时才能很好工作。如果应用需要做一些决策,例如在悲苦上执行读操作是否安全,就需要直接连接到服务器。除了可能出现的一些特定逻辑,应用为负载均衡做决策是非常高效的。例如,如果有两个完全相同的备库,你可以使用其中的一个来处理特定分片的数据查询,另一个处理其他的查询。这样能够有效利用备库的内存,因为每个备库只会缓存一部分数据。如果其中一个备库失效,另外一个备库拥有所有的数据,仍然能提供服务。
1.复制上的读/写分离。
MySQL复制产生了多个数据副本,你可以选择在悲苦还是主库上执行查询。由于备库复制是异步的,因此主要的难点是如何处理备库上的脏数据。应该将备库用作只读的,而主库可以同时处理读和写查询。通常需要修改应用以适应这种分离需求。然后应用就可以使用主库来进行写操作,并将读操作分配到主库和备库上;如果不太关心数据是否是脏的,可以使用备库,而对需要即时数据的请求使用主库。我们将这种称为读/写分离。如果使用的是主动——被动模式的主——主复制对,同样也要考虑这个问题。使用这种配置时,只有主动服务器接受写操作。如果能够接受读到脏数据,可以将读分配给被动服务器。
最大的问题时如何避免由于读了脏数据引起的奇怪问题。一个典型的例子时当一个用户做了某些修改,例如增加了一条博客文章的评论,然后重新加载页面,但并没有看到更新,因为应用从备库读取到了脏的数据。比较常见的读/写分离方法如下:
1.基于查询分离
最简单的分离方法时将所有不能容忍脏数据的读和写查询分配到主动或者主库服务器上。其他的读查询分配到备库或者被动服务器上。该策略很容易实现,但事实上无法有效地使用备库,因为只有很少的查询能容忍脏数据
2.基于脏数据分离
这是对基于查询分离方法的小改进。徐奥做一些额外的工作,让应用检查复制延迟,以确定备库数据是否太旧。许多报表类应用都使用这个策略:只需要晚上加载到备库即可,它们并不关心是不是100%跟上了主库
3.基于会话分离
另一个决定能否从备库读数据的稍微复杂一点的方法时判断用户自己是否修改了数据,用户不需要看到其他用户的最新数据,但需要看到自己的更新。可以在会话层设置一个标记位,表明做了更新,就将该用户的查询在一段时间内总是指向主库。这是我们通常推荐的策略,因为它是在简单和有效性之间的一种很好的妥协。如果有足够的想象力,可以把基于会话的分离方法和复制延迟监控结合起来。如果用户在10秒前更新了数据,而所有备库延迟在5秒内,就可以安全地从备库中读取数据,但为整个会话选择同一个备库是一个很好的主意,否则用户可能会奇怪有些备库的更新速度比其他服务器要慢。
4.基本版本分离
这和基于会话的分离方法相似:你可以跟踪对象的版本好以及/或者时间戳,通过从备库读取对象的版本或时间戳来判断数据是否足够新。如果备库的数据太久,可以从主库获取最新的数据。即使对象本身没有变化,但如果是顶层对象,只要下面的任何对象有比那花,也可以增加版本好,这简化了脏数据检查(只需要检查顶层对象一处就能判断是否有更新)。例如,在用户发表了一篇新文章后,可以更新用户的版本。这样就会从主库去读取数据了
5.基于全局版本/会话分离
这个办法是基于版本分离和基于会话分离的变种。当应用执行写操作时,在提交事务后,执行一次SHOW MASTER STATUS操作。然后在缓存中存储主库日志坐标,作为被修改对象以及/或者会话的版本号。当应用连接到备库时,执行SHOW SLAVE STATUS并将备库上的坐标和缓存中的版本号相对比。如果备库相比记录点更新,就可以安全地读取备库数据。
大多数读/写分离解决方案都需要监控复制延迟来决策读查询的分配,不管时通过复制或负载均衡器,或是一个中间系统。如果这么做,需要注意通过SHOW SLAVE STATUS得到的Seconds_behind_master列的值并不能准确地用于监控延迟。Percona Toolkit中的pt-heartbeat工具能够帮助监控延迟,并维护元数据,例如二进制日志未知,这可以减轻之前我们讨论的一些策略存在的问题。
如果不在乎用昂贵的硬件来承载压力,也就可以不适用复制来扩展读操作,这样当然更简单。这可以避免在主备上分离读的复杂性。有些人认为这很有意义;也有人认为浪费硬件。这种分歧时由于不同的目的引起的:你是只需要可扩展性,还是要同时具有可扩展性和高利用率?如果需要高利用率,那么备库除了保存数据副本还需要承担其他任务,就不得不处理这些额外的复杂度
MySQL复制产生了多个数据副本,你可以选择在悲苦还是主库上执行查询。由于备库复制是异步的,因此主要的难点是如何处理备库上的脏数据。应该将备库用作只读的,而主库可以同时处理读和写查询。通常需要修改应用以适应这种分离需求。然后应用就可以使用主库来进行写操作,并将读操作分配到主库和备库上;如果不太关心数据是否是脏的,可以使用备库,而对需要即时数据的请求使用主库。我们将这种称为读/写分离。如果使用的是主动——被动模式的主——主复制对,同样也要考虑这个问题。使用这种配置时,只有主动服务器接受写操作。如果能够接受读到脏数据,可以将读分配给被动服务器。
最大的问题时如何避免由于读了脏数据引起的奇怪问题。一个典型的例子时当一个用户做了某些修改,例如增加了一条博客文章的评论,然后重新加载页面,但并没有看到更新,因为应用从备库读取到了脏的数据。比较常见的读/写分离方法如下:
1.基于查询分离
最简单的分离方法时将所有不能容忍脏数据的读和写查询分配到主动或者主库服务器上。其他的读查询分配到备库或者被动服务器上。该策略很容易实现,但事实上无法有效地使用备库,因为只有很少的查询能容忍脏数据
2.基于脏数据分离
这是对基于查询分离方法的小改进。徐奥做一些额外的工作,让应用检查复制延迟,以确定备库数据是否太旧。许多报表类应用都使用这个策略:只需要晚上加载到备库即可,它们并不关心是不是100%跟上了主库
3.基于会话分离
另一个决定能否从备库读数据的稍微复杂一点的方法时判断用户自己是否修改了数据,用户不需要看到其他用户的最新数据,但需要看到自己的更新。可以在会话层设置一个标记位,表明做了更新,就将该用户的查询在一段时间内总是指向主库。这是我们通常推荐的策略,因为它是在简单和有效性之间的一种很好的妥协。如果有足够的想象力,可以把基于会话的分离方法和复制延迟监控结合起来。如果用户在10秒前更新了数据,而所有备库延迟在5秒内,就可以安全地从备库中读取数据,但为整个会话选择同一个备库是一个很好的主意,否则用户可能会奇怪有些备库的更新速度比其他服务器要慢。
4.基本版本分离
这和基于会话的分离方法相似:你可以跟踪对象的版本好以及/或者时间戳,通过从备库读取对象的版本或时间戳来判断数据是否足够新。如果备库的数据太久,可以从主库获取最新的数据。即使对象本身没有变化,但如果是顶层对象,只要下面的任何对象有比那花,也可以增加版本好,这简化了脏数据检查(只需要检查顶层对象一处就能判断是否有更新)。例如,在用户发表了一篇新文章后,可以更新用户的版本。这样就会从主库去读取数据了
5.基于全局版本/会话分离
这个办法是基于版本分离和基于会话分离的变种。当应用执行写操作时,在提交事务后,执行一次SHOW MASTER STATUS操作。然后在缓存中存储主库日志坐标,作为被修改对象以及/或者会话的版本号。当应用连接到备库时,执行SHOW SLAVE STATUS并将备库上的坐标和缓存中的版本号相对比。如果备库相比记录点更新,就可以安全地读取备库数据。
大多数读/写分离解决方案都需要监控复制延迟来决策读查询的分配,不管时通过复制或负载均衡器,或是一个中间系统。如果这么做,需要注意通过SHOW SLAVE STATUS得到的Seconds_behind_master列的值并不能准确地用于监控延迟。Percona Toolkit中的pt-heartbeat工具能够帮助监控延迟,并维护元数据,例如二进制日志未知,这可以减轻之前我们讨论的一些策略存在的问题。
如果不在乎用昂贵的硬件来承载压力,也就可以不适用复制来扩展读操作,这样当然更简单。这可以避免在主备上分离读的复杂性。有些人认为这很有意义;也有人认为浪费硬件。这种分歧时由于不同的目的引起的:你是只需要可扩展性,还是要同时具有可扩展性和高利用率?如果需要高利用率,那么备库除了保存数据副本还需要承担其他任务,就不得不处理这些额外的复杂度
2.修改应用的配置。
还有一个分发负载的办法是重新配置应用。例如,你可以配置多个机器来分担生成大报表操作的负载。每台机器可以配置成连接到不同的MySQL备库,并为第N个用户或网站生成报表。
这样的系统很容易实现,但如果需要修改一些代码——包括配置文件修改——会变得脆弱且难以处理。硬编码有着固有的限制,需要在每台机器上修改硬编码,或者在一个中心服务器上修改,然后通过文件副本或代码控制更新命令"发布"到其他服务器上,如果将配置存储在服务器或缓存中,就可以避免这些麻烦。
还有一个分发负载的办法是重新配置应用。例如,你可以配置多个机器来分担生成大报表操作的负载。每台机器可以配置成连接到不同的MySQL备库,并为第N个用户或网站生成报表。
这样的系统很容易实现,但如果需要修改一些代码——包括配置文件修改——会变得脆弱且难以处理。硬编码有着固有的限制,需要在每台机器上修改硬编码,或者在一个中心服务器上修改,然后通过文件副本或代码控制更新命令"发布"到其他服务器上,如果将配置存储在服务器或缓存中,就可以避免这些麻烦。
3.修改DNS名。
这是一个比较粗糙的负载均衡技术,但对于一些简单的应用,为不同的目的创建DNS还是很实用的。你可以为不同的服务器指定一个合适的名字。最简单的方法是只读服务器有一个NDS名,而给负责写操作的服务器起另外一个DNS名。如果备库能够跟上主库,那就把只读DNS名指定给备库,当出现延迟时,再将该DNS名指定给主库。这种DNS技术非常容易实现,但也有很多缺点。最大的问题是无法完全控制DNS.
1.修改DNS并不是立刻生效的,也不是原子的。将DNS的变化传递到整个网络或在网络间传播都需要比较长的时间
2.DNS数据会在各个地方缓存下来,它的过期时间是建议性质的,而非强制的
3.可能需要应用或服务器重启才能使修改后的DNS完全生效。
4.多个IP共用一个DNS名依赖于轮询行为来均衡请求,这并不是一个好主意,因为轮询行为并不宗总是可预知的
5.DBA可能没有权限直接访问DNS
除非应用非常简单,否则依赖不受控制的系统会非常危险。你可以通过修改/etc/hosts/文件而非DNS来改善对系统的控制。当发布一个对该文件的更新时,会知道该变更已经生效。这比等待缓存的DNS失效要好得多。但这仍然不是理想的办法。通常建议人们构建一个完全不依赖DNS的应用。即使应用很简单也适用,因为你无法预知应用会增长到多大规模。
这是一个比较粗糙的负载均衡技术,但对于一些简单的应用,为不同的目的创建DNS还是很实用的。你可以为不同的服务器指定一个合适的名字。最简单的方法是只读服务器有一个NDS名,而给负责写操作的服务器起另外一个DNS名。如果备库能够跟上主库,那就把只读DNS名指定给备库,当出现延迟时,再将该DNS名指定给主库。这种DNS技术非常容易实现,但也有很多缺点。最大的问题是无法完全控制DNS.
1.修改DNS并不是立刻生效的,也不是原子的。将DNS的变化传递到整个网络或在网络间传播都需要比较长的时间
2.DNS数据会在各个地方缓存下来,它的过期时间是建议性质的,而非强制的
3.可能需要应用或服务器重启才能使修改后的DNS完全生效。
4.多个IP共用一个DNS名依赖于轮询行为来均衡请求,这并不是一个好主意,因为轮询行为并不宗总是可预知的
5.DBA可能没有权限直接访问DNS
除非应用非常简单,否则依赖不受控制的系统会非常危险。你可以通过修改/etc/hosts/文件而非DNS来改善对系统的控制。当发布一个对该文件的更新时,会知道该变更已经生效。这比等待缓存的DNS失效要好得多。但这仍然不是理想的办法。通常建议人们构建一个完全不依赖DNS的应用。即使应用很简单也适用,因为你无法预知应用会增长到多大规模。
4.转移IP地址。
一些负载均衡解决方案依赖于在服务器间转移虚拟地址(虚拟IP地址不是直接连接到任何特定的计算机或网络端口,而是"漂浮"在计算机之间),一般能够很好地工作。这听起来和修改DNS很像,但完全时两码事。服务器不会根据DNS名去监听网络流量,而是根据指定的IP地址去见挺流量,所以转移IP地址允许DNS名保持不变。你可以通过ARP(地址解析协议)命令强制IP地址的更改快速而且原子性地通知到网络上。看过的适用最普遍的技术时Pacemaker,这是Linux-HA享目的Heartbeat工具的继承者。你可以适用单个IP地址,为其分配一个角色,例如read-only,当需要在机器间转移IP地址时,它能够感知到。其他的类似的工具包括LVS和Wackamole.
一个比较方便的技术时为每个物理服务器分配一个固定的IP地址。该IP地址固定在服务器上,不再改变。然后可以为每个逻辑上的"服务"使用一个虚拟IP地址。它们能够很方便地在服务器间转移,这使得转移服务和应用实例无须再重新配置应用,因此更加容易。即使不怎么经常转移IP地址,这也是一个很好地特性。
一些负载均衡解决方案依赖于在服务器间转移虚拟地址(虚拟IP地址不是直接连接到任何特定的计算机或网络端口,而是"漂浮"在计算机之间),一般能够很好地工作。这听起来和修改DNS很像,但完全时两码事。服务器不会根据DNS名去监听网络流量,而是根据指定的IP地址去见挺流量,所以转移IP地址允许DNS名保持不变。你可以通过ARP(地址解析协议)命令强制IP地址的更改快速而且原子性地通知到网络上。看过的适用最普遍的技术时Pacemaker,这是Linux-HA享目的Heartbeat工具的继承者。你可以适用单个IP地址,为其分配一个角色,例如read-only,当需要在机器间转移IP地址时,它能够感知到。其他的类似的工具包括LVS和Wackamole.
一个比较方便的技术时为每个物理服务器分配一个固定的IP地址。该IP地址固定在服务器上,不再改变。然后可以为每个逻辑上的"服务"使用一个虚拟IP地址。它们能够很方便地在服务器间转移,这使得转移服务和应用实例无须再重新配置应用,因此更加容易。即使不怎么经常转移IP地址,这也是一个很好地特性。
引入中间件。
迄今为止,我们所讨论的方案都嘉定应用跟MySQL服务器使直接相连的。但是许多负载均衡解决方案都会引入一个中间件,作为网络通信的代理。它以便接受所有的通信请求,另一边将这些请求派发到指定的服务器上,然后把执行结果发送回请求的机器上。中间件可以实硬件设备或时软件(你可以把诸如LVS这样的解决方案配置成只有一个用需要创建一个新连接时才参与进来,此后不再作为中间价)。如图描述了这种架构,这种解决方案通常能工作得很好,当然除非为负载均衡器本身增加冗余,这样才能避免单点故障引起的整个系统瘫痪。从开源软件,如HAProxy,到许多广为人知的商业系统,有许多负载均衡器得到了成功的应用
迄今为止,我们所讨论的方案都嘉定应用跟MySQL服务器使直接相连的。但是许多负载均衡解决方案都会引入一个中间件,作为网络通信的代理。它以便接受所有的通信请求,另一边将这些请求派发到指定的服务器上,然后把执行结果发送回请求的机器上。中间件可以实硬件设备或时软件(你可以把诸如LVS这样的解决方案配置成只有一个用需要创建一个新连接时才参与进来,此后不再作为中间价)。如图描述了这种架构,这种解决方案通常能工作得很好,当然除非为负载均衡器本身增加冗余,这样才能避免单点故障引起的整个系统瘫痪。从开源软件,如HAProxy,到许多广为人知的商业系统,有许多负载均衡器得到了成功的应用
1.负载均衡器。
在市场上有许多负载均衡硬件和软件,但很少有专门为MySQL服务器设计的(MySQL Proxy是个例外,但目前还未能证明能够很好地工作,因为它会带来一些问题,例如延迟增加以及可扩展性瓶颈)。Web服务器通常更需要负载均衡,因此许多多用途的负载均衡设备都会支持HTTP,而对其他用途则只有一些很少的基本特性。MySQL连接都只是正常的TCP/IP连接,所以可以在MySQL上适用多用途负载均衡器。但由于缺少MySQL专有的特性,因此回多一些限制。
1.除非负载均衡器知道MySQL的真实负载,否则在分发请求时可能无法做到很好的负载均衡。不是所有的请求都是等同的,但多用途负载均衡器通常对所有的请求一视同仁。
2.许多负载均衡器知道如何检查一个HTTP请求并把会话"固定"到一个服务器上以保护在Web服务器上的会话状态。MySQL连接也是有状态的,但负载均衡器可能并不知道如何把所有从单个HTTP会话发送的连接请求"固定"到一个MySQL服务器上,这回损失一部分效率(如果单个会话的请求都是发到同一个MySQL服务器,服务器的缓存会更优效率)
3.连接池和长连接可能会阻碍负载均衡器分发连接请求。例如,一个连接池打开了预先配置好的连接数,负载均衡器在已有的四个MySQL服务器上分发这些连接。现在增加了两个以上的MySQL服务器。由于连接池不会请求新连接,因而新的服务器会一直空闲着。池中的连接会在服务器间不公平地分配负载,导致一些服务器超出负载,一些则几乎没有负载。可以在多个层面为连接设置失效时间来缓解这个问题,但这很复杂并且很难做到。连接池方案只有它们本身能够处理负载均衡时才能工作得很好。
4.许多多用途负载均衡器只会针对HTTP服务器做健康和负载检查。一个简单的负载均衡器最少能够核实服务器在一个TCP端口上接受的连接数。更好的负载均衡器能够自动发起一个HTTP请求,并检查返回值以确定这个Web服务器是否正常运转。MySQL并不接受3306端口的HTTP,因此需要自己来构建健康检查方法。你可以在MySQL服务器上安装一个HTTP服务器如那件,并将负载均衡器指向一个脚本,这个脚本检查MySQL服务器的状态并返回一个对应的状态值(实际上,如果能编码实现一个监听80端口的程序,或者配置xinetd来调用程序,甚至不需要再安装一个Web服务器)。最重要的时检查操作系统负载(通过查看/proc/loadavg)、复制状态,以及MySQL的连接数。
在市场上有许多负载均衡硬件和软件,但很少有专门为MySQL服务器设计的(MySQL Proxy是个例外,但目前还未能证明能够很好地工作,因为它会带来一些问题,例如延迟增加以及可扩展性瓶颈)。Web服务器通常更需要负载均衡,因此许多多用途的负载均衡设备都会支持HTTP,而对其他用途则只有一些很少的基本特性。MySQL连接都只是正常的TCP/IP连接,所以可以在MySQL上适用多用途负载均衡器。但由于缺少MySQL专有的特性,因此回多一些限制。
1.除非负载均衡器知道MySQL的真实负载,否则在分发请求时可能无法做到很好的负载均衡。不是所有的请求都是等同的,但多用途负载均衡器通常对所有的请求一视同仁。
2.许多负载均衡器知道如何检查一个HTTP请求并把会话"固定"到一个服务器上以保护在Web服务器上的会话状态。MySQL连接也是有状态的,但负载均衡器可能并不知道如何把所有从单个HTTP会话发送的连接请求"固定"到一个MySQL服务器上,这回损失一部分效率(如果单个会话的请求都是发到同一个MySQL服务器,服务器的缓存会更优效率)
3.连接池和长连接可能会阻碍负载均衡器分发连接请求。例如,一个连接池打开了预先配置好的连接数,负载均衡器在已有的四个MySQL服务器上分发这些连接。现在增加了两个以上的MySQL服务器。由于连接池不会请求新连接,因而新的服务器会一直空闲着。池中的连接会在服务器间不公平地分配负载,导致一些服务器超出负载,一些则几乎没有负载。可以在多个层面为连接设置失效时间来缓解这个问题,但这很复杂并且很难做到。连接池方案只有它们本身能够处理负载均衡时才能工作得很好。
4.许多多用途负载均衡器只会针对HTTP服务器做健康和负载检查。一个简单的负载均衡器最少能够核实服务器在一个TCP端口上接受的连接数。更好的负载均衡器能够自动发起一个HTTP请求,并检查返回值以确定这个Web服务器是否正常运转。MySQL并不接受3306端口的HTTP,因此需要自己来构建健康检查方法。你可以在MySQL服务器上安装一个HTTP服务器如那件,并将负载均衡器指向一个脚本,这个脚本检查MySQL服务器的状态并返回一个对应的状态值(实际上,如果能编码实现一个监听80端口的程序,或者配置xinetd来调用程序,甚至不需要再安装一个Web服务器)。最重要的时检查操作系统负载(通过查看/proc/loadavg)、复制状态,以及MySQL的连接数。
2.负载均衡算法。
有许都算法用来决定哪个服务器接受下一个连接。每个厂商都有各自不同的算法,下面这个清单列出了一些可用的方法:
1.随机
负载均衡器随机地从可用的服务器池中选择一个服务器来处理请求
2.轮询
负载均衡器以循环顺序发送请求到服务器,例如:A,B,C,A,B,C
3.最少连接数
下一个连接请求分配给拥有最少活跃连接的服务器
4.最快响应
能够最快处理请求的服务器接受下一个连接。当服务器池里同时存在快速和慢速服务器时,这很有小。即使同样的查询在不同的场景下运行也会有不同的表现,例如当查询结果已经缓存在查询缓存中,或者服务器缓存中已经包含了所需要的数据时
5.哈希
负载均衡器通过连接的源IP地址进行哈希,将其映射到池中的同一个服务器上。每次从同一个IP地址发起请求,负载均衡器都会将请求发送给同样的服务器。只有当池中服务器数目改变时这种绑定才会发生变化
6.权重
负载均衡器能够结合适用上述几种算法。例如,你可能拥有单CPU核双CPU的机器。双CPU机器有接近两倍的性能,所以可以让负载均衡器分派两倍的请求给双CPU机器
哪种算法最优取决于具体的工作负载。例如最少连接算法,如果有新机器加入,可能会有大量连接涌入该服务器,而这时候它的缓存还没有包含热数据。你需要通过测试来为你的工作负载找到最好的性能。除了正常的日常运转,还需要考虑极端情况。在比较极端的情况下——例如负载升高,修改模式,或者多台服务器下线——至少要避免系统出现重大错误。我们这里只描述了即时处理请求的算法,无须对连接请求排队。但有时候适用排队算法可能更有效。例如,一个算法可能只维护给定的数据库服务器并发数目,同一时刻只允许不超过N个活跃事务。如果有太多的活跃事务,就将新的请求放到一个队列里,然后让可用服务器列表的第一个来处理它。有些连接池也支持队列算法
有许都算法用来决定哪个服务器接受下一个连接。每个厂商都有各自不同的算法,下面这个清单列出了一些可用的方法:
1.随机
负载均衡器随机地从可用的服务器池中选择一个服务器来处理请求
2.轮询
负载均衡器以循环顺序发送请求到服务器,例如:A,B,C,A,B,C
3.最少连接数
下一个连接请求分配给拥有最少活跃连接的服务器
4.最快响应
能够最快处理请求的服务器接受下一个连接。当服务器池里同时存在快速和慢速服务器时,这很有小。即使同样的查询在不同的场景下运行也会有不同的表现,例如当查询结果已经缓存在查询缓存中,或者服务器缓存中已经包含了所需要的数据时
5.哈希
负载均衡器通过连接的源IP地址进行哈希,将其映射到池中的同一个服务器上。每次从同一个IP地址发起请求,负载均衡器都会将请求发送给同样的服务器。只有当池中服务器数目改变时这种绑定才会发生变化
6.权重
负载均衡器能够结合适用上述几种算法。例如,你可能拥有单CPU核双CPU的机器。双CPU机器有接近两倍的性能,所以可以让负载均衡器分派两倍的请求给双CPU机器
哪种算法最优取决于具体的工作负载。例如最少连接算法,如果有新机器加入,可能会有大量连接涌入该服务器,而这时候它的缓存还没有包含热数据。你需要通过测试来为你的工作负载找到最好的性能。除了正常的日常运转,还需要考虑极端情况。在比较极端的情况下——例如负载升高,修改模式,或者多台服务器下线——至少要避免系统出现重大错误。我们这里只描述了即时处理请求的算法,无须对连接请求排队。但有时候适用排队算法可能更有效。例如,一个算法可能只维护给定的数据库服务器并发数目,同一时刻只允许不超过N个活跃事务。如果有太多的活跃事务,就将新的请求放到一个队列里,然后让可用服务器列表的第一个来处理它。有些连接池也支持队列算法
3.在服务器池中增加/移除服务器。
增加一个服务器到池中并不是简单地插入禁区,然后通知负载均衡器就可以了,你可能以为只要不是一下子涌进大量连接请求就可以了,但并不一定如此。有时候你会缓慢增加一台服务器的负载,但一些缓存还是"冷"的服务器可能会慢到一段时间内都无法处理任何的用户请求。如果用户浏览一个页面需要30秒才能返回数据,即使流量很小,这个服务器也是不可用的。有一个方法可以避免这个问题,在通知负载均衡器有新服务器加入前,可以暂时把SELECT查询映射到一台活跃服务器上,然后再新开启的服务器上读取和重放活跃服务器上的日志文件,或者捕捉生产服务器上的网络通信,并重放它的一部分查询。Percona Toolkit中的pt-query-digest工具能够有所帮助。另一个有效的办法是适用Percona Server或MySQL5.6的快速预热特性。
在配置连接池中的服务器时,要保证有足够多未使用的容量,以备在撤下服务器做维护时适用,或者当服务求失效时可以派上用场。每台服务器上都应该保留高于"足够'的容量。
要确保配置的限制值足够高,即使从池中撤出一些服务器也能够工作。举个例子,如果你发现每个MySQL服务器一般有100个连接,应该设置池中每个服务器的max_connections值为200.这样就算一半的服务器失效,服务器池也能处理同样数量的请求
增加一个服务器到池中并不是简单地插入禁区,然后通知负载均衡器就可以了,你可能以为只要不是一下子涌进大量连接请求就可以了,但并不一定如此。有时候你会缓慢增加一台服务器的负载,但一些缓存还是"冷"的服务器可能会慢到一段时间内都无法处理任何的用户请求。如果用户浏览一个页面需要30秒才能返回数据,即使流量很小,这个服务器也是不可用的。有一个方法可以避免这个问题,在通知负载均衡器有新服务器加入前,可以暂时把SELECT查询映射到一台活跃服务器上,然后再新开启的服务器上读取和重放活跃服务器上的日志文件,或者捕捉生产服务器上的网络通信,并重放它的一部分查询。Percona Toolkit中的pt-query-digest工具能够有所帮助。另一个有效的办法是适用Percona Server或MySQL5.6的快速预热特性。
在配置连接池中的服务器时,要保证有足够多未使用的容量,以备在撤下服务器做维护时适用,或者当服务求失效时可以派上用场。每台服务器上都应该保留高于"足够'的容量。
要确保配置的限制值足够高,即使从池中撤出一些服务器也能够工作。举个例子,如果你发现每个MySQL服务器一般有100个连接,应该设置池中每个服务器的max_connections值为200.这样就算一半的服务器失效,服务器池也能处理同样数量的请求
一主多备间的负载均衡。
最常见的复制拓扑结构就是一个主库加多个备库。我们很难绕开这个架构,许多应用都假设只有一个目标机器用于所有的写操作,或者所有的数据都可以从单个服务器上获得。尽管这个架构不太具有很好的可扩展性,但可以通过一些办法结合负载均衡来获得很好的效果。
1.功能分区
对于特定的目的可以通过配置备库或一组备库来极大地阔扎脑容量。一些比较常见的功能包括报表、分析、数据仓库,以及全文检索
2.过滤和数据分区
可以适用适用复制过滤技术在相似的备库上对数据进行分区。只要数据在主库上已经被隔离到不同的数据库或表中,这种方法就可以奏效。不幸的是,没有内建的八幡在行级别上进行复制过滤。你需要适用一些独创性的技术来实现这一点,例如适用触发器和一组不同的表。即使不把数据分区到各个备库上,也可以通过对读进行分区而不是随机分配来提高缓存效率。例如,可以把对以字母A-M开头的用户的读操作分配给一个给定的备库,把N-Z开头的分配给另外一个。这能够更好地利用每台机器的缓存,因为分离读更可能在缓存中找到相关的数据。最好的情况下,当没有写操作时,这样适用的缓存相当于两台服务器缓存的综合。相比之下,如果随机地在备库上分配读操作,每个机器的缓存本质上还是重复的数据,而总的有效缓存效率和一个备库缓存一样,不管你有多少台备库。
3.将部分写操作转移到备库
主库并不总是需要处理写操作中的所有工作。你可以分解写擦汗寻,并在备库上执行其中的一部分,从而显著减少主库的工作量
4.保证备库跟上主库
如果要在备库执行某种操作,他需要即使知道数据处于哪个时间点——哪怕需要等待一会儿才能到达这个点——可以适用函数MASTER_POS_WAIT()阻塞直到备库赶上了设置的主库同步点。另一种替代方案是复制复制心跳来检查延迟情况
5.同步写操作
也可以使用MASTER_POS_WAIT()函数来确保写操作已经被同步到一个或多个备库上。如果应用需要模拟同步复制来确保数据安全性,就可以在多个备库上轮流执行MASTER_POS_WAIT()函数。这就类似创建了一个"同步屏障",但任意一个备库出现复制延迟时,都可能花费很长时间完成,所以最好在确实需要的时候才适用这种方法(如果你的目的只是确保某些备库拥有时间,可以只等待一台备库接收到时间,MySQL5.5增加了半同步复制,能够支持这项技术)
最常见的复制拓扑结构就是一个主库加多个备库。我们很难绕开这个架构,许多应用都假设只有一个目标机器用于所有的写操作,或者所有的数据都可以从单个服务器上获得。尽管这个架构不太具有很好的可扩展性,但可以通过一些办法结合负载均衡来获得很好的效果。
1.功能分区
对于特定的目的可以通过配置备库或一组备库来极大地阔扎脑容量。一些比较常见的功能包括报表、分析、数据仓库,以及全文检索
2.过滤和数据分区
可以适用适用复制过滤技术在相似的备库上对数据进行分区。只要数据在主库上已经被隔离到不同的数据库或表中,这种方法就可以奏效。不幸的是,没有内建的八幡在行级别上进行复制过滤。你需要适用一些独创性的技术来实现这一点,例如适用触发器和一组不同的表。即使不把数据分区到各个备库上,也可以通过对读进行分区而不是随机分配来提高缓存效率。例如,可以把对以字母A-M开头的用户的读操作分配给一个给定的备库,把N-Z开头的分配给另外一个。这能够更好地利用每台机器的缓存,因为分离读更可能在缓存中找到相关的数据。最好的情况下,当没有写操作时,这样适用的缓存相当于两台服务器缓存的综合。相比之下,如果随机地在备库上分配读操作,每个机器的缓存本质上还是重复的数据,而总的有效缓存效率和一个备库缓存一样,不管你有多少台备库。
3.将部分写操作转移到备库
主库并不总是需要处理写操作中的所有工作。你可以分解写擦汗寻,并在备库上执行其中的一部分,从而显著减少主库的工作量
4.保证备库跟上主库
如果要在备库执行某种操作,他需要即使知道数据处于哪个时间点——哪怕需要等待一会儿才能到达这个点——可以适用函数MASTER_POS_WAIT()阻塞直到备库赶上了设置的主库同步点。另一种替代方案是复制复制心跳来检查延迟情况
5.同步写操作
也可以使用MASTER_POS_WAIT()函数来确保写操作已经被同步到一个或多个备库上。如果应用需要模拟同步复制来确保数据安全性,就可以在多个备库上轮流执行MASTER_POS_WAIT()函数。这就类似创建了一个"同步屏障",但任意一个备库出现复制延迟时,都可能花费很长时间完成,所以最好在确实需要的时候才适用这种方法(如果你的目的只是确保某些备库拥有时间,可以只等待一台备库接收到时间,MySQL5.5增加了半同步复制,能够支持这项技术)
高可用性
概述。
接下来分析提到的复制、可扩展性以及高可用性三个主题中的第三个。归根结底,高可用性实际上意味着"更少的宕机时间"。然而糟糕的是,高可用性经常和其他的概念混淆,例如冗余、保障数据不丢失,以及负载均衡。
高可用性实际上优点像神秘的野兽。它通常以百分比表示,这本身也是一种按时:高可用性不是绝对的,只有相对更高的可用性。100%的可用性是不可能达到的。可用性的"9"规则是表示可用性目标最普遍的方法,你可能也知道"5个9"表示99.999%的正常可用时间。换句话说,每年只允许5分钟的宕机时间。对于大多数应用这已经是令人惊叹的数字,尽管还有一些人视图获得更多的"9".
每个应用对可用性的需求各不相同。在设定一个可用时间的目标之前,先问问自己,是不是确实需要达到这个目标。可用性每提高一点,所花费的成本都会远超之前;可用性的效果和开销的比例并不是线性的。需要保证多少可用时间,取决于能够承担多少成本。高可用性实际上是在宕机造成的损失与降低宕机时间所花费的成本之间取一个平衡。换句话说,如果需要花大量金钱去获得更好的可用时间,但所带来的收益却很低,可能就不值得做。总地来说,应用在超过一定的点以后追求更高的可用性是非常困难的。成本也会很高,因此我们建议设定一个更现实的目标并且避免过度设计。幸运的是,建立2个9或3个9的可用时间的目标可能并不困难,具体情况取决于应用。
有时候人们将可用性定义成服务正在运行的时间段。我们认为可用性的定义还应该包括应用是否能以足够好的性能处理请求。有许多方法可以让一个服务器保持运行,但服务并不是真正可用。对一个很大的服务器而言,重启MySQL之后,可能需要几个消失才能充分预热以保证查询请求的响应时间是可以接受的,即使服务器只接收了正常流量的一小部分也是如此。
另一个需要考虑的问题是,即使应用并没有停止服务,但是否可能丢失了数据。如果服务器遭遇灾难性故障,可能多少都会丢失一些数据,例如最近已经写入(最新丢失的)二进制日志但尚未传递到备库的中继日志中的事务。你能够容忍吗?大多数应用能够容忍;因为替代方案大多非常昂贵且复杂,或者有一些性能开销。例如,可以使用同步复制,或是将二进制日志档到一个通过DRBD进行复制的设备上,这样就算服务器完全失效也不用担心丢失数据(但是整个数据中心也有可能会掉电)。
一个良好的应用架构通常可以降低可用性方面的需求,至少对部分系统而言是这样的,良好的架构也更容易做到高可用。将应用中重要和不重要的部分进行分离可以节约不少工作量和金钱,因为对于一个更小的系统改进可用性会更容易。可以通过计算"风险敞口(risk exposure)",将失效概率与失效代价相乘来确认高优先级的风险,画一个简单的风险计算表,以概率、代价和风险敞口作为列,这样很容易找到需要优先处理的享目。
在前面讨论如何避免导致糟糕的可扩展性的原因,来退出如何获得更好的可扩展性。这里也会使用相似的方法来讨论可用性,因为我们相信,理解可用性最好的方法就是研究它的反面——宕机时间。
接下来分析提到的复制、可扩展性以及高可用性三个主题中的第三个。归根结底,高可用性实际上意味着"更少的宕机时间"。然而糟糕的是,高可用性经常和其他的概念混淆,例如冗余、保障数据不丢失,以及负载均衡。
高可用性实际上优点像神秘的野兽。它通常以百分比表示,这本身也是一种按时:高可用性不是绝对的,只有相对更高的可用性。100%的可用性是不可能达到的。可用性的"9"规则是表示可用性目标最普遍的方法,你可能也知道"5个9"表示99.999%的正常可用时间。换句话说,每年只允许5分钟的宕机时间。对于大多数应用这已经是令人惊叹的数字,尽管还有一些人视图获得更多的"9".
每个应用对可用性的需求各不相同。在设定一个可用时间的目标之前,先问问自己,是不是确实需要达到这个目标。可用性每提高一点,所花费的成本都会远超之前;可用性的效果和开销的比例并不是线性的。需要保证多少可用时间,取决于能够承担多少成本。高可用性实际上是在宕机造成的损失与降低宕机时间所花费的成本之间取一个平衡。换句话说,如果需要花大量金钱去获得更好的可用时间,但所带来的收益却很低,可能就不值得做。总地来说,应用在超过一定的点以后追求更高的可用性是非常困难的。成本也会很高,因此我们建议设定一个更现实的目标并且避免过度设计。幸运的是,建立2个9或3个9的可用时间的目标可能并不困难,具体情况取决于应用。
有时候人们将可用性定义成服务正在运行的时间段。我们认为可用性的定义还应该包括应用是否能以足够好的性能处理请求。有许多方法可以让一个服务器保持运行,但服务并不是真正可用。对一个很大的服务器而言,重启MySQL之后,可能需要几个消失才能充分预热以保证查询请求的响应时间是可以接受的,即使服务器只接收了正常流量的一小部分也是如此。
另一个需要考虑的问题是,即使应用并没有停止服务,但是否可能丢失了数据。如果服务器遭遇灾难性故障,可能多少都会丢失一些数据,例如最近已经写入(最新丢失的)二进制日志但尚未传递到备库的中继日志中的事务。你能够容忍吗?大多数应用能够容忍;因为替代方案大多非常昂贵且复杂,或者有一些性能开销。例如,可以使用同步复制,或是将二进制日志档到一个通过DRBD进行复制的设备上,这样就算服务器完全失效也不用担心丢失数据(但是整个数据中心也有可能会掉电)。
一个良好的应用架构通常可以降低可用性方面的需求,至少对部分系统而言是这样的,良好的架构也更容易做到高可用。将应用中重要和不重要的部分进行分离可以节约不少工作量和金钱,因为对于一个更小的系统改进可用性会更容易。可以通过计算"风险敞口(risk exposure)",将失效概率与失效代价相乘来确认高优先级的风险,画一个简单的风险计算表,以概率、代价和风险敞口作为列,这样很容易找到需要优先处理的享目。
在前面讨论如何避免导致糟糕的可扩展性的原因,来退出如何获得更好的可扩展性。这里也会使用相似的方法来讨论可用性,因为我们相信,理解可用性最好的方法就是研究它的反面——宕机时间。
导致宕机的原因。
我们经常听到导致数据库宕机最主要的原因是编写的SQL查询性能很差,真的是这样吗?2009年我们决定分析我们客户的数据库所遇到的问题,以找出那些真正引起宕机的问题,以及如何避免这些问题。结果正是了一些已有的猜想,但也否定了一些(错误的)认识,并从中学到了很多。
我们首先对宕机时间按表现方式而非导致的原因进行分类。一般来说,"运行环境"是排名第一的宕机类别,大于35%的时间属于这一类。运行环境可以看作是支持数据库服务器运行的系统和资源集合,包括操作系统、硬盘以及网络等。性能问题紧随其后,也是约占35%;然后是复制,占20%,最后剩下的10%包含各种类型的数据丢失或损坏以及其他问题。我们对时间按类型进行分类后,确定了导致这些时间的原因。以下是一些需要足以的地方:
1.在运行环境的问题中,最普遍的是磁盘空间耗尽
2.在性能问题中,最普遍的宕机原因确实是运行很糟糕的SQL,但也不一定都是这个原因,比如也有很多问题是由于服务器Bug或错误的行为导致的
3.糟糕的Schema和索引设计是第二大影响性能的问题
4.复制问题通常由于主备数据不一致导致
5.数据丢失问题通常由于DROP TABLE的误操作导致,并总是伴随着缺少可用备份的问题。
复制虽然常被人们用来改善可用时间,但却也可能导致宕机。这主要是由于不正确的使用导致的,即便如此,它也阐明了一个普遍的情况:许多高可用性策略可能会产生反作用
我们经常听到导致数据库宕机最主要的原因是编写的SQL查询性能很差,真的是这样吗?2009年我们决定分析我们客户的数据库所遇到的问题,以找出那些真正引起宕机的问题,以及如何避免这些问题。结果正是了一些已有的猜想,但也否定了一些(错误的)认识,并从中学到了很多。
我们首先对宕机时间按表现方式而非导致的原因进行分类。一般来说,"运行环境"是排名第一的宕机类别,大于35%的时间属于这一类。运行环境可以看作是支持数据库服务器运行的系统和资源集合,包括操作系统、硬盘以及网络等。性能问题紧随其后,也是约占35%;然后是复制,占20%,最后剩下的10%包含各种类型的数据丢失或损坏以及其他问题。我们对时间按类型进行分类后,确定了导致这些时间的原因。以下是一些需要足以的地方:
1.在运行环境的问题中,最普遍的是磁盘空间耗尽
2.在性能问题中,最普遍的宕机原因确实是运行很糟糕的SQL,但也不一定都是这个原因,比如也有很多问题是由于服务器Bug或错误的行为导致的
3.糟糕的Schema和索引设计是第二大影响性能的问题
4.复制问题通常由于主备数据不一致导致
5.数据丢失问题通常由于DROP TABLE的误操作导致,并总是伴随着缺少可用备份的问题。
复制虽然常被人们用来改善可用时间,但却也可能导致宕机。这主要是由于不正确的使用导致的,即便如此,它也阐明了一个普遍的情况:许多高可用性策略可能会产生反作用
如何实现高可用性。
可以通过同时进行以下两步来获得高可用性。首先,可以尝试避免导致宕机的原因来减少宕机时间。许多问题其实很容易避免,例如通过适当的配置、监控,以及规范或安全保障措施来避免认为错误。第二,金狼保证在发生宕机时能够快速恢复。最常见的策略时在系统中制造冗余,并且具备故障转移的能力。这两个维度的高可用性可以通过两个相关的度量来确定:平均失效时间(MTBF)和平均恢复时间(MTTR)。一些阻止会非常仔细地追踪这些度量值.
第二步——通过冗余快速恢复——很不幸,这里时应该最注意的地方,但预防措施的投资回报率会很高。
可以通过同时进行以下两步来获得高可用性。首先,可以尝试避免导致宕机的原因来减少宕机时间。许多问题其实很容易避免,例如通过适当的配置、监控,以及规范或安全保障措施来避免认为错误。第二,金狼保证在发生宕机时能够快速恢复。最常见的策略时在系统中制造冗余,并且具备故障转移的能力。这两个维度的高可用性可以通过两个相关的度量来确定:平均失效时间(MTBF)和平均恢复时间(MTTR)。一些阻止会非常仔细地追踪这些度量值.
第二步——通过冗余快速恢复——很不幸,这里时应该最注意的地方,但预防措施的投资回报率会很高。
提升平均失效时间(MTBF).
其实只要尽职尽责地做好一些应做的事情,就可以避免很多宕机。在分类整理宕机事件并追查导致宕机的根源时,还发现,很多宕机本来是有一些方法可以避免的。我们发现大部分宕机事件都可以通过全面的常识性系统管理办法来避免。以下是做一些指导性的建议:
1.测试恢复工具和流程,包括从备份中恢复数据
2.遵从最小权限原则
3.保持系统干净、整洁
4.使用好的命名和组织约定来避免产生混乱,例如服务器是用于开发还是用于生产环境
5.谨慎安排升级数据库服务器
6.在升级前,使用诸如Percona Toolkit中的pt-upgrade之类的工具仔细检查系统
7.使用InnoDB并进行适当的配置,确保InnoDB是默认存储引擎。如果存储引擎被禁止,服务器就无法启动
8.确认基本的服务器配置是正确的
9.通过skip_name_resolve禁止DNS
10.除非能证明有效,否则禁用查询缓存
11.避免使用复杂的特性,例如复制过滤和触发器,除非确实需要
12.监控重要的组建和功能,特别是像磁盘空间和RAID卷状态这样的关键享目,但也要避免误报,只有当确实发生问题时才发送告警
13.定期检查复制完整性
14.讲备库设置为只读,不要让复制自动启动
15.定期进行查询语句审查
16.归档并清理不需要的数据
17.为文件系统保留一些空间。在GNU/Linux中,可以使用-m选项来为文件系统本身保留空间。还可以在LVM卷组中留下一些空闲空间。或者,更简单的方法,仅仅创建一个巨大的空文件,在文件系统快满时,直接将其删除(这是100%跨平台兼容的)
18.养成习惯,评估和管理系统的改变、状态以及性能信息
我们发现对系统变更管理的缺失时所有导致宕机的实践中最扑鼻那的原因。典型的错误包括粗心的升级导致升级失败并遭遇一些Bug,或是尚未测试就将Schema或查询语句的更改直接运行到线上,或者没有为一些失败的情况制定计划,例如达到了磁盘容量限制。另外一个导致问题的主要原因是缺少严格的评估,例如因为疏忽没有确认备份是否是可以恢复的。最后,可能没有正确地监控MySQL的相关信息。例如缓存命中率报警并不能说明问题,并且可能产生大量的误报,这会使监控系统被认为不太有用,于是一些人就会忽略报警。有时候监控系统失效了,甚至没人会注意到,直至你的老板质问你,"为什么Nagios没有告诉我们磁盘已经满了?"
其实只要尽职尽责地做好一些应做的事情,就可以避免很多宕机。在分类整理宕机事件并追查导致宕机的根源时,还发现,很多宕机本来是有一些方法可以避免的。我们发现大部分宕机事件都可以通过全面的常识性系统管理办法来避免。以下是做一些指导性的建议:
1.测试恢复工具和流程,包括从备份中恢复数据
2.遵从最小权限原则
3.保持系统干净、整洁
4.使用好的命名和组织约定来避免产生混乱,例如服务器是用于开发还是用于生产环境
5.谨慎安排升级数据库服务器
6.在升级前,使用诸如Percona Toolkit中的pt-upgrade之类的工具仔细检查系统
7.使用InnoDB并进行适当的配置,确保InnoDB是默认存储引擎。如果存储引擎被禁止,服务器就无法启动
8.确认基本的服务器配置是正确的
9.通过skip_name_resolve禁止DNS
10.除非能证明有效,否则禁用查询缓存
11.避免使用复杂的特性,例如复制过滤和触发器,除非确实需要
12.监控重要的组建和功能,特别是像磁盘空间和RAID卷状态这样的关键享目,但也要避免误报,只有当确实发生问题时才发送告警
13.定期检查复制完整性
14.讲备库设置为只读,不要让复制自动启动
15.定期进行查询语句审查
16.归档并清理不需要的数据
17.为文件系统保留一些空间。在GNU/Linux中,可以使用-m选项来为文件系统本身保留空间。还可以在LVM卷组中留下一些空闲空间。或者,更简单的方法,仅仅创建一个巨大的空文件,在文件系统快满时,直接将其删除(这是100%跨平台兼容的)
18.养成习惯,评估和管理系统的改变、状态以及性能信息
我们发现对系统变更管理的缺失时所有导致宕机的实践中最扑鼻那的原因。典型的错误包括粗心的升级导致升级失败并遭遇一些Bug,或是尚未测试就将Schema或查询语句的更改直接运行到线上,或者没有为一些失败的情况制定计划,例如达到了磁盘容量限制。另外一个导致问题的主要原因是缺少严格的评估,例如因为疏忽没有确认备份是否是可以恢复的。最后,可能没有正确地监控MySQL的相关信息。例如缓存命中率报警并不能说明问题,并且可能产生大量的误报,这会使监控系统被认为不太有用,于是一些人就会忽略报警。有时候监控系统失效了,甚至没人会注意到,直至你的老板质问你,"为什么Nagios没有告诉我们磁盘已经满了?"
降低平均恢复时间(MTTR).
之前提到,可以通过减少恢复时间来获得高可用性。事实上,一些人走的更远,只专注于减少恢复时间的某个方面:通过在系统中建立冗余来避免系统完全失效,并避免单点失效问题。在降低恢复时间上进行投资非常虫咬,一个能够提供冗余和故障转移能力的系统架构则是降低恢复时间的关键环节。但实现高可用性不单单是技术问题,还有许多个人和组织的因素。组织和个人在避免宕机和从宕机事件中恢复的成熟度和能力层次各不相同。
团队成员是最重要的高可用性资产,所以为恢复制定一个好的流程非常重要。拥有熟练技能、应变能力、训练有素的雇员,以及处理紧急事件的详细文档和经过仔细测试的流程,对从宕机中恢复有巨大的作用。但也不能完全依赖工具和系统,因为它们并不能理解实际情况的细微差别,有时候它们的行为在一般情况下是正确的,但在某些场景下却会是个灾难
对宕机事件进行评估有助于提升组织学习能力,可以帮助避免未来发生相似的错误,但是不要对"事后反思"或"事后的调查分析"期待太高。后见之明被严重曲解,并且一味想找到导致问题的唯一根源,这可能会影响你的判断力。许多流行的方法,例如"五个为什么",可能会被过度使用,导致一些人将他们的经历集中在找到唯一的替罪羊。很难去回顾我们解决的问题当时所处的状况,也很难理解真正的原因,因为原因通常是多方面的。因此,尽管事后反思可能是有用的,但也应该对结论有所保留。即使是这里给出的建议,也是基于长期研究导致宕机事件的原因以及如何预防它们所得,并且只是这里的观点而已。
这里要反复提醒:所有的宕机事件都是由多方面的失效联合在一起导致的。因此可以通过利用合适的方法确保单点的安全来避免。整个链条必须要打断,而不仅仅是单个环节。例如,那些问我们求助恢复数据的人不仅遭受数据丢失(存储失效,DBA误操作等)。同时还缺少一个可用的备份。
这样说来,当开始调查并尝试阻止失效或加速恢复时,大多数人和组织不应太过于内疚,而是要专注于技术上的一些措施——特别是那些很酷的方法,例如集群系统和冗余架构。这些是有用的,但要记住这些系统依然会失效。事实上,前面提到的MMM复制管理,已经对它失去了型取,因为它被证明可能导致更多的宕机事件。你应该不会奇怪一组Perl脚本会陷入混乱,但即使是特别昂贵并精密设计的系统也会出现灾难性的失效——是的,即使是花费了大量金钱的SAN也是如此。已经见过太多的SAN失效
之前提到,可以通过减少恢复时间来获得高可用性。事实上,一些人走的更远,只专注于减少恢复时间的某个方面:通过在系统中建立冗余来避免系统完全失效,并避免单点失效问题。在降低恢复时间上进行投资非常虫咬,一个能够提供冗余和故障转移能力的系统架构则是降低恢复时间的关键环节。但实现高可用性不单单是技术问题,还有许多个人和组织的因素。组织和个人在避免宕机和从宕机事件中恢复的成熟度和能力层次各不相同。
团队成员是最重要的高可用性资产,所以为恢复制定一个好的流程非常重要。拥有熟练技能、应变能力、训练有素的雇员,以及处理紧急事件的详细文档和经过仔细测试的流程,对从宕机中恢复有巨大的作用。但也不能完全依赖工具和系统,因为它们并不能理解实际情况的细微差别,有时候它们的行为在一般情况下是正确的,但在某些场景下却会是个灾难
对宕机事件进行评估有助于提升组织学习能力,可以帮助避免未来发生相似的错误,但是不要对"事后反思"或"事后的调查分析"期待太高。后见之明被严重曲解,并且一味想找到导致问题的唯一根源,这可能会影响你的判断力。许多流行的方法,例如"五个为什么",可能会被过度使用,导致一些人将他们的经历集中在找到唯一的替罪羊。很难去回顾我们解决的问题当时所处的状况,也很难理解真正的原因,因为原因通常是多方面的。因此,尽管事后反思可能是有用的,但也应该对结论有所保留。即使是这里给出的建议,也是基于长期研究导致宕机事件的原因以及如何预防它们所得,并且只是这里的观点而已。
这里要反复提醒:所有的宕机事件都是由多方面的失效联合在一起导致的。因此可以通过利用合适的方法确保单点的安全来避免。整个链条必须要打断,而不仅仅是单个环节。例如,那些问我们求助恢复数据的人不仅遭受数据丢失(存储失效,DBA误操作等)。同时还缺少一个可用的备份。
这样说来,当开始调查并尝试阻止失效或加速恢复时,大多数人和组织不应太过于内疚,而是要专注于技术上的一些措施——特别是那些很酷的方法,例如集群系统和冗余架构。这些是有用的,但要记住这些系统依然会失效。事实上,前面提到的MMM复制管理,已经对它失去了型取,因为它被证明可能导致更多的宕机事件。你应该不会奇怪一组Perl脚本会陷入混乱,但即使是特别昂贵并精密设计的系统也会出现灾难性的失效——是的,即使是花费了大量金钱的SAN也是如此。已经见过太多的SAN失效
避免单点失效。
找到并消除系统中的可能失效的单点,并结合切换到备用组件的机制,这是一种通过减少恢复时间(MTTR)来改善可用性的方法。如果你够聪明,有时候甚至能将实际的恢复时间降低至0,但总的来说这很困难。(即使一些非常引人注目的技术,例如昂贵的负载均衡器,在发现问题并进行反馈时也会导致一定的延迟)。思考并梳理整个应用,尝试去定位任何可能失效的单点。是一个硬盘驱动器,一台服务器,一台交换或路由器,还是某个机架的电源?所有数据都在一个数据中心,或者冗余数据中心是由同一个公司提供的吗?系统中任何不冗余的部分都是一个可能失效的单点。其他比较普遍的单点失效依赖于一些服务,例如DNS,单一网络提供商(感觉太偏执了?检查你的冗余网络连接是不是真的连接到不同的互联网主干,确保它们的物理位置不在同一条街道或者同一个电线杆上,这样它们才不会被同一个挖土机或者汽车破坏掉)、单个云"可用区域",以及单个电力输送网,具体有哪些取决于你的关注点。
单点失效并不总是能够消除。增加冗余或许也无法做到,因为有些限制无法避免,例如地理位置,预算,或者时间限制等。试着去理解每一个影响可用性的部分,采取一种平衡的观点来看待风险,并首先解决其中影响最大的那个。一些人试图编写一个软件来处理所有的硬件失效,但软件本身导致的宕机时间可能比它节约的还要多。也有人想建立一种"永不沉没"的系统,包括各种冗余,但它们忘记了数据中心可能掉电或失去连接。或许它们彻底忘记了恶意攻击者和程序错误的可能性,这些情况可能会删除或损坏数据——一个不小心执行的DROP TABLE也会产生宕机时间.
可以采用两种方法来为系统增加冗余:增加空余容量和重复组件。增加容量余量通常很简单——可以使用前面讨论的任何技术。一个提升可用性的方法是创建一个集群或服务器池,并使用负载均衡解决方案。如果一台服务器失效,其他服务器可以接管它的负载。有些人有意识地不使用组建地全部能力,这样可以保留一些"动态余量"来处理因为负载增加或组件失效导致的性能问题。
处于很多方面的考虑会需要冗余组件,并在主要组件失效时能有一个备件来随时替换。冗余组件可以是空闲的网卡、路由器或者硬盘驱动器——任何能想到鞥多可能失效的东西。完全冗余MySQL服务器可能优点困难,因为一个服务器在没有数据时毫无用处。这意味着你必须确保备用服务器能够获得主服务器上的数据。共享或复制存储是一个比较流行的办法。但这真的是一个靠可用架构吗?让我们深入其中看看
找到并消除系统中的可能失效的单点,并结合切换到备用组件的机制,这是一种通过减少恢复时间(MTTR)来改善可用性的方法。如果你够聪明,有时候甚至能将实际的恢复时间降低至0,但总的来说这很困难。(即使一些非常引人注目的技术,例如昂贵的负载均衡器,在发现问题并进行反馈时也会导致一定的延迟)。思考并梳理整个应用,尝试去定位任何可能失效的单点。是一个硬盘驱动器,一台服务器,一台交换或路由器,还是某个机架的电源?所有数据都在一个数据中心,或者冗余数据中心是由同一个公司提供的吗?系统中任何不冗余的部分都是一个可能失效的单点。其他比较普遍的单点失效依赖于一些服务,例如DNS,单一网络提供商(感觉太偏执了?检查你的冗余网络连接是不是真的连接到不同的互联网主干,确保它们的物理位置不在同一条街道或者同一个电线杆上,这样它们才不会被同一个挖土机或者汽车破坏掉)、单个云"可用区域",以及单个电力输送网,具体有哪些取决于你的关注点。
单点失效并不总是能够消除。增加冗余或许也无法做到,因为有些限制无法避免,例如地理位置,预算,或者时间限制等。试着去理解每一个影响可用性的部分,采取一种平衡的观点来看待风险,并首先解决其中影响最大的那个。一些人试图编写一个软件来处理所有的硬件失效,但软件本身导致的宕机时间可能比它节约的还要多。也有人想建立一种"永不沉没"的系统,包括各种冗余,但它们忘记了数据中心可能掉电或失去连接。或许它们彻底忘记了恶意攻击者和程序错误的可能性,这些情况可能会删除或损坏数据——一个不小心执行的DROP TABLE也会产生宕机时间.
可以采用两种方法来为系统增加冗余:增加空余容量和重复组件。增加容量余量通常很简单——可以使用前面讨论的任何技术。一个提升可用性的方法是创建一个集群或服务器池,并使用负载均衡解决方案。如果一台服务器失效,其他服务器可以接管它的负载。有些人有意识地不使用组建地全部能力,这样可以保留一些"动态余量"来处理因为负载增加或组件失效导致的性能问题。
处于很多方面的考虑会需要冗余组件,并在主要组件失效时能有一个备件来随时替换。冗余组件可以是空闲的网卡、路由器或者硬盘驱动器——任何能想到鞥多可能失效的东西。完全冗余MySQL服务器可能优点困难,因为一个服务器在没有数据时毫无用处。这意味着你必须确保备用服务器能够获得主服务器上的数据。共享或复制存储是一个比较流行的办法。但这真的是一个靠可用架构吗?让我们深入其中看看
共享存储或磁盘复制。
共享存储能够为数据库服务器和存储解耦合,通常使用的是SAN.使用共享存储时,服务器能够正常挂载文件系统并进行操作。如果服务器挂了,备用服务器可以挂载相同的文件系统,执行需要的恢复操作,并在失效服务器的数据上启动MySQL.这个过程在逻辑上跟修复那台故障的服务器没什么两样,不过更快速,因为备用服务器已经启动,随时可以运行。当开始故障转移时,检查文件系统、恢复InnoDB以及预热(Percona Server提供了一个新特性,能够把buffer pool保存下来并在重启后还原,在使用共享存储时能够很好地工作。这可以减少几个小时甚至好几天的预热时间。MySQL5.6也有相似的特性。)是最有可能遇到延迟的地方,但检测失效本身在许多设置中也会花费很长时间。
共享存储有两个优点:可以避免除存储外的其他任何组件失效所引起的数据丢失,并为非存储组件建立冗余提供可能。因此它有助于减少系统一些部分的可用性需求,这样就可以集中精力关注一小部分组件来获得高可用性。不过,共享存储本身仍然是可能失效的单点。如果共享存储失效了,那整个系统也失效了,尽管SAN通常设计良好,但也可能失效,有时候需要特别关注,就算SAN本身拥有冗余也会失效。
共享存储本身也有风险,如果MySQL崩溃等故障导致数据文件损坏,可能会导致备用服务器无法恢复。我们强烈建议在使用共享存储策略时选择InnoDB存储引擎或其他稳定的ACID存储引擎。一次崩溃几乎肯定会损坏MyISAM表,需要花费很长时间来修复,并且会丢失数据。我们也强烈建议使用日志型文件系统。我们见过比较严重的情况是,使用非日志型文件系统和SAN(这是文件系统的问题,跟SAN无关)导致数据损坏无法恢复。
磁盘复制技术是另外一个获得跟SAN类似效果的方法。MySQL中最普遍使用的磁盘复制技术是DRBP,并结合Linux-HA享目中的工具使用。DRBD是一个以Linux内核模块方式实现的块级别同步复制技术。它通过网卡将主服务器的每个块复制到另外一个服务器上的块设备上(备用设备),并在主设备提交块之前记录下来(事实上可以调整DRBD的同步级别,将其设置成异步等待远程设备接收数据,或者在远程设备将数据写入磁盘前一直阻塞住。同样,强烈建议为DRBD专门使用一块网卡)。由于在备用DRBD设备上的写入必须要在主设备上的写入完成之前,因此备用设备的性能至少要和主设备一样,否则就会限制主设备的写入性能。同样,如果正在使用DRBD磁盘复制技术以保证在主设备失效时有一个可随时替换的备用设备,备用服务器的硬件应该跟主服务器的相匹配。带电池写缓存的RAID控制器对DRBD而言几乎是必需的,因为在没有这样的控制器时性能可能会很差。
如果主服务器失效,可以备用设备提升为主设备。因为DRBD是在磁盘块层进行复制,而文件系统也可能会不一致。这意味着最好是使用日志型文件系统来做快速恢复。一旦设备恢复完成,MySQL还需要运行自身的恢复。原故障服务器恢复后,会与新的主设备进行同步,并假定自身角色为备用设备。
从如何实际地实现故障转移的角度来看,DRBD和SAN很相似:有一个热备机器,开始提供服务时会使用和故障机器相同的数据,最大的不同时,DRBD是复制存储——不是共享存储——所以当使用DRBD时,获得的是一份复制的数据,而SAN则是使用与故障机器同一物理设备上的相同数副本。换句话说,磁盘复制技术的数据是冗余的,所以存储的数据本身都不会存在单点失效问题。这两种情况下,当启动备用机器时,MySQL服务器的缓存都是空的。相比之下,备库的缓存至少是部分预热的。
DRBD有一些很好的特性和功能,可以防止集群软件普遍会遇到的一些问题。一个典型的例子是"脑裂综合征",在两个节点同时提升自己为主服务器时会发生这种问题。可以通过配置DRBD来防止这种事情发生。但是DRBD也不是一个能满足所有需求的完美解决方案。我们来看看它有哪些缺点:
1.DRBD的故障转移无法做到秒级别以内。它通常需要至少几秒钟时间来将设备提升成主设备,这还不包括任何必要的文件系统恢复和MySQL恢复
2.它很昂贵,因为必需在主动——被动模式下运行。热备服务器的复制设备因为处于被动模式,无法用于其他任务。当然这不是缺点取决于看问题的角度。如果你希望获得真正的高可用性并且在发生故障时不能容忍服务降级,就不应该在一台机器上运行两台服务器的负载量,因为如果这么做了,当其中一台发生故障时,就无法处理这些负载了。可以用这些备用服务器做一些其他用途,例如用作备库,但还是会有一些资源浪费。
3.对于MyISAM表实际上用处不打,因为MyISAM表崩溃后需要花费很长时间来检查和修复。对任何期望获得高可用性的系统而言,MyISAM都不是一个好选择;请使用InnoDB或其他支持快速、安全恢复的存储引擎来代替MyISAM
4.DRBD无法代替备份。如果磁盘由于蓄意的破坏、误操作、Bug或者其他硬件故障导致数据损坏,DRBD将无济于事。此时复制的数据只是被损坏数据的完美副本。你需要使用备份(或MySQL延时复制)来避免这些问题。
5.对写操作而言增加了负担。具体会增加多少负担呢?通常可以使用百分比来表示,但这并不是一个好的度量方法。你需要理解写入时增加的延迟主要由网络往返开销和远程服务器存储导致,特别时对于小的写入而言延迟会更大。尽管增加的延迟可能也就0.3ms,这看起来比在本地磁盘上IO的4~10ms的延迟要小很多,但却是正常的带有写缓存的RAID控制器的延迟的3~4倍。使用DRBD导致服务器变慢最常见的原因时MySQL使用InnoDB并采取了完全持久化模式(这里的意思时Innodb_flush_log_at_trx_commit=1的情况)。这会导致许多小的写入和fsync()调用,通过DRBD同步时会非常慢
共享存储能够为数据库服务器和存储解耦合,通常使用的是SAN.使用共享存储时,服务器能够正常挂载文件系统并进行操作。如果服务器挂了,备用服务器可以挂载相同的文件系统,执行需要的恢复操作,并在失效服务器的数据上启动MySQL.这个过程在逻辑上跟修复那台故障的服务器没什么两样,不过更快速,因为备用服务器已经启动,随时可以运行。当开始故障转移时,检查文件系统、恢复InnoDB以及预热(Percona Server提供了一个新特性,能够把buffer pool保存下来并在重启后还原,在使用共享存储时能够很好地工作。这可以减少几个小时甚至好几天的预热时间。MySQL5.6也有相似的特性。)是最有可能遇到延迟的地方,但检测失效本身在许多设置中也会花费很长时间。
共享存储有两个优点:可以避免除存储外的其他任何组件失效所引起的数据丢失,并为非存储组件建立冗余提供可能。因此它有助于减少系统一些部分的可用性需求,这样就可以集中精力关注一小部分组件来获得高可用性。不过,共享存储本身仍然是可能失效的单点。如果共享存储失效了,那整个系统也失效了,尽管SAN通常设计良好,但也可能失效,有时候需要特别关注,就算SAN本身拥有冗余也会失效。
共享存储本身也有风险,如果MySQL崩溃等故障导致数据文件损坏,可能会导致备用服务器无法恢复。我们强烈建议在使用共享存储策略时选择InnoDB存储引擎或其他稳定的ACID存储引擎。一次崩溃几乎肯定会损坏MyISAM表,需要花费很长时间来修复,并且会丢失数据。我们也强烈建议使用日志型文件系统。我们见过比较严重的情况是,使用非日志型文件系统和SAN(这是文件系统的问题,跟SAN无关)导致数据损坏无法恢复。
磁盘复制技术是另外一个获得跟SAN类似效果的方法。MySQL中最普遍使用的磁盘复制技术是DRBP,并结合Linux-HA享目中的工具使用。DRBD是一个以Linux内核模块方式实现的块级别同步复制技术。它通过网卡将主服务器的每个块复制到另外一个服务器上的块设备上(备用设备),并在主设备提交块之前记录下来(事实上可以调整DRBD的同步级别,将其设置成异步等待远程设备接收数据,或者在远程设备将数据写入磁盘前一直阻塞住。同样,强烈建议为DRBD专门使用一块网卡)。由于在备用DRBD设备上的写入必须要在主设备上的写入完成之前,因此备用设备的性能至少要和主设备一样,否则就会限制主设备的写入性能。同样,如果正在使用DRBD磁盘复制技术以保证在主设备失效时有一个可随时替换的备用设备,备用服务器的硬件应该跟主服务器的相匹配。带电池写缓存的RAID控制器对DRBD而言几乎是必需的,因为在没有这样的控制器时性能可能会很差。
如果主服务器失效,可以备用设备提升为主设备。因为DRBD是在磁盘块层进行复制,而文件系统也可能会不一致。这意味着最好是使用日志型文件系统来做快速恢复。一旦设备恢复完成,MySQL还需要运行自身的恢复。原故障服务器恢复后,会与新的主设备进行同步,并假定自身角色为备用设备。
从如何实际地实现故障转移的角度来看,DRBD和SAN很相似:有一个热备机器,开始提供服务时会使用和故障机器相同的数据,最大的不同时,DRBD是复制存储——不是共享存储——所以当使用DRBD时,获得的是一份复制的数据,而SAN则是使用与故障机器同一物理设备上的相同数副本。换句话说,磁盘复制技术的数据是冗余的,所以存储的数据本身都不会存在单点失效问题。这两种情况下,当启动备用机器时,MySQL服务器的缓存都是空的。相比之下,备库的缓存至少是部分预热的。
DRBD有一些很好的特性和功能,可以防止集群软件普遍会遇到的一些问题。一个典型的例子是"脑裂综合征",在两个节点同时提升自己为主服务器时会发生这种问题。可以通过配置DRBD来防止这种事情发生。但是DRBD也不是一个能满足所有需求的完美解决方案。我们来看看它有哪些缺点:
1.DRBD的故障转移无法做到秒级别以内。它通常需要至少几秒钟时间来将设备提升成主设备,这还不包括任何必要的文件系统恢复和MySQL恢复
2.它很昂贵,因为必需在主动——被动模式下运行。热备服务器的复制设备因为处于被动模式,无法用于其他任务。当然这不是缺点取决于看问题的角度。如果你希望获得真正的高可用性并且在发生故障时不能容忍服务降级,就不应该在一台机器上运行两台服务器的负载量,因为如果这么做了,当其中一台发生故障时,就无法处理这些负载了。可以用这些备用服务器做一些其他用途,例如用作备库,但还是会有一些资源浪费。
3.对于MyISAM表实际上用处不打,因为MyISAM表崩溃后需要花费很长时间来检查和修复。对任何期望获得高可用性的系统而言,MyISAM都不是一个好选择;请使用InnoDB或其他支持快速、安全恢复的存储引擎来代替MyISAM
4.DRBD无法代替备份。如果磁盘由于蓄意的破坏、误操作、Bug或者其他硬件故障导致数据损坏,DRBD将无济于事。此时复制的数据只是被损坏数据的完美副本。你需要使用备份(或MySQL延时复制)来避免这些问题。
5.对写操作而言增加了负担。具体会增加多少负担呢?通常可以使用百分比来表示,但这并不是一个好的度量方法。你需要理解写入时增加的延迟主要由网络往返开销和远程服务器存储导致,特别时对于小的写入而言延迟会更大。尽管增加的延迟可能也就0.3ms,这看起来比在本地磁盘上IO的4~10ms的延迟要小很多,但却是正常的带有写缓存的RAID控制器的延迟的3~4倍。使用DRBD导致服务器变慢最常见的原因时MySQL使用InnoDB并采取了完全持久化模式(这里的意思时Innodb_flush_log_at_trx_commit=1的情况)。这会导致许多小的写入和fsync()调用,通过DRBD同步时会非常慢
主动——主动访问模式下的共享存储怎么样?
在一个SAN、NAS或者集群文件系统上以主动——主动模式运行多个实例怎么样?MySQL不能这么做。因为MySQL并没有被设计成和其他MySQL实例同步对数据的访问,所以无法在同一份数据上开启多个MySQL实例(如果在一份只读的静态数据上使用MyISAM,技术上是可行的,但还没有见过任何实际的应用)。MySQL 5.6.8之后,InnoDB也增加了一个只读模式,可以只读的方式用多个实例访问同一份只读数据文件。MySQL的一个名为ScaleDB的存储引擎在底层提供了操作共享存储的API,但还没有进行评估过,也没有见过忍耐和在生产环境使用。
在一个SAN、NAS或者集群文件系统上以主动——主动模式运行多个实例怎么样?MySQL不能这么做。因为MySQL并没有被设计成和其他MySQL实例同步对数据的访问,所以无法在同一份数据上开启多个MySQL实例(如果在一份只读的静态数据上使用MyISAM,技术上是可行的,但还没有见过任何实际的应用)。MySQL 5.6.8之后,InnoDB也增加了一个只读模式,可以只读的方式用多个实例访问同一份只读数据文件。MySQL的一个名为ScaleDB的存储引擎在底层提供了操作共享存储的API,但还没有进行评估过,也没有见过忍耐和在生产环境使用。
我们倾向于只使用DRBD复制存放二进制日志的设备。如果主动节点失效,可以在被动节点上开启一个日志服务器,然后对失效主库的所有备库一个用这些二进制日志。接下来可以选择其中一个备库提升为主库,以代替失效的系统。
说到底,共享存储和磁盘复制与其说是高可用性(低宕机时间)解决方案,不如说是一种保证数据安全的方法。只要拥有数据,就可以从故障中恢复,并且比无法恢复的情况的MTTR(平均恢复时间)更低(即使是很长的恢复时间也比不能恢复要快。)但是相比于备用服务器启动并一直运行的架构,大多数共享存储或磁盘复制架构会增加MTTR.有两种启用备用设备并运行的方法:MySQL的同步复制
说到底,共享存储和磁盘复制与其说是高可用性(低宕机时间)解决方案,不如说是一种保证数据安全的方法。只要拥有数据,就可以从故障中恢复,并且比无法恢复的情况的MTTR(平均恢复时间)更低(即使是很长的恢复时间也比不能恢复要快。)但是相比于备用服务器启动并一直运行的架构,大多数共享存储或磁盘复制架构会增加MTTR.有两种启用备用设备并运行的方法:MySQL的同步复制
MySQL同步复制。
当使用同步复制时,主库上的事务只有在至少一个备库上提交后才能认为其执行完成。这实现了两个目标:当服务器崩溃时没有提交的事务会丢失,并且至少有一个备库拥有实时的数据副本。大多数同步复制架构运行在主动——主动模式。这意味着每个服务器在任何时候都是故障转移的候选者,这使得通过冗余关于获得高可用性更加容易,早期版本的MySQL不支持同步复制(MySQL5.5支持半同步复制),还有两个基于MySQL的集群解决方案支持同步复制。
当使用同步复制时,主库上的事务只有在至少一个备库上提交后才能认为其执行完成。这实现了两个目标:当服务器崩溃时没有提交的事务会丢失,并且至少有一个备库拥有实时的数据副本。大多数同步复制架构运行在主动——主动模式。这意味着每个服务器在任何时候都是故障转移的候选者,这使得通过冗余关于获得高可用性更加容易,早期版本的MySQL不支持同步复制(MySQL5.5支持半同步复制),还有两个基于MySQL的集群解决方案支持同步复制。
1.MySQL Cluster
MySQL中的同步复制首先出现在MySQL Cluster(NDB Cluster)。它在所有节点上进行同步的主——主复制。这意味着可以在任何节点上写入;这些节点拥有等同的读写能力。每一行都是冗余存储的,这样即使丢失了一个节点,也不会丢失数据,并且集群仍然能提供服务。尽管MySQL Cluster还不是是哟个与所有应用的完美解决方案,但正如我们在前面提到的,在最近的版本中它做了非常快速的改进,现在已经拥有了大量的新特性和功能:非索引数据的磁盘存储、增加数据节点能够在线扩展、使用ndbinfo表来管理集群、配置和管理集群的脚本、多线程操作、下推(push-down)的关联操作(现在称为自适应查询本地化)、能够处理BLOB列和很多列的表、集中式的用户管理,以及通过像memcached协议一样的NDB API来实现NoSQL访问。在下一个版本中将包含最终一致运行模式,包括为跨数据中心的主动-主动复制提供事务冲突检测和跨WAN解决方案。简而言之,MySQL Cluster是一项引人注目的技术。
现在至少有两个为简化集群部署和管理提供附加产品的供应商:Oracle针对MySQL Cluster的服务支持包含了MySQL Cluster Manager工具;Severalnines提供了Cluster Control工具,该工具还能够帮助部署和管理复制集群
MySQL中的同步复制首先出现在MySQL Cluster(NDB Cluster)。它在所有节点上进行同步的主——主复制。这意味着可以在任何节点上写入;这些节点拥有等同的读写能力。每一行都是冗余存储的,这样即使丢失了一个节点,也不会丢失数据,并且集群仍然能提供服务。尽管MySQL Cluster还不是是哟个与所有应用的完美解决方案,但正如我们在前面提到的,在最近的版本中它做了非常快速的改进,现在已经拥有了大量的新特性和功能:非索引数据的磁盘存储、增加数据节点能够在线扩展、使用ndbinfo表来管理集群、配置和管理集群的脚本、多线程操作、下推(push-down)的关联操作(现在称为自适应查询本地化)、能够处理BLOB列和很多列的表、集中式的用户管理,以及通过像memcached协议一样的NDB API来实现NoSQL访问。在下一个版本中将包含最终一致运行模式,包括为跨数据中心的主动-主动复制提供事务冲突检测和跨WAN解决方案。简而言之,MySQL Cluster是一项引人注目的技术。
现在至少有两个为简化集群部署和管理提供附加产品的供应商:Oracle针对MySQL Cluster的服务支持包含了MySQL Cluster Manager工具;Severalnines提供了Cluster Control工具,该工具还能够帮助部署和管理复制集群
2.Percona XtraDB Cluster
Percona XtraDB Cluster是一个相对比较新的技术,基于已有的XtraDB(InnoDB)存储引擎增加了同步复制和集群特性,而不是通过一个新的存储引擎或外部服务器来实现。它是基于Galera(支持在集群中跨节点复制写操作)实现的(Galera技术由Codership Oy开发,可以作为一个补丁在标准的MySQL和InnoDB中使用。Percona XtraDB Cluster除了其他特性和功能外,还包含这组补丁的修改版本,Percona XtraDB Cluster是一个可以直接使用的基于Galera的解决方案。)这是在一个集群中不同节点复制写操作的库。跟MySQL Cluster类似,Percona XtraDB Cluster提供同步多主库复制(你可以通过配置主备只写入其中一个节点来实现,但在集群配置中,对于这种模式的操作没有什么不同)支持真正的任意节点写入能力,能够在节点失效时保证数据零丢失(持久性,ACID中的D),另外还提供高可用性,在整个集群没有失效的情况下,就算单个节点失效也没有关系。
Galera作为底层技术,使用一种被称为写入集合(write-set)复制的技术。写入集合实际上被作为基于行的二进制日志时间进行编码,目的是在集群中的节点间传输并进行更新,但是这不要求二进制日志是打开的。
Percona XtraDB Cluster的速度很快。跨节点复制实际上比没有集群还要快,因为在完全持久性模式下,写入远程RAM比写入本地磁盘要快。如果你源异,可以选择通过降低每个节点的持久性来获得更好的性能,并且可以依赖于多个节点上的数据副本来获得持久性。NDB也是基于同样的原理实现的。集群在整体上的持久性并没有降低;仅仅是降低了本地节点的持久性。除此之外,还支持行级别的并发(多线程)复制,这样就可以利用多个CPU核心来执行写入集合。这些特性结合起来是的Percona XtraDB Cluster非常适合云计算环境,因为云计算环境中的CPU和磁盘通常比较慢。
在集群中通过设置auto_increment_offset和auto_increment_increment来实现自增键,以使节点间不会生成冲突的主键值。锁机制和标准InnoDB完全相同,使用的是乐观并发控制。当事务提交时,所有的更新是序列化的,并在节点间传输,同时还有一个检测过程,以保证一旦发生更新冲突,其中一些更新操作需要丢弃。这样如果许多节点同时修改同样的数据,可能产生大量的死锁和回滚。
Percona XtraDB Cluster只要集群内在线的节点数不少于"法定人数(quorum)"就能保证服务的高可用性。如果发现某个节点不属于"法定人数"中的医院,就会从集群中将其踢出。被踢出的节点在再次加入集群前必需重新同步。因此集群也无法处理"脑裂综合征";如果出现脑裂则集群会停止服务。在一个只有两个节点的集群中,如果其中一个节点失效,剩下的一个节点达不到"法定认数"。集群将停止服务,所以实际上最少需要三个节点才能实现高可用的集群。
Percona XtraDB Cluster是一个相对比较新的技术,基于已有的XtraDB(InnoDB)存储引擎增加了同步复制和集群特性,而不是通过一个新的存储引擎或外部服务器来实现。它是基于Galera(支持在集群中跨节点复制写操作)实现的(Galera技术由Codership Oy开发,可以作为一个补丁在标准的MySQL和InnoDB中使用。Percona XtraDB Cluster除了其他特性和功能外,还包含这组补丁的修改版本,Percona XtraDB Cluster是一个可以直接使用的基于Galera的解决方案。)这是在一个集群中不同节点复制写操作的库。跟MySQL Cluster类似,Percona XtraDB Cluster提供同步多主库复制(你可以通过配置主备只写入其中一个节点来实现,但在集群配置中,对于这种模式的操作没有什么不同)支持真正的任意节点写入能力,能够在节点失效时保证数据零丢失(持久性,ACID中的D),另外还提供高可用性,在整个集群没有失效的情况下,就算单个节点失效也没有关系。
Galera作为底层技术,使用一种被称为写入集合(write-set)复制的技术。写入集合实际上被作为基于行的二进制日志时间进行编码,目的是在集群中的节点间传输并进行更新,但是这不要求二进制日志是打开的。
Percona XtraDB Cluster的速度很快。跨节点复制实际上比没有集群还要快,因为在完全持久性模式下,写入远程RAM比写入本地磁盘要快。如果你源异,可以选择通过降低每个节点的持久性来获得更好的性能,并且可以依赖于多个节点上的数据副本来获得持久性。NDB也是基于同样的原理实现的。集群在整体上的持久性并没有降低;仅仅是降低了本地节点的持久性。除此之外,还支持行级别的并发(多线程)复制,这样就可以利用多个CPU核心来执行写入集合。这些特性结合起来是的Percona XtraDB Cluster非常适合云计算环境,因为云计算环境中的CPU和磁盘通常比较慢。
在集群中通过设置auto_increment_offset和auto_increment_increment来实现自增键,以使节点间不会生成冲突的主键值。锁机制和标准InnoDB完全相同,使用的是乐观并发控制。当事务提交时,所有的更新是序列化的,并在节点间传输,同时还有一个检测过程,以保证一旦发生更新冲突,其中一些更新操作需要丢弃。这样如果许多节点同时修改同样的数据,可能产生大量的死锁和回滚。
Percona XtraDB Cluster只要集群内在线的节点数不少于"法定人数(quorum)"就能保证服务的高可用性。如果发现某个节点不属于"法定人数"中的医院,就会从集群中将其踢出。被踢出的节点在再次加入集群前必需重新同步。因此集群也无法处理"脑裂综合征";如果出现脑裂则集群会停止服务。在一个只有两个节点的集群中,如果其中一个节点失效,剩下的一个节点达不到"法定认数"。集群将停止服务,所以实际上最少需要三个节点才能实现高可用的集群。
Percona XtraDB Cluster有许多优点:
1.提供基于InnoDB的透明集群,所以无须转换到另外的技术,例如NDB这样完全不同的技术需要很多学习成本和管理
2.提供了真正的高可用性,所有节点等效,并在任何时候提供读写服务。相比较而言,MySQL内建的异步复制和半同步复制必须要有一个主库,并且不能保证数据被复制道备库,也无法保证备库数据是最新的并能够随时提升为族库
3.节点失效时保证数据不丢失。实际上,由于所有的节点都拥有全部数据,因此可以丢失任一个节点而不会丢失数据(即使集群出现脑裂并停止工作)。这和NDB不同,NDB通过节点组进行分区,当在一个节点组中的所有服务器失效时就可能丢失数据。
4.备库不会延迟,因为在事务提交前,写入集合已经在集群的所有节点上传播并被确认了
5.因为是使用基于行的日志事件在备库上进行更新,所以执行写入集合比直接执行更新的开销要小很多,就和使用基于行的复制差不多。当结合多线程应用的写入集合时,可以使其比MySQL本身的复制更具备可扩展性。
当然Percona XtraDB Cluster也有一些缺点:
1.它很新,因此还没有足够的经验来证明其优点和缺点,也缺乏合适的使用案例
2.整个集群的写入速度由最差的节点决定。因此所有的节点最好拥有相同的硬件配置,如果一个节点慢下来(例如,RAID卡做了一次battery-learn循环),所有的节点都会慢下来。如果一个节点接收写入操作变慢的可能性为P,那么3个节点的集群变慢的可能性为3P
3.没有NDB那样节省空间,因为每个节点都需要保存全部数据,而不仅仅是一部分。但另一方面,它基于Percona XtraDB(InnoDB的增强版本),也就没有NDB关于磁盘数据限制的担忧
4.当前不支持一些在异步复制中可以做的操作,例如在备库上离线修改schema,然后将其提升为主库,然后在其他节点上重复离线修改操作。当前可替代的选择时使用诸如Percona Toolkit中的在线schema修复工具。不过滚动式schema升级(rolling schema upgrade)也即将发布
5.当向集群中增加一个新节点时,需要复制所有的数据,还需要跟上不断进行的写入操作,所以一个拥有大量写入的大型集群很难进行扩容。这实际上限制了集群的数据大小。我们无法确定具体的数据。但悲观地估计可能低至100GB或更小,也可能会大得多。这一点需要时间和经验来证明
6.复制协议在写入时对网络波动比较敏感,这可能导致节点停止并从集群中踢出。所以我们推荐使用高性能网络,另外还需要很好的冗余。如果没有可靠的网络,可能会导致需要频繁地将节点加入到集群中,这需要重新同步数据。还有一个几乎接近可用的特性即通过增量状态传输来避免完全复制数据集,因此未来这并不是一个问题。还可以配置Galera以容忍更大的网络延迟(以延迟故障检测为代价)
7.如果没有仔细关注,集群可能会增长得太大,以至于无法重启失效节点,就像在一个合理的时间范围内,如果在日常工作中没有定期做恢复演练,备份也会变得太过庞大而无法用于恢复。我们需要更多的实践经验来了解它事实上是如何工作的
8.由于事务提交时需要进行跨节点通信,写入会更慢,随者集群增加的节点越来越多,死锁和回滚也会更加频繁
Percona XtraDB Cluster和Galera都处于生命周期的早期,正在被快速地修改和改进。现在正在进行或即将进行地改进包括群体行为、安全性、同步性、内存管理、状态转移等。未来还可以为离线节点执行诸如滚动式schema变更的操作
1.提供基于InnoDB的透明集群,所以无须转换到另外的技术,例如NDB这样完全不同的技术需要很多学习成本和管理
2.提供了真正的高可用性,所有节点等效,并在任何时候提供读写服务。相比较而言,MySQL内建的异步复制和半同步复制必须要有一个主库,并且不能保证数据被复制道备库,也无法保证备库数据是最新的并能够随时提升为族库
3.节点失效时保证数据不丢失。实际上,由于所有的节点都拥有全部数据,因此可以丢失任一个节点而不会丢失数据(即使集群出现脑裂并停止工作)。这和NDB不同,NDB通过节点组进行分区,当在一个节点组中的所有服务器失效时就可能丢失数据。
4.备库不会延迟,因为在事务提交前,写入集合已经在集群的所有节点上传播并被确认了
5.因为是使用基于行的日志事件在备库上进行更新,所以执行写入集合比直接执行更新的开销要小很多,就和使用基于行的复制差不多。当结合多线程应用的写入集合时,可以使其比MySQL本身的复制更具备可扩展性。
当然Percona XtraDB Cluster也有一些缺点:
1.它很新,因此还没有足够的经验来证明其优点和缺点,也缺乏合适的使用案例
2.整个集群的写入速度由最差的节点决定。因此所有的节点最好拥有相同的硬件配置,如果一个节点慢下来(例如,RAID卡做了一次battery-learn循环),所有的节点都会慢下来。如果一个节点接收写入操作变慢的可能性为P,那么3个节点的集群变慢的可能性为3P
3.没有NDB那样节省空间,因为每个节点都需要保存全部数据,而不仅仅是一部分。但另一方面,它基于Percona XtraDB(InnoDB的增强版本),也就没有NDB关于磁盘数据限制的担忧
4.当前不支持一些在异步复制中可以做的操作,例如在备库上离线修改schema,然后将其提升为主库,然后在其他节点上重复离线修改操作。当前可替代的选择时使用诸如Percona Toolkit中的在线schema修复工具。不过滚动式schema升级(rolling schema upgrade)也即将发布
5.当向集群中增加一个新节点时,需要复制所有的数据,还需要跟上不断进行的写入操作,所以一个拥有大量写入的大型集群很难进行扩容。这实际上限制了集群的数据大小。我们无法确定具体的数据。但悲观地估计可能低至100GB或更小,也可能会大得多。这一点需要时间和经验来证明
6.复制协议在写入时对网络波动比较敏感,这可能导致节点停止并从集群中踢出。所以我们推荐使用高性能网络,另外还需要很好的冗余。如果没有可靠的网络,可能会导致需要频繁地将节点加入到集群中,这需要重新同步数据。还有一个几乎接近可用的特性即通过增量状态传输来避免完全复制数据集,因此未来这并不是一个问题。还可以配置Galera以容忍更大的网络延迟(以延迟故障检测为代价)
7.如果没有仔细关注,集群可能会增长得太大,以至于无法重启失效节点,就像在一个合理的时间范围内,如果在日常工作中没有定期做恢复演练,备份也会变得太过庞大而无法用于恢复。我们需要更多的实践经验来了解它事实上是如何工作的
8.由于事务提交时需要进行跨节点通信,写入会更慢,随者集群增加的节点越来越多,死锁和回滚也会更加频繁
Percona XtraDB Cluster和Galera都处于生命周期的早期,正在被快速地修改和改进。现在正在进行或即将进行地改进包括群体行为、安全性、同步性、内存管理、状态转移等。未来还可以为离线节点执行诸如滚动式schema变更的操作
基于复制的冗余。
复制管理器是使用标准MySQL复制来创建冗余的工具(冗余并不等同于高可用性)。尽管可以通过复制来改善可用性,但也有一些"玻璃天花板"会阻止MySQL当前版本的异步复制和半同步复制获得和真正的同步复制相同的结果。复制无法保证实时的故障转移和数据零丢失,也无法将所有节点等同对待。
复制管理器通常监控和管理三件事:应用和MySQL间的通信、MySQL服务器的健康度,以及MySQL服务器间的复制关系。它们既可以修改负载均衡的配置,也可以在必要的时候转移虚拟IP地址以使应用连接到合适的服务器上,还能够在一个伪集群中操作复制以选择一个服务器作为写入节点。大体上操作并不复杂:只需要确定写入不会发送到一个还没有准备好提供写服务的服务器上,并保证当需要提升一台备库为主库时记录下正确的复制坐标。
这听起来在理论上是可行的,但经验表明实际上并不总是能有效工作。事实上这非常糟糕,有些时候最好有一些轻量级的工具集来帮助从常见的故障中恢复并以很少的开销获得较高的可用性。不幸的是,还没有听说过任何一个好的工具及可以可靠地完成这一点。后面会介绍两个复制管理器,其中一个很新,而另外一个则有很多问题。
我们发现很多人试图去写自己的复制管理器。它们常常会陷入很多人已经遭遇过的陷阱,自己去写一个复制管理器并不是好主意。异步组件有大量的故障形式,很多你从未亲身经历过,其中一些甚至无法理解,并且程序也无法适当处理。因此从这些异步组件中得到正确的行为相当困难,并且可能遭遇数据丢失的危险。事实上,机器刚开始出现问题时,由一个经验丰富的人来解决时很快的。但如果其他人做了一些错误的修复操作则可能导致问题更严重
我们要提到的第一个复制管理器时MMM.有些人认为它在一些人工——故障转移模式下的场景下比较有用,而有些人甚至从不使用这个工具。许多人在自动——故障转移模式下使用该工具时确实遇到了许多严重的问题。它会导致健康的服务器离线,也可能将写入发送到错误的地点,并将备库移动到错误的坐标。有时混乱就接踵而至.
另外一个比较新一点的工具时Yoshinori Matsunobu 的MHA工具集,它和MMM一样时一组脚本,使用相同的通用技术来建立一个伪集群,但它不是一个完全的替换这;它不会去做太多的事情,并且依赖于Pecemaker来转移虚拟IP地址。一个主要的不同是,MHA有一个很好的测试集,可以防止一些MMM遇到过的问题。除此之外对该工具集还没有更多的认识。
基于复制的冗余最终来说好坏参半。只有在可用性的重要性远比一致性或数据零丢失保证更重要时才推荐使用。例如,一些人并不会真的从它们的网站功能重获利,而是从它的可用性重赚钱。谁会在乎是否出现了故障导致一张照片丢失了几条评论或者其他什么东西呢?只要广告收益继续滚滚而来,可能并不值得花更多成本去实现真正的高可用性。但还是可以通过复制来建立"尽可能的"高可用性,当遇到一些很难处理的严重宕机时,可能会有所帮助。这是一个大赌注,并且可能对大多数人而言太过于冒险,除非是哪些老成(或者专业)的用户。
问题时许多用户不知道如何去证明自己有资格并评估复制"轮盘赌"是否适合它们。这有两个方面的原因。第一,它们并没有看到"玻璃天花板",错误地认为一组虚拟IP地址、复制以及管理脚本能够实现真正的高可用性。第二,它们低估了技术的复杂度,因此也低估了严重故障发生后从中恢复的难度。一些人认为它们能够使用基于复制的冗余技术,但随后它们可能会更希望选择一个有更强保障的简单系统。
其他一些类型的复制,例如DRBD或者SAN,也有他们的缺点——但也不要认为将这些技术说的无所不能而把MySQL自身的复制贬得一团糟,那不是本意。你可以为DRBD写出低质量的故障转移脚本,这很简单,就像为MySQL复制编写脚本一样。主要的区别时MySQL复制非常复杂,有很多非常细小的差别,并且不会阻止你干坏事
复制管理器是使用标准MySQL复制来创建冗余的工具(冗余并不等同于高可用性)。尽管可以通过复制来改善可用性,但也有一些"玻璃天花板"会阻止MySQL当前版本的异步复制和半同步复制获得和真正的同步复制相同的结果。复制无法保证实时的故障转移和数据零丢失,也无法将所有节点等同对待。
复制管理器通常监控和管理三件事:应用和MySQL间的通信、MySQL服务器的健康度,以及MySQL服务器间的复制关系。它们既可以修改负载均衡的配置,也可以在必要的时候转移虚拟IP地址以使应用连接到合适的服务器上,还能够在一个伪集群中操作复制以选择一个服务器作为写入节点。大体上操作并不复杂:只需要确定写入不会发送到一个还没有准备好提供写服务的服务器上,并保证当需要提升一台备库为主库时记录下正确的复制坐标。
这听起来在理论上是可行的,但经验表明实际上并不总是能有效工作。事实上这非常糟糕,有些时候最好有一些轻量级的工具集来帮助从常见的故障中恢复并以很少的开销获得较高的可用性。不幸的是,还没有听说过任何一个好的工具及可以可靠地完成这一点。后面会介绍两个复制管理器,其中一个很新,而另外一个则有很多问题。
我们发现很多人试图去写自己的复制管理器。它们常常会陷入很多人已经遭遇过的陷阱,自己去写一个复制管理器并不是好主意。异步组件有大量的故障形式,很多你从未亲身经历过,其中一些甚至无法理解,并且程序也无法适当处理。因此从这些异步组件中得到正确的行为相当困难,并且可能遭遇数据丢失的危险。事实上,机器刚开始出现问题时,由一个经验丰富的人来解决时很快的。但如果其他人做了一些错误的修复操作则可能导致问题更严重
我们要提到的第一个复制管理器时MMM.有些人认为它在一些人工——故障转移模式下的场景下比较有用,而有些人甚至从不使用这个工具。许多人在自动——故障转移模式下使用该工具时确实遇到了许多严重的问题。它会导致健康的服务器离线,也可能将写入发送到错误的地点,并将备库移动到错误的坐标。有时混乱就接踵而至.
另外一个比较新一点的工具时Yoshinori Matsunobu 的MHA工具集,它和MMM一样时一组脚本,使用相同的通用技术来建立一个伪集群,但它不是一个完全的替换这;它不会去做太多的事情,并且依赖于Pecemaker来转移虚拟IP地址。一个主要的不同是,MHA有一个很好的测试集,可以防止一些MMM遇到过的问题。除此之外对该工具集还没有更多的认识。
基于复制的冗余最终来说好坏参半。只有在可用性的重要性远比一致性或数据零丢失保证更重要时才推荐使用。例如,一些人并不会真的从它们的网站功能重获利,而是从它的可用性重赚钱。谁会在乎是否出现了故障导致一张照片丢失了几条评论或者其他什么东西呢?只要广告收益继续滚滚而来,可能并不值得花更多成本去实现真正的高可用性。但还是可以通过复制来建立"尽可能的"高可用性,当遇到一些很难处理的严重宕机时,可能会有所帮助。这是一个大赌注,并且可能对大多数人而言太过于冒险,除非是哪些老成(或者专业)的用户。
问题时许多用户不知道如何去证明自己有资格并评估复制"轮盘赌"是否适合它们。这有两个方面的原因。第一,它们并没有看到"玻璃天花板",错误地认为一组虚拟IP地址、复制以及管理脚本能够实现真正的高可用性。第二,它们低估了技术的复杂度,因此也低估了严重故障发生后从中恢复的难度。一些人认为它们能够使用基于复制的冗余技术,但随后它们可能会更希望选择一个有更强保障的简单系统。
其他一些类型的复制,例如DRBD或者SAN,也有他们的缺点——但也不要认为将这些技术说的无所不能而把MySQL自身的复制贬得一团糟,那不是本意。你可以为DRBD写出低质量的故障转移脚本,这很简单,就像为MySQL复制编写脚本一样。主要的区别时MySQL复制非常复杂,有很多非常细小的差别,并且不会阻止你干坏事
故障转移和故障恢复。
冗余是很好的技术,但实际上只有在遇到故障需要恢复时才会用到。(见鬼,这可以用备份来实现)。冗余一点儿也不会增加可用性或减少宕机。在故障转移的过程中,高可用性是建立在冗余的基础上。当有一个组件失效,但存在冗余时,可以停止使用发生故障的组件,而使用冗余备件。冗余和故障转移结合可以帮助更快地恢复,如你所知,MTTR(平均恢复时间)的减少将降低宕机时间并改善可用性。在继续这个话题之前,我们先来定义一些术语。我们统一使用"故障转移(failover)",有些人使用"回退(fallback)"来表达同一意思。有时候也有人说"切换(switchover)",以表明一次计划中的切换而不是故障后的应对措施。我们也会使用"故障恢复"来表示故障转移的反面。如果系统拥有故障恢复能力,故障转移就是一个双向过程:当服务器A失效,服务器B代替它,在修复服务器A后可以再替换回来.故障转移比仅仅从故障中恢复更好。也可以针对一些情况制定故障转移计划,例如升级、schema变更、应用修改,或者定期维护,当发生故障时可以根据计划进行故障转移来减少宕机时间(改善可用性)。
你需要确定故障转移到底需要多快,也要知道在一次故障转移后替换一个失效组件应该多快。在你恢复系统耗尽的备件容量之前,会出现冗余不足,并面临额外风险。因此,拥有一个备件并不能消除即时替换失效组件的需求。构建一个新的备用服务器,安装操作系统,并赋值数据的最新副本,可以多快呢?有足够的备用机器码?你可能需要不止一台以上。
故障转移的缘由各不相同。因为负载均衡和故障转移在很多方面很相似,它们之间的分界线比较模糊。总的来说,我们认为一个完全的故障转移解决方案至少能够监控并自动替换组件。它对应用应该是透明的。负载均衡不需要系统这些功能。
在UNIX领域,故障转移常常使用High Availability Linux项目提供的工具来完成,该享目可在许多类UNIX系统上运行,而不仅仅是Linux.Linux-HA栈在最近几年明显多了许多新特性。现在大多数认为Pacemaker是栈中的一个主要组件。Pacemaker替代了老的心跳工具。还有其他一些工具实现了IP托管和负载均衡功能。可以将它们跟DRBD和/或者LVS结合起来使用。
故障转移最重要的部分就是故障恢复。如果服务器间不能自如切换,故障转移就是一个死胡同,只能是延缓宕机时间而已。这也是我们倾向于对称复制布局。例如双主配置,而不会选择使用三台或更多的联合主库(co-master)来进行环形复制的原因。如果配置是对等的,故障转移和故障恢复就是在相反方向上的相同操作。(值得一提的是DRBD具有内建的故障恢复功能)。
在一些应用中,故障转移和故障恢复需要尽量快速并且具备原子性。即便这不是决定性的,不依靠那些不受你控制的东西也依然是个好主意,例如DNS变更或者应用程序配置i文件。一些问题知道系统变得更加庞大时才会显现出来,例如当应用程序强制重启以及原子性需求出现时。
由于负载均衡和故障转移两者联系较紧密,有些硬件和软件是同时为这两个目的设计的,因此我们建议所选择的任何负载均衡技术应该都提供故障转移功能。这也是我们建议避免使用DNS和修改代码来做负载均衡的真实原因。如果为负载均衡采用了这些策略,就需要做一些额外的工作;当需要高可用性时,不得不重写受影响的代码。
冗余是很好的技术,但实际上只有在遇到故障需要恢复时才会用到。(见鬼,这可以用备份来实现)。冗余一点儿也不会增加可用性或减少宕机。在故障转移的过程中,高可用性是建立在冗余的基础上。当有一个组件失效,但存在冗余时,可以停止使用发生故障的组件,而使用冗余备件。冗余和故障转移结合可以帮助更快地恢复,如你所知,MTTR(平均恢复时间)的减少将降低宕机时间并改善可用性。在继续这个话题之前,我们先来定义一些术语。我们统一使用"故障转移(failover)",有些人使用"回退(fallback)"来表达同一意思。有时候也有人说"切换(switchover)",以表明一次计划中的切换而不是故障后的应对措施。我们也会使用"故障恢复"来表示故障转移的反面。如果系统拥有故障恢复能力,故障转移就是一个双向过程:当服务器A失效,服务器B代替它,在修复服务器A后可以再替换回来.故障转移比仅仅从故障中恢复更好。也可以针对一些情况制定故障转移计划,例如升级、schema变更、应用修改,或者定期维护,当发生故障时可以根据计划进行故障转移来减少宕机时间(改善可用性)。
你需要确定故障转移到底需要多快,也要知道在一次故障转移后替换一个失效组件应该多快。在你恢复系统耗尽的备件容量之前,会出现冗余不足,并面临额外风险。因此,拥有一个备件并不能消除即时替换失效组件的需求。构建一个新的备用服务器,安装操作系统,并赋值数据的最新副本,可以多快呢?有足够的备用机器码?你可能需要不止一台以上。
故障转移的缘由各不相同。因为负载均衡和故障转移在很多方面很相似,它们之间的分界线比较模糊。总的来说,我们认为一个完全的故障转移解决方案至少能够监控并自动替换组件。它对应用应该是透明的。负载均衡不需要系统这些功能。
在UNIX领域,故障转移常常使用High Availability Linux项目提供的工具来完成,该享目可在许多类UNIX系统上运行,而不仅仅是Linux.Linux-HA栈在最近几年明显多了许多新特性。现在大多数认为Pacemaker是栈中的一个主要组件。Pacemaker替代了老的心跳工具。还有其他一些工具实现了IP托管和负载均衡功能。可以将它们跟DRBD和/或者LVS结合起来使用。
故障转移最重要的部分就是故障恢复。如果服务器间不能自如切换,故障转移就是一个死胡同,只能是延缓宕机时间而已。这也是我们倾向于对称复制布局。例如双主配置,而不会选择使用三台或更多的联合主库(co-master)来进行环形复制的原因。如果配置是对等的,故障转移和故障恢复就是在相反方向上的相同操作。(值得一提的是DRBD具有内建的故障恢复功能)。
在一些应用中,故障转移和故障恢复需要尽量快速并且具备原子性。即便这不是决定性的,不依靠那些不受你控制的东西也依然是个好主意,例如DNS变更或者应用程序配置i文件。一些问题知道系统变得更加庞大时才会显现出来,例如当应用程序强制重启以及原子性需求出现时。
由于负载均衡和故障转移两者联系较紧密,有些硬件和软件是同时为这两个目的设计的,因此我们建议所选择的任何负载均衡技术应该都提供故障转移功能。这也是我们建议避免使用DNS和修改代码来做负载均衡的真实原因。如果为负载均衡采用了这些策略,就需要做一些额外的工作;当需要高可用性时,不得不重写受影响的代码。
提升备库或切换角色。
提升一台备库为主库,或者在一个主——主复制结构中调换主动和被动角色,这些都是许多MySQL故障转移策略很重要的一部分。我们不能认定自动化工具总能在所有的情况下做正确地事情。你不应该假定在发生故障时能够立刻切换到被动备库,这要看具体的工作负载。备库会重放主库的写入,但如果不用来提供读操作,就无法进行预热来为生产环境负载提供服务。如果希望有一个随时能承担读负载的备库,就要不断地"训练"它,既可以将其用于分担工作负载,也可以将生产环境地读查询镜像到备库上。我们有时候通过监听TCP流量,截取除其中地SELECT查询,然后在备库上重放来实现这个目的。Percona Toolkit中有一些工具可以做到这一点。
提升一台备库为主库,或者在一个主——主复制结构中调换主动和被动角色,这些都是许多MySQL故障转移策略很重要的一部分。我们不能认定自动化工具总能在所有的情况下做正确地事情。你不应该假定在发生故障时能够立刻切换到被动备库,这要看具体的工作负载。备库会重放主库的写入,但如果不用来提供读操作,就无法进行预热来为生产环境负载提供服务。如果希望有一个随时能承担读负载的备库,就要不断地"训练"它,既可以将其用于分担工作负载,也可以将生产环境地读查询镜像到备库上。我们有时候通过监听TCP流量,截取除其中地SELECT查询,然后在备库上重放来实现这个目的。Percona Toolkit中有一些工具可以做到这一点。
虚拟IP地址或IP接管。
可以为需要提供特定服务的MySQL实例指定一个逻辑IP地址。当MySQL实例失效时,可以将IP地址转移到另一台MySQL服务器上。这和前面提到的思想本质是相同的,唯一的不同是现在是用于故障转移,而不是负载均衡。
这种方法的好处是对应用透明。它会中断已有的连接,但不要求修改配置。有时候还可以原子地转移IP地址,保证所有的应用在同一时间看到这一变更。当服务器在可用和不可用状态间"摇摆"时,这一点尤其重要。以下是它的一些不足之处:
1.需要把所有的IP地址定义在同一网段,或者使用网络桥接
2.改变IP地址需要系统root权限
3.有时候还需要更新ARP缓存。有些网络设备可能会把ARP信息保存太久,以致无法即时将一个IP地址切换到另一个MAC地址上。我们看到过很多网络设备或其他组件不配合切换的例子,结果系统的许多部分可能无法确定IP地址到底在哪里
4.需要确定网络硬件支持快速IP接管。有些硬件需要克隆MAC地址后才能工作
5.有些服务器即使完全丧失功能也会保持持有IP地址,所以可能需要从物理上关闭或断开网络连接。这就是为人所熟知的"击中其他节点的头部"(shoot the other node in the head 简称STONITH).他还有一个更加微妙并且比较官方的名字:击剑(fencing)
浮动IP地址和IP接管能够很好地应付彼此临近(也就是同一子网内)的机器之间的故障转移。但是最后需要提醒的是,这种策略并不总是万无一失,还取决于网络硬件等因素
可以为需要提供特定服务的MySQL实例指定一个逻辑IP地址。当MySQL实例失效时,可以将IP地址转移到另一台MySQL服务器上。这和前面提到的思想本质是相同的,唯一的不同是现在是用于故障转移,而不是负载均衡。
这种方法的好处是对应用透明。它会中断已有的连接,但不要求修改配置。有时候还可以原子地转移IP地址,保证所有的应用在同一时间看到这一变更。当服务器在可用和不可用状态间"摇摆"时,这一点尤其重要。以下是它的一些不足之处:
1.需要把所有的IP地址定义在同一网段,或者使用网络桥接
2.改变IP地址需要系统root权限
3.有时候还需要更新ARP缓存。有些网络设备可能会把ARP信息保存太久,以致无法即时将一个IP地址切换到另一个MAC地址上。我们看到过很多网络设备或其他组件不配合切换的例子,结果系统的许多部分可能无法确定IP地址到底在哪里
4.需要确定网络硬件支持快速IP接管。有些硬件需要克隆MAC地址后才能工作
5.有些服务器即使完全丧失功能也会保持持有IP地址,所以可能需要从物理上关闭或断开网络连接。这就是为人所熟知的"击中其他节点的头部"(shoot the other node in the head 简称STONITH).他还有一个更加微妙并且比较官方的名字:击剑(fencing)
浮动IP地址和IP接管能够很好地应付彼此临近(也就是同一子网内)的机器之间的故障转移。但是最后需要提醒的是,这种策略并不总是万无一失,还取决于网络硬件等因素
等待更新扩散。
经常有这种情况,在某一层帝国一了一个冗余后,需要等待低层执行一些改变。我们指出通过DNS修改服务器是一个很脆弱的解决方案,因为DNS的更新扩散速度很慢,改变IP地址可给予你更多的哦内阁制,但在一个LAN中的IP地址同样依赖于更底层——ARP——来扩散更新
经常有这种情况,在某一层帝国一了一个冗余后,需要等待低层执行一些改变。我们指出通过DNS修改服务器是一个很脆弱的解决方案,因为DNS的更新扩散速度很慢,改变IP地址可给予你更多的哦内阁制,但在一个LAN中的IP地址同样依赖于更底层——ARP——来扩散更新
中间件解决方案。
可以使用代理、端口转发、网络地址转换(NAT)或者硬件负载均衡来实现故障转移和故障恢复。这些都是很好的解决方法,不像其他方法可能会引入一些不确定性(所有系统组件认同哪一个是主库码?它能够即使并原子地更改吗?)它们是控制应用和服务器连接的中枢。但是,它们自身也引入了单点失效,需要准备冗余来避免这个问题。
使用这样的解决方案,你可以将一个远程数据中心设置成看起来好像和应用在同一个网络里。这样就可以使用诸如浮动IP地址这样的技术让应用和一个完全不同的数据中心开始通信。你可以配置每个数据中心的每台应用服务器,通过它自己的中间件连接,将流量路由到活跃数据中心的机器上,如图所示。
如果活跃数据中心安装的MySQL彻底崩溃了,中间件可以路由流量到另外一个数据中心的服务器池中,应用无须知道这个变化。
这种配置方法的主要缺点是在一个数据中心的Apache服务器和另外一个数据中心的MySQL服务器之间的延迟比较大。为了缓和这个问题,可以把Web服务器设置重定向模式。这样通信都会被重定向到放置活跃MySQL服务器的数据中心。还可以使用HTTP代理来实现这一目标。如图显示了如何使用代理来连接MySQL服务器,也可以将这个方法和许多别的中间件架构结合在一起,例如LVS和硬件负载均衡器
可以使用代理、端口转发、网络地址转换(NAT)或者硬件负载均衡来实现故障转移和故障恢复。这些都是很好的解决方法,不像其他方法可能会引入一些不确定性(所有系统组件认同哪一个是主库码?它能够即使并原子地更改吗?)它们是控制应用和服务器连接的中枢。但是,它们自身也引入了单点失效,需要准备冗余来避免这个问题。
使用这样的解决方案,你可以将一个远程数据中心设置成看起来好像和应用在同一个网络里。这样就可以使用诸如浮动IP地址这样的技术让应用和一个完全不同的数据中心开始通信。你可以配置每个数据中心的每台应用服务器,通过它自己的中间件连接,将流量路由到活跃数据中心的机器上,如图所示。
如果活跃数据中心安装的MySQL彻底崩溃了,中间件可以路由流量到另外一个数据中心的服务器池中,应用无须知道这个变化。
这种配置方法的主要缺点是在一个数据中心的Apache服务器和另外一个数据中心的MySQL服务器之间的延迟比较大。为了缓和这个问题,可以把Web服务器设置重定向模式。这样通信都会被重定向到放置活跃MySQL服务器的数据中心。还可以使用HTTP代理来实现这一目标。如图显示了如何使用代理来连接MySQL服务器,也可以将这个方法和许多别的中间件架构结合在一起,例如LVS和硬件负载均衡器
在应用中处理故障转移。
有时候让应用来处理故障转移会更加简单或者更加灵活。例如,如果应用遇到一个错误,这个错误外部观察者正常情况下是无法察觉的,例如关于数据库损坏的错误日志信息,那么应用可以自己来处理故障转移过程。
虽然把故障转移处理过程整合到应用中看起来比较吸引人,但可能没有想象中那么有效。大多数应用有许多组件,例如cron任务、配置文件,以及用不同语言编写的脚本。将故障转移整合到应用中可能导致应用变得太过笨拙,尤其是当应用增大并变得更加复杂时。但是将监控构建到应用中是一个好主意,当需要时,能够立刻开始故障转移过程。应用应该也能够管理用户体验,例如提供降级功能,并显示给用户合适的信息.
有时候让应用来处理故障转移会更加简单或者更加灵活。例如,如果应用遇到一个错误,这个错误外部观察者正常情况下是无法察觉的,例如关于数据库损坏的错误日志信息,那么应用可以自己来处理故障转移过程。
虽然把故障转移处理过程整合到应用中看起来比较吸引人,但可能没有想象中那么有效。大多数应用有许多组件,例如cron任务、配置文件,以及用不同语言编写的脚本。将故障转移整合到应用中可能导致应用变得太过笨拙,尤其是当应用增大并变得更加复杂时。但是将监控构建到应用中是一个好主意,当需要时,能够立刻开始故障转移过程。应用应该也能够管理用户体验,例如提供降级功能,并显示给用户合适的信息.
应用层优化
概述。
如果在提高MySQL的性能上花费太多时间,容易使事业局限于MySQL本身,而忽略了用户体验。回过头来看,也许可以意识到,或许MySQL已经足够优化,对于用户看到的响应时间而言,其所占的比重已经非常之小,此时应该关注下其他部分了。这是个很不错的观点,尤其是对DBA而言,这是很值得去做的正确的事。但如果不是MySQL,那有时什么导致了问题?在前面的讨论中,可以通过测量可以快速而准确地给出答案。如果能顺着应用的逻辑过程从头到尾来剖析,那么找到问题的源头一般来说并不困难。有时,尽管问题在MySQL上,也很容易在系统的另一部分得到解决。
无论问题出在哪里,都至少可以找到一个靠谱的工具来帮助进行分析,而且通常事免费的。例如,如果有JavaScript或者页面渲染的问题,可以使用包括FireFox浏览器的Firebug插件在内的调优工具,或者使用Yahoo!的YSlow工具。一些工具甚至可以剖析整个堆栈:New Relic是一个很好的例子,它可以剖析Web应用的前端、应用以及后端。
如果在提高MySQL的性能上花费太多时间,容易使事业局限于MySQL本身,而忽略了用户体验。回过头来看,也许可以意识到,或许MySQL已经足够优化,对于用户看到的响应时间而言,其所占的比重已经非常之小,此时应该关注下其他部分了。这是个很不错的观点,尤其是对DBA而言,这是很值得去做的正确的事。但如果不是MySQL,那有时什么导致了问题?在前面的讨论中,可以通过测量可以快速而准确地给出答案。如果能顺着应用的逻辑过程从头到尾来剖析,那么找到问题的源头一般来说并不困难。有时,尽管问题在MySQL上,也很容易在系统的另一部分得到解决。
无论问题出在哪里,都至少可以找到一个靠谱的工具来帮助进行分析,而且通常事免费的。例如,如果有JavaScript或者页面渲染的问题,可以使用包括FireFox浏览器的Firebug插件在内的调优工具,或者使用Yahoo!的YSlow工具。一些工具甚至可以剖析整个堆栈:New Relic是一个很好的例子,它可以剖析Web应用的前端、应用以及后端。
常见问题。
我们在应用中反复看到一些相同的问题,经常是因为人们使用了缺乏设计的线程系统或者简单开发的流行框架。虽然有时候可以通过这些框架更快更简单地构建系统,但是如果不清楚这些框架背后做了什么操作,反而会增加系统的风险。下面是我们经常会碰到的问题清单,通过这些过程可以激发你的思维。
1.什么东西在消耗系统中每台主机的CPU、磁盘、网络,以及内存资源?这些值是否合理?如果不合理,对应用程序做基本的检查,看什么占用了资源。配置文件通常是解决问题最简单的方式。例如,如果Apache因为创建1000各需要50MB内存的工作进程而导致内存溢出,就可以配置应用程序少使用一些Apache工作进程。也可以配置每个进程少使用一些内存。
2.应用真的需要所有获取到的数据吗?获取1000行数据但只显示10行,而丢弃剩下的990行,这是常见的错误。(如果应用程序缓存了另外的990行备用,这也许是有意的优化)
3.应用在处理本应由数据库处理的事情吗?或者反过来?这里有两个例子,从表中获取所有的行在应用中进行统计计数,或者在数据库中执行复杂的字符串操作。数据库擅长统计计数,而应用擅长正则表达式。要善于使用正确的工具来完成任务。
4.应用执行了太多的查询?ORM宣称的把程序员从写SQL中解放出来的语句解耦通常是罪魁祸首。数据库服务器为从多个表匹配数据做了很多优化,因此应用程序完全可以删掉多余的嵌套循环,而使用数据库的关联来代替。
5.应用执行的查询太少了?好吧,上面之说了执行太多SQL可能成为问题。到那时,有时候让应用来做"手工关联"以及类似的操作也可能是个好主意。因为它们允许更细的粒度控制和更有效的使用缓存,以及更少的锁争用,甚至有时应用代码里模拟的哈希关联会更快(MySQL的嵌套循环的关联方法并不总是高效的)
6.应用创建了没必要的MySQL连接吗?如果可以从缓存中获得数据,就不要再连接数据库
7.应用对一个MySQL实例创建连接的次数太多了吗?(也许因为应用的不同部分打开了它们自己的连接)通常来说更好的办法是重用相同的连接。
8.应用做了太多的"垃圾"查询?一个常见的例子是发送查询前先发送一个ping命令看数据库是否存活,或者每次执行SQL前选择需要的数据库。总是连接到一个特定的数据库并使用完整的表名也许是更好的方法(这也使得从日志或者通过SHOW PROCESSLIST看SQL更容易了,因为执行日志中的SQL语句的时候不用再切换到特定的数据,数据库名已经包含在SQL语句中了。)"预备"(Preparing)连接时另一个常见问题。Java驱动在预备期间会做大量的操作,其中大部分可以禁用。另一个常见的垃圾查询是SET NAMES UFT8。这是一个错误的方法(它不会改变客户端库的字符集,只会影响服务器的设置)。如果应用在大部分情况使用特定的字符集工作,可以修改配置文件把特定字符集设为默认值,而不需要在每次执行时去做修改。
9.应用使用了连接池吗?这既可能是好事,也可能是坏事。连接池可以帮助限制总的连接数,有大量SQL执行的时候效果不错(Ajax应用是一个典型的例子)。然而连接池也可能有一些副作用,比如说应用的事务、临时表、连接相关的配置项,以及用户自定义变量之间相互干扰等
10.应用是否适用长连接?这可能导致太多连接。通常来说长连接不是个好主意,除非网络环境很慢导致创建连接的开销很大,或者连接只被一或两个很快的SQL使用,或者连接频率很高导致客户端本地端口不够用。如果MySQL的配置正确,也许就不需要长连接了。比如使用skip-name-resolve来避免DNS反向查询,确保thread_cache足够大,并且增加back_log。
11.应用是否在不使用的时候还保持连接打开?如果是这样,尤其是连接到很多服务器时,可能会过多地消耗其他进程所需要地连接。例如,假设你连接到10个MySQL服务器。从一个Apache进程中获取10个连接不是问题,但是任意时刻其中只有1个在真正工作。其他9个大部分时间都处于Sleep状态。如果其中一台服务器变慢了,或者有一个很长地网络请求,其他地服务器就可能因为连接数过多而受到影响。解决方案时控制应用怎么使用连接。例如,可以将操作批量地依次发送到每个MySQL实例,并且在下一次执行SQL前关闭每个连接。如果执行的是比较消耗时间的操作,例如调用Web服务接口,甚至可以先关闭MySQL连接,执行耗时的工作,再重新打开MySQL连接继续在数据库上工作。
长连接和连接池的区别可能使人困惑。长连接可能跟连接池有同样的副作用,因为重用的链接在这两种情况下都是有状态的。然而,连接池通常不会导致服务器连接过多,因为它们会在进程间排队和共享连接。另一方面,长连接是在每个进程基础上创建,不会在进程间共享。
连接池也比共享连接的方式对连接策略有更强的控制力。连接池可以配置为自动扩展,但是通常的实践经验是,当遇到连接池完全占满时,应该将连接请求进行排队而不是扩展连接池。这样做可以在应用服务器上进行排队等,而不是将压力传递到MySQL数据库服务器上导致连接数太多而过载。
有很多方法可以使得查询和连接更快,但是一般的规则是,如果能够直接避免进行查询和连接,肯定比努力提升查询和连接的性能能获得更好的优化结果
我们在应用中反复看到一些相同的问题,经常是因为人们使用了缺乏设计的线程系统或者简单开发的流行框架。虽然有时候可以通过这些框架更快更简单地构建系统,但是如果不清楚这些框架背后做了什么操作,反而会增加系统的风险。下面是我们经常会碰到的问题清单,通过这些过程可以激发你的思维。
1.什么东西在消耗系统中每台主机的CPU、磁盘、网络,以及内存资源?这些值是否合理?如果不合理,对应用程序做基本的检查,看什么占用了资源。配置文件通常是解决问题最简单的方式。例如,如果Apache因为创建1000各需要50MB内存的工作进程而导致内存溢出,就可以配置应用程序少使用一些Apache工作进程。也可以配置每个进程少使用一些内存。
2.应用真的需要所有获取到的数据吗?获取1000行数据但只显示10行,而丢弃剩下的990行,这是常见的错误。(如果应用程序缓存了另外的990行备用,这也许是有意的优化)
3.应用在处理本应由数据库处理的事情吗?或者反过来?这里有两个例子,从表中获取所有的行在应用中进行统计计数,或者在数据库中执行复杂的字符串操作。数据库擅长统计计数,而应用擅长正则表达式。要善于使用正确的工具来完成任务。
4.应用执行了太多的查询?ORM宣称的把程序员从写SQL中解放出来的语句解耦通常是罪魁祸首。数据库服务器为从多个表匹配数据做了很多优化,因此应用程序完全可以删掉多余的嵌套循环,而使用数据库的关联来代替。
5.应用执行的查询太少了?好吧,上面之说了执行太多SQL可能成为问题。到那时,有时候让应用来做"手工关联"以及类似的操作也可能是个好主意。因为它们允许更细的粒度控制和更有效的使用缓存,以及更少的锁争用,甚至有时应用代码里模拟的哈希关联会更快(MySQL的嵌套循环的关联方法并不总是高效的)
6.应用创建了没必要的MySQL连接吗?如果可以从缓存中获得数据,就不要再连接数据库
7.应用对一个MySQL实例创建连接的次数太多了吗?(也许因为应用的不同部分打开了它们自己的连接)通常来说更好的办法是重用相同的连接。
8.应用做了太多的"垃圾"查询?一个常见的例子是发送查询前先发送一个ping命令看数据库是否存活,或者每次执行SQL前选择需要的数据库。总是连接到一个特定的数据库并使用完整的表名也许是更好的方法(这也使得从日志或者通过SHOW PROCESSLIST看SQL更容易了,因为执行日志中的SQL语句的时候不用再切换到特定的数据,数据库名已经包含在SQL语句中了。)"预备"(Preparing)连接时另一个常见问题。Java驱动在预备期间会做大量的操作,其中大部分可以禁用。另一个常见的垃圾查询是SET NAMES UFT8。这是一个错误的方法(它不会改变客户端库的字符集,只会影响服务器的设置)。如果应用在大部分情况使用特定的字符集工作,可以修改配置文件把特定字符集设为默认值,而不需要在每次执行时去做修改。
9.应用使用了连接池吗?这既可能是好事,也可能是坏事。连接池可以帮助限制总的连接数,有大量SQL执行的时候效果不错(Ajax应用是一个典型的例子)。然而连接池也可能有一些副作用,比如说应用的事务、临时表、连接相关的配置项,以及用户自定义变量之间相互干扰等
10.应用是否适用长连接?这可能导致太多连接。通常来说长连接不是个好主意,除非网络环境很慢导致创建连接的开销很大,或者连接只被一或两个很快的SQL使用,或者连接频率很高导致客户端本地端口不够用。如果MySQL的配置正确,也许就不需要长连接了。比如使用skip-name-resolve来避免DNS反向查询,确保thread_cache足够大,并且增加back_log。
11.应用是否在不使用的时候还保持连接打开?如果是这样,尤其是连接到很多服务器时,可能会过多地消耗其他进程所需要地连接。例如,假设你连接到10个MySQL服务器。从一个Apache进程中获取10个连接不是问题,但是任意时刻其中只有1个在真正工作。其他9个大部分时间都处于Sleep状态。如果其中一台服务器变慢了,或者有一个很长地网络请求,其他地服务器就可能因为连接数过多而受到影响。解决方案时控制应用怎么使用连接。例如,可以将操作批量地依次发送到每个MySQL实例,并且在下一次执行SQL前关闭每个连接。如果执行的是比较消耗时间的操作,例如调用Web服务接口,甚至可以先关闭MySQL连接,执行耗时的工作,再重新打开MySQL连接继续在数据库上工作。
长连接和连接池的区别可能使人困惑。长连接可能跟连接池有同样的副作用,因为重用的链接在这两种情况下都是有状态的。然而,连接池通常不会导致服务器连接过多,因为它们会在进程间排队和共享连接。另一方面,长连接是在每个进程基础上创建,不会在进程间共享。
连接池也比共享连接的方式对连接策略有更强的控制力。连接池可以配置为自动扩展,但是通常的实践经验是,当遇到连接池完全占满时,应该将连接请求进行排队而不是扩展连接池。这样做可以在应用服务器上进行排队等,而不是将压力传递到MySQL数据库服务器上导致连接数太多而过载。
有很多方法可以使得查询和连接更快,但是一般的规则是,如果能够直接避免进行查询和连接,肯定比努力提升查询和连接的性能能获得更好的优化结果
Web服务器问题。
Apache是最流行的Web应用服务器软件。它在许多情况下都运行良好, 但如果使用不当也会消耗大量的资源。最常见的问题是保持它的进程的存活(alive)时间过长,或者使用各种不同的用途下混合使用,而不是分别对不同类型的工作进行优化。Apache通常是通过prefork配置来使用mod_php、mod_perl和mod_python模块的。 prefork模式会为每个请求预分配进程。因为PHP、Perl和Python脚本是可以定制化的,每个进程使用50MB或100MB内存的情况并不少见。当一个请求完成后,会释放大部分内存给操作系统,但并不是全部。Apache会保持进程处于打开状态以备后来的请求重用。这意味着,如果下一个请求是请求静态文件,比如一个CSS文件或者一张图片,就会出现用一个占用内存很多的进程来为一个很小的请求服务的情况。这就是使用Apache作为通用Web服务器很危险的原因。它的确是为通用目的而设计的,但如果能够有针对性地使用其长处,会获得更好的性能。
另一个主要的问题是,如果开启了Keep-Alive设置,进程可能很长时间处于繁忙状态。当然,即使没有开启Keep-Alive,某些进程也可能存活很久,"填鸭式"地将内容传给客户端可能导致获取数据很慢,(填鸭式抓取发送在一个客户端发起HTTP请求,但是没有迅速获取结果时。直到客户端获取整个结果,HTTP连接——以及处理地Apache进程——都将保持活跃)。人们常犯的另外一个错误,就是保持那些Apache默认开启的模块不动。最好能够精简Apache的模块,移除掉那些不需要的。这很简单:只需要检查Apache的配置文件,注释掉不想要的模块,然后重启Apache就行。也可以在php.ini文件中删除不适用的PHP模块。
最差情况是,如果用一个通用目的的Apache配置直接用于Web服务,最后很可能产生很多重量级的Apache进程。这将浪费Web服务器的资源。它们还可以保持大量MySQL连接,浪费MySQL的资源。下面是一些可以降低服务器负载的方法.不要使用Apache来做静态内容服务,或者至少和动态服务使用不同的Apache实例。流行的替代品有Nginx和lighttpd.
1.使用缓存代理服务器,比如Squid或者Varnish,放置所有的请求都到达Web服务器。这个层面即使不能缓存所有页面,并且使用像ESI(Edge Side Includes)这样的技术来将部分页面中的小块的动态内容嵌入到静态缓存部分
2.对动态和静态资源都设置过期策略。可以使用Squid这样的缓存代理显式地使内容过期。
有时也许还需要修改应用程序,以便得到更长的过期时间。例如,如果你告诉浏览器永久缓存CSS和JavaScript文件,然后对站点的HTML做了一个修改,这个页面渲染将会出问题。这种情况可以为文件的每个版本设定唯一的文件名。例如,你可以定制网站的发布脚本,复制CSS文件到/css/123_forntpage.css,这里的123就是版本管理器中的版本好。对图片文件的文件名也可以这么做——永不重用文件名,这样页面就不会在升级时出问题,浏览器缓存多久的文件都没问题。
1.不要让Apache填鸭式地服务客户端,这不仅仅会导致慢,也会导致DDos攻击变得简单。硬件负载均衡器通常可以做缓冲,所以Apache可以快速地完成,让负载均衡器通过缓存响应客户端的请求,也可以在应用服务器前端使用Nginx、Squid或者事件驱动模式下的Apache
2.打开gzip压缩。对于现在的CPU而言这样做的代价很小,但是可以节省大部分流量。如果想节省CPU周期,可以使用缓存,或者诸如Nginx这样的轻量级服务器保存压缩过的页面版本
3.不要为用于长距离连接的Apache配置启用Keep-Alive选项,因为这会使得重量级的Apache进程存活很长时间。可以用服务器端的代理来处理保持连接的工作,从而防止Apache进程存活很长时间。可以用服务器端的代理来处理保持连接的工作,从而防止Apache被客户端拖垮。配置Apache到代理之间的连接使用Keep-Alive是可以的,因为代理只会使用很少的Apache连接去获取数据,如图所示。
这些策略可以使Apache进程存活时间变得很短,所以会有比实际需求更多的进程。无论如何,有些操作依然可能导致Apache进程存活时间太长,并且占用大量资源。举个例子,一个请求查询延时非常大的外部资源,例如远程的Web服务,就会出现Apache进程存活时间太长的问题。这种问题通常是无解的
Apache是最流行的Web应用服务器软件。它在许多情况下都运行良好, 但如果使用不当也会消耗大量的资源。最常见的问题是保持它的进程的存活(alive)时间过长,或者使用各种不同的用途下混合使用,而不是分别对不同类型的工作进行优化。Apache通常是通过prefork配置来使用mod_php、mod_perl和mod_python模块的。 prefork模式会为每个请求预分配进程。因为PHP、Perl和Python脚本是可以定制化的,每个进程使用50MB或100MB内存的情况并不少见。当一个请求完成后,会释放大部分内存给操作系统,但并不是全部。Apache会保持进程处于打开状态以备后来的请求重用。这意味着,如果下一个请求是请求静态文件,比如一个CSS文件或者一张图片,就会出现用一个占用内存很多的进程来为一个很小的请求服务的情况。这就是使用Apache作为通用Web服务器很危险的原因。它的确是为通用目的而设计的,但如果能够有针对性地使用其长处,会获得更好的性能。
另一个主要的问题是,如果开启了Keep-Alive设置,进程可能很长时间处于繁忙状态。当然,即使没有开启Keep-Alive,某些进程也可能存活很久,"填鸭式"地将内容传给客户端可能导致获取数据很慢,(填鸭式抓取发送在一个客户端发起HTTP请求,但是没有迅速获取结果时。直到客户端获取整个结果,HTTP连接——以及处理地Apache进程——都将保持活跃)。人们常犯的另外一个错误,就是保持那些Apache默认开启的模块不动。最好能够精简Apache的模块,移除掉那些不需要的。这很简单:只需要检查Apache的配置文件,注释掉不想要的模块,然后重启Apache就行。也可以在php.ini文件中删除不适用的PHP模块。
最差情况是,如果用一个通用目的的Apache配置直接用于Web服务,最后很可能产生很多重量级的Apache进程。这将浪费Web服务器的资源。它们还可以保持大量MySQL连接,浪费MySQL的资源。下面是一些可以降低服务器负载的方法.不要使用Apache来做静态内容服务,或者至少和动态服务使用不同的Apache实例。流行的替代品有Nginx和lighttpd.
1.使用缓存代理服务器,比如Squid或者Varnish,放置所有的请求都到达Web服务器。这个层面即使不能缓存所有页面,并且使用像ESI(Edge Side Includes)这样的技术来将部分页面中的小块的动态内容嵌入到静态缓存部分
2.对动态和静态资源都设置过期策略。可以使用Squid这样的缓存代理显式地使内容过期。
有时也许还需要修改应用程序,以便得到更长的过期时间。例如,如果你告诉浏览器永久缓存CSS和JavaScript文件,然后对站点的HTML做了一个修改,这个页面渲染将会出问题。这种情况可以为文件的每个版本设定唯一的文件名。例如,你可以定制网站的发布脚本,复制CSS文件到/css/123_forntpage.css,这里的123就是版本管理器中的版本好。对图片文件的文件名也可以这么做——永不重用文件名,这样页面就不会在升级时出问题,浏览器缓存多久的文件都没问题。
1.不要让Apache填鸭式地服务客户端,这不仅仅会导致慢,也会导致DDos攻击变得简单。硬件负载均衡器通常可以做缓冲,所以Apache可以快速地完成,让负载均衡器通过缓存响应客户端的请求,也可以在应用服务器前端使用Nginx、Squid或者事件驱动模式下的Apache
2.打开gzip压缩。对于现在的CPU而言这样做的代价很小,但是可以节省大部分流量。如果想节省CPU周期,可以使用缓存,或者诸如Nginx这样的轻量级服务器保存压缩过的页面版本
3.不要为用于长距离连接的Apache配置启用Keep-Alive选项,因为这会使得重量级的Apache进程存活很长时间。可以用服务器端的代理来处理保持连接的工作,从而防止Apache进程存活很长时间。可以用服务器端的代理来处理保持连接的工作,从而防止Apache被客户端拖垮。配置Apache到代理之间的连接使用Keep-Alive是可以的,因为代理只会使用很少的Apache连接去获取数据,如图所示。
这些策略可以使Apache进程存活时间变得很短,所以会有比实际需求更多的进程。无论如何,有些操作依然可能导致Apache进程存活时间太长,并且占用大量资源。举个例子,一个请求查询延时非常大的外部资源,例如远程的Web服务,就会出现Apache进程存活时间太长的问题。这种问题通常是无解的
寻找最优并发度。
每个Web服务器都有一个最佳并发度——就是说,让进程处理请求尽可能快,并且不超过系统负载的最优的并发连接数。这就是前面说的最大系统容量。进行一个简单的测量和建模,或者只是反复试验,就可以找到这个"神奇的数",为此花一些时间是值得的。
对于大流量的网站,Web服务器同一时刻处理上千个连接是很常见的。然而,只有一小部分连接需要进程实时处理。其他的可能是读请求,处理文件上传,填鸭式服务内容,或者只是等待客户端的下一步请求。随者并发的增加,服务器会逐渐到达它的最大吞吐量。在这之后,吞吐量通常开始降低,更重要的是,响应时间(延迟)也会i那位排队而开始增加。为什么会这样呢?试想,如果服务器只有一个CPU,同时接受到100个请求,,会发生什么事情呢?j假设CPU每秒能够处理一个请求。即便理想情况下操作系统没有调度的开销,也没有上下文切换的成本,那100个请求也需要CPU花费整整100s才能完成。
处理请求i的最好办法是什么?可以将其一个个地排到队列中,也可以并行地执行在不同请求之间切换,每次切换都给每个请求相同的服务时间。在这两种情况下,吞吐量都是每秒处理一个请求。然而,如果使用队列(并发=1),平均延时是50s,如果是并发执行(并发=100)则是100s。在实践中,并发执行会使平均延时更高,主要是因为上下文切换的代价。
对于CPU密集型工作负载,最佳并发度等于CPU数量(或者CPU核数)。然而,进程并不总是处于可运行状态的,因为会有一些阻塞式请求,例如IO、数据库查询,以及网络请求。因此,最佳并发度通常会比CPU数量高一些。
可以预测最优并发度,但是这需要精确的分析。尝试不同的并发值,看看在不增加响应时间的情况下的最大吞吐量是多少,或者测量真正的工作负载进行分析,这通常更容易。Percona Toolkit的pt-tcp-model工具可以帮助从TCP转储中测量和建模分析系统的可扩展性和性能特性。
每个Web服务器都有一个最佳并发度——就是说,让进程处理请求尽可能快,并且不超过系统负载的最优的并发连接数。这就是前面说的最大系统容量。进行一个简单的测量和建模,或者只是反复试验,就可以找到这个"神奇的数",为此花一些时间是值得的。
对于大流量的网站,Web服务器同一时刻处理上千个连接是很常见的。然而,只有一小部分连接需要进程实时处理。其他的可能是读请求,处理文件上传,填鸭式服务内容,或者只是等待客户端的下一步请求。随者并发的增加,服务器会逐渐到达它的最大吞吐量。在这之后,吞吐量通常开始降低,更重要的是,响应时间(延迟)也会i那位排队而开始增加。为什么会这样呢?试想,如果服务器只有一个CPU,同时接受到100个请求,,会发生什么事情呢?j假设CPU每秒能够处理一个请求。即便理想情况下操作系统没有调度的开销,也没有上下文切换的成本,那100个请求也需要CPU花费整整100s才能完成。
处理请求i的最好办法是什么?可以将其一个个地排到队列中,也可以并行地执行在不同请求之间切换,每次切换都给每个请求相同的服务时间。在这两种情况下,吞吐量都是每秒处理一个请求。然而,如果使用队列(并发=1),平均延时是50s,如果是并发执行(并发=100)则是100s。在实践中,并发执行会使平均延时更高,主要是因为上下文切换的代价。
对于CPU密集型工作负载,最佳并发度等于CPU数量(或者CPU核数)。然而,进程并不总是处于可运行状态的,因为会有一些阻塞式请求,例如IO、数据库查询,以及网络请求。因此,最佳并发度通常会比CPU数量高一些。
可以预测最优并发度,但是这需要精确的分析。尝试不同的并发值,看看在不增加响应时间的情况下的最大吞吐量是多少,或者测量真正的工作负载进行分析,这通常更容易。Percona Toolkit的pt-tcp-model工具可以帮助从TCP转储中测量和建模分析系统的可扩展性和性能特性。
缓存。
缓存对高负载应用来说至关重要。一个典型的Web应用程序会提高大量的内容,直接生成这些内容的成本比采用缓存要高得多(包括检查和缓存超时的开销),所以采用缓存通常可以获得数量级的性能提升。诀窍是找到正确的粒度和缓存过期策略组合。另外也需要决定哪些内容适合缓存,缓存在哪里。
典型的高负载应用会有很多层缓存。缓存并不仅仅发生在服务器上,而是在每一个环节,甚至包括用户的Web浏览器(这就是内容过期头的用处)。通常,缓存越接近客户端,就越节省资源并且效率更高。从浏览器缓存提供一张图片比从Web服务器的内容获取快得多,而从服务器的内存读取又比从服务器的磁盘上读取好得多。每种类型的缓存有其不一样的特点,例如容量和延时。
可以把缓存分成两大类:被动缓存和主动缓存。被动缓存除了存储和返回数据外不做任何事情。当从被动缓存请求一些内容时,要么可以得到结果,要么得到"结果不存在"。被动缓存的一个典型例子时memcached.相比之下,主动缓存会在访问未命中时做一些额外的工作。通常会将请求转发给应用的其他部分来生成请求结果,然后存储该结果并返回给应用。Squid缓存代理服务器就是一个主动缓存。设计应用程序时,通常希望缓存是主动的(也可以叫做透明的),因为它们对应用隐藏了检查——生成——存储这个逻辑过程。也可以在被动缓存的前面构建一个主动缓存。
缓存对高负载应用来说至关重要。一个典型的Web应用程序会提高大量的内容,直接生成这些内容的成本比采用缓存要高得多(包括检查和缓存超时的开销),所以采用缓存通常可以获得数量级的性能提升。诀窍是找到正确的粒度和缓存过期策略组合。另外也需要决定哪些内容适合缓存,缓存在哪里。
典型的高负载应用会有很多层缓存。缓存并不仅仅发生在服务器上,而是在每一个环节,甚至包括用户的Web浏览器(这就是内容过期头的用处)。通常,缓存越接近客户端,就越节省资源并且效率更高。从浏览器缓存提供一张图片比从Web服务器的内容获取快得多,而从服务器的内存读取又比从服务器的磁盘上读取好得多。每种类型的缓存有其不一样的特点,例如容量和延时。
可以把缓存分成两大类:被动缓存和主动缓存。被动缓存除了存储和返回数据外不做任何事情。当从被动缓存请求一些内容时,要么可以得到结果,要么得到"结果不存在"。被动缓存的一个典型例子时memcached.相比之下,主动缓存会在访问未命中时做一些额外的工作。通常会将请求转发给应用的其他部分来生成请求结果,然后存储该结果并返回给应用。Squid缓存代理服务器就是一个主动缓存。设计应用程序时,通常希望缓存是主动的(也可以叫做透明的),因为它们对应用隐藏了检查——生成——存储这个逻辑过程。也可以在被动缓存的前面构建一个主动缓存。
应用层以下的缓存。
MySQL服务器有自己的内部缓存,但也可以构建你自己的缓存和汇总表。可以对缓存表量身定制,使他们最有效地过滤、排序、与其它表关联、计数,或者用于其他用途。缓存表也比许多应用层缓存更持久,因为在服务器重启后它们还存在。
缓存并不总是有用。
必需确认缓存真的可以提升性能,因为有时候缓存可能没有任何帮助。例如,在实践中发现从Nginx的内存中获取内容比从缓存中代理中获取要快,如果代理的缓存在磁盘上则尤其如此.
原因很简单:缓存自身也有一些开销。比如检查缓存是否存在,如果命中则直接从缓存中返回数据。另外将缓存对象失效或者写入新的缓存对象都会有开销。缓存只在这些开销比没有缓存的情况下生成和提供数据的开销少时才有用。如果直到所有这些操作的开销,就可以计算出缓存能提供多少帮助。没有缓存时的开销就是每个请求生成数据的开销。有缓存的开销是检查缓存的开销加上缓存不命中的概率乘以生成数据的开销,再加上缓存命中的概率乘以缓存提供数据的开销。如果有缓存时的开销比没有时要低,则说明缓存可能有用,但依然不能保证。还要记住,就像从Nginx的内存中获取数据比从代理在磁盘中的缓存获取要好一样,有些缓存的开销比另外一些要低
MySQL服务器有自己的内部缓存,但也可以构建你自己的缓存和汇总表。可以对缓存表量身定制,使他们最有效地过滤、排序、与其它表关联、计数,或者用于其他用途。缓存表也比许多应用层缓存更持久,因为在服务器重启后它们还存在。
缓存并不总是有用。
必需确认缓存真的可以提升性能,因为有时候缓存可能没有任何帮助。例如,在实践中发现从Nginx的内存中获取内容比从缓存中代理中获取要快,如果代理的缓存在磁盘上则尤其如此.
原因很简单:缓存自身也有一些开销。比如检查缓存是否存在,如果命中则直接从缓存中返回数据。另外将缓存对象失效或者写入新的缓存对象都会有开销。缓存只在这些开销比没有缓存的情况下生成和提供数据的开销少时才有用。如果直到所有这些操作的开销,就可以计算出缓存能提供多少帮助。没有缓存时的开销就是每个请求生成数据的开销。有缓存的开销是检查缓存的开销加上缓存不命中的概率乘以生成数据的开销,再加上缓存命中的概率乘以缓存提供数据的开销。如果有缓存时的开销比没有时要低,则说明缓存可能有用,但依然不能保证。还要记住,就像从Nginx的内存中获取数据比从代理在磁盘中的缓存获取要好一样,有些缓存的开销比另外一些要低
应用层缓存。
应用层缓存通常在同一台机器的内存中存储数据,或者通过网络存在另一台机器的内存中。因为应用可以缓存部分计算结果,所以应用缓存可能比更低层次的缓存更有效。因此应用层缓存可以节省两方面的工作:获取数据以及基于这些数据进行计算。一个很好的例子是HTML文本块。应用程序可以生成例如头条新闻的标题这样的HTML片段,并且做好缓存。后续的页面试图就可以简单地插入这个缓存过的文本。一般来说,在缓存数据前对数据做的处理越多,缓存命中节省的工作越多。
但应用层缓存也有缺点,那就是缓存命中率可能更低,并且可能使用较多的内存。假设需要50个不同版本的头条新闻标题,以使不同地区生活的用户看到不同的内容,那就需要足够的内存去存储全部50个版本,任何给定版本的标题命中次数都会更少,并且失效策略也会更加复杂。
应用缓存有许多种,下面是其中的一小部分:
应用层缓存通常在同一台机器的内存中存储数据,或者通过网络存在另一台机器的内存中。因为应用可以缓存部分计算结果,所以应用缓存可能比更低层次的缓存更有效。因此应用层缓存可以节省两方面的工作:获取数据以及基于这些数据进行计算。一个很好的例子是HTML文本块。应用程序可以生成例如头条新闻的标题这样的HTML片段,并且做好缓存。后续的页面试图就可以简单地插入这个缓存过的文本。一般来说,在缓存数据前对数据做的处理越多,缓存命中节省的工作越多。
但应用层缓存也有缺点,那就是缓存命中率可能更低,并且可能使用较多的内存。假设需要50个不同版本的头条新闻标题,以使不同地区生活的用户看到不同的内容,那就需要足够的内存去存储全部50个版本,任何给定版本的标题命中次数都会更少,并且失效策略也会更加复杂。
应用缓存有许多种,下面是其中的一小部分:
1.本地缓存
这种缓存通常很小,只在进程处理请求期间存在于进程内存中。本地缓存可以有效地避免对某些资源的重复请求。这种类型的缓存技术并不复杂:通常只是应用代码中的一个变量或者哈希表。这种类型的缓存技术并不复杂:通常只是应用代码中的一个变量或者哈希表。例如,假设需要显式一个用户名,而且已经直到其ID,就可以创建一个get_name_from_id()函数并且在其中增加缓存。像下面这样:
```php
<?php
function get_name_from_id($user_id) {
static $name; // static makes the variable persist
if (!$name) {
// fetch name from database
}
return $name;
}
?>
```
如果使用的是Perl,那么Memoize模块是函数调用结果标准的缓存方式:
```perl
use Memoize qw(memoize);
memoize 'get_name_from_id';
sub get_name_from_id {
my ($user_id) = @_;
my $name = # get name from database
return $name;
}
```
这种缓存通常很小,只在进程处理请求期间存在于进程内存中。本地缓存可以有效地避免对某些资源的重复请求。这种类型的缓存技术并不复杂:通常只是应用代码中的一个变量或者哈希表。这种类型的缓存技术并不复杂:通常只是应用代码中的一个变量或者哈希表。例如,假设需要显式一个用户名,而且已经直到其ID,就可以创建一个get_name_from_id()函数并且在其中增加缓存。像下面这样:
```php
<?php
function get_name_from_id($user_id) {
static $name; // static makes the variable persist
if (!$name) {
// fetch name from database
}
return $name;
}
?>
```
如果使用的是Perl,那么Memoize模块是函数调用结果标准的缓存方式:
```perl
use Memoize qw(memoize);
memoize 'get_name_from_id';
sub get_name_from_id {
my ($user_id) = @_;
my $name = # get name from database
return $name;
}
```
2.本地共享内存缓存。
这种缓存一般是中等大小(几个GB),快速,难以在多台机器间同步。它们对小型的半静态位数据比较合适。例如每个州的城市列表,分片数据存储的分区函数(映射表),或者使用存活时间(TTL)策略进行失效的数据等。共享内存最大的好处是访问非常快——通常比其他任何远程缓存访问都要快不少
3.分布式内存缓存。
最常见的分布式内存缓存的例子是memcached.分布式缓存比本地共享内存缓存要大得多,增长也容易。缓存中创建的数据每一个比特都只有一份副本,这样既不会浪费内存,也不会因为相同的数据存在不同的地方而引入一致性问题。分布式内存非常适合存储共享对象,例如用户资料,评论,以及HTML片段。分布式缓存比本地共享缓存的延时要高得多,所以最高效的使用方法是批量进行多个获取操作(例如,在一次循环中获取多个对象)。分布式缓存还需要考虑怎么增加更多的节点,以及某个节点崩溃了怎么处理。对于这两个场景,应用程序必需决定在节点间怎么分布或重分布缓存对象。当缓存集群增加或减少一台服务器时,一致性缓存对避免性能问题而言是非常重要的。
4.磁盘上的缓存。
磁盘是很慢的,所以缓存在磁盘上的最好是持久化对象,很难全部装进内存的对象,或者静态内容(例如预处理的自定义图片)。对于磁盘上的缓存和Web服务器,一个非常有用的技巧是使用404错误处理机制来捕捉缓存未命中的情况。假设Web应用要在头部展示一张基于用于名("欢迎回来,John")的自定义图片。并且通过/images/welcomeback/john.jpg这样的访问路径引用此图片。如果图片不存在,将会导致一个404错误,并且触发上述错误处理。这个错误处理可以生成图片,在磁盘上存储它,然后发出一个重定向或者将该图片传回浏览器。后续的请求只需要从文件中直接返回图片。有很多类型的内容可以使用这种技巧。例如,不用再将最近的标题作为HTML部分进行缓存,可以在Javascript文件中存储这些东西,然后在网页头重引用这个文件:latest_headlines.js.缓存失效很简单:删除文件即可。可以通过执行一个删除N分钟前所创建的文件的定时任务,来实现TTL失效。如果想要限制缓存大小,也可以通过按最近访问时间排序来删除文件,从而实现最近最少使用(LRU)失效算法。如果失效策略是基于最近访问时间,则必须在文件系统挂载参数中打开访问时间记录(忽略noatime选项即可),如果这么做,应该使用内存文件系统来避免大量磁盘操作。
这种缓存一般是中等大小(几个GB),快速,难以在多台机器间同步。它们对小型的半静态位数据比较合适。例如每个州的城市列表,分片数据存储的分区函数(映射表),或者使用存活时间(TTL)策略进行失效的数据等。共享内存最大的好处是访问非常快——通常比其他任何远程缓存访问都要快不少
3.分布式内存缓存。
最常见的分布式内存缓存的例子是memcached.分布式缓存比本地共享内存缓存要大得多,增长也容易。缓存中创建的数据每一个比特都只有一份副本,这样既不会浪费内存,也不会因为相同的数据存在不同的地方而引入一致性问题。分布式内存非常适合存储共享对象,例如用户资料,评论,以及HTML片段。分布式缓存比本地共享缓存的延时要高得多,所以最高效的使用方法是批量进行多个获取操作(例如,在一次循环中获取多个对象)。分布式缓存还需要考虑怎么增加更多的节点,以及某个节点崩溃了怎么处理。对于这两个场景,应用程序必需决定在节点间怎么分布或重分布缓存对象。当缓存集群增加或减少一台服务器时,一致性缓存对避免性能问题而言是非常重要的。
4.磁盘上的缓存。
磁盘是很慢的,所以缓存在磁盘上的最好是持久化对象,很难全部装进内存的对象,或者静态内容(例如预处理的自定义图片)。对于磁盘上的缓存和Web服务器,一个非常有用的技巧是使用404错误处理机制来捕捉缓存未命中的情况。假设Web应用要在头部展示一张基于用于名("欢迎回来,John")的自定义图片。并且通过/images/welcomeback/john.jpg这样的访问路径引用此图片。如果图片不存在,将会导致一个404错误,并且触发上述错误处理。这个错误处理可以生成图片,在磁盘上存储它,然后发出一个重定向或者将该图片传回浏览器。后续的请求只需要从文件中直接返回图片。有很多类型的内容可以使用这种技巧。例如,不用再将最近的标题作为HTML部分进行缓存,可以在Javascript文件中存储这些东西,然后在网页头重引用这个文件:latest_headlines.js.缓存失效很简单:删除文件即可。可以通过执行一个删除N分钟前所创建的文件的定时任务,来实现TTL失效。如果想要限制缓存大小,也可以通过按最近访问时间排序来删除文件,从而实现最近最少使用(LRU)失效算法。如果失效策略是基于最近访问时间,则必须在文件系统挂载参数中打开访问时间记录(忽略noatime选项即可),如果这么做,应该使用内存文件系统来避免大量磁盘操作。
缓存控制策略。
缓存也有像反范式化数据库设计一样的问题:重复数据,也就是说有多个地方需要更新数据,所以需要想办法避免读到脏数据。下面是一些最常见的缓存控制策略:
1.TTL(time to live,存活时间)
缓存对象存储时设置一个过期时间;可以通过清理进程在达到过期时间后删掉最想,或者先留着直到下次访问时再清理(清理后需要使用新的版本替换)。对于数据很少变更或者没有新数据的情况,这是最好的失效策略
2.显式失效
如果不能接受脏数据,那么进程在更新原始数据时需要同时使缓存失效。这种策略有两个写——失效和写——更新。写——失效策略很简单:只需要标记缓存数据已经过期(是否清理缓存数据是可选的)。写——更新策略需要多做一些工作,因为在更新数据时就需要替换掉缓存项。无论如何,这都是非常有益的,特别是当生成缓存数据代价很昂贵时(写线程也许已经做了)。如果更新缓存数据,后续的请求将不在需要等待应用来生成。如果在后台做失效处理,例如基于TTL的失效,就可以在一个从用户请求完全分离出来的进程中生成失效数据的新版本
3.读时失效
在更改旧数据时,为了避免要同时失效派生出来的脏数据,可以在缓存中保存一些信息,当从缓存读数据时可以利用这些信息判断数据是否已经失效。和显式失效策略相比,这样做有很大的优势:成本固定且可以分散在不同时间内。假设要失效一个有一百万缓存对象依赖的对象,如果采用写时失效,需要一次在缓存中失效一百万个对象,即使有高效的方法来找到这些对象,也可能需要很长的时间才能完成。如果采用读时失效,写操作可以立即完成,但后续这一百万对象的读操作可能会有略微的延迟。这样就把失效一百万对象的开销分散了,并且可以帮助避免出现负载冲高和延迟增大的峰值。
一种最简单的读时失效的办法时采用对象版本控制。使用这种方法,在缓存中存储一个对象时,也可以存储对象所依赖的数据的当前版本号或者时间戳。例如,假设要缓存用户博客日志的统计信息,包括用户所发表的博客数。当缓存blog_stats对象时,也可以同一时间存储用户的当前版本号,因为该统计信息是依赖于用户的。
不管什么时候更新依赖于用户的数据,都需要更新用户的版本号,假设用户的版本号初始为0,并且由你来生成和缓存统计信息。当用户发表了一篇博客,就增加用户的版本好到1(当然也要同时存储这篇博客,尽管在这个例子并没有用到博客数据)。然后当需要显示统计数据的时候,可以对缓存中blog_stats对象的版本与缓存的用户版本进行比较。因为用户的版本比对象的版本高,所以可以直到缓存的统计信息已经过期了,需要重新计算。
这是一个非常粗糙的内容失效方式,因为它假设依赖于用户的每一个比特的数据与所有其他数据都有交互。但这个假设并不总是成立的。举个例子,如果一个用户对一篇博客做了编辑,你也需要增加用户的版本号,着就会导致存储的统计信息失效,而实际上统计信息(发表的博客数)并没真的改变。这个取舍是很简单的。一个简单的缓存失效策略不只是更容易创建,也可能更加高效。
对象版本控制是一种简单的标记缓存方法,它可以处理更复杂的依赖关系。一个标记的缓存可以识别不同类型的依赖,并且分别跟踪每个依赖的版本。在前面的图书俱乐部的例子中,你可以通过下面的版本好标记评论,使缓存的评论依赖于用户的版本和书的版本:user_ver=1234和book_ver=5678.任一版本号变了,都应该刷新缓存的评论
缓存也有像反范式化数据库设计一样的问题:重复数据,也就是说有多个地方需要更新数据,所以需要想办法避免读到脏数据。下面是一些最常见的缓存控制策略:
1.TTL(time to live,存活时间)
缓存对象存储时设置一个过期时间;可以通过清理进程在达到过期时间后删掉最想,或者先留着直到下次访问时再清理(清理后需要使用新的版本替换)。对于数据很少变更或者没有新数据的情况,这是最好的失效策略
2.显式失效
如果不能接受脏数据,那么进程在更新原始数据时需要同时使缓存失效。这种策略有两个写——失效和写——更新。写——失效策略很简单:只需要标记缓存数据已经过期(是否清理缓存数据是可选的)。写——更新策略需要多做一些工作,因为在更新数据时就需要替换掉缓存项。无论如何,这都是非常有益的,特别是当生成缓存数据代价很昂贵时(写线程也许已经做了)。如果更新缓存数据,后续的请求将不在需要等待应用来生成。如果在后台做失效处理,例如基于TTL的失效,就可以在一个从用户请求完全分离出来的进程中生成失效数据的新版本
3.读时失效
在更改旧数据时,为了避免要同时失效派生出来的脏数据,可以在缓存中保存一些信息,当从缓存读数据时可以利用这些信息判断数据是否已经失效。和显式失效策略相比,这样做有很大的优势:成本固定且可以分散在不同时间内。假设要失效一个有一百万缓存对象依赖的对象,如果采用写时失效,需要一次在缓存中失效一百万个对象,即使有高效的方法来找到这些对象,也可能需要很长的时间才能完成。如果采用读时失效,写操作可以立即完成,但后续这一百万对象的读操作可能会有略微的延迟。这样就把失效一百万对象的开销分散了,并且可以帮助避免出现负载冲高和延迟增大的峰值。
一种最简单的读时失效的办法时采用对象版本控制。使用这种方法,在缓存中存储一个对象时,也可以存储对象所依赖的数据的当前版本号或者时间戳。例如,假设要缓存用户博客日志的统计信息,包括用户所发表的博客数。当缓存blog_stats对象时,也可以同一时间存储用户的当前版本号,因为该统计信息是依赖于用户的。
不管什么时候更新依赖于用户的数据,都需要更新用户的版本号,假设用户的版本号初始为0,并且由你来生成和缓存统计信息。当用户发表了一篇博客,就增加用户的版本好到1(当然也要同时存储这篇博客,尽管在这个例子并没有用到博客数据)。然后当需要显示统计数据的时候,可以对缓存中blog_stats对象的版本与缓存的用户版本进行比较。因为用户的版本比对象的版本高,所以可以直到缓存的统计信息已经过期了,需要重新计算。
这是一个非常粗糙的内容失效方式,因为它假设依赖于用户的每一个比特的数据与所有其他数据都有交互。但这个假设并不总是成立的。举个例子,如果一个用户对一篇博客做了编辑,你也需要增加用户的版本号,着就会导致存储的统计信息失效,而实际上统计信息(发表的博客数)并没真的改变。这个取舍是很简单的。一个简单的缓存失效策略不只是更容易创建,也可能更加高效。
对象版本控制是一种简单的标记缓存方法,它可以处理更复杂的依赖关系。一个标记的缓存可以识别不同类型的依赖,并且分别跟踪每个依赖的版本。在前面的图书俱乐部的例子中,你可以通过下面的版本好标记评论,使缓存的评论依赖于用户的版本和书的版本:user_ver=1234和book_ver=5678.任一版本号变了,都应该刷新缓存的评论
缓存对象分层。
分层缓存对象对检索、失效和内存利用都有帮助。相对于只缓存对象,也可以缓存对象的ID、对象的ID组等通常需要一起检索的数据。电子商务网站的搜索结果是这种技术很好的例子。一次搜索可能返回一个匹配产品的列表,包括名称、描述、缩略图,以及价格。缓存整个列表的效率很低:其他的搜索也可能会包含一些相同的产品,这就会导致数据重复,并且浪费内存。这种策略也使得当一个产品价格变动时,找出并失效搜索结果变得很困难,因为你必须查看每个列表,找到哪些列表包含了更新过的产品。
可以缓存关于搜索的最小信息,而不必缓存整个列表,例如返回结果的数量以及列表中的产品ID。然后可以再单独缓存每个产品。这样做可以解决 两个问题:不会重复存放任何结果数据,也更容易在失效产品的粒度上去失效缓存。缺点则是,相对于一次性获得整个搜索结果,必须在缓存中检索多个对象。然而不管怎么说,为搜索结果缓存产品ID的列表都是更有效的做法。先在一个缓存命中返回ID的列表,再使用这些ID去请求缓存获得产品信息。如果缓存允许在一次调用里返回多个结果,第二次请求就可以返回多个产品(memcached通过mget()调用来支持)
如果使用不当,这种方法可能会导致奇怪的结果。假设使用TTL策略来失效搜索结果,并且当产品变更时显式地区失效单个产品。现在想象以下,一个产品地描述发生了变化,不再包含搜索中匹配地关键字,但是搜索结果地缓存还没有过期失效,此时用户就会看到错误地搜索结果,因为缓存的搜索结果将会引用这个变化了的产品,即使它不再包含匹配搜索的关键字。
对于大多数应用程序来说,这不是问题。如果应用程序不能容忍这种情况,可以使用基于版本的缓存,并在执行搜索时在结果中存储产品的版本好。当发现搜索结果在缓存中时,可以将当前搜索结果的版本号和搜索结果中的每个产品的版本号做比较。如果发现任何一个产品的版本数据不一致,可以重新搜索并且重新缓存结果。这对理解远程缓存访问的花销是多么昂贵非常重要。虽然缓存很快,也可以避免很多工作,但在LAN环境下网络往返缓存服务器通常也需要0.3ms左右。我们见过很多案例,复杂的网页需要一千次左右的缓存访问来组合页面结果,这将会耗费3s左右的网络延时,意味着你的页面可能慢得不可接受,即使它甚至不需要访问数据库!因此,在这种情况下对缓存使用批量获取调用是非常重要的。对缓存分层,采用小一些的本地缓存,也可能获得很大的收益
分层缓存对象对检索、失效和内存利用都有帮助。相对于只缓存对象,也可以缓存对象的ID、对象的ID组等通常需要一起检索的数据。电子商务网站的搜索结果是这种技术很好的例子。一次搜索可能返回一个匹配产品的列表,包括名称、描述、缩略图,以及价格。缓存整个列表的效率很低:其他的搜索也可能会包含一些相同的产品,这就会导致数据重复,并且浪费内存。这种策略也使得当一个产品价格变动时,找出并失效搜索结果变得很困难,因为你必须查看每个列表,找到哪些列表包含了更新过的产品。
可以缓存关于搜索的最小信息,而不必缓存整个列表,例如返回结果的数量以及列表中的产品ID。然后可以再单独缓存每个产品。这样做可以解决 两个问题:不会重复存放任何结果数据,也更容易在失效产品的粒度上去失效缓存。缺点则是,相对于一次性获得整个搜索结果,必须在缓存中检索多个对象。然而不管怎么说,为搜索结果缓存产品ID的列表都是更有效的做法。先在一个缓存命中返回ID的列表,再使用这些ID去请求缓存获得产品信息。如果缓存允许在一次调用里返回多个结果,第二次请求就可以返回多个产品(memcached通过mget()调用来支持)
如果使用不当,这种方法可能会导致奇怪的结果。假设使用TTL策略来失效搜索结果,并且当产品变更时显式地区失效单个产品。现在想象以下,一个产品地描述发生了变化,不再包含搜索中匹配地关键字,但是搜索结果地缓存还没有过期失效,此时用户就会看到错误地搜索结果,因为缓存的搜索结果将会引用这个变化了的产品,即使它不再包含匹配搜索的关键字。
对于大多数应用程序来说,这不是问题。如果应用程序不能容忍这种情况,可以使用基于版本的缓存,并在执行搜索时在结果中存储产品的版本好。当发现搜索结果在缓存中时,可以将当前搜索结果的版本号和搜索结果中的每个产品的版本号做比较。如果发现任何一个产品的版本数据不一致,可以重新搜索并且重新缓存结果。这对理解远程缓存访问的花销是多么昂贵非常重要。虽然缓存很快,也可以避免很多工作,但在LAN环境下网络往返缓存服务器通常也需要0.3ms左右。我们见过很多案例,复杂的网页需要一千次左右的缓存访问来组合页面结果,这将会耗费3s左右的网络延时,意味着你的页面可能慢得不可接受,即使它甚至不需要访问数据库!因此,在这种情况下对缓存使用批量获取调用是非常重要的。对缓存分层,采用小一些的本地缓存,也可能获得很大的收益
预生成内容。
除了在应用程序几倍缓存位数据,也可以在后台预先请求一些页面,并且将结果存为静态页面。如果页面是动态的,也可以预先生成页面的部分内容,然后使用像服务端包含(SSI)这样的技术创建最终页面。这有助于减小生成预生成内容的大小和开销,否则可能在将不同部分拼装到最终页面的时候,由于微小的变化产生大量的重复内容。几乎可以对任何类型的缓存使用预生成策略,包括memcached.预生成内容有几个重要的好处。
1.应用代码没有复杂的命中和未命中处理路径
2.当未命中的处理路径慢得不可接受时,这种方案可以很好地工作,因为它保证了未命中的情况永远不会发生。实际上,在任何时候设计任何类型的缓存系统,总是应该考虑未命中的路径有多曼。如果平均性能提升很大,但是因为要预生成缓存内容,偶尔有一些请求变得非常缓慢,这时可能比不用缓存还糟糕。性能的持续稳定通常跟高性能一样重要
3.预生成可以避免在缓存未命中时异常的雪崩效应
缓存预生成号的内容可能占用大量空间,并且并不总能预生成所有东西。无论是哪种形式的缓存,需要预生成的内容中最重要的部分是哪些最经常被请求,或者生成的成本最高的,所以可以通过404错误处理机制来按需生成。预生成的内容有时候也可以从内存文件系统中获益,因为可以避免磁盘IO
除了在应用程序几倍缓存位数据,也可以在后台预先请求一些页面,并且将结果存为静态页面。如果页面是动态的,也可以预先生成页面的部分内容,然后使用像服务端包含(SSI)这样的技术创建最终页面。这有助于减小生成预生成内容的大小和开销,否则可能在将不同部分拼装到最终页面的时候,由于微小的变化产生大量的重复内容。几乎可以对任何类型的缓存使用预生成策略,包括memcached.预生成内容有几个重要的好处。
1.应用代码没有复杂的命中和未命中处理路径
2.当未命中的处理路径慢得不可接受时,这种方案可以很好地工作,因为它保证了未命中的情况永远不会发生。实际上,在任何时候设计任何类型的缓存系统,总是应该考虑未命中的路径有多曼。如果平均性能提升很大,但是因为要预生成缓存内容,偶尔有一些请求变得非常缓慢,这时可能比不用缓存还糟糕。性能的持续稳定通常跟高性能一样重要
3.预生成可以避免在缓存未命中时异常的雪崩效应
缓存预生成号的内容可能占用大量空间,并且并不总能预生成所有东西。无论是哪种形式的缓存,需要预生成的内容中最重要的部分是哪些最经常被请求,或者生成的成本最高的,所以可以通过404错误处理机制来按需生成。预生成的内容有时候也可以从内存文件系统中获益,因为可以避免磁盘IO
作为基础组件的缓存。
缓存有可能成为基础设施的重要组成部分。也很容易陷入一个陷阱,认为缓存虽然很好用,但并不是重要到非有不可得东西。你也许会辩驳,如果缓存服务器宕机或者缓存被清空,请求也可以直接落在数据库上,系统依然可以正常运行。如果是刚刚将缓存加入应用系统,这也许是对的,但缓存的加入可以使得在应用压力显著增长时不需要对系统的某些部分同比增加资源投入——通常是数据部分。因此,系统可能慢慢地变得对缓存非常依赖,却没有被发觉。
例如,如果高速缓存命中率是90%,当由于某种原因失去缓存,数据库上的负载将增加到原来的10倍。这很可能导致压力超过数据库服务器的性能极限。为了避免像这样的意外,应该设计一些高可用性缓存(包括数据和服务)的解决方案,或者至少是评估好禁用缓存或丢失缓存时的性能影响。比如说可以设计应用在遇到这样的情况时能够进行降级处理。
缓存有可能成为基础设施的重要组成部分。也很容易陷入一个陷阱,认为缓存虽然很好用,但并不是重要到非有不可得东西。你也许会辩驳,如果缓存服务器宕机或者缓存被清空,请求也可以直接落在数据库上,系统依然可以正常运行。如果是刚刚将缓存加入应用系统,这也许是对的,但缓存的加入可以使得在应用压力显著增长时不需要对系统的某些部分同比增加资源投入——通常是数据部分。因此,系统可能慢慢地变得对缓存非常依赖,却没有被发觉。
例如,如果高速缓存命中率是90%,当由于某种原因失去缓存,数据库上的负载将增加到原来的10倍。这很可能导致压力超过数据库服务器的性能极限。为了避免像这样的意外,应该设计一些高可用性缓存(包括数据和服务)的解决方案,或者至少是评估好禁用缓存或丢失缓存时的性能影响。比如说可以设计应用在遇到这样的情况时能够进行降级处理。
使用HandlerSocket和memcached。
相对于数据存储在MySQL中而缓存在MySQL外部的缓存方案,另外有一种替代方法是为MySQL创建一个更快的访问路径,直接绕过使用缓存。对于小儿简单的查询语句,很大一部分开销来自解析SQL,检查权限,生成执行计划,等等。如果这种开销可以避免,MySQL在处理简单查询时将非常快。目前有两个解决方案可以用所谓的NoSQL方式访问MySQL。第一种时一个后台进程插件,成为HandlerSocket,由DeNA开发,这是日本最大的社交网站。HandlerSocket允许通过一个简单的协议访问InnoDB Handler对象。实际上,也就是绕过了上层的服务器层,通过网络直接连接到了InnoDB引擎层。有报告称HandlerSocket每秒可以执行超过750 000条查询。Percona Server分支中自带了HandlerSocket插件引擎层。第二个方案时通过memcached协议访问InnoDB。MySQL5.6的实验室版本有一个插件提供了这个接口。两种方法都有一些限制——特别是memcached的方法,这种方法对很多访问数据的方法都不支持。为什么会希望采用SQL以外的办法访问数据呢?除了速度之外,最大的可能是简单。这样做最大的好处是可以摆脱缓存,以及所有的失效逻辑,还有为它们服务的额外的基础设施
相对于数据存储在MySQL中而缓存在MySQL外部的缓存方案,另外有一种替代方法是为MySQL创建一个更快的访问路径,直接绕过使用缓存。对于小儿简单的查询语句,很大一部分开销来自解析SQL,检查权限,生成执行计划,等等。如果这种开销可以避免,MySQL在处理简单查询时将非常快。目前有两个解决方案可以用所谓的NoSQL方式访问MySQL。第一种时一个后台进程插件,成为HandlerSocket,由DeNA开发,这是日本最大的社交网站。HandlerSocket允许通过一个简单的协议访问InnoDB Handler对象。实际上,也就是绕过了上层的服务器层,通过网络直接连接到了InnoDB引擎层。有报告称HandlerSocket每秒可以执行超过750 000条查询。Percona Server分支中自带了HandlerSocket插件引擎层。第二个方案时通过memcached协议访问InnoDB。MySQL5.6的实验室版本有一个插件提供了这个接口。两种方法都有一些限制——特别是memcached的方法,这种方法对很多访问数据的方法都不支持。为什么会希望采用SQL以外的办法访问数据呢?除了速度之外,最大的可能是简单。这样做最大的好处是可以摆脱缓存,以及所有的失效逻辑,还有为它们服务的额外的基础设施
拓展MySQL。
如果MySQL不能做你需要的事,一种可能是拓展其功能。在这里不会展示如何去做到这一点,但会提供一些可能的方向。如果你对进一步探索有兴趣,那么有很多很好的在线资源。当我们说"MySQL不能做你需要的事",我们指的是两件事情:MySQL根本做不到这一点或者MySQL可以做到,但是只能通过缓慢或笨拙的方法,总之做得不够好。无论哪个都是需要对MySQL拓展的原因。好消息事,MySQL已经越来越模块化和通用。存储引擎是拓展MySQL的一个很好的方式。Brian Aker已经写了一个存储引擎的框架,还有一系列介绍有关如何开始编写自己的存储引擎的文章。这是目前几个主要的第三方存储引擎的基础。许多公司都编写了它们自己的内部存储引擎。例如,一些社交网站公司使用了特殊的为社交图形操作设计的存储引擎,我们还知道有个公司定制了一个用于模糊搜索的引擎。写一个简单的自定义存储引擎并不难。还可以使用存储引擎作为另一个软件的接口。Sphinx引擎就是一个很好的例子,该引擎是Spinx全文检索软件的接口
如果MySQL不能做你需要的事,一种可能是拓展其功能。在这里不会展示如何去做到这一点,但会提供一些可能的方向。如果你对进一步探索有兴趣,那么有很多很好的在线资源。当我们说"MySQL不能做你需要的事",我们指的是两件事情:MySQL根本做不到这一点或者MySQL可以做到,但是只能通过缓慢或笨拙的方法,总之做得不够好。无论哪个都是需要对MySQL拓展的原因。好消息事,MySQL已经越来越模块化和通用。存储引擎是拓展MySQL的一个很好的方式。Brian Aker已经写了一个存储引擎的框架,还有一系列介绍有关如何开始编写自己的存储引擎的文章。这是目前几个主要的第三方存储引擎的基础。许多公司都编写了它们自己的内部存储引擎。例如,一些社交网站公司使用了特殊的为社交图形操作设计的存储引擎,我们还知道有个公司定制了一个用于模糊搜索的引擎。写一个简单的自定义存储引擎并不难。还可以使用存储引擎作为另一个软件的接口。Sphinx引擎就是一个很好的例子,该引擎是Spinx全文检索软件的接口
MySQL的替代品。
MySQL并不是适合每一个场景的解决方案。有些工作通常在MySQL以外来做会更好,即使MySQL理论上也可以做到。最明显的一个例子是在传统的文件系统中存储文件,而不是在表中。图像文件是静丹案例:虽然可以把它们放到一个BLOB列,但这通常不是个好办法(使用MySQL的复制来快速分布镜像到其他机器更有优势,我们知道一些程序使用这种技术)。一般的做法是,在文件系统中存储图片或其他大型二进制文件,而在MySQL中只存储文件名;然后应用程序在MySQL之外存取文件。对于Web应用程序,可以把文件名放在<img>元素的src属性中,这样就可以实现对文件的存取。
全文检索是另一个最好放在MySQL之外处理额例子——MySQL在全文搜索方面明显不如Lucene和Sphinx。 NDB API也可能对某些任务有用。例如,尽管MySQL的NDB集群存储引擎(目前还)不适合存储一个高性能Web应用程序的全部数据,但用NDB API直接存储网站会话数据或用户注册信息还是可能的。在如下网站可以了解到更多NDB API的内容.还有供Apache使用的NDB模块,mod_ndb.
最后,对于某些操作——如图形关系和树遍历——关系型数据并不总是正确的典范,MySQL并不擅长分布式数据处理,因为它缺乏并行执行查询的能力。处于这些目的情况还是建议使用其他工具(可能与MySQL结合).现在想到的例子包括:
1.对于简单的键——值存储,在复制严重落后的非常高速的访问场景中,我们建议用Redis替换MySQL.即使MySQL主库可以承担这样的压力,备库的延迟也是非常让人头疼的。Redis也常用来做队列,因为它对队列操作支持得很好
2.Hadoop是房间中得大象,一语双关。混合MySQL/Hadoop得部署在处理大型或半结构化数据时非常常见
MySQL并不是适合每一个场景的解决方案。有些工作通常在MySQL以外来做会更好,即使MySQL理论上也可以做到。最明显的一个例子是在传统的文件系统中存储文件,而不是在表中。图像文件是静丹案例:虽然可以把它们放到一个BLOB列,但这通常不是个好办法(使用MySQL的复制来快速分布镜像到其他机器更有优势,我们知道一些程序使用这种技术)。一般的做法是,在文件系统中存储图片或其他大型二进制文件,而在MySQL中只存储文件名;然后应用程序在MySQL之外存取文件。对于Web应用程序,可以把文件名放在<img>元素的src属性中,这样就可以实现对文件的存取。
全文检索是另一个最好放在MySQL之外处理额例子——MySQL在全文搜索方面明显不如Lucene和Sphinx。 NDB API也可能对某些任务有用。例如,尽管MySQL的NDB集群存储引擎(目前还)不适合存储一个高性能Web应用程序的全部数据,但用NDB API直接存储网站会话数据或用户注册信息还是可能的。在如下网站可以了解到更多NDB API的内容.还有供Apache使用的NDB模块,mod_ndb.
最后,对于某些操作——如图形关系和树遍历——关系型数据并不总是正确的典范,MySQL并不擅长分布式数据处理,因为它缺乏并行执行查询的能力。处于这些目的情况还是建议使用其他工具(可能与MySQL结合).现在想到的例子包括:
1.对于简单的键——值存储,在复制严重落后的非常高速的访问场景中,我们建议用Redis替换MySQL.即使MySQL主库可以承担这样的压力,备库的延迟也是非常让人头疼的。Redis也常用来做队列,因为它对队列操作支持得很好
2.Hadoop是房间中得大象,一语双关。混合MySQL/Hadoop得部署在处理大型或半结构化数据时非常常见
备份与恢复
概述。
如果没有提前做好备份规划,也许以后会发现已经错失了一些最佳得选择。例如,在服务器已经配置好了以后,才想起应该使用LVM,以便可以获取文件系统的快照——但这时已经太迟了。在为别分配置系统参数时,可能没有注意到某些系统配置对性能有着重要影响。如果没有计划做定期的恢复演练,当真的需要恢复时,就会发现并没有那么顺利。在此假设大部分用户主要使用InnoDB而不是MyISAM。也不会涵盖一个精心设计的备份和恢复解决方案的所有部分——而仅涉及与MySQL相关的部分。不打算包括的话题如下:
1.安全(访问备份,恢复数据的权限,文件是否需要加密)
2.备份存储在哪里,包括它们应该离数据多远(在一块不同的盘上,一台不同的服务器上,或离线存储),以及如何将数据从源头移动到目的地
3.保留策略、审计、法律要求,以及相关的条款
4.存储解决方案和介质,压缩,以及增量备份
5.存储的格式
6.对备份的监控和报告
7.存储层内置备份功能,或者其他专用设备,例如预制式文件服务器
让我们先澄清几个核心术语。首先,经常可以听到所谓的热备份、暖备份和冷备份。人们经常使用这些词来表示一个备份的影响:例如"热"备份不需要任何的服务停机时间。问题是对这些术语的理解因人而异。有些工具虽然在名字中使用了"热备份",但实际上并不是所认为的那样。我们尽量避开这些术语,而直接说明某个特别的技术或工具对服务器的影响。另外两个让人困惑的词是还原和恢复。还原意味着从备份文件中获取数据,可以加载这些文件到MySQL里,也可以将这些文件放置到MySQL期望的路径中。恢复一般意味着当某些异常发生后对一个系统或其部分的拯救。包括从备份中还原数据,以及使服务器完全恢复功能的所有必要步骤,例如重启MySQL、改变配置和预热服务器的缓存等。
在很多人的概念中,恢复仅意味着修复崩溃后损坏的表。这与恢复一个完整的服务器使不同的。存储引擎的崩溃恢复要求数据和日志文件一致。要确保数据文件中只包含已经提交的事务所做的修改,恢复操作会将日志中还没有应用到数据文件的事务重新执行。这也许是恢复过程的一部分,甚至是备份的一部分。然而,这和一个意外的DROP TABLE事故后需要做的事是不一样的。
如果没有提前做好备份规划,也许以后会发现已经错失了一些最佳得选择。例如,在服务器已经配置好了以后,才想起应该使用LVM,以便可以获取文件系统的快照——但这时已经太迟了。在为别分配置系统参数时,可能没有注意到某些系统配置对性能有着重要影响。如果没有计划做定期的恢复演练,当真的需要恢复时,就会发现并没有那么顺利。在此假设大部分用户主要使用InnoDB而不是MyISAM。也不会涵盖一个精心设计的备份和恢复解决方案的所有部分——而仅涉及与MySQL相关的部分。不打算包括的话题如下:
1.安全(访问备份,恢复数据的权限,文件是否需要加密)
2.备份存储在哪里,包括它们应该离数据多远(在一块不同的盘上,一台不同的服务器上,或离线存储),以及如何将数据从源头移动到目的地
3.保留策略、审计、法律要求,以及相关的条款
4.存储解决方案和介质,压缩,以及增量备份
5.存储的格式
6.对备份的监控和报告
7.存储层内置备份功能,或者其他专用设备,例如预制式文件服务器
让我们先澄清几个核心术语。首先,经常可以听到所谓的热备份、暖备份和冷备份。人们经常使用这些词来表示一个备份的影响:例如"热"备份不需要任何的服务停机时间。问题是对这些术语的理解因人而异。有些工具虽然在名字中使用了"热备份",但实际上并不是所认为的那样。我们尽量避开这些术语,而直接说明某个特别的技术或工具对服务器的影响。另外两个让人困惑的词是还原和恢复。还原意味着从备份文件中获取数据,可以加载这些文件到MySQL里,也可以将这些文件放置到MySQL期望的路径中。恢复一般意味着当某些异常发生后对一个系统或其部分的拯救。包括从备份中还原数据,以及使服务器完全恢复功能的所有必要步骤,例如重启MySQL、改变配置和预热服务器的缓存等。
在很多人的概念中,恢复仅意味着修复崩溃后损坏的表。这与恢复一个完整的服务器使不同的。存储引擎的崩溃恢复要求数据和日志文件一致。要确保数据文件中只包含已经提交的事务所做的修改,恢复操作会将日志中还没有应用到数据文件的事务重新执行。这也许是恢复过程的一部分,甚至是备份的一部分。然而,这和一个意外的DROP TABLE事故后需要做的事是不一样的。
为什么要备份?
下面是备份非常重要的几个理由:
1.灾难恢复
灾难恢复是下列场景下需要做的事情:硬件故障、一个不经意的Bug导致数据损坏,或者服务器及其数据由于欧协原因不可获取或无法使用等。你需要准备好应付很多问题:某人偶尔连错数据库服务器执行了一个ALTER TABLE的操作,计放大楼被炸毁,恶意的黑客攻击或MySQL的Bug等。尽管遭受任何一个特殊的灾难的几率都非常低,但所有的风险叠加在一起就很有可能会碰到
2.人们改变象发
不必惊讶,很多人经常会在删除某些数据后又想要恢复这些数据
3.审计
有时候需要知道数据或Schema在过去的某个时间点是什么样的。例如,你也许被卷入一场法律官司,或发现了应用的一个Bug,想知道这段代码之前干了什么(有时候仅仅依靠代码的版本控制还不够)
4.测试
一个最简单的基于实际数据来测试的方法是,定期用最新的生产环境数据更i性能测试服务器。如果使用备份的方案就非常简单:只要把备份文件还原到测试服务器上即可。
检查你的假设。例如, 你认为共享虚拟主机供应商会提供MySQL服务器的备份?许多主机供应商根本不备份MySQL服务器,另外一些也仅仅在服务器运行时复制文件,这可能会创建一个损坏的没有用处的备份
下面是备份非常重要的几个理由:
1.灾难恢复
灾难恢复是下列场景下需要做的事情:硬件故障、一个不经意的Bug导致数据损坏,或者服务器及其数据由于欧协原因不可获取或无法使用等。你需要准备好应付很多问题:某人偶尔连错数据库服务器执行了一个ALTER TABLE的操作,计放大楼被炸毁,恶意的黑客攻击或MySQL的Bug等。尽管遭受任何一个特殊的灾难的几率都非常低,但所有的风险叠加在一起就很有可能会碰到
2.人们改变象发
不必惊讶,很多人经常会在删除某些数据后又想要恢复这些数据
3.审计
有时候需要知道数据或Schema在过去的某个时间点是什么样的。例如,你也许被卷入一场法律官司,或发现了应用的一个Bug,想知道这段代码之前干了什么(有时候仅仅依靠代码的版本控制还不够)
4.测试
一个最简单的基于实际数据来测试的方法是,定期用最新的生产环境数据更i性能测试服务器。如果使用备份的方案就非常简单:只要把备份文件还原到测试服务器上即可。
检查你的假设。例如, 你认为共享虚拟主机供应商会提供MySQL服务器的备份?许多主机供应商根本不备份MySQL服务器,另外一些也仅仅在服务器运行时复制文件,这可能会创建一个损坏的没有用处的备份
定义恢复需求。
如果一切正常,那么永远也不需要考虑恢复。但是,一旦需要恢复,只有世界上最好的备份系统是没用的,还需要一个强大的恢复系统。
不幸的是,让备份系统平滑工作比构造良好的恢复过程和工具更容易。原因如下:
1.备份在先。只有已经做了备份才可能恢复,因此在构建系统时,注意力自然会集中在备份上
2.备份由脚本和任务自动完成。经常不经意地,我们会花些时间调优备份过程。花5分钟来对备份过程做小地调整看起来并不重要,但是你是否天天同样地重视恢复呢?
3.备份时日常任务,但恢复常常发生在危急情形下
4.因为安全的需要,如果正在做异地备份,可能需要对备份数据进行加密,或采取其他措施来进行保护,安全性往往只关注数据被盗用的后果,但是有没有人想过,如果没有人能对用来恢复数据的加密卷解锁,或需要从一个整块的加密文件中抽取单个文件时,损害又是多大?
5.只有一个人来规划、设计和实施备份。当灾难袭来时,那个人可能不在。因此需要培养几个人并有计划地互为备份,这样就不会要求一个不合格的人来恢复数据
这里有一个看到的真实例子:一个客户报告说当mysqldump加上-d选项后,备份变得像闪电一般快,他想知道为什么没有一个人提出该选项可以如此快地加速备份过程。如果这个客户已经尝试还原这些备份,就不难发现其原因:使用-d选项将不会备份数据!这个客户关注备份,却没有关注恢复,因此完全没有意识到这个问题。规划备份和恢复策略时,有两个重要的需求可以帮助思考:恢复点目标(PRO)和恢复时间目标(RTO)。它们定义了可以容忍丢失多少数据,以及需要等待多久将数据恢复。在定义RPO和RTO时,先尝试回答下面几类问题:
1.在不导致严重后果的情况下,可以容忍丢失多少数据?需要故障恢复,还是可以接受自上次日常备份后所有的工作全部丢失?是否有法律法规的要求?
2.恢复需要在多长时间内完成?哪种类型的宕机是可以接受的?哪种影响(例如,部分服务不可用)是应用和用户可以接受的?当哪些场景发生时,又该如何持续服务?
3.需要恢复什么?常见的需求是护肤整个服务器,单个数据库,单个表,或仅仅是特定的事务或语句
建议将上面问题的答案明确地用文档记录下来,同时还应该明确备份策略,以及备份过程
如果一切正常,那么永远也不需要考虑恢复。但是,一旦需要恢复,只有世界上最好的备份系统是没用的,还需要一个强大的恢复系统。
不幸的是,让备份系统平滑工作比构造良好的恢复过程和工具更容易。原因如下:
1.备份在先。只有已经做了备份才可能恢复,因此在构建系统时,注意力自然会集中在备份上
2.备份由脚本和任务自动完成。经常不经意地,我们会花些时间调优备份过程。花5分钟来对备份过程做小地调整看起来并不重要,但是你是否天天同样地重视恢复呢?
3.备份时日常任务,但恢复常常发生在危急情形下
4.因为安全的需要,如果正在做异地备份,可能需要对备份数据进行加密,或采取其他措施来进行保护,安全性往往只关注数据被盗用的后果,但是有没有人想过,如果没有人能对用来恢复数据的加密卷解锁,或需要从一个整块的加密文件中抽取单个文件时,损害又是多大?
5.只有一个人来规划、设计和实施备份。当灾难袭来时,那个人可能不在。因此需要培养几个人并有计划地互为备份,这样就不会要求一个不合格的人来恢复数据
这里有一个看到的真实例子:一个客户报告说当mysqldump加上-d选项后,备份变得像闪电一般快,他想知道为什么没有一个人提出该选项可以如此快地加速备份过程。如果这个客户已经尝试还原这些备份,就不难发现其原因:使用-d选项将不会备份数据!这个客户关注备份,却没有关注恢复,因此完全没有意识到这个问题。规划备份和恢复策略时,有两个重要的需求可以帮助思考:恢复点目标(PRO)和恢复时间目标(RTO)。它们定义了可以容忍丢失多少数据,以及需要等待多久将数据恢复。在定义RPO和RTO时,先尝试回答下面几类问题:
1.在不导致严重后果的情况下,可以容忍丢失多少数据?需要故障恢复,还是可以接受自上次日常备份后所有的工作全部丢失?是否有法律法规的要求?
2.恢复需要在多长时间内完成?哪种类型的宕机是可以接受的?哪种影响(例如,部分服务不可用)是应用和用户可以接受的?当哪些场景发生时,又该如何持续服务?
3.需要恢复什么?常见的需求是护肤整个服务器,单个数据库,单个表,或仅仅是特定的事务或语句
建议将上面问题的答案明确地用文档记录下来,同时还应该明确备份策略,以及备份过程
备份误区1:"复制就是备份"
这是我们经常碰到地一个误区。复制不是备份,当然使用RAID阵列也不是备份。为什么这么说?可以考虑以下,如果意外地在生产库上执行了DROP DATABASE,它们是否可以帮你恢复所有的数据?RAID和复制连这个简单的测试都没法通过。它们不是备份,也不是备份的替代品。只有备份才能满足备份的要求
这是我们经常碰到地一个误区。复制不是备份,当然使用RAID阵列也不是备份。为什么这么说?可以考虑以下,如果意外地在生产库上执行了DROP DATABASE,它们是否可以帮你恢复所有的数据?RAID和复制连这个简单的测试都没法通过。它们不是备份,也不是备份的替代品。只有备份才能满足备份的要求
设计MySQL备份方案。
备份MySQL比看起来难。最基本的,备份仅仅是数据的一个副本,但是受限于应用程序的要求、MySQL的存储引擎架构,以及系统配置等因素,会让复制一份数据都变得很困难。在深入所有选项细节之前,先来看以下建议:
1.在生产实践中,对于大数据库来说,物理备份是必需的:逻辑备份太慢并受到资源限制,从逻辑备份中恢复需要很长实践。基于快照的备份,例如Percona XtraBackup和MySQL Enterprise Backup是最好的选择。对于较小的数据库,逻辑备份可以很好地胜任
2.保留多个备份集
3.定期从逻辑备份(或者物理备份)中抽取数据进行恢复测试
4.保存二进制日志以用于故障时点的恢复,expire_logs_days参数应该设置得足够长。至少可以从最近两次物理备份中做基于时间点得恢复,这样就可以在保持主库运行且不应用任何二进制日志得情况下创建一个备库。备份二进制日志与过期设置无关,二进制日志备份需要保存足够长得实践,宜宾啊能从最近得逻辑备份进行恢复
5.完全不借助备份工具本身来监控备份和备份得过程。需要另外验证备份是否正常
6.通过演练整个恢复过程来测试备份和恢复。测算恢复所需要的资源(CPU、磁盘空间、实际实践,以及网络带宽等)
7.对安全性要仔细考虑。如果有人能接触生产服务器,它是否也能访问备份服务器?反过来呢。
弄清楚PRO(恢复点目标)和RTO(恢复时间目标)可以指导备份策略。是需要基于故障时间点的恢复能力,还是从昨晚的备份中恢复但会丢失此后的所有数据就足够了?如果需要基于故障时间点的恢复,可能要建立日常备份并保证所需要的二进制日志是有效的,这样才能从备份中还原,并通过重放二进制日志来恢复到想要的时间点。
一般来说,能承受的数据丢失越多,备份越简单。如果有非常苛刻的需求,要确保能恢复所有数据,备份就很困难。基于故障时间点的恢复也有积累。一个"宽松"的故障时间点恢复需求意味着需要重建数据,直到"足够接近"问题发生的时刻。一个"硬性"的需求意味着不能容忍任何一个已提交的事务,即使某些可怕的事情发生(例如服务器着火了)。这需要特别的技术,例如将二进制日志保存在一个独立的SAN卷或使用DRBD磁盘复制
备份MySQL比看起来难。最基本的,备份仅仅是数据的一个副本,但是受限于应用程序的要求、MySQL的存储引擎架构,以及系统配置等因素,会让复制一份数据都变得很困难。在深入所有选项细节之前,先来看以下建议:
1.在生产实践中,对于大数据库来说,物理备份是必需的:逻辑备份太慢并受到资源限制,从逻辑备份中恢复需要很长实践。基于快照的备份,例如Percona XtraBackup和MySQL Enterprise Backup是最好的选择。对于较小的数据库,逻辑备份可以很好地胜任
2.保留多个备份集
3.定期从逻辑备份(或者物理备份)中抽取数据进行恢复测试
4.保存二进制日志以用于故障时点的恢复,expire_logs_days参数应该设置得足够长。至少可以从最近两次物理备份中做基于时间点得恢复,这样就可以在保持主库运行且不应用任何二进制日志得情况下创建一个备库。备份二进制日志与过期设置无关,二进制日志备份需要保存足够长得实践,宜宾啊能从最近得逻辑备份进行恢复
5.完全不借助备份工具本身来监控备份和备份得过程。需要另外验证备份是否正常
6.通过演练整个恢复过程来测试备份和恢复。测算恢复所需要的资源(CPU、磁盘空间、实际实践,以及网络带宽等)
7.对安全性要仔细考虑。如果有人能接触生产服务器,它是否也能访问备份服务器?反过来呢。
弄清楚PRO(恢复点目标)和RTO(恢复时间目标)可以指导备份策略。是需要基于故障时间点的恢复能力,还是从昨晚的备份中恢复但会丢失此后的所有数据就足够了?如果需要基于故障时间点的恢复,可能要建立日常备份并保证所需要的二进制日志是有效的,这样才能从备份中还原,并通过重放二进制日志来恢复到想要的时间点。
一般来说,能承受的数据丢失越多,备份越简单。如果有非常苛刻的需求,要确保能恢复所有数据,备份就很困难。基于故障时间点的恢复也有积累。一个"宽松"的故障时间点恢复需求意味着需要重建数据,直到"足够接近"问题发生的时刻。一个"硬性"的需求意味着不能容忍任何一个已提交的事务,即使某些可怕的事情发生(例如服务器着火了)。这需要特别的技术,例如将二进制日志保存在一个独立的SAN卷或使用DRBD磁盘复制
在线备份还是离线备份。
如果可能,关闭MySQL做备份是最简单最安全的,也是所有获取一致性副本的方法中最好的。而且损坏或不一致的风险最小。如果关闭了MySQL,就根本不用关心InnoDB缓冲池中的脏页或其他缓存。也不需要担心数据在尝试备份的过程被修改,并且因为服务器不对应用提供访问,所以可以更快地完成备份。
尽管如此,让服务器停机的代价可能比看起来要更昂贵。即使能最小化停机时间,在高负载和高数据量下关闭和重启MySQL也可能要花很长一段时间,尽管有一些能使这个影响最小化的技术,但并不能将其减少为零。因此,必需要设计不需要生产服务器停机的备份。即便如此,由于一致性的需要,对服务器进行在线备份仍然会有明显的服务中断。
在众多的备份方法中,一个最大问题就是它们会使用FLUSH TABLES WITH READ LOCK操作,这会导致MySQL关闭并锁住所有的表,将MyISAM的数据文件刷新到磁盘上(但InnoDB不是这样的!),并且刷新查询缓存。该操作需要非常常的时间来完成。具体需要多长时间是不可预估的;如果全局读锁要等待一个长时间运行的语句完成,或有许多表,那么时间会更长。除非锁被释放,否则就不能在服务器上更改任何数据,一切都会被阻塞和积压(是的,即使SELECT查询也会被阻塞,因为如果有一个查询需要修改某些数据,只要它开始等待表上的写锁,所有尝试获取读锁的查询也必需等待)。FLUSH TABLES WITH READ LOCK不像关闭服务器的代价那么高,因为大部分缓存仍然在内存中,并且服务器一直是"预热"的,但是它也有非常大的破坏性。如果有人说这样做很快,可能是准备向你推销某种从来没有在真正的线上服务器上运行过的东西。避免使用FLUSH TABLES WITH READ LOCK的最好的方法是只使用InnoDB表。在权限和其他系统信息表中使用MyISAM表是不可避免地,但是如果数据改变量很少(正常情况下),你可以只刷新和锁住这些表,这不会有什么问题。
在规划备份时,有一些与性能相关地因素需要考虑.
1.锁时间
需要持有锁多长时间,例如在备份期间持有地全局FLUSH TABLES WITH READ LOCK?
2.备份时间
复制备份到目的地需要多久?
3.备份负载
在复制备份到目的地时对服务器性能的影响有多少?
4.恢复时间
把备份镜像从存储位置复制到MySQL服务器,重放二进制日志等,需要多久?
最大的权衡是备份时间与备份负载。可以牺牲其一以增强另外一个。例如可以提高备份的优先级,代价是降低服务器性能。同样,也可以利用负载的特性来设计备份。例如,如果服务器在完上的8小时内仅仅有50%的负载,那么可以尝试规划备份,使得服务器的负载低于50%且仍能在8小时内完成。可以采用许多方法来完成这个目标,例如,可以用ioice和nice来提高复制或压缩操作的优先级,使用不同的压缩等级,或在备份服务器上压缩而不是在MySQL服务器上。甚至可以使用lzo或pigz以获取更快的压缩。也可以使用0_DIRECT或fadvise()在复制操作时绕开操作系统的缓存,以避免污染服务器的缓存。像Percona XtraBackup和MySQL Enterprise Backup这样的工具都有下六选项,可以使用pv时加上--rate-limit选项来限制备份脚本的吞吐量
如果可能,关闭MySQL做备份是最简单最安全的,也是所有获取一致性副本的方法中最好的。而且损坏或不一致的风险最小。如果关闭了MySQL,就根本不用关心InnoDB缓冲池中的脏页或其他缓存。也不需要担心数据在尝试备份的过程被修改,并且因为服务器不对应用提供访问,所以可以更快地完成备份。
尽管如此,让服务器停机的代价可能比看起来要更昂贵。即使能最小化停机时间,在高负载和高数据量下关闭和重启MySQL也可能要花很长一段时间,尽管有一些能使这个影响最小化的技术,但并不能将其减少为零。因此,必需要设计不需要生产服务器停机的备份。即便如此,由于一致性的需要,对服务器进行在线备份仍然会有明显的服务中断。
在众多的备份方法中,一个最大问题就是它们会使用FLUSH TABLES WITH READ LOCK操作,这会导致MySQL关闭并锁住所有的表,将MyISAM的数据文件刷新到磁盘上(但InnoDB不是这样的!),并且刷新查询缓存。该操作需要非常常的时间来完成。具体需要多长时间是不可预估的;如果全局读锁要等待一个长时间运行的语句完成,或有许多表,那么时间会更长。除非锁被释放,否则就不能在服务器上更改任何数据,一切都会被阻塞和积压(是的,即使SELECT查询也会被阻塞,因为如果有一个查询需要修改某些数据,只要它开始等待表上的写锁,所有尝试获取读锁的查询也必需等待)。FLUSH TABLES WITH READ LOCK不像关闭服务器的代价那么高,因为大部分缓存仍然在内存中,并且服务器一直是"预热"的,但是它也有非常大的破坏性。如果有人说这样做很快,可能是准备向你推销某种从来没有在真正的线上服务器上运行过的东西。避免使用FLUSH TABLES WITH READ LOCK的最好的方法是只使用InnoDB表。在权限和其他系统信息表中使用MyISAM表是不可避免地,但是如果数据改变量很少(正常情况下),你可以只刷新和锁住这些表,这不会有什么问题。
在规划备份时,有一些与性能相关地因素需要考虑.
1.锁时间
需要持有锁多长时间,例如在备份期间持有地全局FLUSH TABLES WITH READ LOCK?
2.备份时间
复制备份到目的地需要多久?
3.备份负载
在复制备份到目的地时对服务器性能的影响有多少?
4.恢复时间
把备份镜像从存储位置复制到MySQL服务器,重放二进制日志等,需要多久?
最大的权衡是备份时间与备份负载。可以牺牲其一以增强另外一个。例如可以提高备份的优先级,代价是降低服务器性能。同样,也可以利用负载的特性来设计备份。例如,如果服务器在完上的8小时内仅仅有50%的负载,那么可以尝试规划备份,使得服务器的负载低于50%且仍能在8小时内完成。可以采用许多方法来完成这个目标,例如,可以用ioice和nice来提高复制或压缩操作的优先级,使用不同的压缩等级,或在备份服务器上压缩而不是在MySQL服务器上。甚至可以使用lzo或pigz以获取更快的压缩。也可以使用0_DIRECT或fadvise()在复制操作时绕开操作系统的缓存,以避免污染服务器的缓存。像Percona XtraBackup和MySQL Enterprise Backup这样的工具都有下六选项,可以使用pv时加上--rate-limit选项来限制备份脚本的吞吐量
逻辑备份还是物理备份。
有两种主要的方法来备份MySQL数据:逻辑备份(也叫"导出")和直接复制原始文件的物理备份。逻辑备份将数据包含在一种MySQL能够解析的格式中,要么时SQL,要么时以某个符号分割的文本。(由mysqldump生成的逻辑备份并不一定是文本文件。SQL导出会包含许多不同的字符集,同样也会包含二进制数据,这些数据并不是有效的字符。对于许多编辑器来说,文件行也可能会太长。但是,大多数这样的文件还是可以被编辑器打开和读取,特别是mysqldump使用了--hex-blob选项时)。原始文件是指存在硬盘上的文件。任何一种备份都有其优点和缺点.
有两种主要的方法来备份MySQL数据:逻辑备份(也叫"导出")和直接复制原始文件的物理备份。逻辑备份将数据包含在一种MySQL能够解析的格式中,要么时SQL,要么时以某个符号分割的文本。(由mysqldump生成的逻辑备份并不一定是文本文件。SQL导出会包含许多不同的字符集,同样也会包含二进制数据,这些数据并不是有效的字符。对于许多编辑器来说,文件行也可能会太长。但是,大多数这样的文件还是可以被编辑器打开和读取,特别是mysqldump使用了--hex-blob选项时)。原始文件是指存在硬盘上的文件。任何一种备份都有其优点和缺点.
逻辑备份
逻辑备份有如下优点:
1.逻辑备份是可以用编辑器或像grep 和sed之类的命令查看和操作的普通文件。当需要恢复数据或只想查看数据但不恢复时,这都非常有帮助
2.恢复非常简单。可以通过管道把它们输入到mysql,或者使用mysqlimport。
3.可以通过网络来备份和恢复——就是说,可以在与MySQL主机不同的另外一台及其上操作
4.可以在类似Amazon RDS这样不能访问底层文件系统的系统中使用
5.非常灵活,因为mysqldump——大部分人喜欢的工具——可以接受许多选项,例如可以用WHERE子句来限制需要备份哪些行。
6.与存储引擎无关。因为是从MySQL服务器中提取数据而生成,所以消除了底层数据存储和不同。因此,可以从InnoDB表中备份,然后只需极小的工作量就可以还原到MyISAM表中。而对于原始数据却不能这么做。
7.有助于避免数据损坏。如果磁盘驱动器有故障而要复制原始文件时,你将会得到一个错误并且/或生成一个部分或损坏的备份。如果MySQL在内存中的数据还没有损坏,当不能得到一个正常的原始文件复制时,有时可以得到一个可以信赖的逻辑备份
尽管如此,逻辑备份也有它的缺点:
1.必需由数据库服务器完成生成逻辑备份的工作,因此要使用更多的CPU周期
2.逻辑备份在某些场景下比数据库文件本身更大(以经验来看,逻辑备份往往比物理备份要小许多,但也并不总是如此)。ASCII形式的数据不总是和存储引擎存储数据一样高效。例如,一个整型需要4字节来存储,但是用ASCII写入时,可能需要12个字符。当然也可以压缩文件以得到一个更小的备份文件,但这样会使用更多的CPU资源。(如果索引比较多,逻辑备份一般要比物理备份小)
3.无法保证导出后再还原出来的一定是同样的数据。浮点表示的问题、软件Bug等都会导致问题,尽管非常少见
4.从逻辑备份中还原需要MySQL加载和解释语句,转化为存储格式,ing重建索引,所有这一切会很慢。
最大的缺点时从MySQL中导出数据和通过SQL语句将其加载回去的开销。如果使用逻辑备份,测试恢复需要的时间将非常重要。Percona Server中包含的mysqldump,在使用InnoDB表时能起到帮助作用,因为它会对输出格式化,以便在重新加载时利用InnoDB的快速建索引的优点。测试显示这样做可以减少2/3甚至更多的还原事件。索引越多,好处越明显(如图所示,(上图是cat /var/lib/mysql-bin.000001操作所示))
逻辑备份有如下优点:
1.逻辑备份是可以用编辑器或像grep 和sed之类的命令查看和操作的普通文件。当需要恢复数据或只想查看数据但不恢复时,这都非常有帮助
2.恢复非常简单。可以通过管道把它们输入到mysql,或者使用mysqlimport。
3.可以通过网络来备份和恢复——就是说,可以在与MySQL主机不同的另外一台及其上操作
4.可以在类似Amazon RDS这样不能访问底层文件系统的系统中使用
5.非常灵活,因为mysqldump——大部分人喜欢的工具——可以接受许多选项,例如可以用WHERE子句来限制需要备份哪些行。
6.与存储引擎无关。因为是从MySQL服务器中提取数据而生成,所以消除了底层数据存储和不同。因此,可以从InnoDB表中备份,然后只需极小的工作量就可以还原到MyISAM表中。而对于原始数据却不能这么做。
7.有助于避免数据损坏。如果磁盘驱动器有故障而要复制原始文件时,你将会得到一个错误并且/或生成一个部分或损坏的备份。如果MySQL在内存中的数据还没有损坏,当不能得到一个正常的原始文件复制时,有时可以得到一个可以信赖的逻辑备份
尽管如此,逻辑备份也有它的缺点:
1.必需由数据库服务器完成生成逻辑备份的工作,因此要使用更多的CPU周期
2.逻辑备份在某些场景下比数据库文件本身更大(以经验来看,逻辑备份往往比物理备份要小许多,但也并不总是如此)。ASCII形式的数据不总是和存储引擎存储数据一样高效。例如,一个整型需要4字节来存储,但是用ASCII写入时,可能需要12个字符。当然也可以压缩文件以得到一个更小的备份文件,但这样会使用更多的CPU资源。(如果索引比较多,逻辑备份一般要比物理备份小)
3.无法保证导出后再还原出来的一定是同样的数据。浮点表示的问题、软件Bug等都会导致问题,尽管非常少见
4.从逻辑备份中还原需要MySQL加载和解释语句,转化为存储格式,ing重建索引,所有这一切会很慢。
最大的缺点时从MySQL中导出数据和通过SQL语句将其加载回去的开销。如果使用逻辑备份,测试恢复需要的时间将非常重要。Percona Server中包含的mysqldump,在使用InnoDB表时能起到帮助作用,因为它会对输出格式化,以便在重新加载时利用InnoDB的快速建索引的优点。测试显示这样做可以减少2/3甚至更多的还原事件。索引越多,好处越明显(如图所示,(上图是cat /var/lib/mysql-bin.000001操作所示))
物理备份。
物理备份有如下好处:
1.基于文件的物理备份,只需要将需要的文件复制到其他地方即可完成备份。不需要其他额外的工作来生成原始文件。
2.物理备份的恢复可能就更简单了,这取决于存储引擎。对于MyISAM,只需要简单地复制文件到目的地即可。对于InnoDB则需要停止数据库,可能还要采取其他一些步骤
3.InnoDB和MyISAM的物理备份非常容易跨平台、操作系统和MySQL斑斑额(逻辑导出亦如此。这里特别指出这一点是为了消除大家的打新)
4.从物理备份中恢复会更快,因为MySQL服务器不需要执行任何SQL或构建索引。如果有很大的InnoDB表,无法完全缓存到内存中,则物理备份的恢复要快非常多——至少一个数量级。事实上,逻辑备份最可怕的地方就是不确定的还原事件
物理备份也有其缺点,比如:
1.InnoDB的原始文件通常要比相应的逻辑备份要大得多。InnoDB的表空间往往包含很多未使用的空间。还有很多空间被用来做存储数据意外的用途(插入缓冲,回滚段等)。
2.物理备份不总是可跨平台、操作系统及MySQL版本。文件名大小写敏感和浮点格式可能会遇到麻烦。很可能因浮点格式不同而不能移动文件到另一个系统(虽然主流处理器都使用IEEE浮点格式)
物理备份通常更加简单高效(值得一提的是物理备份会更容易出错;很难相Mysqldump一样简单)尽管如此,对于需要长期保留的备份,或者是满足法律合规要求的备份,尽量不要完全依赖物理备份。至少每隔一段时间还是需要做一次逻辑备份。
除非经过测试,不要假定备份(特别是物理备份)是正常的。对InnoDB来说,这意味着需要启动一个MySQL实例,执行InnoDB恢复操作,然后运行CHECK TABLES。也可以跳过这一操作,仅对文件运行innochecksum,但不建议这样做。对于MyISAM可以运行CHECK TABLES,或者使用mysqlcheck.然后,周期性地使用mysqldump执行逻辑备份。这样做可以获得两种方法的优点,不会使生产服务器在导出时有过度负担。如果能够方便地利用文件系统的快照,也可以生成一个快照,将该快照复制到另外一个服务器上并释放,然后测试原始文件,再执行逻辑备份呢
物理备份有如下好处:
1.基于文件的物理备份,只需要将需要的文件复制到其他地方即可完成备份。不需要其他额外的工作来生成原始文件。
2.物理备份的恢复可能就更简单了,这取决于存储引擎。对于MyISAM,只需要简单地复制文件到目的地即可。对于InnoDB则需要停止数据库,可能还要采取其他一些步骤
3.InnoDB和MyISAM的物理备份非常容易跨平台、操作系统和MySQL斑斑额(逻辑导出亦如此。这里特别指出这一点是为了消除大家的打新)
4.从物理备份中恢复会更快,因为MySQL服务器不需要执行任何SQL或构建索引。如果有很大的InnoDB表,无法完全缓存到内存中,则物理备份的恢复要快非常多——至少一个数量级。事实上,逻辑备份最可怕的地方就是不确定的还原事件
物理备份也有其缺点,比如:
1.InnoDB的原始文件通常要比相应的逻辑备份要大得多。InnoDB的表空间往往包含很多未使用的空间。还有很多空间被用来做存储数据意外的用途(插入缓冲,回滚段等)。
2.物理备份不总是可跨平台、操作系统及MySQL版本。文件名大小写敏感和浮点格式可能会遇到麻烦。很可能因浮点格式不同而不能移动文件到另一个系统(虽然主流处理器都使用IEEE浮点格式)
物理备份通常更加简单高效(值得一提的是物理备份会更容易出错;很难相Mysqldump一样简单)尽管如此,对于需要长期保留的备份,或者是满足法律合规要求的备份,尽量不要完全依赖物理备份。至少每隔一段时间还是需要做一次逻辑备份。
除非经过测试,不要假定备份(特别是物理备份)是正常的。对InnoDB来说,这意味着需要启动一个MySQL实例,执行InnoDB恢复操作,然后运行CHECK TABLES。也可以跳过这一操作,仅对文件运行innochecksum,但不建议这样做。对于MyISAM可以运行CHECK TABLES,或者使用mysqlcheck.然后,周期性地使用mysqldump执行逻辑备份。这样做可以获得两种方法的优点,不会使生产服务器在导出时有过度负担。如果能够方便地利用文件系统的快照,也可以生成一个快照,将该快照复制到另外一个服务器上并释放,然后测试原始文件,再执行逻辑备份呢
备份什么?
恢复的需求决定需要备份什么。最简单的策略是只备份数据和表定义,但这是一个最低的要求。在生产环境中恢复数据库一般需要更多的工作。下面是MySQL备份需要考虑的几点:
1.非显著数据
不要忘记哪些容易被忽略的数据:例如m二进制日志和InnoDB事务日志。
2.代码
现在的MySQL服务器可以存储许多代码,例如触发器和存储过程。如果备份了mysql数据库,那么大部分这类代码也备份了,但入股哦需要还原单个业务数据库会比较麻烦,因为这个数据库中的部分"数据",例如存储过程,实际是存放在mysql数据库中的
3.复制配置
如果恢复一个涉及复制关系的服务器,应该备份所有与复制相关的文件,例如二进制日志、中继日志、日志索引文件和.info文件。至少应该包含SHOW MASTER STATUS和/或SHOW SLAVE STATUS的输出。执行FLUSH LOGS也非常有好处,可以让MySQL从一个新的二进制日志开始。从日志文件的开头做基于故障时间点的恢复要比从中间更容易
4.服务器配置
假设要从一个实际的灾难中恢复,比如说,地震过后在一个新数据中心构建服务器,如果备份中包含服务器配置,你一定会喜出望外
5.选定的操作系统文件
对于服务器配置来说,备份中对生产服务器至关重要的任何外部配置,都十分重要。在UNIX服务器上,这可能包括cron任务、用户和组的配置、管理脚本,以及sudo规则
这些建议在许多场景下会被当作"备份一切"。然而,如果有大量的数据,这样做的开销将非常高,如何做备份,需要更加明智的考虑。特别是,可能需要在不同备份中备份不同的数据。例如,可以单独地备份数据、二进制日志和操作系统及系统配置
恢复的需求决定需要备份什么。最简单的策略是只备份数据和表定义,但这是一个最低的要求。在生产环境中恢复数据库一般需要更多的工作。下面是MySQL备份需要考虑的几点:
1.非显著数据
不要忘记哪些容易被忽略的数据:例如m二进制日志和InnoDB事务日志。
2.代码
现在的MySQL服务器可以存储许多代码,例如触发器和存储过程。如果备份了mysql数据库,那么大部分这类代码也备份了,但入股哦需要还原单个业务数据库会比较麻烦,因为这个数据库中的部分"数据",例如存储过程,实际是存放在mysql数据库中的
3.复制配置
如果恢复一个涉及复制关系的服务器,应该备份所有与复制相关的文件,例如二进制日志、中继日志、日志索引文件和.info文件。至少应该包含SHOW MASTER STATUS和/或SHOW SLAVE STATUS的输出。执行FLUSH LOGS也非常有好处,可以让MySQL从一个新的二进制日志开始。从日志文件的开头做基于故障时间点的恢复要比从中间更容易
4.服务器配置
假设要从一个实际的灾难中恢复,比如说,地震过后在一个新数据中心构建服务器,如果备份中包含服务器配置,你一定会喜出望外
5.选定的操作系统文件
对于服务器配置来说,备份中对生产服务器至关重要的任何外部配置,都十分重要。在UNIX服务器上,这可能包括cron任务、用户和组的配置、管理脚本,以及sudo规则
这些建议在许多场景下会被当作"备份一切"。然而,如果有大量的数据,这样做的开销将非常高,如何做备份,需要更加明智的考虑。特别是,可能需要在不同备份中备份不同的数据。例如,可以单独地备份数据、二进制日志和操作系统及系统配置
增量备份和差异备份。
当数据量很庞大时,一个常见的策略时做定期的增量或差异备份。它们之间的区别有点容易让人混淆,所以先来澄清这两个术语:差异备份是对自上次全备份后所有改变的部分而做的备份,而增量备份则是自从任意类型的上次备份后所有修改做的备份。
例如,加入在每周日做一个全备份。在周一,在自周日以来所有的改变做一个差异备份。在周二,就有两个选择:备份自周日以来所有的改变(差异),或只备份自从周一备份后所有的改变(增量)。增量和差异备份都是部分备份:它们一般不包含完整的数据集,因为某些数据几乎肯定没有改变。部分备份对减少服务器开销、备份时间及备份空间而言都很适合。尽管某些部分备份并不会真正减少服务器的开胸啊。例如,Percona XtraBackup和MySQL Enterprise Backup,仍然会扫描服务器上的所有数据块,因而并不会节约太多的开销,但它们确实会减少一定量的备份时间和大量用于压缩的CPU时间,当然也会减少磁盘空间使用(Percona XtraBackup正在开发"真正的"增量备份特性。它能够将备份变更的块,而需要扫描每个块)。不会因为会用高级备份技术而字符,解决方案越复杂,可能面临的风险也越大。要注意分析隐藏的危险,如果多次迭代备份紧密地耦合在一起,则只要其中的一次迭代备份有损坏,就可能会导致所有的备份都无效。下面有一些建议:
1.使用Percona XtraBackup和MySQL Enterprise Backup中的增量备份特性
2.备份二进制日志。可以在每次备份后使用FLUSH LOGS来开始一个新的二进制日志,这样就只需要备份新的二进制日志。
3.不要备份没有改变的表。有些存储引擎,例如MyISAM,会记录每个表最后修改时间。可以通过查看磁盘上的文件或运行SHOW TABLE STATUS来看这个时间。如果使用InnoDB,可以利用触发器记录修改时间到一个小的"最后修改时间"表中,帮助跟踪最新的修改操作。需要确保只对变更不频繁的表进行跟踪,这样才能降低开销。通过定制的备份脚本可以轻松获取到哪些表有变更。例如,如果有包含不同语种各个月的名称列表,或者州或区域的间歇之类的"查找"表,将它们放在一个单独的数据库中是个好主意,这样就不需要每次都备份这些表
4.不要备份没有改变的行,如果一个表只做插入,例如记录网页页面点击的表,那么可以增加一个时间戳的列,然后只备份自上次备份后插入的行
5.某些数据根本不需要备份。有时候这样做影响会很大——例如,如果有一个从其他数据构建的数据仓库,从技术上讲完全是冗余的,就可以仅备份构建仓库的数据,而不是数据仓库本身。即使从源数据文件重建仓库的"恢复"时间较长,这也是个好想法。相对于从全备中可能获得的快速恢复时间,避免备份可以节约更多的总的时间开胸啊。临时数据也可以不用备份,例如保留网站会话数据的表
6.备份所有的数据,然后发送到一个有去重特性的目的地,例如ZFS文件管理程序
增量备份的缺点包括增加恢复复杂性,额外的风险,以及更长的恢复时间。如果可以做全备,考虑到简便性,建议尽量做全备。不管如何,还是需要经常做全备份——建议至少一周一次。你肯定不会希望使用一个月的所有增量备份来进行恢复。即使一周也还是有很多的工作和风险的
当数据量很庞大时,一个常见的策略时做定期的增量或差异备份。它们之间的区别有点容易让人混淆,所以先来澄清这两个术语:差异备份是对自上次全备份后所有改变的部分而做的备份,而增量备份则是自从任意类型的上次备份后所有修改做的备份。
例如,加入在每周日做一个全备份。在周一,在自周日以来所有的改变做一个差异备份。在周二,就有两个选择:备份自周日以来所有的改变(差异),或只备份自从周一备份后所有的改变(增量)。增量和差异备份都是部分备份:它们一般不包含完整的数据集,因为某些数据几乎肯定没有改变。部分备份对减少服务器开销、备份时间及备份空间而言都很适合。尽管某些部分备份并不会真正减少服务器的开胸啊。例如,Percona XtraBackup和MySQL Enterprise Backup,仍然会扫描服务器上的所有数据块,因而并不会节约太多的开销,但它们确实会减少一定量的备份时间和大量用于压缩的CPU时间,当然也会减少磁盘空间使用(Percona XtraBackup正在开发"真正的"增量备份特性。它能够将备份变更的块,而需要扫描每个块)。不会因为会用高级备份技术而字符,解决方案越复杂,可能面临的风险也越大。要注意分析隐藏的危险,如果多次迭代备份紧密地耦合在一起,则只要其中的一次迭代备份有损坏,就可能会导致所有的备份都无效。下面有一些建议:
1.使用Percona XtraBackup和MySQL Enterprise Backup中的增量备份特性
2.备份二进制日志。可以在每次备份后使用FLUSH LOGS来开始一个新的二进制日志,这样就只需要备份新的二进制日志。
3.不要备份没有改变的表。有些存储引擎,例如MyISAM,会记录每个表最后修改时间。可以通过查看磁盘上的文件或运行SHOW TABLE STATUS来看这个时间。如果使用InnoDB,可以利用触发器记录修改时间到一个小的"最后修改时间"表中,帮助跟踪最新的修改操作。需要确保只对变更不频繁的表进行跟踪,这样才能降低开销。通过定制的备份脚本可以轻松获取到哪些表有变更。例如,如果有包含不同语种各个月的名称列表,或者州或区域的间歇之类的"查找"表,将它们放在一个单独的数据库中是个好主意,这样就不需要每次都备份这些表
4.不要备份没有改变的行,如果一个表只做插入,例如记录网页页面点击的表,那么可以增加一个时间戳的列,然后只备份自上次备份后插入的行
5.某些数据根本不需要备份。有时候这样做影响会很大——例如,如果有一个从其他数据构建的数据仓库,从技术上讲完全是冗余的,就可以仅备份构建仓库的数据,而不是数据仓库本身。即使从源数据文件重建仓库的"恢复"时间较长,这也是个好想法。相对于从全备中可能获得的快速恢复时间,避免备份可以节约更多的总的时间开胸啊。临时数据也可以不用备份,例如保留网站会话数据的表
6.备份所有的数据,然后发送到一个有去重特性的目的地,例如ZFS文件管理程序
增量备份的缺点包括增加恢复复杂性,额外的风险,以及更长的恢复时间。如果可以做全备,考虑到简便性,建议尽量做全备。不管如何,还是需要经常做全备份——建议至少一周一次。你肯定不会希望使用一个月的所有增量备份来进行恢复。即使一周也还是有很多的工作和风险的
存储引擎和一致性。
MySQL对存储引擎的选择会导致备份明显更复杂。问题是,对于给定的存储引擎,如何得到一致的备份。实际上有两类一致性需要考虑:数据一致性和文件一致性。
1.数据一致性
当备份时,应该考虑是否需要数据在指定时间点一致。例如,在一个电子商务数据库中,可能需要确保发货单和付款之间一致。恢复付款时如果不考虑相应的发货单,或反过来,都会导致麻烦。如果做在线备份(从一个运行的服务器做备份),可能需要所有相关表的一致性备份。这意味着不能一次锁住一张表然后做备份——因而意味着备份可能比预想的要更有侵入性。如果使用的不是事务型存储引擎,则只能在备份时用LOCK TABLES来锁住所有要一起备份的表,备份完成后再释放锁。
InnoDB的多版本控制功能可以帮到我们。开始一个事务,转储一组相关的表,然后提交事务。(如果使用了事务获取一致性备份,则不能用LOCK TABLES,因为它会隐式地提交事务——详情参见MySQL手册)。只要在服务器上使用REPEATABLE READ事务隔离级别,并且没有任何DDL,就一定会有完美的一致性,以及基于时间点的数据快照,且在备份过程中不会阻塞任何后续的工作。尽管如此,这种方法并不能保护逻辑设计很差的应用。假如在电子商务库中插入一条付款记录,提交事务,然后在另外一个事务中插入一条发货单记录。备份过程可能在这两个操作之间开始,备份了付款记录却不包括发货单记录。这就是必需仔细设计事务以确保相关的操作放在一个组内的原因。也可以用mysqldump来获得InnoDB表的一致性逻辑备份,采用--single-transaction选项可以按照我们所描述的那样工作。但是,这可能会导致一个非常长的事务,在某些负载下会导致开销大到不可接受
2.文件一致性
每个文件内部的一致性也非常重要——例如,一条大的UPDATE语句执行时备份反映不出文件的状态——并且所有要备份的文件相互间也应一致。如果没有内部一致的文件,还原时可能会感到惊讶(它们可能已经损坏)。如果是在不同的时间复制相关的文件,它们彼此可能也不一致。MyISAM的.MYD和.MYI文件就是个例子。InnoDB如果检测到不一致或损坏,会记录错误日志乃至让服务器崩溃。对于非事务型存储引擎,例如MyISAM,可能的选项是锁住并刷新表。这意味着要么用LOCK TABLES和FLUSH TABLES结合的方法以使服务器将内存中的变更刷到磁盘上,要么用FLUSH TABLES WITH READ LOCK.一旦刷新完成,就可以安全地复制MyISAM的原始文件。对于InnoDB,确保文件在磁盘上一致更困难,即使使用FLUSH TABLES WITH READ LOCK,InnoDB依旧在后台运行:插入缓存、日志和写线程继续将变更合并到日志和表空间文件中。这些线程设计上是异步的——在后台执行这些工作可以帮助InnoDB取得更高的并发性——正因为如此它们与LOCK TABLES无关。因此,不仅需要确保每个文件内部是一致的,还需要同时复制同一个时间点的日志和表空间文件。如果在备份时有其他线程在修改文件,或在与表空间文件不同的时间点备份日志文件,会在恢复后再次因系统损坏而告终。可以通过下面几个方法规避这个问题。
1.等待直到InnoDB的清除线程和插入缓冲合并线程完成。可以观察SHOW INNODB STATUS的输出,当没有脏缓存或挂起的写时,就可以复制文件。尽管如此,这种方法可能需要很长一段时间;因为InnoDB的后台线程涉及太多的干扰而不太安全。所以我们不推荐这种方法
2.在一个类似LVM的系统中获取数据和日志文件一致的快照,必须让数据和日志文件一致的快照,必须让数据和日志文件在快照时相互一致;单独取它们的快照是没有意义的,
3.发送一个STOP信号给MySQL,做备份,然后再发送一个CONT信号来再次唤醒MySQL。看起来像是一个很少推荐的方法,但如果另外一种方法是在备份过程中需要关闭服务器,则这种方法值得考虑。至少这种技术不需要在重启服务器后预热。
在复制数据文件到其他地方后,就可以释放锁以使MySQL服务器再次正常运行
MySQL对存储引擎的选择会导致备份明显更复杂。问题是,对于给定的存储引擎,如何得到一致的备份。实际上有两类一致性需要考虑:数据一致性和文件一致性。
1.数据一致性
当备份时,应该考虑是否需要数据在指定时间点一致。例如,在一个电子商务数据库中,可能需要确保发货单和付款之间一致。恢复付款时如果不考虑相应的发货单,或反过来,都会导致麻烦。如果做在线备份(从一个运行的服务器做备份),可能需要所有相关表的一致性备份。这意味着不能一次锁住一张表然后做备份——因而意味着备份可能比预想的要更有侵入性。如果使用的不是事务型存储引擎,则只能在备份时用LOCK TABLES来锁住所有要一起备份的表,备份完成后再释放锁。
InnoDB的多版本控制功能可以帮到我们。开始一个事务,转储一组相关的表,然后提交事务。(如果使用了事务获取一致性备份,则不能用LOCK TABLES,因为它会隐式地提交事务——详情参见MySQL手册)。只要在服务器上使用REPEATABLE READ事务隔离级别,并且没有任何DDL,就一定会有完美的一致性,以及基于时间点的数据快照,且在备份过程中不会阻塞任何后续的工作。尽管如此,这种方法并不能保护逻辑设计很差的应用。假如在电子商务库中插入一条付款记录,提交事务,然后在另外一个事务中插入一条发货单记录。备份过程可能在这两个操作之间开始,备份了付款记录却不包括发货单记录。这就是必需仔细设计事务以确保相关的操作放在一个组内的原因。也可以用mysqldump来获得InnoDB表的一致性逻辑备份,采用--single-transaction选项可以按照我们所描述的那样工作。但是,这可能会导致一个非常长的事务,在某些负载下会导致开销大到不可接受
2.文件一致性
每个文件内部的一致性也非常重要——例如,一条大的UPDATE语句执行时备份反映不出文件的状态——并且所有要备份的文件相互间也应一致。如果没有内部一致的文件,还原时可能会感到惊讶(它们可能已经损坏)。如果是在不同的时间复制相关的文件,它们彼此可能也不一致。MyISAM的.MYD和.MYI文件就是个例子。InnoDB如果检测到不一致或损坏,会记录错误日志乃至让服务器崩溃。对于非事务型存储引擎,例如MyISAM,可能的选项是锁住并刷新表。这意味着要么用LOCK TABLES和FLUSH TABLES结合的方法以使服务器将内存中的变更刷到磁盘上,要么用FLUSH TABLES WITH READ LOCK.一旦刷新完成,就可以安全地复制MyISAM的原始文件。对于InnoDB,确保文件在磁盘上一致更困难,即使使用FLUSH TABLES WITH READ LOCK,InnoDB依旧在后台运行:插入缓存、日志和写线程继续将变更合并到日志和表空间文件中。这些线程设计上是异步的——在后台执行这些工作可以帮助InnoDB取得更高的并发性——正因为如此它们与LOCK TABLES无关。因此,不仅需要确保每个文件内部是一致的,还需要同时复制同一个时间点的日志和表空间文件。如果在备份时有其他线程在修改文件,或在与表空间文件不同的时间点备份日志文件,会在恢复后再次因系统损坏而告终。可以通过下面几个方法规避这个问题。
1.等待直到InnoDB的清除线程和插入缓冲合并线程完成。可以观察SHOW INNODB STATUS的输出,当没有脏缓存或挂起的写时,就可以复制文件。尽管如此,这种方法可能需要很长一段时间;因为InnoDB的后台线程涉及太多的干扰而不太安全。所以我们不推荐这种方法
2.在一个类似LVM的系统中获取数据和日志文件一致的快照,必须让数据和日志文件一致的快照,必须让数据和日志文件在快照时相互一致;单独取它们的快照是没有意义的,
3.发送一个STOP信号给MySQL,做备份,然后再发送一个CONT信号来再次唤醒MySQL。看起来像是一个很少推荐的方法,但如果另外一种方法是在备份过程中需要关闭服务器,则这种方法值得考虑。至少这种技术不需要在重启服务器后预热。
在复制数据文件到其他地方后,就可以释放锁以使MySQL服务器再次正常运行
3.复制
从备库中备份最大的好处是可以不干扰主库,避免在主库上增加额外的负载。这是一个建立备库的好理由,即使不需要用它做负载均衡或高可用。如果钱是个问题,也可以把备份用的备库用于其他用户,例如报表服务——只要不对其做写操作,以确保备份不会修改数据。备库不必只用于备份的目的;只需要在下次备份时能及时跟上主库,即使有时因作为其他用途导致复制延时也没有关系。当从备库备份时,应该保存所有关于复制进程的信息,例如备库相对于主库的位置。这对于很多情况都非常有用:克隆新的备库,重新应用二进制日志到主库上以获得指定时间点的恢复,将备库提升为主库等。如果停止备库,需要确保没有打开的临时表,因为它们可能导致不饿能重启备库。故意将一个备库延时一段时间对于某些灾难场景非常有用。例如延时复制一小时,当一个不期望的语句在主库上运行后,将有一个小时的时间观察到并在中继日志中方之前停掉复制。然后可以将备库提升为主库,重放少量的日志事件,跳过错误的语句。这比后面要讨论的指定事件点的恢复技术可能要快得多。Percona Toolkit中pt-slave-delay工具可以帮助实现这个方案。
备库可能与主库数据不完全一样。许多人认为备库是主库完全一样的副本,但以经验,主库与备库数据不匹配是很常见,并且MySQL没有方法检测这个问题。检测这个问题的唯一方法是使用Percona Toolkit中的pt-table-checksum之类的工具。拥有一个复制的备库可鞥在诸如主库的硬盘烧坏时提供帮助,但却不能提供保证。复制不是备份。
从备库中备份最大的好处是可以不干扰主库,避免在主库上增加额外的负载。这是一个建立备库的好理由,即使不需要用它做负载均衡或高可用。如果钱是个问题,也可以把备份用的备库用于其他用户,例如报表服务——只要不对其做写操作,以确保备份不会修改数据。备库不必只用于备份的目的;只需要在下次备份时能及时跟上主库,即使有时因作为其他用途导致复制延时也没有关系。当从备库备份时,应该保存所有关于复制进程的信息,例如备库相对于主库的位置。这对于很多情况都非常有用:克隆新的备库,重新应用二进制日志到主库上以获得指定时间点的恢复,将备库提升为主库等。如果停止备库,需要确保没有打开的临时表,因为它们可能导致不饿能重启备库。故意将一个备库延时一段时间对于某些灾难场景非常有用。例如延时复制一小时,当一个不期望的语句在主库上运行后,将有一个小时的时间观察到并在中继日志中方之前停掉复制。然后可以将备库提升为主库,重放少量的日志事件,跳过错误的语句。这比后面要讨论的指定事件点的恢复技术可能要快得多。Percona Toolkit中pt-slave-delay工具可以帮助实现这个方案。
备库可能与主库数据不完全一样。许多人认为备库是主库完全一样的副本,但以经验,主库与备库数据不匹配是很常见,并且MySQL没有方法检测这个问题。检测这个问题的唯一方法是使用Percona Toolkit中的pt-table-checksum之类的工具。拥有一个复制的备库可鞥在诸如主库的硬盘烧坏时提供帮助,但却不能提供保证。复制不是备份。
管理和备份二进制日志。
服务器的二进制日志时备份的最重要因素之一。它们对于基于时间点的恢复是必需的,并且通常比数据要小,所以更容易进行频繁的备份。如果有某个时间点的数据备份和所有从那时以后的二进制日志,就可以重放自上次全备以来的二进制日志并"前滚"所有的变更。MySQL复制也使用二进制日志。因此备份和恢复的策略经常和复制配置相互影响。二进制日志很"特别"。如果丢失了数据,你一定不希望同时丢失了二进制日志。为了让这种情况发送的几率减少到最小,可以在不同的卷上保存数据和二进制日志,即使在LVM下生成二进制日志的快照,也是可以的。为了额外的安全起见,可以将它们保存在SAN上,或用DRBD复制到另外一个设备上。经常备份二进制日志是个好主意。如果不能承受丢失超过30分钟数据的价值,至少需要每30分钟就备份一次。也可以用一个配置--log_slave_update的只读备库,这样可以获得额外的安全性。备库上日志位置与主库不匹配,但找到恢复时正确的位置并不难。最后,MySQL5.6版本的mysqlbinlog有一个非常方便的特性,可以连接到服务器上来实时对二进制日志做镜像,比起运行一个mysqld实例要简单和轻便,它与老版本时向后兼容的。
服务器的二进制日志时备份的最重要因素之一。它们对于基于时间点的恢复是必需的,并且通常比数据要小,所以更容易进行频繁的备份。如果有某个时间点的数据备份和所有从那时以后的二进制日志,就可以重放自上次全备以来的二进制日志并"前滚"所有的变更。MySQL复制也使用二进制日志。因此备份和恢复的策略经常和复制配置相互影响。二进制日志很"特别"。如果丢失了数据,你一定不希望同时丢失了二进制日志。为了让这种情况发送的几率减少到最小,可以在不同的卷上保存数据和二进制日志,即使在LVM下生成二进制日志的快照,也是可以的。为了额外的安全起见,可以将它们保存在SAN上,或用DRBD复制到另外一个设备上。经常备份二进制日志是个好主意。如果不能承受丢失超过30分钟数据的价值,至少需要每30分钟就备份一次。也可以用一个配置--log_slave_update的只读备库,这样可以获得额外的安全性。备库上日志位置与主库不匹配,但找到恢复时正确的位置并不难。最后,MySQL5.6版本的mysqlbinlog有一个非常方便的特性,可以连接到服务器上来实时对二进制日志做镜像,比起运行一个mysqld实例要简单和轻便,它与老版本时向后兼容的。
二进制日志格式。
二进制日志包含一系列的事件。每个事件有一个固定长度的头,其中有各种信息,例如当前时间戳和默认的数据库。可以使用mysqlbinlog工具来查看二进制日志的内容,打印一些头信息。下面是一个输出的例子。
```bash
mysqlbinlog mysql-bin.000002
```
第一行包含日志文件内的偏移字节值
第二行宝行以下几项:
1.事件的日期和事件,MySQL会使用它们来产生SET TIMESTAMP语句。
2.原服务器的服务器ID,对于防止复制之间无限循环和其他问题是非常有必要的。
3.end_log_pos,下一个事件的偏移字节值。该值对一个多语句事务中的大部分事件是不正确的。在此类事务过程中,MySQL的主库会复制事件到一个缓冲区,但这样做的时候它并不知道下个日志事件的位置
4.事件类型。本例中的类型是Query,但还有许多不同的类型
5.原服务器上执行事件的线程ID,对于审计和执行CONNECTION_ID()函数很重要。
6.exec_time,这是语句的时间戳和写入二进制日志的时间之差。不要依赖这个值,因为它可能在复制落后的备库上会有很大的偏差
7.在原服务器上事件产生的错误代码。如果事件在一个备库上重放时导致不同的错误,那么复制将因安全预警而失败。
后续的行包含重放变更时所需的数据。用户自定义的变更和任何其他特定设置,例如当语句执行时有效的时间戳,也将会出现在这里。如果使用的是MySQL5.1中基于行的日志,事件将不再是SQL.而是可读性较差的由语句对表所做变更的"镜像"
二进制日志包含一系列的事件。每个事件有一个固定长度的头,其中有各种信息,例如当前时间戳和默认的数据库。可以使用mysqlbinlog工具来查看二进制日志的内容,打印一些头信息。下面是一个输出的例子。
```bash
mysqlbinlog mysql-bin.000002
```
第一行包含日志文件内的偏移字节值
第二行宝行以下几项:
1.事件的日期和事件,MySQL会使用它们来产生SET TIMESTAMP语句。
2.原服务器的服务器ID,对于防止复制之间无限循环和其他问题是非常有必要的。
3.end_log_pos,下一个事件的偏移字节值。该值对一个多语句事务中的大部分事件是不正确的。在此类事务过程中,MySQL的主库会复制事件到一个缓冲区,但这样做的时候它并不知道下个日志事件的位置
4.事件类型。本例中的类型是Query,但还有许多不同的类型
5.原服务器上执行事件的线程ID,对于审计和执行CONNECTION_ID()函数很重要。
6.exec_time,这是语句的时间戳和写入二进制日志的时间之差。不要依赖这个值,因为它可能在复制落后的备库上会有很大的偏差
7.在原服务器上事件产生的错误代码。如果事件在一个备库上重放时导致不同的错误,那么复制将因安全预警而失败。
后续的行包含重放变更时所需的数据。用户自定义的变更和任何其他特定设置,例如当语句执行时有效的时间戳,也将会出现在这里。如果使用的是MySQL5.1中基于行的日志,事件将不再是SQL.而是可读性较差的由语句对表所做变更的"镜像"
安全地清除老的二进制日志。
需要决定日志的过期策略以防止磁盘被二进制日志写满。日志增长多大取决于负载和日志格式(基于行的日志回导致更大的日志记录)。我们建议,如果可能,只要日志有用就尽可能保留。保留日志对于设置复制、分析服务器负载、审计和从上次全备按时间点进行恢复,都很有帮助。当决定想要保留日志多久时,应该考虑这些需求。
一个常见的设置是使用expire_log_days变量来告诉MySQL定期清理日志。这个变量直到MySQL4.1才引入;在此之前的版本,必须手动清理二进制日志。因此,你可能看到一些用类似下面的cron项来删除老的二进制日志的建议。
```sh
0 0 * * * /usr/bin/ find /var/log/mysql -mtime +N -name "mysql-bin.[0-9]*" | xargs rm
```
尽管这是在MySQL 4.1之前清除日志的唯一办法,但在新版本中不要这么做!用rm删除日志会导致mysql-bin.index状态文件与磁盘上的文件不一致,有些语句,例如SHOW MASTER LOGS可能会受到影响而悄然失败。手动修改mysql-bin.index文件也不会修复这个问题。应该用类似下面的cron命令
```sh
0 0 * * * /usr/bin/mysql -e "PURGE MASTER LOGS BEFORE CURRENT_DATE - INTERVAL N DAY"
```
expire_logs_days设置在服务器启动或MySQL切换二进制日志时生效,因此,如果二进制日志从没有增长和切换,服务器不会清除老条目。此设置时通过查看日志的修改事件而不是内容来决定哪个文件需要被清除。
需要决定日志的过期策略以防止磁盘被二进制日志写满。日志增长多大取决于负载和日志格式(基于行的日志回导致更大的日志记录)。我们建议,如果可能,只要日志有用就尽可能保留。保留日志对于设置复制、分析服务器负载、审计和从上次全备按时间点进行恢复,都很有帮助。当决定想要保留日志多久时,应该考虑这些需求。
一个常见的设置是使用expire_log_days变量来告诉MySQL定期清理日志。这个变量直到MySQL4.1才引入;在此之前的版本,必须手动清理二进制日志。因此,你可能看到一些用类似下面的cron项来删除老的二进制日志的建议。
```sh
0 0 * * * /usr/bin/ find /var/log/mysql -mtime +N -name "mysql-bin.[0-9]*" | xargs rm
```
尽管这是在MySQL 4.1之前清除日志的唯一办法,但在新版本中不要这么做!用rm删除日志会导致mysql-bin.index状态文件与磁盘上的文件不一致,有些语句,例如SHOW MASTER LOGS可能会受到影响而悄然失败。手动修改mysql-bin.index文件也不会修复这个问题。应该用类似下面的cron命令
```sh
0 0 * * * /usr/bin/mysql -e "PURGE MASTER LOGS BEFORE CURRENT_DATE - INTERVAL N DAY"
```
expire_logs_days设置在服务器启动或MySQL切换二进制日志时生效,因此,如果二进制日志从没有增长和切换,服务器不会清除老条目。此设置时通过查看日志的修改事件而不是内容来决定哪个文件需要被清除。
备份数据。
大多时候,生成备份有好的也有差的方法——有时候显而易见的方法并不是好方法。一个有用的技巧时应该最大化利用网络、磁盘和CPU的能力以尽可能快地完成备份。这是一个需要不断取平衡的事情,必须通过实验以找到"最佳平衡点"
大多时候,生成备份有好的也有差的方法——有时候显而易见的方法并不是好方法。一个有用的技巧时应该最大化利用网络、磁盘和CPU的能力以尽可能快地完成备份。这是一个需要不断取平衡的事情,必须通过实验以找到"最佳平衡点"
生成逻辑备份。
对于逻辑备份,首先要意识到的是它们并不是以同样方式创建的。实际上有两种类型的逻辑备份:SQL导出和符号分割文件。
1.SQL导出
SQL导出是很多人所熟悉的,因为它们是mysqldump默认的方式。例如,用默认选项导出一个小表将产生如下输出:
可以是如下命令:
```bash
mysqldump -u root -p sakila actor > myactor.sql
```
导出文件包含表结构和数据,均以有效的SQL命令形式写出。文件以设置MySQL各种选项的注释开始。这些要么是为了使恢复工作更高效,要么使因为兼容性和正确性。接下来可以看到表结构,然后是数据,最后,脚本重置在导出开始时变更的选项。导出的输出对于还原操作来说是可执行的。这很方便。但mysqldump默认选项读与生成一个巨大的备份却不是太适合。mysqldump不是生成SQL逻辑备份的唯一工具。例如,也可以用mydumper或phpMyAdmin工具来创建。想指出的是,不是某一个特定的工具有多大的问题,而是做SQL逻辑备份本身就有一些缺点。下面是主要问题点:
1.Schema和数据存储在一起
如果想从单个文件恢复这样做会非常方便,但如果只想恢复一个表或指向恢复数据就很困难了。可以通过导出两次的方法来环节这个问题——一次只导出数据,另外一次只导出Schema——但还会有下一个麻烦
2.巨大的SQL语句
服务器分析和执行SQL语句的工作量非常大,所以加载数据时会非常慢
3.单个巨大的文件
大部分文本编辑器不能编辑巨大的或者包含非常长的行的文件。尽管有时候可以用命令行的流编辑器——例如sed或grep——来抽出需要的数据,但保持文件小型化仍然是更合适的
4.逻辑备份的成本很高
比起逻辑备份这种从存储引擎中读取数据然后通过客户端/服务器协议发送结果集的方式,还有其他更高效的方式
这些限制意味着SQL导出在表变大时可能变得不可用。不过,还有另外一个选择;导出数据到符号分割的文件中
对于逻辑备份,首先要意识到的是它们并不是以同样方式创建的。实际上有两种类型的逻辑备份:SQL导出和符号分割文件。
1.SQL导出
SQL导出是很多人所熟悉的,因为它们是mysqldump默认的方式。例如,用默认选项导出一个小表将产生如下输出:
可以是如下命令:
```bash
mysqldump -u root -p sakila actor > myactor.sql
```
导出文件包含表结构和数据,均以有效的SQL命令形式写出。文件以设置MySQL各种选项的注释开始。这些要么是为了使恢复工作更高效,要么使因为兼容性和正确性。接下来可以看到表结构,然后是数据,最后,脚本重置在导出开始时变更的选项。导出的输出对于还原操作来说是可执行的。这很方便。但mysqldump默认选项读与生成一个巨大的备份却不是太适合。mysqldump不是生成SQL逻辑备份的唯一工具。例如,也可以用mydumper或phpMyAdmin工具来创建。想指出的是,不是某一个特定的工具有多大的问题,而是做SQL逻辑备份本身就有一些缺点。下面是主要问题点:
1.Schema和数据存储在一起
如果想从单个文件恢复这样做会非常方便,但如果只想恢复一个表或指向恢复数据就很困难了。可以通过导出两次的方法来环节这个问题——一次只导出数据,另外一次只导出Schema——但还会有下一个麻烦
2.巨大的SQL语句
服务器分析和执行SQL语句的工作量非常大,所以加载数据时会非常慢
3.单个巨大的文件
大部分文本编辑器不能编辑巨大的或者包含非常长的行的文件。尽管有时候可以用命令行的流编辑器——例如sed或grep——来抽出需要的数据,但保持文件小型化仍然是更合适的
4.逻辑备份的成本很高
比起逻辑备份这种从存储引擎中读取数据然后通过客户端/服务器协议发送结果集的方式,还有其他更高效的方式
这些限制意味着SQL导出在表变大时可能变得不可用。不过,还有另外一个选择;导出数据到符号分割的文件中
符号分隔文件备份
可以使用SQL命令SELECT INTO OUTFILE以符号分隔文件格式创建数据的逻辑备份。(可以用mysqldump的 --tab选项导出到符号分隔文件中)。符号分隔文件包含以ASCII展示的原始数据,没有SQL、注释和列名。下面是一个导出为逗号分隔值(CVS)格式的例子。对于表个形式的数据来说这是一个很好的通用格式。
```sql
mysql> SHOW VARIABLES LIKE '%secure%';
mysql> SELECT * INTO OUTFILE '/var/lib/mysql-files//actor.txt' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' FROM sakila.actor;
```
导出时你可能会遇到报错,按照图中所示导入到mysql认为安全的目录下就行。比起SQL导出文件,符号分隔文件要更紧凑且更易于用命令行工具操作,这种方法最大的优点时备份和还原速度更快。可以和导出时使用一样的选项,用LOAD DATA INFILE方法加载数据到表中。
```sql
mysql> LOAD DATA INFILE '/var/lib/mysql-files/actor.txt' INTO TABLE sakila.actor FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n';
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
```
图中也可以看到,由于主键发生了冲突所以没有导入成功,但最起码说明这个导入数据的动作是有的。
下面这个非正式的测试演示了SQL文件和符号分隔文件在备份和还原上的速度差异。在测试中,我们对生产数据做了修改。导出的表看起来像下面这样:
```sql
CREATE TABLE load_test(
col1 date NOT NULL,
col2 int NOT NULL,
col3 smallint unsigned NOT NULL,
col4 mediumint NOT NULL,
col5 mediumint NOT NULL,
col6 mediumint NOT NULL,
col7 decimal(3,1) default NULL,
col8 varchar(10) NOT NULL default '',
col9 int NOT NULL,
PRIMARY KEY (col1, col2)
) ENGINE =InnoDB;
```
这张表有1500万行,占用近700MB的磁盘空格键。对比了两种备份和还原方法的性能。可以看到测试中还原时间有较大的差异。但是SELECT INTO OUTFILE方法也有一些限制。
1.只能备份到运行MySQL服务器的机器上的文件中(可以写一个自定义SELECT INTO OUTFILE程序,在读取SELECT结果的同时写到磁盘文件中,我们已经看到有些人采用这种方法)
2.运行MySQL的系统用户必须有文件目录的写权限,因为是由MySQL服务器来执行文件的写入,而不是运行SQL命令的用户
3.出于安全原因,不能覆盖已经存在的文件,不管文件权限如何
4.不能直接导出到压缩文件中
5.某些情况下很难进行正确的导出或导入,例如非标准的字符集
可以使用SQL命令SELECT INTO OUTFILE以符号分隔文件格式创建数据的逻辑备份。(可以用mysqldump的 --tab选项导出到符号分隔文件中)。符号分隔文件包含以ASCII展示的原始数据,没有SQL、注释和列名。下面是一个导出为逗号分隔值(CVS)格式的例子。对于表个形式的数据来说这是一个很好的通用格式。
```sql
mysql> SHOW VARIABLES LIKE '%secure%';
mysql> SELECT * INTO OUTFILE '/var/lib/mysql-files//actor.txt' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' FROM sakila.actor;
```
导出时你可能会遇到报错,按照图中所示导入到mysql认为安全的目录下就行。比起SQL导出文件,符号分隔文件要更紧凑且更易于用命令行工具操作,这种方法最大的优点时备份和还原速度更快。可以和导出时使用一样的选项,用LOAD DATA INFILE方法加载数据到表中。
```sql
mysql> LOAD DATA INFILE '/var/lib/mysql-files/actor.txt' INTO TABLE sakila.actor FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n';
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
```
图中也可以看到,由于主键发生了冲突所以没有导入成功,但最起码说明这个导入数据的动作是有的。
下面这个非正式的测试演示了SQL文件和符号分隔文件在备份和还原上的速度差异。在测试中,我们对生产数据做了修改。导出的表看起来像下面这样:
```sql
CREATE TABLE load_test(
col1 date NOT NULL,
col2 int NOT NULL,
col3 smallint unsigned NOT NULL,
col4 mediumint NOT NULL,
col5 mediumint NOT NULL,
col6 mediumint NOT NULL,
col7 decimal(3,1) default NULL,
col8 varchar(10) NOT NULL default '',
col9 int NOT NULL,
PRIMARY KEY (col1, col2)
) ENGINE =InnoDB;
```
这张表有1500万行,占用近700MB的磁盘空格键。对比了两种备份和还原方法的性能。可以看到测试中还原时间有较大的差异。但是SELECT INTO OUTFILE方法也有一些限制。
1.只能备份到运行MySQL服务器的机器上的文件中(可以写一个自定义SELECT INTO OUTFILE程序,在读取SELECT结果的同时写到磁盘文件中,我们已经看到有些人采用这种方法)
2.运行MySQL的系统用户必须有文件目录的写权限,因为是由MySQL服务器来执行文件的写入,而不是运行SQL命令的用户
3.出于安全原因,不能覆盖已经存在的文件,不管文件权限如何
4.不能直接导出到压缩文件中
5.某些情况下很难进行正确的导出或导入,例如非标准的字符集
文件系统快照。
文件系统快照是一种非常好的在线备份方法。支持快照的文件系统能够瞬间创建用来备份的内容一致的镜像。支持快照的文件系统和设备包括FreeBSD的文件系统、ZFS文件系统、GNU/Linux的逻辑卷管理(LVM)、以及许多的SAN系统和文件存储解决方案,例如NetApp存储。不要把快照和备份相混淆。创建快照是减少必须持有锁的时间的一个简单方法:释放锁后,必须复制文件到备份中。事实上,有些时候甚至可以创建InnoDB快照而不需要锁定。我们将要展示两种使用LVM来对InnoDB文件系统做备份的方法,可以选择最小化锁或零锁的方案。快照对于特别用途的备份是一个非常好的方法。一个例子是在升级过程中遇到有问题而回退的情况。可以在升级前创建一个镜像,这样如果升级有问题,只需要回滚到该镜像。可以对任何不确定和有风险的操作都这么做,例如对一个巨大的表做变更(需要多少时间是未知的)
文件系统快照是一种非常好的在线备份方法。支持快照的文件系统能够瞬间创建用来备份的内容一致的镜像。支持快照的文件系统和设备包括FreeBSD的文件系统、ZFS文件系统、GNU/Linux的逻辑卷管理(LVM)、以及许多的SAN系统和文件存储解决方案,例如NetApp存储。不要把快照和备份相混淆。创建快照是减少必须持有锁的时间的一个简单方法:释放锁后,必须复制文件到备份中。事实上,有些时候甚至可以创建InnoDB快照而不需要锁定。我们将要展示两种使用LVM来对InnoDB文件系统做备份的方法,可以选择最小化锁或零锁的方案。快照对于特别用途的备份是一个非常好的方法。一个例子是在升级过程中遇到有问题而回退的情况。可以在升级前创建一个镜像,这样如果升级有问题,只需要回滚到该镜像。可以对任何不确定和有风险的操作都这么做,例如对一个巨大的表做变更(需要多少时间是未知的)
LVM快照是如何工作的。
LVM使用写时复制(copy-on-write)的技术来创建快照——例如,对整个卷的某个瞬间的逻辑副本,这与数据库中的MVCC有点像,不同的是它只保留一个老的数据版本。注意,我们说的不是物理副本。逻辑副本看起来好像包含了创建快照卷中所有的数据,但实际上一开始快照是不包含数据的。相比复制数据到快照中,LVM只是简单地标记创建快照的时间点,然后对该快照请求读数据时,实际上是从原始卷中读取的。因此初始的复制基本上是一个瞬间就能完成的操作,不管创建快照的卷有多大。当原始卷中某些数据有变化时,LVM在任何变更写入之前,会复制受影响的块到快照预留的区域中。LVM不保留数据的多个"老版本",因此对原始卷中变更块的额外写入并不需要对快照做其他更多的工作。换句话说,对每个块只有第一次写入才会导致写时复制到预留的区域。
现在,在快照中请求这些块时,LVM会从复制块中而不是从原始卷中读取。所以,可以继续看到快照中相同时间点的数据而不需要阻塞任何原始卷。如图所示。快照会在/dev目录下创建一个新的逻辑卷,可以像挂载其他设备一样挂载它。理论上讲,这种技术可以对一个非常大的卷做快照,而只需要非常少的物理存储空间,但是,必须设置足够的空间,保证在快照打开时,能够保存所有期望在原始卷上更新的块。如果不预留足够的写时复制空间,当快照用完所有的空间后,设备就会变得不可用。这个影响就像拔出一个外部设备:任何从设备上读的备份工作都会因IO错误而失败
LVM使用写时复制(copy-on-write)的技术来创建快照——例如,对整个卷的某个瞬间的逻辑副本,这与数据库中的MVCC有点像,不同的是它只保留一个老的数据版本。注意,我们说的不是物理副本。逻辑副本看起来好像包含了创建快照卷中所有的数据,但实际上一开始快照是不包含数据的。相比复制数据到快照中,LVM只是简单地标记创建快照的时间点,然后对该快照请求读数据时,实际上是从原始卷中读取的。因此初始的复制基本上是一个瞬间就能完成的操作,不管创建快照的卷有多大。当原始卷中某些数据有变化时,LVM在任何变更写入之前,会复制受影响的块到快照预留的区域中。LVM不保留数据的多个"老版本",因此对原始卷中变更块的额外写入并不需要对快照做其他更多的工作。换句话说,对每个块只有第一次写入才会导致写时复制到预留的区域。
现在,在快照中请求这些块时,LVM会从复制块中而不是从原始卷中读取。所以,可以继续看到快照中相同时间点的数据而不需要阻塞任何原始卷。如图所示。快照会在/dev目录下创建一个新的逻辑卷,可以像挂载其他设备一样挂载它。理论上讲,这种技术可以对一个非常大的卷做快照,而只需要非常少的物理存储空间,但是,必须设置足够的空间,保证在快照打开时,能够保存所有期望在原始卷上更新的块。如果不预留足够的写时复制空间,当快照用完所有的空间后,设备就会变得不可用。这个影响就像拔出一个外部设备:任何从设备上读的备份工作都会因IO错误而失败
先决条件和配置。
创建一个快照的消耗几乎微不足道,但还是需要确保系统配置可以让你获取在备份瞬间的所有需要的文件的一致性副本。首先,确保系统满足下面这些条件。
1.所有的InnoDB文件(InnoDB的表空间文件和InnoDB的事务日志)必须时在单个逻辑卷(分区)。你需要绝对的时间点一致性,LVM不能为多于一个卷做某个时间点一致的快照。(这是LVM的一个限制;其他一些系统没有这个问题)
2.如果需要备份表定义,MySQL数据目录必须在相同的逻辑卷中。如果使用另外一种方法来备份表的帝国一,例如只备份Schema到版本控制系统中,就不需要担心这个问题
3.必须在卷组中有足够的空闲空间来创建快照。需要多少取决于负载。当配置系统时,应该留一些未分配的空间以便后面做快照。
LVM有卷组的概念,它包含一个或多个逻辑卷。可以按照如下的方式查看系统中的卷组:
```bash
vgs
VG:vg
#PV:1
#LV:4
#SN:0
Attr:wz--n-
VSize:534.18G
VFree:249.18G
```
输出显示了一个分布在一个物理卷上的卷组,它有四个逻辑卷,大概有250GB空间空闲。入股哦需要,可用vgdisplay命令产生更详细的输出。现在让我们看下系统上的逻辑卷
创建一个快照的消耗几乎微不足道,但还是需要确保系统配置可以让你获取在备份瞬间的所有需要的文件的一致性副本。首先,确保系统满足下面这些条件。
1.所有的InnoDB文件(InnoDB的表空间文件和InnoDB的事务日志)必须时在单个逻辑卷(分区)。你需要绝对的时间点一致性,LVM不能为多于一个卷做某个时间点一致的快照。(这是LVM的一个限制;其他一些系统没有这个问题)
2.如果需要备份表定义,MySQL数据目录必须在相同的逻辑卷中。如果使用另外一种方法来备份表的帝国一,例如只备份Schema到版本控制系统中,就不需要担心这个问题
3.必须在卷组中有足够的空闲空间来创建快照。需要多少取决于负载。当配置系统时,应该留一些未分配的空间以便后面做快照。
LVM有卷组的概念,它包含一个或多个逻辑卷。可以按照如下的方式查看系统中的卷组:
```bash
vgs
VG:vg
#PV:1
#LV:4
#SN:0
Attr:wz--n-
VSize:534.18G
VFree:249.18G
```
输出显示了一个分布在一个物理卷上的卷组,它有四个逻辑卷,大概有250GB空间空闲。入股哦需要,可用vgdisplay命令产生更详细的输出。现在让我们看下系统上的逻辑卷
```bash
lvs
LV:home
VG:vg
Attr:-wi-ao
LSize:40.00G
Origin Snap%:
Move Log Copy%:
LV:mysql
VG:vg
Attr:-wi-ao
LSize:225.00G
Origin Snap%:
Move Log Copy%:
LV:tmp
VG:vg
Attr:-wi-ao
LSize:10.00G
Origin Snap%:
Move Log Copy%:
LV:var
VG:vg
Attr:-wi-ao
LSize:10.00G
Origin Snap%:
Move Log Copy%:
```
lvs
LV:home
VG:vg
Attr:-wi-ao
LSize:40.00G
Origin Snap%:
Move Log Copy%:
LV:mysql
VG:vg
Attr:-wi-ao
LSize:225.00G
Origin Snap%:
Move Log Copy%:
LV:tmp
VG:vg
Attr:-wi-ao
LSize:10.00G
Origin Snap%:
Move Log Copy%:
LV:var
VG:vg
Attr:-wi-ao
LSize:10.00G
Origin Snap%:
Move Log Copy%:
```
创建、挂载和删除LVM快照。
一条命令就能创建快照。只需要决定快照存放的未知和分配给写时复制的空间大小即可。不要纠结于是否适用比想象中的需求更多的空间。LVM不会马上使用完所有指定的空格键,只是为后续适用预留而已。因此多预留一点空间并没有坏处,除非你必须同时为其他快照预留空间。让我们来练习创建一个快照。我们给它16GB的写时复制空间,名字为backup_mysql.
```bash
lvcreate --size 16G --snapshot --name backup_mysql /dev/vg/mysq
```
这里特意命名为backup_mysql卷而不是mysql_backup,是为了避免Tab键自动补全造成误会。这有助于避免因为Tab键自动补全导致突然误删除mysql卷组的可能。我们可以适用lvs看看新创建的卷的状态。快照的属性与原设备不同,而且该输出还显示了一点额外的信息:原始卷组和分配了16GB的写时复制空间目前已经使用了多少。备份对此进行监控是个非常好的主意,可以知道是否会因为设备写满而备份失败。可以交互地监控设备的状态,或使用诸如Nagios这样的监控系统。
```bash
wathc 'lvs | grep backup'
```
从前面mount的输出可以看到,mysql卷包含一个文件系统。这意味着快照也同样如此,可以像其他文件系统一样挂载。
```bash
mkdir /tmp/backup
mount /dev/mapper/vg-backup_mysql /tmp/backup
ls -l /tmp/backup/mysql
```
这里只是为了联系,因此我们卸载这个快照并用lvremove命令将其删除
```bash
umount /tmp/backup
rmdir /tmp/backup
lvremove --force /dev/vg/backup_mysql
```
一条命令就能创建快照。只需要决定快照存放的未知和分配给写时复制的空间大小即可。不要纠结于是否适用比想象中的需求更多的空间。LVM不会马上使用完所有指定的空格键,只是为后续适用预留而已。因此多预留一点空间并没有坏处,除非你必须同时为其他快照预留空间。让我们来练习创建一个快照。我们给它16GB的写时复制空间,名字为backup_mysql.
```bash
lvcreate --size 16G --snapshot --name backup_mysql /dev/vg/mysq
```
这里特意命名为backup_mysql卷而不是mysql_backup,是为了避免Tab键自动补全造成误会。这有助于避免因为Tab键自动补全导致突然误删除mysql卷组的可能。我们可以适用lvs看看新创建的卷的状态。快照的属性与原设备不同,而且该输出还显示了一点额外的信息:原始卷组和分配了16GB的写时复制空间目前已经使用了多少。备份对此进行监控是个非常好的主意,可以知道是否会因为设备写满而备份失败。可以交互地监控设备的状态,或使用诸如Nagios这样的监控系统。
```bash
wathc 'lvs | grep backup'
```
从前面mount的输出可以看到,mysql卷包含一个文件系统。这意味着快照也同样如此,可以像其他文件系统一样挂载。
```bash
mkdir /tmp/backup
mount /dev/mapper/vg-backup_mysql /tmp/backup
ls -l /tmp/backup/mysql
```
这里只是为了联系,因此我们卸载这个快照并用lvremove命令将其删除
```bash
umount /tmp/backup
rmdir /tmp/backup
lvremove --force /dev/vg/backup_mysql
```
用于在线备份的LVM快照。
现在已经知道如何创建、加载和删除快照,可以使用它们来进行备份了。首先看一下如何在不停止MySQL服务的情况下备份InnoDB数据库,这里需要使用一个全局的读锁。连接MySQL服务器并使用一个全局读锁将表刷到磁盘上,然后获取二进制日志的位置:
```sql
mysql>FLUSH TABLES WITH READ LOCK; SHOW MASTER STATUS;
```
记录SHOW MASTER STATUS的输出,确保到MySQL的连接处于打开状态,以使读锁不被释放。然后获取LVM的快照并立刻释放该读锁,可以使用UNLOCK TABLES或直接关闭连接来释放锁。最后,加载快照并赋值文件到备份位置。这种方法最主要的问题是,获取读锁可能需要一点时间,特别时当有许多长时间运行的查询时。当连接等待全局读锁时,所有的查询都将被阻塞,并且不可预测这会持续多久。
现在已经知道如何创建、加载和删除快照,可以使用它们来进行备份了。首先看一下如何在不停止MySQL服务的情况下备份InnoDB数据库,这里需要使用一个全局的读锁。连接MySQL服务器并使用一个全局读锁将表刷到磁盘上,然后获取二进制日志的位置:
```sql
mysql>FLUSH TABLES WITH READ LOCK; SHOW MASTER STATUS;
```
记录SHOW MASTER STATUS的输出,确保到MySQL的连接处于打开状态,以使读锁不被释放。然后获取LVM的快照并立刻释放该读锁,可以使用UNLOCK TABLES或直接关闭连接来释放锁。最后,加载快照并赋值文件到备份位置。这种方法最主要的问题是,获取读锁可能需要一点时间,特别时当有许多长时间运行的查询时。当连接等待全局读锁时,所有的查询都将被阻塞,并且不可预测这会持续多久。
文件系统快照和InnoDB.
即使锁住所有的表,InnoDB的后台线程仍会继续工作,因此,即使在创建快照时,仍然可以往文件中写入。并且,由于InnoDB没有执行关闭操作。如果服务器意外断电,快照中InnoDB的文件会和服务器意外掉电后文件的遭遇一样。这不是什么问题,因为InnoDB是个ACID系统。任何时刻(例如快照时),每隔提交的事务要么在InnoDB数据文件中要么在日志文件中。在还原快照后启动MySQL时,InnoDB将运行恢复进程,就像服务器断过电一样。它会查找事务日志中任何提交但没有应用到数据文件中的事务然后应用,因此不会丢失任何事务。这正是要强制InnoDB数据文件和日志文件在一起快照的原因。这也是在备份后需要测试的原因。启动一个MySQL实例,把它指向一个新备份,让InnoDB执行崩溃恢复过程,然后检测所有的表。通过这种方法,就不会备份损坏了却还不知道(文件可能由于任何原因损坏)。这么做的另外一个好处是,维拉i需要从备份中还原时会更快,因为已经在备份上运行过一遍恢复程序了。甚至还可以在将快照复制到备份目的地之前,直接在快照上做上面的操作,但增加一点点额外开销。所以需要确保这是计划内的操作
即使锁住所有的表,InnoDB的后台线程仍会继续工作,因此,即使在创建快照时,仍然可以往文件中写入。并且,由于InnoDB没有执行关闭操作。如果服务器意外断电,快照中InnoDB的文件会和服务器意外掉电后文件的遭遇一样。这不是什么问题,因为InnoDB是个ACID系统。任何时刻(例如快照时),每隔提交的事务要么在InnoDB数据文件中要么在日志文件中。在还原快照后启动MySQL时,InnoDB将运行恢复进程,就像服务器断过电一样。它会查找事务日志中任何提交但没有应用到数据文件中的事务然后应用,因此不会丢失任何事务。这正是要强制InnoDB数据文件和日志文件在一起快照的原因。这也是在备份后需要测试的原因。启动一个MySQL实例,把它指向一个新备份,让InnoDB执行崩溃恢复过程,然后检测所有的表。通过这种方法,就不会备份损坏了却还不知道(文件可能由于任何原因损坏)。这么做的另外一个好处是,维拉i需要从备份中还原时会更快,因为已经在备份上运行过一遍恢复程序了。甚至还可以在将快照复制到备份目的地之前,直接在快照上做上面的操作,但增加一点点额外开销。所以需要确保这是计划内的操作
使用LVM快照无所InnoDB备份。
无锁备份只有一点不同。区别是不需要执行FLUSH TABLES WITH READ LOCK.这意味着不能保证MyISAM文件在磁盘上一致,如果只使用InnoDB,这就不是问题。mysql系统数据库中依然有部分MyISAM表,但如果是典型的工作负载,在快照时这些表不太可能发生改变。如果你认为mysql系统表可能会变更,那么可以锁住并刷新这些表。一般不会对这些表有长时间运行的查询,所以通常会很快:
```sql
mysql> LOCK TABLES mysql.user READ, mysql.db READ;
mysql> FLUSH TABLES mysql.user, mysql.db;
```
由于没有用全局读锁,因此不会从SHOW MASTER STATUS 中获取到任何有用的信息。尽管如此,基于快照启动MySQL(来验证备份的完整性)时,也将会在日志文件中看到像下面的内容:
```bash
InnoDB: Doing recovery: scanned up to log sequence number 0 40817239
InnoDB: Starting an apply batch of log records to the database...
InnoDB:Progress in percents: 3 4 5 6 .. [omitted] ... 97 98 99
InnoDB:Apply batch completed
InnoDB:Last MySQL binlog file position 0 3304937,file name /var/log/mysql/mysql-bin.000001
070928 14:08:42 InnoDB:Started; log sequence number 0 40817239
```
InnoDB记录了MySQL已经恢复得时间点对应的二进制日志位置,这个二进制日志位置可以用来做基于时间点的恢复。使用快照进行无锁备份的方法在MySQL5.0或更新版本中有变动。这些MySQL版本使用XA来协调InnoDB和二进制日志。如果还原到一个与备份时server_id不同的服务器,服务器在准备事务阶段可能发现这是从另外一个与自己不同ID的服务器来的。在这种情况下,服务器会变得困惑,恢复事务时可能会卡在PREPARED状态。这种情况很少发生,但是存在可能性。这也是只有经过验证才可以说备份成功的原因。有些备份也许是不能恢复的。如果时在备库上获取快照,InnoDB恢复时还会打印如下几行日志:
```bash
InnoDB:In a MySQL replica the last master binlog file
InnoDB:position 0 115, file name mysql-bin.001717
```
输出显示了InnoDB已经恢复了基于主库的二进制位置(相对于备库二进制日志位置),这对基于备库备份或基于其他备库克隆备库来说非常有用
无锁备份只有一点不同。区别是不需要执行FLUSH TABLES WITH READ LOCK.这意味着不能保证MyISAM文件在磁盘上一致,如果只使用InnoDB,这就不是问题。mysql系统数据库中依然有部分MyISAM表,但如果是典型的工作负载,在快照时这些表不太可能发生改变。如果你认为mysql系统表可能会变更,那么可以锁住并刷新这些表。一般不会对这些表有长时间运行的查询,所以通常会很快:
```sql
mysql> LOCK TABLES mysql.user READ, mysql.db READ;
mysql> FLUSH TABLES mysql.user, mysql.db;
```
由于没有用全局读锁,因此不会从SHOW MASTER STATUS 中获取到任何有用的信息。尽管如此,基于快照启动MySQL(来验证备份的完整性)时,也将会在日志文件中看到像下面的内容:
```bash
InnoDB: Doing recovery: scanned up to log sequence number 0 40817239
InnoDB: Starting an apply batch of log records to the database...
InnoDB:Progress in percents: 3 4 5 6 .. [omitted] ... 97 98 99
InnoDB:Apply batch completed
InnoDB:Last MySQL binlog file position 0 3304937,file name /var/log/mysql/mysql-bin.000001
070928 14:08:42 InnoDB:Started; log sequence number 0 40817239
```
InnoDB记录了MySQL已经恢复得时间点对应的二进制日志位置,这个二进制日志位置可以用来做基于时间点的恢复。使用快照进行无锁备份的方法在MySQL5.0或更新版本中有变动。这些MySQL版本使用XA来协调InnoDB和二进制日志。如果还原到一个与备份时server_id不同的服务器,服务器在准备事务阶段可能发现这是从另外一个与自己不同ID的服务器来的。在这种情况下,服务器会变得困惑,恢复事务时可能会卡在PREPARED状态。这种情况很少发生,但是存在可能性。这也是只有经过验证才可以说备份成功的原因。有些备份也许是不能恢复的。如果时在备库上获取快照,InnoDB恢复时还会打印如下几行日志:
```bash
InnoDB:In a MySQL replica the last master binlog file
InnoDB:position 0 115, file name mysql-bin.001717
```
输出显示了InnoDB已经恢复了基于主库的二进制位置(相对于备库二进制日志位置),这对基于备库备份或基于其他备库克隆备库来说非常有用
规划LVM备份。
LVM快照备份也是有开销的。服务器写到原始卷的越多,引发的额外开销也越多。当服务器随机修改许多不同块时,磁头需要需要自写时复制空间来来回回寻址,并且将数据的老版本写到写时复制空间。从快照中读取也有开销,因为LVM需要从原始卷中读取大部分数据。只有快照创建后修改过的数据从写时复制空间读取;因此,逻辑顺序读取快照数据实际上也可能导致磁头来回移动。所以应该为此规划好快照。快照实际上会导致原始卷和快照都比正常的读/写性能要差——如果使用过多的写时复制空间,性能可能会差很多。这会降低MySQL服务器和复制文件进行备份的性能。我们做了基准测试,发现LVM快照的开销要远高于它本应该有的——我们发现性能最多可能会慢5倍,具体取决于负载和文件系统。再规划备份时要记得这一点。
规划中另外一个重要的事情是,为快照分配足够多的空间。我们一般采取下面的方法.
1.记住,LVM只需要复制每个修改块到快照一次。MySQL写一个块到原始卷中时,它会复制这个块到快照中,然后对复制的块在例外表中生成一个标记。后续对这个块的写不会产生任何到快照的复制
2.如果只使用InnoDB,要考虑InnoDB是如何写数据的。InnoDB实际需要对数据写两遍,至少一半的InnoDB的写IO会到双写缓冲区(doublewrite buffer)、日志文件,以及其他磁盘上相对小的区域中。这部分会多次重用相同的磁盘块,因此第一次时对快照有影响,但写过一次以后就不会对快照带来写压力
3.接下来,相对于反复修改同样的数据,需要评估有多少IO需要写入到那些还没有复制到快照写时复制的空间的块中,对评估的结果要保留足够的余量
4.使用vmstat或iostat来手机服务器每秒写多少块的统计信息
5.衡量(或评估)复制备份到其他地方需要多久。换言之,需要在复制起见保持LVM快照打开多长时间。
假设评估出有一半的写会导致往快照的写时复制空间的写操作,并且服务器支持10MB/s的写入。如果需要一个小时(3600s)将快照复制到另外一个服务器上,那么将需要1/2 * 10MB * 3600 即18GB的快照空间。考虑到容错,还要增加一些额外的空间。有时候当快照打开时,很容易计算会有多少数据发生改变。让我们看个例子。BoardReader论坛搜索引擎每隔存储节点有1TB的InnoDB表。但是,我们知道最大的开销时加载心数据。每天新增近10GB的数据,因此50GB的快照空间应该完全足够。然而这样来评估不总是正确的。假设在某个时间点,有一个长时间运行的依次修改每个分片的ALTER TABLE操作,它会修改超过50GB的数据;在这个时间点,就不能做备份的操作。为了避免这样的问题,可以稍后再创建快照,因为创建快照后会导致一个负载的高峰。
LVM快照备份也是有开销的。服务器写到原始卷的越多,引发的额外开销也越多。当服务器随机修改许多不同块时,磁头需要需要自写时复制空间来来回回寻址,并且将数据的老版本写到写时复制空间。从快照中读取也有开销,因为LVM需要从原始卷中读取大部分数据。只有快照创建后修改过的数据从写时复制空间读取;因此,逻辑顺序读取快照数据实际上也可能导致磁头来回移动。所以应该为此规划好快照。快照实际上会导致原始卷和快照都比正常的读/写性能要差——如果使用过多的写时复制空间,性能可能会差很多。这会降低MySQL服务器和复制文件进行备份的性能。我们做了基准测试,发现LVM快照的开销要远高于它本应该有的——我们发现性能最多可能会慢5倍,具体取决于负载和文件系统。再规划备份时要记得这一点。
规划中另外一个重要的事情是,为快照分配足够多的空间。我们一般采取下面的方法.
1.记住,LVM只需要复制每个修改块到快照一次。MySQL写一个块到原始卷中时,它会复制这个块到快照中,然后对复制的块在例外表中生成一个标记。后续对这个块的写不会产生任何到快照的复制
2.如果只使用InnoDB,要考虑InnoDB是如何写数据的。InnoDB实际需要对数据写两遍,至少一半的InnoDB的写IO会到双写缓冲区(doublewrite buffer)、日志文件,以及其他磁盘上相对小的区域中。这部分会多次重用相同的磁盘块,因此第一次时对快照有影响,但写过一次以后就不会对快照带来写压力
3.接下来,相对于反复修改同样的数据,需要评估有多少IO需要写入到那些还没有复制到快照写时复制的空间的块中,对评估的结果要保留足够的余量
4.使用vmstat或iostat来手机服务器每秒写多少块的统计信息
5.衡量(或评估)复制备份到其他地方需要多久。换言之,需要在复制起见保持LVM快照打开多长时间。
假设评估出有一半的写会导致往快照的写时复制空间的写操作,并且服务器支持10MB/s的写入。如果需要一个小时(3600s)将快照复制到另外一个服务器上,那么将需要1/2 * 10MB * 3600 即18GB的快照空间。考虑到容错,还要增加一些额外的空间。有时候当快照打开时,很容易计算会有多少数据发生改变。让我们看个例子。BoardReader论坛搜索引擎每隔存储节点有1TB的InnoDB表。但是,我们知道最大的开销时加载心数据。每天新增近10GB的数据,因此50GB的快照空间应该完全足够。然而这样来评估不总是正确的。假设在某个时间点,有一个长时间运行的依次修改每个分片的ALTER TABLE操作,它会修改超过50GB的数据;在这个时间点,就不能做备份的操作。为了避免这样的问题,可以稍后再创建快照,因为创建快照后会导致一个负载的高峰。
备份误区2:"快照就是备份".
一个快照,不论是LVM快照、ZFS快照,还是SAN快照,都不是实际的备份,因为它不包含数据的完整副本。正因为快照是写时复制的,所以它只包含实际数据和快照发生的时间点的数据之间的差异数据。如果一个没有被修改的块在备份副本时被损坏,那就没有该块的正常副本可以用来恢复,并且备份副本时每隔快照看到的都是相同的损坏的块。可以使用快照来"冻结"备份时的数据,但不要把快照当作一个备份
一个快照,不论是LVM快照、ZFS快照,还是SAN快照,都不是实际的备份,因为它不包含数据的完整副本。正因为快照是写时复制的,所以它只包含实际数据和快照发生的时间点的数据之间的差异数据。如果一个没有被修改的块在备份副本时被损坏,那就没有该块的正常副本可以用来恢复,并且备份副本时每隔快照看到的都是相同的损坏的块。可以使用快照来"冻结"备份时的数据,但不要把快照当作一个备份
快照的其他用途和替代方案。
快照有更多的其他用途,而不仅仅用于备份。例如,之前提到,在一个有潜在危险的动作之前生成一个"检查点"会有帮助。有些系统允许将快照提升为原文件系统,这使得回滚到生成快照的时间点的数据非常简单。文件系统快照不是取得数据瞬间副本的唯一方法。另一个选择是RAID分裂;举个例子,如果有一个三磁盘的软RAID镜像,就可以从该RAID组中移出来一个磁盘单独加载。这样做没有写时复制的代价,并且需要时将此类"快照"提升为主副本的操作也很简单。不错,如果要将磁盘加回到RAID集合,就必须重新进行同步,当然,天下没有免费的午餐。
快照有更多的其他用途,而不仅仅用于备份。例如,之前提到,在一个有潜在危险的动作之前生成一个"检查点"会有帮助。有些系统允许将快照提升为原文件系统,这使得回滚到生成快照的时间点的数据非常简单。文件系统快照不是取得数据瞬间副本的唯一方法。另一个选择是RAID分裂;举个例子,如果有一个三磁盘的软RAID镜像,就可以从该RAID组中移出来一个磁盘单独加载。这样做没有写时复制的代价,并且需要时将此类"快照"提升为主副本的操作也很简单。不错,如果要将磁盘加回到RAID集合,就必须重新进行同步,当然,天下没有免费的午餐。
从备份中恢复。
如何恢复数据取决于是怎么备份的。可能需要以下部分或全部步骤。
1.停止MySQL服务器
2.记录服务器的配置和文件权限
3.将数据从备份中移到MySQL数据目录
4.改变配置
5.改变文件权限
6.以限制访问模式重启服务器,等待完成启动
7.载入逻辑备份文件
8.检查和重放二进制日志
9.检测已经还原的数据
10.以完全权限重启服务器
如果有机会使用文件的当前版本,就不要用备份中的文件来代替。例如,如果备份包含二进制日志,并且需要重放这些日志来做基于时间点的恢复,那么不要把当前二进制日志用备份中的老的副本替代。如果有需要,可以将其重命名或移动到其他地方。
在恢复过程中,保证MySQL除了恢复进程外不接受其他访问,这一点往往比较重要。我们喜欢以--skip-networking和--socket=/tmp/mysql_recover.sock选项来启动MySQL.以确保它对于已经存在的应用不可访问,直到我们检测完并重新提供服务。这对于按块加载的逻辑备份的恢复来说尤其重要
如何恢复数据取决于是怎么备份的。可能需要以下部分或全部步骤。
1.停止MySQL服务器
2.记录服务器的配置和文件权限
3.将数据从备份中移到MySQL数据目录
4.改变配置
5.改变文件权限
6.以限制访问模式重启服务器,等待完成启动
7.载入逻辑备份文件
8.检查和重放二进制日志
9.检测已经还原的数据
10.以完全权限重启服务器
如果有机会使用文件的当前版本,就不要用备份中的文件来代替。例如,如果备份包含二进制日志,并且需要重放这些日志来做基于时间点的恢复,那么不要把当前二进制日志用备份中的老的副本替代。如果有需要,可以将其重命名或移动到其他地方。
在恢复过程中,保证MySQL除了恢复进程外不接受其他访问,这一点往往比较重要。我们喜欢以--skip-networking和--socket=/tmp/mysql_recover.sock选项来启动MySQL.以确保它对于已经存在的应用不可访问,直到我们检测完并重新提供服务。这对于按块加载的逻辑备份的恢复来说尤其重要
恢复物理备份。
恢复物理备份往往非常直接——换言之,没有太多的选项。这可能是好事,也可能是坏事,具体取决于恢复的需求。一般过程是简单地复制文件到正确位置。是否需要关闭MySQL取决于存储引擎。MyISAM的文件一般相互独立,即使服务器正在运行,简单地复制每个表的.frm .MYI和.MYD文件也可以正常操作。一旦有任何对此表的查询,或者其他会导致服务器访问此表的操作(例如,执行SHOW TABLES),MySQL都会立刻找到这些表。如果在复制这些文件时表是打开的,可能会有麻烦,因为此操作要么删除或重命名该表,要么使用LOCK TABLES和FLUSH TABLES来关闭它。InnoDB的情况有所不同。如果用传统的InnoDB的步骤来还原,即所有表都存储在单个表空间,就必须关闭MySQL,复制或移动文件到正确位置上,然后重启。同样也需要InnoDB的事务日志文件与表空间文件匹配。如果文件不匹配——例如,替换了表空间文件但没有替换事务日志文件——InnoDB将会拒绝启动。这也是将日志和数据文件一起备份非常关键的一个原因。入股哦使用InnoDB file-per-table特性(innodb_file_per_table),InnoDB会将每隔表的数据和索引存储于一个.ibd文件中,这就像MyISAM的.MYI和.MYD文件合在一起。可以在服务器运行时通过复制这些文件来备份和还原单个表,但这并不像MyISAM中那样简单。这些文件并不完全独立于InnoDB。每个.ibd文件都有一些内部的信息,保存着它与主(共享)表空间之前的关系。在还原这样的文件时,需要让InnoDB先"导入"这个文件。这个过程有许多的西安至,如果有需要可以阅读MySQL手册 中关于每个表使用独立表空间中的部分。最大的限制是只能在当初备份的服务器上还原单个表。用这种配置来备份和还原多个表不是不可能,但可能比想象得要更棘手。
所有这些复杂度意味着还原物理备份会非常乏味,并且容易出错。一个好的值得倡导的规则是,恢复过程越难越复杂,也就需要逻辑备份的保护。为了防止一些无法意料的情况或者某些无法使用物理备份的场景,准备好逻辑备份总是值得推荐的。
恢复物理备份往往非常直接——换言之,没有太多的选项。这可能是好事,也可能是坏事,具体取决于恢复的需求。一般过程是简单地复制文件到正确位置。是否需要关闭MySQL取决于存储引擎。MyISAM的文件一般相互独立,即使服务器正在运行,简单地复制每个表的.frm .MYI和.MYD文件也可以正常操作。一旦有任何对此表的查询,或者其他会导致服务器访问此表的操作(例如,执行SHOW TABLES),MySQL都会立刻找到这些表。如果在复制这些文件时表是打开的,可能会有麻烦,因为此操作要么删除或重命名该表,要么使用LOCK TABLES和FLUSH TABLES来关闭它。InnoDB的情况有所不同。如果用传统的InnoDB的步骤来还原,即所有表都存储在单个表空间,就必须关闭MySQL,复制或移动文件到正确位置上,然后重启。同样也需要InnoDB的事务日志文件与表空间文件匹配。如果文件不匹配——例如,替换了表空间文件但没有替换事务日志文件——InnoDB将会拒绝启动。这也是将日志和数据文件一起备份非常关键的一个原因。入股哦使用InnoDB file-per-table特性(innodb_file_per_table),InnoDB会将每隔表的数据和索引存储于一个.ibd文件中,这就像MyISAM的.MYI和.MYD文件合在一起。可以在服务器运行时通过复制这些文件来备份和还原单个表,但这并不像MyISAM中那样简单。这些文件并不完全独立于InnoDB。每个.ibd文件都有一些内部的信息,保存着它与主(共享)表空间之前的关系。在还原这样的文件时,需要让InnoDB先"导入"这个文件。这个过程有许多的西安至,如果有需要可以阅读MySQL手册 中关于每个表使用独立表空间中的部分。最大的限制是只能在当初备份的服务器上还原单个表。用这种配置来备份和还原多个表不是不可能,但可能比想象得要更棘手。
所有这些复杂度意味着还原物理备份会非常乏味,并且容易出错。一个好的值得倡导的规则是,恢复过程越难越复杂,也就需要逻辑备份的保护。为了防止一些无法意料的情况或者某些无法使用物理备份的场景,准备好逻辑备份总是值得推荐的。
还原物理备份后启动MySQL.
在启动正在恢复的MySQL服务器之前,还有些步骤要做,首先,最重要且最容易忘记的事情,是在启动MySQL服务器之前检查服务器的配置,确保恢复的文件有正确的归属和权限。这些属性必须完全正确,否则MySQL可能无法启动。这些属性因系统的不同而不同,因此要仔细检查是否和之前做的记录温和。一般都需要mysql用户和组拥有这些文件和目录,并且只有这个用户和组拥有可读/写权限。建议观察MySQL启动时的错误日志。在UNIX类系统上,可以如下观察文件。
```bash
tail -f /var/log/mysql/mysql.err
```
注意错误日志的准确位置会有所不同。一旦开始监测文件,就额可以启动MySQL服务器并监测错误。如果一切进展顺利,MySQL启动后就有一个恢复好的数据库服务器了。观察错误日志对于新的MySQL版本更为重要。老版本在InnoDB有错时不会启动,但新版本不管怎样都会启动,而只是让InnoDB失效。即使服务器看起来启动没有任何问题,也应该对每隔数据库运行SHOW TABLE STATUS来再次检测错误日志
在启动正在恢复的MySQL服务器之前,还有些步骤要做,首先,最重要且最容易忘记的事情,是在启动MySQL服务器之前检查服务器的配置,确保恢复的文件有正确的归属和权限。这些属性必须完全正确,否则MySQL可能无法启动。这些属性因系统的不同而不同,因此要仔细检查是否和之前做的记录温和。一般都需要mysql用户和组拥有这些文件和目录,并且只有这个用户和组拥有可读/写权限。建议观察MySQL启动时的错误日志。在UNIX类系统上,可以如下观察文件。
```bash
tail -f /var/log/mysql/mysql.err
```
注意错误日志的准确位置会有所不同。一旦开始监测文件,就额可以启动MySQL服务器并监测错误。如果一切进展顺利,MySQL启动后就有一个恢复好的数据库服务器了。观察错误日志对于新的MySQL版本更为重要。老版本在InnoDB有错时不会启动,但新版本不管怎样都会启动,而只是让InnoDB失效。即使服务器看起来启动没有任何问题,也应该对每隔数据库运行SHOW TABLE STATUS来再次检测错误日志
还原逻辑备份。
如果还原的是逻辑备份而不是物理备份,则与使用操作系统简单地复制文件到适当位置的方式不同,需要使用MySQL服务器本身来加载数据到表中。在加载导出文件之前,应该先花一点时间考虑文件有多大,需要多久加载完,以及在启动之前还需要做什么事情,例如通知用户或禁用掉部分应用。禁掉二进制日志也是个好主意,除非需要将还原操作复制到备库:服务器加载一个巨大的导出文件的代价很高,并且写二进制日志会增加更多的(可能没有必要的)开销。加载巨大的文件对于一些存储引擎也有影响。例如,在单个事务中加载100GB数据到InnoDB就不是个好想法,因为巨大的回滚段将会导致问题。应该以可控大小的块来加载,并且逐个提交事务。有两种类型的逻辑备份,所以相应地有两种类型的还原操作。
如果还原的是逻辑备份而不是物理备份,则与使用操作系统简单地复制文件到适当位置的方式不同,需要使用MySQL服务器本身来加载数据到表中。在加载导出文件之前,应该先花一点时间考虑文件有多大,需要多久加载完,以及在启动之前还需要做什么事情,例如通知用户或禁用掉部分应用。禁掉二进制日志也是个好主意,除非需要将还原操作复制到备库:服务器加载一个巨大的导出文件的代价很高,并且写二进制日志会增加更多的(可能没有必要的)开销。加载巨大的文件对于一些存储引擎也有影响。例如,在单个事务中加载100GB数据到InnoDB就不是个好想法,因为巨大的回滚段将会导致问题。应该以可控大小的块来加载,并且逐个提交事务。有两种类型的逻辑备份,所以相应地有两种类型的还原操作。
加载SQL文件
如果有一个SQL导出文件,它将包含可执行的SQL.需要做的就是运行这个文件。假设备份Sakila示例数据库和Schema到单个文件,下面是用来还原的常用命令。
```sql
mysql < sakila-backup.sql
```
也可以从mysql米精灵行客户端用SOURCE命令加载文件。这只是做相同事情的不同方法,不过该方法使得某些事情更简单。例如,如果你是MySQL管理用户,就可以关闭用客户端连接执行时的二进制记录,然后加载文件而不需要重启MySQL服务器。
```sql
mysql>SET SQL_LOG_BIN =0;
mysql>SOURCE sakila-backup.sql;
mysql>SET SQL_LOG_BIN=1;
```
需要注意的时,如果使用SOURCE,当定向文件到mysql时,默认秦广下,发生一个错误不会导致一批语句退出。如果备份做过压缩,那么不要分别解压缩和加载。应该在单个操作中完成解压缩和加载,这样做会快很多。
```bash
gunzip -c sakila-backup.sql.gz | mysql
```
如果想用SOURCE命令加载一个压缩文件,请见下面关于命名管道的讨论。如果只想恢复单个表(例如,actor表),要怎么做呢?如果数据没有分行但有schema信息,那么还原数据并不难。
```bash
grep 'INSERT INTO `actor` sakila-backup.sql | mysql sakila'
```
或者,如果文件是压缩过的,那么命令如下:
```bash
gunzip -c sakila-backup.sql.gz | grep 'INSERT INTO `actor`' | mysql sakila
```
如果需要创建表并还原数据,而在单个文件中有整个数据库,则必须先编辑这个文件。这也是有一些人喜欢导出每个表到各自文件中的原因。大部分编辑器无法应付巨大的文件,尤其如果它们是压缩过的。另外,也不会想实际地编辑文件本身——只想抽取相关地行——因此可能必须做一些命令工作。使用grep来仅抽出给定表的INSERT语句较简单,就像我们在前面命令中做的那样,但得到CREATE TABLE语句比较难。下面是抽取所需段落的sed脚本。
```bash
sed -e '/./{H;$!d;}' -e 'x;/CREATE TABLE `actor`/!d;q' sakila-backup.sql
```
我们得承认这条命令非常隐晦。如果必须以这种方式还原数据,那只能说明备份设计非常糟糕。如果有一点规划,可能就不会需要痛苦地区尝试弄清楚sed如何工作了。只需要备份每个表到各自地文件,或者可以更进异步,分别备份数据和Schema.
如果有一个SQL导出文件,它将包含可执行的SQL.需要做的就是运行这个文件。假设备份Sakila示例数据库和Schema到单个文件,下面是用来还原的常用命令。
```sql
mysql < sakila-backup.sql
```
也可以从mysql米精灵行客户端用SOURCE命令加载文件。这只是做相同事情的不同方法,不过该方法使得某些事情更简单。例如,如果你是MySQL管理用户,就可以关闭用客户端连接执行时的二进制记录,然后加载文件而不需要重启MySQL服务器。
```sql
mysql>SET SQL_LOG_BIN =0;
mysql>SOURCE sakila-backup.sql;
mysql>SET SQL_LOG_BIN=1;
```
需要注意的时,如果使用SOURCE,当定向文件到mysql时,默认秦广下,发生一个错误不会导致一批语句退出。如果备份做过压缩,那么不要分别解压缩和加载。应该在单个操作中完成解压缩和加载,这样做会快很多。
```bash
gunzip -c sakila-backup.sql.gz | mysql
```
如果想用SOURCE命令加载一个压缩文件,请见下面关于命名管道的讨论。如果只想恢复单个表(例如,actor表),要怎么做呢?如果数据没有分行但有schema信息,那么还原数据并不难。
```bash
grep 'INSERT INTO `actor` sakila-backup.sql | mysql sakila'
```
或者,如果文件是压缩过的,那么命令如下:
```bash
gunzip -c sakila-backup.sql.gz | grep 'INSERT INTO `actor`' | mysql sakila
```
如果需要创建表并还原数据,而在单个文件中有整个数据库,则必须先编辑这个文件。这也是有一些人喜欢导出每个表到各自文件中的原因。大部分编辑器无法应付巨大的文件,尤其如果它们是压缩过的。另外,也不会想实际地编辑文件本身——只想抽取相关地行——因此可能必须做一些命令工作。使用grep来仅抽出给定表的INSERT语句较简单,就像我们在前面命令中做的那样,但得到CREATE TABLE语句比较难。下面是抽取所需段落的sed脚本。
```bash
sed -e '/./{H;$!d;}' -e 'x;/CREATE TABLE `actor`/!d;q' sakila-backup.sql
```
我们得承认这条命令非常隐晦。如果必须以这种方式还原数据,那只能说明备份设计非常糟糕。如果有一点规划,可能就不会需要痛苦地区尝试弄清楚sed如何工作了。只需要备份每个表到各自地文件,或者可以更进异步,分别备份数据和Schema.
(符号分隔样式)
加载符号分隔文件。
如果是通过SELECT INTO OUTFILE导出的符号分隔文件,可以使用LOAD DATA INFILE通过相同的参数来加载。也可以用mysqlimport,这是LOAD DATA INFILE的一个包装。这种方式依赖命名约定决定从哪里加载一个文件的数据。我们希望你导出了Schema,而不仅是数据。如果是这样,那应该是一个SQL导出,就可以使用上一节中描述的技术来加载。使用LOAD DATA FILE有一个非常好的优化技巧。LOAD DATA INFILE必须直接从文本文件中读取,因此,如果是压缩文件很多人会在加载前先解压缩,这是非常慢的磁盘密集型操作。然而,在支持FIFO"命名管道"文件的系统如GNU/Linux上,对这种操作有个很好的方法。首先,创建一个命名管道并将解压缩数据流到它里面。
```bash
mkfifo /tmp/backup/default/sakila/payment.fifo
chmod 666 /tmp/backup/default/sakila/payment.fifo
gunzip -c /tmp/backup/default/sakila/payment.txt.gz > /tmp/backup/default/sakila/payment.fifo
```
注意到我们使用了一个大于号字符(>)来重定向解压缩输出到payment.fifo文件中——而不是在不同程序之间创建匿名管道的管道符号。管道会等待,直到其他程序打开它并从另外一段读取数据。简单一点说,MySQL服务器可以从管道中读取解压缩后的数据,就像其他文件一样。如果可能,不要忘记尽调二进制日志。
```sql
mysql>SET SQL_LOG_BIN = 0; -- Optional
> LOAD DATA INFILE 'tmp/backup/defualt/sakila/payment/fifo'
> INTO TABLE sakila.payment;
```
一旦MySQL加载完数据,gunzip就会退出,然后可以删除该命令管道。在MySQL命令行客户端使用SOURCE命令加载压缩的文件也可以使用此技术。Percona Toolkit中的pt-fifo-split程序还可以帮助分块加载大文件,而不是在单个大事务中操作,这样效率更高
加载符号分隔文件。
如果是通过SELECT INTO OUTFILE导出的符号分隔文件,可以使用LOAD DATA INFILE通过相同的参数来加载。也可以用mysqlimport,这是LOAD DATA INFILE的一个包装。这种方式依赖命名约定决定从哪里加载一个文件的数据。我们希望你导出了Schema,而不仅是数据。如果是这样,那应该是一个SQL导出,就可以使用上一节中描述的技术来加载。使用LOAD DATA FILE有一个非常好的优化技巧。LOAD DATA INFILE必须直接从文本文件中读取,因此,如果是压缩文件很多人会在加载前先解压缩,这是非常慢的磁盘密集型操作。然而,在支持FIFO"命名管道"文件的系统如GNU/Linux上,对这种操作有个很好的方法。首先,创建一个命名管道并将解压缩数据流到它里面。
```bash
mkfifo /tmp/backup/default/sakila/payment.fifo
chmod 666 /tmp/backup/default/sakila/payment.fifo
gunzip -c /tmp/backup/default/sakila/payment.txt.gz > /tmp/backup/default/sakila/payment.fifo
```
注意到我们使用了一个大于号字符(>)来重定向解压缩输出到payment.fifo文件中——而不是在不同程序之间创建匿名管道的管道符号。管道会等待,直到其他程序打开它并从另外一段读取数据。简单一点说,MySQL服务器可以从管道中读取解压缩后的数据,就像其他文件一样。如果可能,不要忘记尽调二进制日志。
```sql
mysql>SET SQL_LOG_BIN = 0; -- Optional
> LOAD DATA INFILE 'tmp/backup/defualt/sakila/payment/fifo'
> INTO TABLE sakila.payment;
```
一旦MySQL加载完数据,gunzip就会退出,然后可以删除该命令管道。在MySQL命令行客户端使用SOURCE命令加载压缩的文件也可以使用此技术。Percona Toolkit中的pt-fifo-split程序还可以帮助分块加载大文件,而不是在单个大事务中操作,这样效率更高
(sql文件)
(sql文件)
你无法从这里到达那里。
从DATETIME变为TIMESTAMP.以节约空间并使处理过程更快,表定义如下:
```sql
CREATE TABLE tbl(
col1 timestamp NOT NULL,
col2 timestamp NOT NULL default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTMAP
......
```
这个表帝国一在MySQL5.0.40版本上导致了一个语法错误,而这是创建时的版本。可以执行导出,但无法加载。这很奇怪,诸如这样无法预料的错误也是测试备份重要的原因之一。你永远不会直到什么会阻止你还原数据
从DATETIME变为TIMESTAMP.以节约空间并使处理过程更快,表定义如下:
```sql
CREATE TABLE tbl(
col1 timestamp NOT NULL,
col2 timestamp NOT NULL default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTMAP
......
```
这个表帝国一在MySQL5.0.40版本上导致了一个语法错误,而这是创建时的版本。可以执行导出,但无法加载。这很奇怪,诸如这样无法预料的错误也是测试备份重要的原因之一。你永远不会直到什么会阻止你还原数据
基于时间点的恢复。
对MySQL做基于时间点的恢复常见的方法是还原最近一次全备份,然后从那个时间点开始重放二进制日志(有时较"前滚恢复")。只要有二进制日志,就可以恢复到任何希望的时间点。甚至可以不太费力地恢复单个数据库。主要的缺点是二进制日志重放可能会是一个很慢的过程。它大体上等同于复制。如果有一个备库,并且已经测量到SQL线程的利用率有多高,那么对重放二进制日志会有多快就会心里有数了。例如,,如果SQL线程约有50%被利用,则恢复一周二进制日志的工作可能在三到四天内完成。一个典型场景是对有害的语句的结果做回滚操作,例如DROP TABLE。让我们看一个简化的例子,看只有MyISAM表的情况下该如何做。假如是在半夜,备份任务在运行与下面所列相当的语句,复制数据库到同一服务器上的其他地方。
```sql
mysql> FLUSH TABLES WITH READ LOCK;
> server1# cp -a /var/lib/mysql/sakila /backup/sakila;
mysql> FLUSH LOGS;
> server1# mysql -e "SHOW MASTER STATUS" --vertical > /backup/master.info
mysql> UNLOCK TABLES;
```
然后,假设有人在晚些时间运行下列语句.
```sql
mysql>USE sakila;
mysql>DROP TABLE sakila.payment;
```
为了便于说明,我们先假设可以单独地恢复这个数据库(即此库中地表不涉及跨库查询)。再假设是直到后来出问题才意识到这个有问题地语句。目标是恢复数据库中除了有问题地语句之外所有发生地事务。也就是说,其他表已经做的所有修改都必须保持,包括有问题的语句运行之后的修改。这并不是很难做到。首先,停掉MySQL以阻止更多的修改,然后从备份中仅恢复sakila数据库。
```bash
server1# /etc/init.d/mysql stop
server1# mv /var/lib/mysql/sakila /var lib/mysql/sakila.tmp
server1# cp -a /backup/sakila /var/lib/mysql
```
再到运行的服务器的my.cnf中添加如下配置以禁止正常的连接
```c
skip-networking
socket=/tmp/mysql_recover.sock
```
现在可以安全地启动服务器了。
```bash
server1# /etc/init.d/mysql start
```
下一个任务是从二进制日志中分出需要重放和忽略的语句。事发时,自半夜的备份依赖服务器只创建了一个二进制日志。我们可以用grep来检查二进制日志文件以找到问题语句
```bash
server1# mysqlbinlog --database=sakila /var/log/mysql/mysql-bin.000215 | grep -B3 -i 'drop table sakila.payment'
```
我们可以看到,想忽略的语句在日志文件中的某个位置,下一个语句的位置是多少。可以用下面的命令重放二进制日志直到某个位置,然后从某个位置继续
```bash
server1# mysqlbinlog --database=sakila /var/log/mysql/mysql-bin.000215 --stop-position=352 | mysql -uroot -p
server1# mysqlbinlog --database=sakila /var/log/mysql/mysql-bin.000215 --start-position=429 | mysql -uroot -p
```
接下来要做的是检测数据以确保没问题,然后关闭服务器并撤销对my.cnf的改变,最后重启服务器
对MySQL做基于时间点的恢复常见的方法是还原最近一次全备份,然后从那个时间点开始重放二进制日志(有时较"前滚恢复")。只要有二进制日志,就可以恢复到任何希望的时间点。甚至可以不太费力地恢复单个数据库。主要的缺点是二进制日志重放可能会是一个很慢的过程。它大体上等同于复制。如果有一个备库,并且已经测量到SQL线程的利用率有多高,那么对重放二进制日志会有多快就会心里有数了。例如,,如果SQL线程约有50%被利用,则恢复一周二进制日志的工作可能在三到四天内完成。一个典型场景是对有害的语句的结果做回滚操作,例如DROP TABLE。让我们看一个简化的例子,看只有MyISAM表的情况下该如何做。假如是在半夜,备份任务在运行与下面所列相当的语句,复制数据库到同一服务器上的其他地方。
```sql
mysql> FLUSH TABLES WITH READ LOCK;
> server1# cp -a /var/lib/mysql/sakila /backup/sakila;
mysql> FLUSH LOGS;
> server1# mysql -e "SHOW MASTER STATUS" --vertical > /backup/master.info
mysql> UNLOCK TABLES;
```
然后,假设有人在晚些时间运行下列语句.
```sql
mysql>USE sakila;
mysql>DROP TABLE sakila.payment;
```
为了便于说明,我们先假设可以单独地恢复这个数据库(即此库中地表不涉及跨库查询)。再假设是直到后来出问题才意识到这个有问题地语句。目标是恢复数据库中除了有问题地语句之外所有发生地事务。也就是说,其他表已经做的所有修改都必须保持,包括有问题的语句运行之后的修改。这并不是很难做到。首先,停掉MySQL以阻止更多的修改,然后从备份中仅恢复sakila数据库。
```bash
server1# /etc/init.d/mysql stop
server1# mv /var/lib/mysql/sakila /var lib/mysql/sakila.tmp
server1# cp -a /backup/sakila /var/lib/mysql
```
再到运行的服务器的my.cnf中添加如下配置以禁止正常的连接
```c
skip-networking
socket=/tmp/mysql_recover.sock
```
现在可以安全地启动服务器了。
```bash
server1# /etc/init.d/mysql start
```
下一个任务是从二进制日志中分出需要重放和忽略的语句。事发时,自半夜的备份依赖服务器只创建了一个二进制日志。我们可以用grep来检查二进制日志文件以找到问题语句
```bash
server1# mysqlbinlog --database=sakila /var/log/mysql/mysql-bin.000215 | grep -B3 -i 'drop table sakila.payment'
```
我们可以看到,想忽略的语句在日志文件中的某个位置,下一个语句的位置是多少。可以用下面的命令重放二进制日志直到某个位置,然后从某个位置继续
```bash
server1# mysqlbinlog --database=sakila /var/log/mysql/mysql-bin.000215 --stop-position=352 | mysql -uroot -p
server1# mysqlbinlog --database=sakila /var/log/mysql/mysql-bin.000215 --start-position=429 | mysql -uroot -p
```
接下来要做的是检测数据以确保没问题,然后关闭服务器并撤销对my.cnf的改变,最后重启服务器
更高级的恢复技术。
复制和基于时间点的恢复使用的是相同的技术:服务器的二进制日志。这意味着复制在恢复时会是个非常有帮助的工具,哪怕方式不是很明显。下面将演示一些可以用到的方法。这里列出来的不是一个完整的列表,但应该可以为你根据需求设计恢复方案带来一些想法。记得编写脚本,并且对恢复过程中需要用到的所有技术进行预演。shijian
1.用于快速恢复的延时复制
在前面已经提到,如果有一个延时的备库,并且在备库执行问题语句之前就发现了问题,那么基于时间点的恢复就更快更容易了。恢复的过程与前面说的有点不一样,但思路是相同的。停止备库,用START SLAVE UNTIL来重放事件直到要执行问题语句。接着,执行SET GLOBAL SQL _SLAVE_SKIP_COUNTER=1来跳过问题语句。如果想跳过多个事件,可以设置一个大于1的值(或简单地使用CHANGE MASTER TO 来前移备库在日志中的位置)。然后要做的就是执行START SLAVE,让备库执行完所有的中继日志。这样就利用贝克u完成了基于时间点的恢复中所有冗余的工作。现在可以将备库提升为主库,整个恢复过程基本上没有中断服务。即使没有延时的备库来加速恢复,普通的备库也有好处,至少会把主库的二进制日志复制到另外的及其上。如果主库的磁盘坏了。备库上的中继日志可能就是唯一能够获取到的最接近主库二进制的东西了
2.使用日志服务器进行恢复
还有另外一种使用复制来做恢复的方法:设置日志服务器。我们感觉复制比mysqlbinlog更可靠,mysqlbinlog可能会有一些导致异常行为的奇怪的Bug和不常见的情况。使用日志服务器进行恢复比mysqlbinlog更灵活更简单,不仅因为START SLAVE UNTIL选项,还因为那些可以采用的复制规则(replicate-do-table)。使用日志服务器,相对其他的方式来说,可以做到更复杂的过滤。例如,使用日志服务器可以轻松地恢复单个表。而用mysqlbinlog和命令行工具则要困难得多——事实上,这样做太复杂了,所以我们一般不建议进行尝试。假设粗心的开发人员像前面地例子一样删除了同样地表,现在想恢复此误操作,但又不想让整个服务器退到昨晚地备份。下面是利用日志服务器进行恢复地步骤:
2.1 将需要恢复的服务器叫作server1
2.2 在另外一台叫作server2的服务器上恢复做完的备份。在这台服务器上运行恢复进程,以免在恢复时犯错而导致事情更糟
2.3 按照前面的做法设置日志服务器来接收server1的二进制日志(复制日志到另外一个服务器并设置日志服务器是个好象发,但是要格外注意)
2.4 改变server2的配置文件,增加如下内容:
```c
replicate-do-table=sakila.payment
```
2.5 重启server2,然后用CHANGE MASTER TO来让它成为日志服务器的备库。配置它从昨晚备份的二进制日志坐标读取。这时候切记不要运行START SLAVE
2.6 检测server2上的SHOW SLAVE STATUS的输出,验证一切正常。要三思而行!
2.7 找到二进制日志中问题语句的位置,在server2上执行START SLAVE UNTIL来重放事件直到该位置。
2.8 在server2上用STOP SLAVE停掉复制进程。现在应该有被删除表,因为现在从库停止在被删除之前的时间点
2.9 将所需表从server2复制到server1.
只有没有任何多表的UPDATE、DELETE或INSERT语句操作这个表时,上述流程才是可行的。任何这样的多表操作语句在被记录的时候,可能时基于多个数据库的状态,而不仅仅时当前要恢复的这个数据库,所以这样恢复出来的数据可能和原始的有所不同。(只有在使用基于语句的二进制日志时才会有这个问题;如果使用的是基于行的日志,重放过程不会碰到这个问题)
复制和基于时间点的恢复使用的是相同的技术:服务器的二进制日志。这意味着复制在恢复时会是个非常有帮助的工具,哪怕方式不是很明显。下面将演示一些可以用到的方法。这里列出来的不是一个完整的列表,但应该可以为你根据需求设计恢复方案带来一些想法。记得编写脚本,并且对恢复过程中需要用到的所有技术进行预演。shijian
1.用于快速恢复的延时复制
在前面已经提到,如果有一个延时的备库,并且在备库执行问题语句之前就发现了问题,那么基于时间点的恢复就更快更容易了。恢复的过程与前面说的有点不一样,但思路是相同的。停止备库,用START SLAVE UNTIL来重放事件直到要执行问题语句。接着,执行SET GLOBAL SQL _SLAVE_SKIP_COUNTER=1来跳过问题语句。如果想跳过多个事件,可以设置一个大于1的值(或简单地使用CHANGE MASTER TO 来前移备库在日志中的位置)。然后要做的就是执行START SLAVE,让备库执行完所有的中继日志。这样就利用贝克u完成了基于时间点的恢复中所有冗余的工作。现在可以将备库提升为主库,整个恢复过程基本上没有中断服务。即使没有延时的备库来加速恢复,普通的备库也有好处,至少会把主库的二进制日志复制到另外的及其上。如果主库的磁盘坏了。备库上的中继日志可能就是唯一能够获取到的最接近主库二进制的东西了
2.使用日志服务器进行恢复
还有另外一种使用复制来做恢复的方法:设置日志服务器。我们感觉复制比mysqlbinlog更可靠,mysqlbinlog可能会有一些导致异常行为的奇怪的Bug和不常见的情况。使用日志服务器进行恢复比mysqlbinlog更灵活更简单,不仅因为START SLAVE UNTIL选项,还因为那些可以采用的复制规则(replicate-do-table)。使用日志服务器,相对其他的方式来说,可以做到更复杂的过滤。例如,使用日志服务器可以轻松地恢复单个表。而用mysqlbinlog和命令行工具则要困难得多——事实上,这样做太复杂了,所以我们一般不建议进行尝试。假设粗心的开发人员像前面地例子一样删除了同样地表,现在想恢复此误操作,但又不想让整个服务器退到昨晚地备份。下面是利用日志服务器进行恢复地步骤:
2.1 将需要恢复的服务器叫作server1
2.2 在另外一台叫作server2的服务器上恢复做完的备份。在这台服务器上运行恢复进程,以免在恢复时犯错而导致事情更糟
2.3 按照前面的做法设置日志服务器来接收server1的二进制日志(复制日志到另外一个服务器并设置日志服务器是个好象发,但是要格外注意)
2.4 改变server2的配置文件,增加如下内容:
```c
replicate-do-table=sakila.payment
```
2.5 重启server2,然后用CHANGE MASTER TO来让它成为日志服务器的备库。配置它从昨晚备份的二进制日志坐标读取。这时候切记不要运行START SLAVE
2.6 检测server2上的SHOW SLAVE STATUS的输出,验证一切正常。要三思而行!
2.7 找到二进制日志中问题语句的位置,在server2上执行START SLAVE UNTIL来重放事件直到该位置。
2.8 在server2上用STOP SLAVE停掉复制进程。现在应该有被删除表,因为现在从库停止在被删除之前的时间点
2.9 将所需表从server2复制到server1.
只有没有任何多表的UPDATE、DELETE或INSERT语句操作这个表时,上述流程才是可行的。任何这样的多表操作语句在被记录的时候,可能时基于多个数据库的状态,而不仅仅时当前要恢复的这个数据库,所以这样恢复出来的数据可能和原始的有所不同。(只有在使用基于语句的二进制日志时才会有这个问题;如果使用的是基于行的日志,重放过程不会碰到这个问题)
InnoDB崩溃恢复。
InnoDB在每次启动时都会检测数据和日志文件,以确认是否需要执行恢复过程。而且InnoDB的恢复过程与前面的讨论不是一回事。它并不是恢复备份的数据;而是根据日志文件将事务应用到数据文件,将未提交的变更从数据文件中回滚。精确地描述InnoDB如何进行恢复工作,这有点太过复杂。我们要关注的焦点是当InnoDB有严重问题时如何实际执行恢复。大部分情况下InnoDB可以很好地解决问题。除非MySQL有Bug或硬件问题,否则不需要做任何非常规的事情,哪怕时服务器意外地断电。InnoDB会在启动时执行正常的恢复,然后一切就正常了,在日志文件中,可以看到如下信息。
```sql
InnoDB Doing recovery :scanned up to log sequence number 0 40817239
InnoDB: Starting an apply batch of log records to the database...
```
InnoDB会在日志文件中输出恢复进度的百分比信息。有些人说直到整个过程完成才能看到这些信息。耐心点,这个恢复过程是急不来的。如果心急而杀掉进程并重启,只会导致需要更长的恢复事件。如果服务器硬件有严重问题,例如内存或磁盘损坏,或遇到了MySQL或InnoDB的Bug,可能就不得不介入,这是要么进行强制恢复,要么阻止正常恢复发生。
InnoDB在每次启动时都会检测数据和日志文件,以确认是否需要执行恢复过程。而且InnoDB的恢复过程与前面的讨论不是一回事。它并不是恢复备份的数据;而是根据日志文件将事务应用到数据文件,将未提交的变更从数据文件中回滚。精确地描述InnoDB如何进行恢复工作,这有点太过复杂。我们要关注的焦点是当InnoDB有严重问题时如何实际执行恢复。大部分情况下InnoDB可以很好地解决问题。除非MySQL有Bug或硬件问题,否则不需要做任何非常规的事情,哪怕时服务器意外地断电。InnoDB会在启动时执行正常的恢复,然后一切就正常了,在日志文件中,可以看到如下信息。
```sql
InnoDB Doing recovery :scanned up to log sequence number 0 40817239
InnoDB: Starting an apply batch of log records to the database...
```
InnoDB会在日志文件中输出恢复进度的百分比信息。有些人说直到整个过程完成才能看到这些信息。耐心点,这个恢复过程是急不来的。如果心急而杀掉进程并重启,只会导致需要更长的恢复事件。如果服务器硬件有严重问题,例如内存或磁盘损坏,或遇到了MySQL或InnoDB的Bug,可能就不得不介入,这是要么进行强制恢复,要么阻止正常恢复发生。
InnoDB损坏的原因。
InnoDB非常健壮且可靠,并且有许多的内建安全检测来防止、检测和修复损坏的数据——比其他MySQL存储引擎要强很多。然而,InnoDB并不能保护自己避免一切错误。最起码,InnoDB依赖于无缓存的IO调用和fsync()调用,直到数据完全地写入到物理介质上才会返回。如果硬件不能保证写入的持久化,InnoDB也就不能保证数据的持久,崩溃就有可能导致数据损坏。很多InnoDB损坏问题都是与硬件有关的(例如,因电力问题或内存损坏而导致损坏页的写入)。然而,在过往的经验中,错误配置的硬件是更多的问题之源。常见的错误配置包括打开了不包含电池备份电源的RAID卡的回写缓存,或打开了硬盘驱动器本身的回写缓存。这些错误将会导致控制器或驱动器"撒谎",在数据实际上只写入到回写缓存上而不是磁盘上时,却说fsync()已经完成。换句话说,硬件没有提供保持InnoDB数据安全的保证。有时候及其默认就会这样配置,因为这样做可以得到更好的性能——对于某些场景确实很好,但是对事务数据服务来说却是个大问题。如果在网络附加存储(NAS)上运行InnoDB,也可能会遇到损坏,因为对NAS设备来说完成fsync()只是意味着设备接收到了数据。如果InnoDB崩溃,数据是安全的,但如果是NAS设备崩溃就不一定了。严重的损坏会使InnoDB或MyISAM崩溃,而不那么严重的损坏则可能只是由于日志文件未真正同步到磁盘而丢掉了某些事务
InnoDB非常健壮且可靠,并且有许多的内建安全检测来防止、检测和修复损坏的数据——比其他MySQL存储引擎要强很多。然而,InnoDB并不能保护自己避免一切错误。最起码,InnoDB依赖于无缓存的IO调用和fsync()调用,直到数据完全地写入到物理介质上才会返回。如果硬件不能保证写入的持久化,InnoDB也就不能保证数据的持久,崩溃就有可能导致数据损坏。很多InnoDB损坏问题都是与硬件有关的(例如,因电力问题或内存损坏而导致损坏页的写入)。然而,在过往的经验中,错误配置的硬件是更多的问题之源。常见的错误配置包括打开了不包含电池备份电源的RAID卡的回写缓存,或打开了硬盘驱动器本身的回写缓存。这些错误将会导致控制器或驱动器"撒谎",在数据实际上只写入到回写缓存上而不是磁盘上时,却说fsync()已经完成。换句话说,硬件没有提供保持InnoDB数据安全的保证。有时候及其默认就会这样配置,因为这样做可以得到更好的性能——对于某些场景确实很好,但是对事务数据服务来说却是个大问题。如果在网络附加存储(NAS)上运行InnoDB,也可能会遇到损坏,因为对NAS设备来说完成fsync()只是意味着设备接收到了数据。如果InnoDB崩溃,数据是安全的,但如果是NAS设备崩溃就不一定了。严重的损坏会使InnoDB或MyISAM崩溃,而不那么严重的损坏则可能只是由于日志文件未真正同步到磁盘而丢掉了某些事务
如何恢复损坏的InnoDB数据。
InnoDB损坏有三种主要类型,它们对数据恢复有着不同程度的要求.
1.二级索引损坏
一般可以用OPTIMIZE TABLE来修复损坏的二级索引;此外,也可以用SELECT INTO OUTFILE,删除和重建表,然后LOAD DATA INFILE的方法。(也可以将表改为使用MyISAM再改回来)。这些过程都是通过构建一个新表重建受影响的索引,来修复损坏的索引数据
2.聚簇索引损坏
如果是聚簇索引损坏,也许只能使用innodb_forece_recovery选项来导出表。有时导出过程会让InnoDB崩溃;如果出现这样的情况,或许需要跳过导致崩溃的损坏页以导出其他的记录.聚簇索引的损坏比二级索引要更难修复,因为它会影响数据行本身,但在多数场合下只需要修复受影响的表。
3.损坏系统结构
系统结构包括InnoDB事务日志,表空间的撤销日志(undo log)区域和数据字典。这种损坏可能需要做整个数据库的导出和还原,因为InnoDB内部绝大部份的工作都可能受到影响
一般可以修复损坏的二级索引而不丢失数据。然而,另外两种情形经常会引起数据的丢失。如果已经有备份,那最好还是从备份中还原,而不是试着从损坏的文件里区提取数据。如果必须从损坏的文件里提取数据,那一般过程是先尝试让InnoDB运行起来,然后使用SELECT INTO OUTFILE导出数据。如果服务器已经崩溃,并且每次启动InnoDB都会崩溃,那么而可以配置InnoDB停止常规恢复和后台进程的运行。这样也许可以启动服务器,然后在缺少或不做完整性检查的情况下做逻辑备份。innodb_forece_recovery参数控制着InnoDB在启动和常规操作时要做哪一种类型的操作。通常情况下这个值是0,可以增大到6.MySQL使用手册里记录了每个数值究竟会产生什么行为;再次我们不会重复这段信息,但是要告诉你:在有点危险的前提下,可以把这个数值调高到4.使用这个设置时,若有数据页损坏,将会丢失一些数据;如果将数值设得更高,可能会从损坏的页里提取到坏掉的数据。或者增加执行SELECT INTO OUTFILE时崩溃的风险。换句话说,这个值直到4都对数据没有损害,但可能丧失修复问题的机会,而到5和6会更主动地修复问题,但损害数据的风险也会很大。当把innodb_force_recovery设为大于0的某个值时,InnoDB基本上是只读的,但是仍然可以创建和删除表。这可以阻止进一步的损坏,InnoDB会放松一些常规检查,以便在发现坏数据时不会特意崩溃。在常规操作中,这样做是由安全保障的。但是在恢复时,最好还是避免这样做。如果需要执行InnoDB强制恢复,有个好主意是配置MySQL使它在操作完成之前不接受常规的连接请求。
如果InnoDB的数据损坏到了根本不能启动MySQL的程度,还可以使用Percona出品的InnoDB Recovery Toolkit从表空间的数据文件里直接抽取数据。Percona Server还有允许服务器在某些表损坏时仍能运行的选项,而不是像MySQL那样在单个表损坏页被检测出时就默认强制崩溃
InnoDB损坏有三种主要类型,它们对数据恢复有着不同程度的要求.
1.二级索引损坏
一般可以用OPTIMIZE TABLE来修复损坏的二级索引;此外,也可以用SELECT INTO OUTFILE,删除和重建表,然后LOAD DATA INFILE的方法。(也可以将表改为使用MyISAM再改回来)。这些过程都是通过构建一个新表重建受影响的索引,来修复损坏的索引数据
2.聚簇索引损坏
如果是聚簇索引损坏,也许只能使用innodb_forece_recovery选项来导出表。有时导出过程会让InnoDB崩溃;如果出现这样的情况,或许需要跳过导致崩溃的损坏页以导出其他的记录.聚簇索引的损坏比二级索引要更难修复,因为它会影响数据行本身,但在多数场合下只需要修复受影响的表。
3.损坏系统结构
系统结构包括InnoDB事务日志,表空间的撤销日志(undo log)区域和数据字典。这种损坏可能需要做整个数据库的导出和还原,因为InnoDB内部绝大部份的工作都可能受到影响
一般可以修复损坏的二级索引而不丢失数据。然而,另外两种情形经常会引起数据的丢失。如果已经有备份,那最好还是从备份中还原,而不是试着从损坏的文件里区提取数据。如果必须从损坏的文件里提取数据,那一般过程是先尝试让InnoDB运行起来,然后使用SELECT INTO OUTFILE导出数据。如果服务器已经崩溃,并且每次启动InnoDB都会崩溃,那么而可以配置InnoDB停止常规恢复和后台进程的运行。这样也许可以启动服务器,然后在缺少或不做完整性检查的情况下做逻辑备份。innodb_forece_recovery参数控制着InnoDB在启动和常规操作时要做哪一种类型的操作。通常情况下这个值是0,可以增大到6.MySQL使用手册里记录了每个数值究竟会产生什么行为;再次我们不会重复这段信息,但是要告诉你:在有点危险的前提下,可以把这个数值调高到4.使用这个设置时,若有数据页损坏,将会丢失一些数据;如果将数值设得更高,可能会从损坏的页里提取到坏掉的数据。或者增加执行SELECT INTO OUTFILE时崩溃的风险。换句话说,这个值直到4都对数据没有损害,但可能丧失修复问题的机会,而到5和6会更主动地修复问题,但损害数据的风险也会很大。当把innodb_force_recovery设为大于0的某个值时,InnoDB基本上是只读的,但是仍然可以创建和删除表。这可以阻止进一步的损坏,InnoDB会放松一些常规检查,以便在发现坏数据时不会特意崩溃。在常规操作中,这样做是由安全保障的。但是在恢复时,最好还是避免这样做。如果需要执行InnoDB强制恢复,有个好主意是配置MySQL使它在操作完成之前不接受常规的连接请求。
如果InnoDB的数据损坏到了根本不能启动MySQL的程度,还可以使用Percona出品的InnoDB Recovery Toolkit从表空间的数据文件里直接抽取数据。Percona Server还有允许服务器在某些表损坏时仍能运行的选项,而不是像MySQL那样在单个表损坏页被检测出时就默认强制崩溃
备份和恢复工具。
有各种各样的好的和不是那么好的备份工具。我们喜欢对LVM使用mylvmbackup做快照备份,使用Percona Xtrabackup(开源)或MySQL Enterprise Backup(收费)做InnoDB热备份。不建议对大数据量使用mysqldump,因为它对服务器有影响,并且漫长的还原时间不可预知。有一些备份工具已经出现多年了,不幸的是有些已经过时。最明显的例子是Maatkkit的mk-parallel-dump。它从没有正确运行,甚至被重新设计过好几次还是不行。另外一个工具是mysqlhotcopy,它适合于古老的MyISAM表。大部分场景下这两个工具都无法让人相信数据是安全的,它们会使人误认为备份了数据实际上却非如此。例如,当使用InnoDB的innodb_file_per_table时,mysqlhotcopy会复制.ibd文件,这会使一些人误认为InnoDB的数据已经备份完成。在某些场景下,这两个工具都对服务器有一些负面影响。如果你在2008或2009年时在看MySQL的路线图,可能听说过MySQL在线备份。这是一个可以用SQL命令来开始备份和还原的特性。它原本是规划在MySQL5.2版本中,后来重新安排在了MySQL6.0中,再后来,据后来所知被永久取消了。
有各种各样的好的和不是那么好的备份工具。我们喜欢对LVM使用mylvmbackup做快照备份,使用Percona Xtrabackup(开源)或MySQL Enterprise Backup(收费)做InnoDB热备份。不建议对大数据量使用mysqldump,因为它对服务器有影响,并且漫长的还原时间不可预知。有一些备份工具已经出现多年了,不幸的是有些已经过时。最明显的例子是Maatkkit的mk-parallel-dump。它从没有正确运行,甚至被重新设计过好几次还是不行。另外一个工具是mysqlhotcopy,它适合于古老的MyISAM表。大部分场景下这两个工具都无法让人相信数据是安全的,它们会使人误认为备份了数据实际上却非如此。例如,当使用InnoDB的innodb_file_per_table时,mysqlhotcopy会复制.ibd文件,这会使一些人误认为InnoDB的数据已经备份完成。在某些场景下,这两个工具都对服务器有一些负面影响。如果你在2008或2009年时在看MySQL的路线图,可能听说过MySQL在线备份。这是一个可以用SQL命令来开始备份和还原的特性。它原本是规划在MySQL5.2版本中,后来重新安排在了MySQL6.0中,再后来,据后来所知被永久取消了。
MySQL Enterprise Backup
这个工具之前叫作InnoDB Hot Backup或ibbackup,是从Oracle购买的MySQL Enterprise中的一部分。使用此工具备份不需要停止MySQL,也不需要设置锁或中断正常的数据库活动(但是会对服务器造成一些额外的负载)。它支持类似压缩备份、增量备份和到其他服务器的流备份的特性。这是MySQL"官方"的备份工具。
这个工具之前叫作InnoDB Hot Backup或ibbackup,是从Oracle购买的MySQL Enterprise中的一部分。使用此工具备份不需要停止MySQL,也不需要设置锁或中断正常的数据库活动(但是会对服务器造成一些额外的负载)。它支持类似压缩备份、增量备份和到其他服务器的流备份的特性。这是MySQL"官方"的备份工具。
Percona XtraBackup
Percona XtraBackup与MySQL Enterprise Backup在很多方面都非常类似,但它是开源并且免费的。除了核心备份工具外,还有一个用Perl写的封装脚本,可以提供更多高级功能。它支持类似流、增量、压缩和多线程(并行)备份操作。也有许多特别的功能,用以降低在高负载的系统上备份的影响。Percona XtraBackup的工作方式是在后台线程不断追踪InnoDB日志文件尾部,然后复制InnoDB数据文件。这是个轻量级侵入过程,依靠特别的检测机制确保复制的数据是一致的。当所有的数据文件被复制完,日志复制线程就结束了。结果是在不同的时间点的所有数据的副本。然后可以使用InnoDB崩溃恢复代码应用事务日志,已达到所有数据文件一致的状态。这一步叫作准备过程。一旦准备好,备份就会完全一致,并且包含文件复制过程最后时间点已经提交的事务。一切都在MySQL外部完成,因此不需要以任何方式连接或访问MySQL.包装脚本包含通过复制备份到原位置的方式进行恢复的能力。还有Lachalan Mulcahy的XtraBack Manager项目
Percona XtraBackup与MySQL Enterprise Backup在很多方面都非常类似,但它是开源并且免费的。除了核心备份工具外,还有一个用Perl写的封装脚本,可以提供更多高级功能。它支持类似流、增量、压缩和多线程(并行)备份操作。也有许多特别的功能,用以降低在高负载的系统上备份的影响。Percona XtraBackup的工作方式是在后台线程不断追踪InnoDB日志文件尾部,然后复制InnoDB数据文件。这是个轻量级侵入过程,依靠特别的检测机制确保复制的数据是一致的。当所有的数据文件被复制完,日志复制线程就结束了。结果是在不同的时间点的所有数据的副本。然后可以使用InnoDB崩溃恢复代码应用事务日志,已达到所有数据文件一致的状态。这一步叫作准备过程。一旦准备好,备份就会完全一致,并且包含文件复制过程最后时间点已经提交的事务。一切都在MySQL外部完成,因此不需要以任何方式连接或访问MySQL.包装脚本包含通过复制备份到原位置的方式进行恢复的能力。还有Lachalan Mulcahy的XtraBack Manager项目
mylvmbackup
Lenz Grimmer的mylvmbackup是一个Perl脚本,它通过LVM快照帮助MySQL自动备份。此工具首先获取全局读锁,创建快照,释放锁。然后通过tar压缩数据并移除快照。它通过备份时的时间戳命名压缩包。它还有几个高级选项,但总的来说,这是一个执行LVM备份的非常简单明了的工具
Lenz Grimmer的mylvmbackup是一个Perl脚本,它通过LVM快照帮助MySQL自动备份。此工具首先获取全局读锁,创建快照,释放锁。然后通过tar压缩数据并移除快照。它通过备份时的时间戳命名压缩包。它还有几个高级选项,但总的来说,这是一个执行LVM备份的非常简单明了的工具
Zmanda Recovery Manager
适用于MySQL的Zmanda Recover Manger 或ZRM,有免费(GPL)和商业两种版本。企业版提供基于网页图形接口的控制台,用来配置、备份、验证、恢复、报告和调度。开源的版本包含了所有核心功能,但缺少一些额外的特性,例如基于网页的控制台。正如其名,ZRM实际上是一个备份和恢复管理器,而并非单一工具。它封装了自由的基于标准工具和技术,例如mysqldump、LVM快照和Percona XtraBackup等之上的功能。它将许多冗长的备份和恢复工作进行自动化。
适用于MySQL的Zmanda Recover Manger 或ZRM,有免费(GPL)和商业两种版本。企业版提供基于网页图形接口的控制台,用来配置、备份、验证、恢复、报告和调度。开源的版本包含了所有核心功能,但缺少一些额外的特性,例如基于网页的控制台。正如其名,ZRM实际上是一个备份和恢复管理器,而并非单一工具。它封装了自由的基于标准工具和技术,例如mysqldump、LVM快照和Percona XtraBackup等之上的功能。它将许多冗长的备份和恢复工作进行自动化。
mydumper
几名MySQL现在和之前的工程师利用他们多年的经验创建了mydumper,用来替代mysqldump。这是一个多线程(并发)的备份和还原MySQL和Drizzle的工具集,有许多很好的特性。大概有许多人会发现多线程备份和还原的速度是这个工具最吸引人的特色。尽管我们知道有些人在生产环境中使用,但我们还没有在任何产品中使用的经验
几名MySQL现在和之前的工程师利用他们多年的经验创建了mydumper,用来替代mysqldump。这是一个多线程(并发)的备份和还原MySQL和Drizzle的工具集,有许多很好的特性。大概有许多人会发现多线程备份和还原的速度是这个工具最吸引人的特色。尽管我们知道有些人在生产环境中使用,但我们还没有在任何产品中使用的经验
mysqldump
大部分在使用这个与MySQL一起发行的程序,因此,尽管它有缺点,但创建数据和Schema的逻辑备份最常见的选择还是mysqldump。这是一个通用工具,可以用于许多的任务,例如在服务器间复制表
```bash
mysqldump --host=server1 test t1 | mysql --host=server2 test
```
在前面展示几个用mysqldump创建逻辑备份的例子。该工具默认会输出包含创建表和填充数据的所有需要的命令;也有选项可以控制输出视图、存储代码和触发器。下面有一些典型的例子。
1.对服务器上所有的内容创建逻辑备份到单个文件中,每个库中所有的表在相同逻辑时间点备份:
```bash
mysqldump --all-databases > dump.sql
```
2.创建只包含Sakila示例数据库的逻辑备份:
```bash
mysqldump --databases sakila > dump.sql
```
3.创建只包含sakila.actor表的逻辑备份
```bash
mysqldump sakila actor > dump.sql
```
可以使用 --result-file选项来指定输出文件,这可以帮助防止在Windows上发生换行符转换:
```bash
mysqldump sakila actor
```
mysqldump的默认选项对于大多数备份目的来说并不够好。多半要显示地指定某些选项以改变输出。下面是一些我们经常使用地选项,可以让mysqldump更加高效,输出更容易使用。
--opt
启用一组优化选项,包括关闭缓冲区(它会使服务器耗尽内存),导出数据时把更多地数据写在更少的SQL语句里,以便在加载的时候更有效率,以及做其他一些有用的事情。更多细节可以阅读帮助文件。如果关闭了这组选项,mysqldump会把表写到磁盘之前,把他们都导出到内存里,这对于大型的表而言是不切实际的。
--allow-keywords, -quote-names
使用户在导出和恢复表时,可以使用保留字作为表的名字
--complete-insert
使用户能在不完全相同列的表之间移动数据
--tz-utc
使用户能在具有不同时区的服务器之间移动数据
--lock-all-tables
使用FLUSH TABLE WITH READ LOCK来获取全局一致的备份。
--tab
用SELECT INTO OUTFILE导出文件
--skip-extended-insert
使每一行数据都有自己的INSERT语句。必要时这可以用于有选择地还原某些行。它的代价是文件更大,导入到MySQL时开销会更大。因此,要确保只有在需要时才启用它。
如果在mysqldump上使用--databases或--all-databases选项,那么最终导出的数据在每隔数据库中都一直,因为mysqldump会在同一时间锁定并导出一个数据库里的所有表。然而,来自不同数据库的各个表就微比是相互一致的。使用--lock-all-tables选项可以解决这个问题。对于InnoDB备份,应该增加--single-transaction选项,这会使用InnoDB的MVCC特性在单个时间点创建一个一致的备份,而不需要使用LOCK TABLES锁定所有的表。如果增加--master-data选项,备份还会包括在备份时服务器的二进制日志文件位置,这对基于时间点的恢复和设置复制非常有帮助,然而也要知道,获得日志位置时需要使用FLUSH TABLES WITH READ LOCK冻结服务器
大部分在使用这个与MySQL一起发行的程序,因此,尽管它有缺点,但创建数据和Schema的逻辑备份最常见的选择还是mysqldump。这是一个通用工具,可以用于许多的任务,例如在服务器间复制表
```bash
mysqldump --host=server1 test t1 | mysql --host=server2 test
```
在前面展示几个用mysqldump创建逻辑备份的例子。该工具默认会输出包含创建表和填充数据的所有需要的命令;也有选项可以控制输出视图、存储代码和触发器。下面有一些典型的例子。
1.对服务器上所有的内容创建逻辑备份到单个文件中,每个库中所有的表在相同逻辑时间点备份:
```bash
mysqldump --all-databases > dump.sql
```
2.创建只包含Sakila示例数据库的逻辑备份:
```bash
mysqldump --databases sakila > dump.sql
```
3.创建只包含sakila.actor表的逻辑备份
```bash
mysqldump sakila actor > dump.sql
```
可以使用 --result-file选项来指定输出文件,这可以帮助防止在Windows上发生换行符转换:
```bash
mysqldump sakila actor
```
mysqldump的默认选项对于大多数备份目的来说并不够好。多半要显示地指定某些选项以改变输出。下面是一些我们经常使用地选项,可以让mysqldump更加高效,输出更容易使用。
--opt
启用一组优化选项,包括关闭缓冲区(它会使服务器耗尽内存),导出数据时把更多地数据写在更少的SQL语句里,以便在加载的时候更有效率,以及做其他一些有用的事情。更多细节可以阅读帮助文件。如果关闭了这组选项,mysqldump会把表写到磁盘之前,把他们都导出到内存里,这对于大型的表而言是不切实际的。
--allow-keywords, -quote-names
使用户在导出和恢复表时,可以使用保留字作为表的名字
--complete-insert
使用户能在不完全相同列的表之间移动数据
--tz-utc
使用户能在具有不同时区的服务器之间移动数据
--lock-all-tables
使用FLUSH TABLE WITH READ LOCK来获取全局一致的备份。
--tab
用SELECT INTO OUTFILE导出文件
--skip-extended-insert
使每一行数据都有自己的INSERT语句。必要时这可以用于有选择地还原某些行。它的代价是文件更大,导入到MySQL时开销会更大。因此,要确保只有在需要时才启用它。
如果在mysqldump上使用--databases或--all-databases选项,那么最终导出的数据在每隔数据库中都一直,因为mysqldump会在同一时间锁定并导出一个数据库里的所有表。然而,来自不同数据库的各个表就微比是相互一致的。使用--lock-all-tables选项可以解决这个问题。对于InnoDB备份,应该增加--single-transaction选项,这会使用InnoDB的MVCC特性在单个时间点创建一个一致的备份,而不需要使用LOCK TABLES锁定所有的表。如果增加--master-data选项,备份还会包括在备份时服务器的二进制日志文件位置,这对基于时间点的恢复和设置复制非常有帮助,然而也要知道,获得日志位置时需要使用FLUSH TABLES WITH READ LOCK冻结服务器
备份脚本化。
为备份写一些脚本是标准做法。展示一个示例程序,其中必定有很多辅助内容,这只会增加篇幅,在这里我们更愿意列举一些典型的备份脚本功能,展示一些Perl脚本的代码片段。你可以把这些当作可重用的代码块,在创建自己的脚本时可以直接组合起来使用。下面将大致按照使用顺序来展示。
1.安全检测
安全检测可以让自己和同事的生活更简单点——打开严格的错误检测,并且使用英文变量名。
```perl
use strict;
use warning FATAL => 'all';
use English qw(-no_match_vars);
```
如果是在Bash下使用脚本,还可以做更严格的变量检测。下面的设置会替换中有未定义的变量或程序出错退出时产生一个错误。
```perl
set -u;
set -e;
```
2.命令行参数
增加命令行选项处理最好的方法是标准库,它已经包含在Perl标准安装中.
```perl
use Getopt::Long;
Getopt::Long::Configure('no_ignore_case','building');
GetOptions(....);
```
3.连接MySQL
标准的Perl DBI库几乎无所不在,提供了许多强大和灵活的功能。使用详情可请参阅Perldoc。可以像下面这样使用DBI 来连接MySQL.
```perl
use DBI;
$dbh = DBI -> connect('DBI:mysql:;host=localhost', 'user','p4ssswOrd', {'RaiseError => 1'});
```
对于编写命令行脚本,请阅读标准mysql程序的--help参数的输出文本,它有许多选项可以更友好地支持脚本。例如,在Bash中遍历数据库列表如下。
```perl
mysql -ss -e 'SHOW DATABASES' | while read DB; do
echo "${DB}"
done
```
4.停止和启动MySQL
停止和启动MySQL最好的方法是使用操作系统推荐的方法,例如运行/etc/init.d/mysql init脚本或通过服务控制(在Windows下)。然而这并不是唯一的方法。可以从Perl中用一个已存在的数据库连接来关闭数据库。
```bash
$dbh -> func("shutdown", 'admin');
```
在这个命令完成时不要太指望MySQL已经被关闭——它可能正在关闭的过程中。也可以通过命令行来停掉MySQL.
```bash
mysqladmin shutdown
```
为备份写一些脚本是标准做法。展示一个示例程序,其中必定有很多辅助内容,这只会增加篇幅,在这里我们更愿意列举一些典型的备份脚本功能,展示一些Perl脚本的代码片段。你可以把这些当作可重用的代码块,在创建自己的脚本时可以直接组合起来使用。下面将大致按照使用顺序来展示。
1.安全检测
安全检测可以让自己和同事的生活更简单点——打开严格的错误检测,并且使用英文变量名。
```perl
use strict;
use warning FATAL => 'all';
use English qw(-no_match_vars);
```
如果是在Bash下使用脚本,还可以做更严格的变量检测。下面的设置会替换中有未定义的变量或程序出错退出时产生一个错误。
```perl
set -u;
set -e;
```
2.命令行参数
增加命令行选项处理最好的方法是标准库,它已经包含在Perl标准安装中.
```perl
use Getopt::Long;
Getopt::Long::Configure('no_ignore_case','building');
GetOptions(....);
```
3.连接MySQL
标准的Perl DBI库几乎无所不在,提供了许多强大和灵活的功能。使用详情可请参阅Perldoc。可以像下面这样使用DBI 来连接MySQL.
```perl
use DBI;
$dbh = DBI -> connect('DBI:mysql:;host=localhost', 'user','p4ssswOrd', {'RaiseError => 1'});
```
对于编写命令行脚本,请阅读标准mysql程序的--help参数的输出文本,它有许多选项可以更友好地支持脚本。例如,在Bash中遍历数据库列表如下。
```perl
mysql -ss -e 'SHOW DATABASES' | while read DB; do
echo "${DB}"
done
```
4.停止和启动MySQL
停止和启动MySQL最好的方法是使用操作系统推荐的方法,例如运行/etc/init.d/mysql init脚本或通过服务控制(在Windows下)。然而这并不是唯一的方法。可以从Perl中用一个已存在的数据库连接来关闭数据库。
```bash
$dbh -> func("shutdown", 'admin');
```
在这个命令完成时不要太指望MySQL已经被关闭——它可能正在关闭的过程中。也可以通过命令行来停掉MySQL.
```bash
mysqladmin shutdown
```
5.获取数据库和表的列表。
每个备份脚本都会查询MySQL以获取数据库和表的列表。要注意那些是实际上并不是数据库的条目,例如一些日志系统中的lost+found文件夹和INFORMATION_SCHEMA。也要确保脚本已经准备好应付视图,同时也要知道SHOW TABLE STATUS在InnoDB中有大量数据时可能耗时很长。
```sql
mysql>SHOW DATABASES;
mysql>SHOW /* !50002 FULL */ TABLES FROM <database>;
mysql>SHOW TABLE STATUS FROM <database>;
```
6.对表加锁、刷新并解锁
如果需要对一个或多个表加锁并且/或刷新,要么按名字锁住所需的表,要么使用全局锁锁住所有的表。
```sql
mysql>LOCK TABLES <database.table> READ [, ...];
mysql>FLUSH TABLES;
mysql>FLUSH TABLES <database.table> [, ...];
mysql>FLUSH TABLES WITH READ LOCK;
mysql>UNLOCK TABLES;
```
在获取所有的表并锁住它们时要格外注意竞争条件。期间可能会有新表创建,或有表被删除或重命名。如果一个表一个表地锁住然后备份,将无法得到一致性的备份
7.刷新二进制日志
让服务器开始一个新的二进制日志非常简单(一般在锁住表后但在备份前做这个操作):
```sql
mysql>FLUSH LOGS;
```
这样做使得恢复和增量备份更简单,因为不需要考虑从一个日志文件中间开始操作。此操作会有一些副作用,比如刷新和重新打开错误日志,也可能销毁老的日志条目,因此,注意不要扔掉需要用到的数据。
8.获取二进制日志位置
脚本应该获取并记录主库和备库的状态——即使服务器仅是个主库或备库。
```sql
mysql>SHOW MASTER STATUS
mysql> SHOW SLAVE STATUS\G
```
执行这两条语句并忽略错误,以使脚本可以获取到所有可能的信息
9.导出数据
最好的选择是使用mysqldump、mysqldumper或SELECT INTO OUTFILE
这些都是构造备份脚本的基础。比较困难的部分是将管理和恢复任务脚本话。如果想获得实现的灵感,可以看看ZRM的源码
每个备份脚本都会查询MySQL以获取数据库和表的列表。要注意那些是实际上并不是数据库的条目,例如一些日志系统中的lost+found文件夹和INFORMATION_SCHEMA。也要确保脚本已经准备好应付视图,同时也要知道SHOW TABLE STATUS在InnoDB中有大量数据时可能耗时很长。
```sql
mysql>SHOW DATABASES;
mysql>SHOW /* !50002 FULL */ TABLES FROM <database>;
mysql>SHOW TABLE STATUS FROM <database>;
```
6.对表加锁、刷新并解锁
如果需要对一个或多个表加锁并且/或刷新,要么按名字锁住所需的表,要么使用全局锁锁住所有的表。
```sql
mysql>LOCK TABLES <database.table> READ [, ...];
mysql>FLUSH TABLES;
mysql>FLUSH TABLES <database.table> [, ...];
mysql>FLUSH TABLES WITH READ LOCK;
mysql>UNLOCK TABLES;
```
在获取所有的表并锁住它们时要格外注意竞争条件。期间可能会有新表创建,或有表被删除或重命名。如果一个表一个表地锁住然后备份,将无法得到一致性的备份
7.刷新二进制日志
让服务器开始一个新的二进制日志非常简单(一般在锁住表后但在备份前做这个操作):
```sql
mysql>FLUSH LOGS;
```
这样做使得恢复和增量备份更简单,因为不需要考虑从一个日志文件中间开始操作。此操作会有一些副作用,比如刷新和重新打开错误日志,也可能销毁老的日志条目,因此,注意不要扔掉需要用到的数据。
8.获取二进制日志位置
脚本应该获取并记录主库和备库的状态——即使服务器仅是个主库或备库。
```sql
mysql>SHOW MASTER STATUS
mysql> SHOW SLAVE STATUS\G
```
执行这两条语句并忽略错误,以使脚本可以获取到所有可能的信息
9.导出数据
最好的选择是使用mysqldump、mysqldumper或SELECT INTO OUTFILE
这些都是构造备份脚本的基础。比较困难的部分是将管理和恢复任务脚本话。如果想获得实现的灵感,可以看看ZRM的源码
MySQL用户工具
概述。
MySQL服务器发行包中并没有包含针对许多常用任务的工具,例如监控服务器或比较不同服务器间数据的工具。幸运的是,Oracle的商业版提供了一些扩展工具,并且MySQL活跃的开源社区和第三方公司也提供了一系列的工具,降低了自己"重复发明轮子"的需要
MySQL服务器发行包中并没有包含针对许多常用任务的工具,例如监控服务器或比较不同服务器间数据的工具。幸运的是,Oracle的商业版提供了一些扩展工具,并且MySQL活跃的开源社区和第三方公司也提供了一系列的工具,降低了自己"重复发明轮子"的需要
接口工具。
接口工具可以帮助运行查询,创建表和用户,以及执行其他日常任务等。接下来讲简单介绍一些用于此用途的最流行的工具。一般可以用SQL查询或命令做所有这些或其中大部分的工作——我们这里讨论的工具只是更为方便,可帮助避免错误和加快工作。
1.MySQL Workbech
MySQL Workbench是一个一站式的工具,可以完成例如管理服务器、写查询、开发存储过程,以及Schema设计图相关的工作。可以通过一个插件接口来编写自己的工具并集成到这个工作平台上,有一些Python脚本和库就使用了这个插件接口。MySQL Workbench有社区版和商业版两个版本,商业版只是增加了一些高级特性。免费版对于大部分需要早已足够了
2.SQLyog
SQLyog是MySQL最流行的可视化工具之一,有许多很好的特性。它与MySQL Workbench是同级别的工具,但两个工具都有一些对方没有的特性。SQLyog只能在微软的Windows下使用,拥有全部特性的版本需要付费,但有限制功能免费版本
3.phpMyAdmin
phpMyAdmin是一个流行的管理工具,运行在Web服务器上,并且提供基于浏览器的MySQL服务器访问接口。尽管基于浏览器的访问有时很好,但phpMyAdmin是个大而复杂的工具,曾被指责有许多安全的问题。对此要格外小心。我们建议不要安装在任何可以从互联网访问的地方。
4.Adminer
Adminer是个基于浏览器的安全的轻量级管理工具,它与phpMyAdmin同类。其他开发者将其定位为phpMyAdmin的更好的替代品。尽管它看起来更安全,但仍建议安装在任何可功开访问的地方时要谨慎
接口工具可以帮助运行查询,创建表和用户,以及执行其他日常任务等。接下来讲简单介绍一些用于此用途的最流行的工具。一般可以用SQL查询或命令做所有这些或其中大部分的工作——我们这里讨论的工具只是更为方便,可帮助避免错误和加快工作。
1.MySQL Workbech
MySQL Workbench是一个一站式的工具,可以完成例如管理服务器、写查询、开发存储过程,以及Schema设计图相关的工作。可以通过一个插件接口来编写自己的工具并集成到这个工作平台上,有一些Python脚本和库就使用了这个插件接口。MySQL Workbench有社区版和商业版两个版本,商业版只是增加了一些高级特性。免费版对于大部分需要早已足够了
2.SQLyog
SQLyog是MySQL最流行的可视化工具之一,有许多很好的特性。它与MySQL Workbench是同级别的工具,但两个工具都有一些对方没有的特性。SQLyog只能在微软的Windows下使用,拥有全部特性的版本需要付费,但有限制功能免费版本
3.phpMyAdmin
phpMyAdmin是一个流行的管理工具,运行在Web服务器上,并且提供基于浏览器的MySQL服务器访问接口。尽管基于浏览器的访问有时很好,但phpMyAdmin是个大而复杂的工具,曾被指责有许多安全的问题。对此要格外小心。我们建议不要安装在任何可以从互联网访问的地方。
4.Adminer
Adminer是个基于浏览器的安全的轻量级管理工具,它与phpMyAdmin同类。其他开发者将其定位为phpMyAdmin的更好的替代品。尽管它看起来更安全,但仍建议安装在任何可功开访问的地方时要谨慎
命令行工具集。
MySQL包含了一些命令行工具集,例如mysqladmin和mysqlcheck。这些在MySQL手册上都有提及和记录。MySQL社区同样创建了大量高质量的工具包,并有很好的文档支撑这些使用工具集。
1.Percona Toolkit
Percona Toolkit是MySQL管理员必备的工具包。它源自Baron早期的工具包Maatkit和Aspersa,很多人认为这两个工具应该是正式的MySQL部署必须强制要求使用的。Percona Toolkit包括许多针对类似日志分析、复制完整性检测、数据同步、模式和索引分析、查询建议和数据归档目的的工具。如果刚开始接触MySQL,建议首先学习这些关键的工具:pt-mysql-summary、pt-table-checksum、pt-table-sync和pt-query-digest
2。Maatkit and Aspersa
这两个工具约从2006年以某种形式出现,两者都被认为是MySQL用户的基本工具。它们现在已经并入Percona Toolkit
3.The openark kit
Shlomi Noach的openark kit包含了可以用来做一系列管理任务的Python脚本
4.MySQL Workbench工具集中的某些工具可以作为单独的Python脚本使用
除了这些工具外,还有一系列没有太正式包装和维护的工具。许多杰出的MySQL社区成员时不时地贡献工具,其中大多数托管在它们自己的网站或MySQL Forge上。可以通过时不时地查看Planet MySQL博客聚合器获取大量地西悉尼,但不幸的是这些工具没有一个集中地目录
MySQL包含了一些命令行工具集,例如mysqladmin和mysqlcheck。这些在MySQL手册上都有提及和记录。MySQL社区同样创建了大量高质量的工具包,并有很好的文档支撑这些使用工具集。
1.Percona Toolkit
Percona Toolkit是MySQL管理员必备的工具包。它源自Baron早期的工具包Maatkit和Aspersa,很多人认为这两个工具应该是正式的MySQL部署必须强制要求使用的。Percona Toolkit包括许多针对类似日志分析、复制完整性检测、数据同步、模式和索引分析、查询建议和数据归档目的的工具。如果刚开始接触MySQL,建议首先学习这些关键的工具:pt-mysql-summary、pt-table-checksum、pt-table-sync和pt-query-digest
2。Maatkit and Aspersa
这两个工具约从2006年以某种形式出现,两者都被认为是MySQL用户的基本工具。它们现在已经并入Percona Toolkit
3.The openark kit
Shlomi Noach的openark kit包含了可以用来做一系列管理任务的Python脚本
4.MySQL Workbench工具集中的某些工具可以作为单独的Python脚本使用
除了这些工具外,还有一系列没有太正式包装和维护的工具。许多杰出的MySQL社区成员时不时地贡献工具,其中大多数托管在它们自己的网站或MySQL Forge上。可以通过时不时地查看Planet MySQL博客聚合器获取大量地西悉尼,但不幸的是这些工具没有一个集中地目录
SQL实用集。
服务器本身也内置有一系列免费的附加组件和实用集可以使用;其中一些确实相当强大。
1.common_schema
Shlomi Noach的common_schema享目是一套针对服务器脚本话和管理的强大的代码和视图。common_schema对于MySQL好比jQuery对于JavaScript.
2.mysql-sr-lib
Giuseppe Maxia为MySQL创建了一个存储过程的代码库,
3.MySQL UDF仓库
Roland Bouman建立了一个MySQL自定义函数的收藏馆。
4.MySQL Forge
在MySQL Forge上,可以找到上百个社区共享的程序、脚本、代码片段、实用集和技巧及陷阱
服务器本身也内置有一系列免费的附加组件和实用集可以使用;其中一些确实相当强大。
1.common_schema
Shlomi Noach的common_schema享目是一套针对服务器脚本话和管理的强大的代码和视图。common_schema对于MySQL好比jQuery对于JavaScript.
2.mysql-sr-lib
Giuseppe Maxia为MySQL创建了一个存储过程的代码库,
3.MySQL UDF仓库
Roland Bouman建立了一个MySQL自定义函数的收藏馆。
4.MySQL Forge
在MySQL Forge上,可以找到上百个社区共享的程序、脚本、代码片段、实用集和技巧及陷阱
监测工具。
以过往的经验来看,大多数MySQL商店需要提供两种类型的监测工具:健康监测工具——检测到异常时告警——和为趋势、诊断、问题排查、容量规划等记录指标的工具。大多数系统仅在这些任务中的一个方面做得很好,而不能两者兼顾。更不幸的是,有十几种工具可选,使得评估和选择一款适合的工具非常耗时。许多监控系统不是专门为MySQL服务器设计。它们是通用系统,用于周期性地检测许多类型的资源,从及其到路由再到软件(例如MySQL)。它们一般有某些类型的插件架构,经常会伴随有一些MySQL插件。一般会在专用服务器上安装监控系统来监测其他服务器。如果是监控重要的资源,它很快会变成架构中至关重要的一部分,因此可能需要采取额外的步骤,例如做监控系统本身的灾备。
以过往的经验来看,大多数MySQL商店需要提供两种类型的监测工具:健康监测工具——检测到异常时告警——和为趋势、诊断、问题排查、容量规划等记录指标的工具。大多数系统仅在这些任务中的一个方面做得很好,而不能两者兼顾。更不幸的是,有十几种工具可选,使得评估和选择一款适合的工具非常耗时。许多监控系统不是专门为MySQL服务器设计。它们是通用系统,用于周期性地检测许多类型的资源,从及其到路由再到软件(例如MySQL)。它们一般有某些类型的插件架构,经常会伴随有一些MySQL插件。一般会在专用服务器上安装监控系统来监测其他服务器。如果是监控重要的资源,它很快会变成架构中至关重要的一部分,因此可能需要采取额外的步骤,例如做监控系统本身的灾备。
开源的监控工具。
下面是一些最受欢迎的开源集成监控系统。
1.Nagios
Nagios也许是开源世界中最流行的问题检测和告警系统。它周期性检测监控的服务器并将结果与默认或自定义的阈值相比较。如果结果超出了限制,Nagio会执行某个程序并且(或)把问题的告警发给某些人。Nagios的通信和告警系统可以将其发送到不同的位置,并且对计划内的宕机可以特殊处理。Nagios同样理解服务之间的依赖,因此,如果是因为中间的路由层或者主机本身宕机导致MySQL实例不可用,Nagios不会发送告警来烦你。Nagios能将任何一个可执行文件以插件形式运行,只要给予其正确参数就可得到z正确输出。因此,Nagios插件在多种语言中都存在,例如shell、Perl、Python、Ruby和其他脚本语言。就算找不到一个能真正满足你需求的插件,自己创建一个也很简单。一个插件只需要接收b标准的参数,以一个合适的状态退出,然后选择性地打印Nagios捕获的输出。然而,Nagios也有一些严重的缺点。即使你很了解它,也仍然难以维护。它将所有配置保存在文件而不是数据库中。文件有一个特别容易出错的语法,当系统增长和发展时,修改配置文件就很费事。Nagios可扩展性并不好;你可以很容易地写出监控插件,但这也是你能够做的一切。最后,它的图形化、趋势化和可视化能力都有限。Nagios将一些性能和其他数据存储到MySQL服务器中,一般从中生成图形,但并不像其他一些系统那么灵活。因为不同"政见"的原因,使得上面所有的问题继续变得更糟。因为或真实、或臆测的涉及代码、参与者的问题,Nagios至少分化除了两个分支。两个分支的名字分别是Opsview和Icinga。它们比Nagios更受到人们的青睐。
2.Zabbitx
Zabbix是一个同时支持监控和指标收集的完整系统。例如,它将所有配置和其他数据存储到数据库而不是配置文件中。它存储了比Nagios更多的数据类型,因而可以得到更好的趋势和历史报表。其网络画图和可视能力也比Nagios更强,配置更简单,更灵活,且更具可扩展性
3.Zenoss
Zenoss是用Python写的,拥有一个基于浏览器的用户界面,使用了Ajax,这使它更快和更高效。它可以自动发现网络上的资源,并将监控、告警、趋势、绘图和记录历史数据整合到一个统一的工具中。Zenoss默认使用SNMP来从远程服务器上收集数据,但也可以使用SSH,并且支持Nagios插件
4.Hyperic HQ
Hyperic HQ是一个基于Java的监控系统,比起同级别的其他大部分系统,它更称得上是企业级监控.像Zenoss一样,它可以自动发现网络上的资源和支持Nagios插件,但它的逻辑阻止和架构不同,有点"笨重"
5.OpenNMS
OpenNMS也是用Java开发,有一个活跃的开发社区。它拥有常规的特性,例如监控和告警,但同样也增加了绘图和趋势功能,它的目标是高性能、可扩展、自动化和灵活。像Hyperic一样,它也致力于为大型和关键系统做企业级监控
6.Groundwokr Open Source
Groundwork Open Source用一个可移植的接口把Nagios和其他几个工具整合到了一个系统中,对于这个工具最好的描述是:如果你是Nagios、Cacti和其他几个工具方面的专家,并且花了很多时间将它们整合在一起,那很可能你是在闭门造车.
下面是一些最受欢迎的开源集成监控系统。
1.Nagios
Nagios也许是开源世界中最流行的问题检测和告警系统。它周期性检测监控的服务器并将结果与默认或自定义的阈值相比较。如果结果超出了限制,Nagio会执行某个程序并且(或)把问题的告警发给某些人。Nagios的通信和告警系统可以将其发送到不同的位置,并且对计划内的宕机可以特殊处理。Nagios同样理解服务之间的依赖,因此,如果是因为中间的路由层或者主机本身宕机导致MySQL实例不可用,Nagios不会发送告警来烦你。Nagios能将任何一个可执行文件以插件形式运行,只要给予其正确参数就可得到z正确输出。因此,Nagios插件在多种语言中都存在,例如shell、Perl、Python、Ruby和其他脚本语言。就算找不到一个能真正满足你需求的插件,自己创建一个也很简单。一个插件只需要接收b标准的参数,以一个合适的状态退出,然后选择性地打印Nagios捕获的输出。然而,Nagios也有一些严重的缺点。即使你很了解它,也仍然难以维护。它将所有配置保存在文件而不是数据库中。文件有一个特别容易出错的语法,当系统增长和发展时,修改配置文件就很费事。Nagios可扩展性并不好;你可以很容易地写出监控插件,但这也是你能够做的一切。最后,它的图形化、趋势化和可视化能力都有限。Nagios将一些性能和其他数据存储到MySQL服务器中,一般从中生成图形,但并不像其他一些系统那么灵活。因为不同"政见"的原因,使得上面所有的问题继续变得更糟。因为或真实、或臆测的涉及代码、参与者的问题,Nagios至少分化除了两个分支。两个分支的名字分别是Opsview和Icinga。它们比Nagios更受到人们的青睐。
2.Zabbitx
Zabbix是一个同时支持监控和指标收集的完整系统。例如,它将所有配置和其他数据存储到数据库而不是配置文件中。它存储了比Nagios更多的数据类型,因而可以得到更好的趋势和历史报表。其网络画图和可视能力也比Nagios更强,配置更简单,更灵活,且更具可扩展性
3.Zenoss
Zenoss是用Python写的,拥有一个基于浏览器的用户界面,使用了Ajax,这使它更快和更高效。它可以自动发现网络上的资源,并将监控、告警、趋势、绘图和记录历史数据整合到一个统一的工具中。Zenoss默认使用SNMP来从远程服务器上收集数据,但也可以使用SSH,并且支持Nagios插件
4.Hyperic HQ
Hyperic HQ是一个基于Java的监控系统,比起同级别的其他大部分系统,它更称得上是企业级监控.像Zenoss一样,它可以自动发现网络上的资源和支持Nagios插件,但它的逻辑阻止和架构不同,有点"笨重"
5.OpenNMS
OpenNMS也是用Java开发,有一个活跃的开发社区。它拥有常规的特性,例如监控和告警,但同样也增加了绘图和趋势功能,它的目标是高性能、可扩展、自动化和灵活。像Hyperic一样,它也致力于为大型和关键系统做企业级监控
6.Groundwokr Open Source
Groundwork Open Source用一个可移植的接口把Nagios和其他几个工具整合到了一个系统中,对于这个工具最好的描述是:如果你是Nagios、Cacti和其他几个工具方面的专家,并且花了很多时间将它们整合在一起,那很可能你是在闭门造车.
相比于集所有功能于一身的系统,还有一系列软件专注于收集指标和画图以及可视化,而不是进行性能监控检查。它们中有很多是建立在RRDTool之上,存储时序数据到轮询数据库(RRD)文件中。RRD文件自动聚集输入数据,对没有预期传送的输入值进行插值,并有强大的绘图工具可以生成漂亮有特色的图。有很多基于RRDTool的系统,下面是最受欢迎的几个.
1.MRTG
Multi Router Traffic Grapher或称MRTG,是典型的基于RRDTool的系统。最初是为记录网络流量而设计的,但同样可以扩展到用于对其他指标进行记录和绘图
2.Cacti
Cacti可能是最流行的基于RRDTool的系统。它采用PHP网页来与RRDTool进行交互,并使用MySQL数据库来定义服务器、插件、图像等。因为是模块驱动,故而可以定义模板然后应用到系统上。Baron为MySQL和其他系统写了一组非常流行的模板.这些也已经被移植到Munin、OpenNMS和Zabbix
3.Ganglia
Ganglia与Cacti类似,但是为监控集群和网格系统而涉及,所以可以汇总查看许多服务器的数据,如果需要也可以细分查看单台服务器的详细数据
4.Munin
Munin手机树并存入RRDTool中,然后以几个不同级别的粒度生成数据图。它从配置中生成静态HTML文件,因此可以很容易地浏览和查看趋势。定义一个图形较容易;只需要创建一个插件脚本,其命令行帮助输出有一些Munin可以识别的特别语法的画图指令。
基于RRDTool的系统有些限制,例如不能用标准查询语言来查询存储的数据,不能永久保留数据,存在某些数据不能轻松地使用简单计数器和标准数值表示的问题,需要预先定义指标和图形等。理想情况下,我们需要的监控系统可以接受任何发送给它的指标,而不需要预先进行定义,并且后续可以绘制任意需要的图形,也不需要预先进行定义。可能我们锁看到的最接近的系统是Graphite.
这些系统都可以用来对MySQL收集、记录和绘制数据图表并且生成报表,有着不同程度的灵活性,目标也稍微有些不同,但它们都缺乏真正可以在问题出现时及时告警的灵活性。
我们提到的大多数系统的主要问题是,它们明显是由那些因为现有系统不能满足它们所有需求的人涉及的,因此它们又重复设计了另一个无法完全满足其他人的所有需求的系统。大部分这样的系统都有一些基础的限制,例如使用一个奇怪的数据模型存储内部数据,而导致在很多场合都无法很好地工作。在很多时候,这都令人沮丧,使用这些系统都像是把一个圆形的钉子钉到了一个方形的洞里面。
1.MRTG
Multi Router Traffic Grapher或称MRTG,是典型的基于RRDTool的系统。最初是为记录网络流量而设计的,但同样可以扩展到用于对其他指标进行记录和绘图
2.Cacti
Cacti可能是最流行的基于RRDTool的系统。它采用PHP网页来与RRDTool进行交互,并使用MySQL数据库来定义服务器、插件、图像等。因为是模块驱动,故而可以定义模板然后应用到系统上。Baron为MySQL和其他系统写了一组非常流行的模板.这些也已经被移植到Munin、OpenNMS和Zabbix
3.Ganglia
Ganglia与Cacti类似,但是为监控集群和网格系统而涉及,所以可以汇总查看许多服务器的数据,如果需要也可以细分查看单台服务器的详细数据
4.Munin
Munin手机树并存入RRDTool中,然后以几个不同级别的粒度生成数据图。它从配置中生成静态HTML文件,因此可以很容易地浏览和查看趋势。定义一个图形较容易;只需要创建一个插件脚本,其命令行帮助输出有一些Munin可以识别的特别语法的画图指令。
基于RRDTool的系统有些限制,例如不能用标准查询语言来查询存储的数据,不能永久保留数据,存在某些数据不能轻松地使用简单计数器和标准数值表示的问题,需要预先定义指标和图形等。理想情况下,我们需要的监控系统可以接受任何发送给它的指标,而不需要预先进行定义,并且后续可以绘制任意需要的图形,也不需要预先进行定义。可能我们锁看到的最接近的系统是Graphite.
这些系统都可以用来对MySQL收集、记录和绘制数据图表并且生成报表,有着不同程度的灵活性,目标也稍微有些不同,但它们都缺乏真正可以在问题出现时及时告警的灵活性。
我们提到的大多数系统的主要问题是,它们明显是由那些因为现有系统不能满足它们所有需求的人涉及的,因此它们又重复设计了另一个无法完全满足其他人的所有需求的系统。大部分这样的系统都有一些基础的限制,例如使用一个奇怪的数据模型存储内部数据,而导致在很多场合都无法很好地工作。在很多时候,这都令人沮丧,使用这些系统都像是把一个圆形的钉子钉到了一个方形的洞里面。
0 条评论
下一页