MySQL数据库

2017/04/11 Database

数据查询语言DQL(表记录查询),数据操纵语言DML(表记录操作,需要提交事务),数据定义语言DDL(库或表结构操作),数据控制语言DCL(数据库操作及授权)。

install

wget http://dev.mysql.com/get/Downloads/MySQL-5.6/MySQL-5.6.16-1.el6.x86_64.rpm-bundle.tar

rpm -ivh MySQL-client-5.6.16-1.el6.x86_64.rpm
rpm -ivh MySQL-devel-5.6.16-1.el6.x86_64.rpm
rpm -ivh MySQL-server-5.6.16-1.el6.x86_64.rpm
# 更新数据库
sudo mysql_upgrade -u root -p

/etc/my.cnf
/etc/mysql/my.cnf

[mysqld]
bind-address=0.0.0.0
lower_case_table_names=

启动和停止

windows

net start mysql
net stop mysql

myql -uroot -ppass -hlocalhost

linux

service mysqld start
service mysqld stop
/etc/init.d/mysql restart

创建用户和授权

select * from mysql.user;
-- 使用安装时生成的默认密码登陆并首次初始化
set password=PASSWORD('admin');
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'admin' WITH GRANT OPTION;

-- 指定IP上登陆
CREATE USER username@ip IDENTIFIED BY 'password';
-- 任意IP登陆
CREATE USER username@'%' IDENTIFIED BY 'password';
SET PASSWORD FOR username=PASSWORD('password');
-- 关闭数据库安全修改(批量更新和删除会报错)
SET SQL_SAFE_UPDATES=0;
GRANT create,alter,drop,insert,update,delete,select ON databasename.* TO username@localhost;
GRANT all ON databasename.* TO username@localhost;
GRANT ALL PRIVILEGES ON *.* TO 'xpress'@'%' IDENTIFIED BY 'admin' WITH GRANT OPTION;
REVOKE all ON databasename.* FROM username@localhost;
FLUSH PRIVILEGES;
SHOW GRANTS FOR username@localhost;

数据库操作

show global variables like '%datadir%';
-- 创建数据库
CREATE DATABASE databasename CHARSET=utf8;
-- 删除数据库
DROP DATABASE databasename;
-- 修改数据库
ALTER DATABASE databasename CHARACTER SET utf8;
-- 查看数据
show databases;
-- 数据库切换
use databasename;
-- 查看版本信息
status;
-- 查看编码
show char set;
show charset;

数据类型

类型 名称 备注
int 整型  
double 浮点型 double(5,2)表示最多五位,其中有两位是小树,最大值为999.99
decimal 浮点型 不会出现精度缺失问题
char 固定长度字符串类型 char(255),数据长度不足时补足到指定长度
varchar 可变长度字符串类型 varchar(65535) 会占用字节存储实际长度
text(clob) 字符串类型 tinytext 2^8-1,text 2^16-1,mediumtext 2^24-1,longtext 2^32-1
blob 字节类型 tinyblob 2^8-1,blob 2^16-1,mediumblob 2^24-1,longblob 2^32-1
date 日期类型 yyyy-MM-dd
time 时间类型 hh:mm:ss
tomestamp 时间戳类型  

ps:使用blob类型时,在my.ini中配置调整允许发送的包大小

max_allowed_packet=10485760

表操作

创建和删除表

CREATE TABLE IF NOT EXISTS tablename(
    columnname int,
    columnname1 char(255)
);
DROP TABLE tablename

查看表

show tables;
-- 查看创建语句
show create table tablename;
-- 查看表结构
desc tablename;

修改表

  • 修改表名
ALTER TABLE tablename RENAME TO newtablename;
ALTER TABLE  `artical` CHANGE  `summary` `summary` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL
  • 增加列
ALTER TABLE tablename ADD(
    columnname int,
    columnname1 char(20)
)
  • 修改列类型

如果别修改列已存在数据,那么新的类型可能影响已存在的数据

ALTER TABLE tablename MODIFY columnname int;
  • 修改列名
ALTER TABLE tablename CHANGE columnname newcolumnname int;
  • 删除列
ALTER TABLE tablename DROP columnname;

数据操作

插入

INSERT INTO tablename(columnname,columnname1) values ('value','value');
-- 与创建表时顺序相同
INSERT INTO tablename values ('value','value');

更新

UPDATE tablename SET columnname='value',columnname1='value' where columnname='value';

查询条件

运算符 备注
=  
!=<>  
><<=>=  
BETWEEN AND 检查你的数据库是如何处理 BETWEEN….AND 操作符边界的
IN(...)  
IS NULLIS NOT NULL =NULL必返回false
NOT  
ORAND  

删除

DELETE FROM tablename WHERE columnname = 'value';

查询

单表查询

检索去重
SELECT * FROM tablename;
-- 去除重复
SELECT DISTINCT columnname FROM tablename;
-- 查询导出到文件,需要权限
select count(1) from table into outfile '/tmp/1.xls';
-- 不需要权限,导出到文件
echo "select * from db_web.help_cat where 1 order by sort desc limit 0,20" | mysql -h127.0.0.1 -uroot > /data/sort.xls
运算
SELECT columnname*1.5 FROM tablename;
-- 防止NULL值相加变成NULL
SELECT columnname+ifnull(columnname1,0) FROM tablename;
拼接
SELECT CONCAT(columnname,'--',columnname1) as alias FROM tablename;
模糊查询
SELECT * FROM tablename WHERE columnname LIKE 'value_';
符号 匹配规则
_ 匹配一个字符
% 匹配0~n个字符
排序
-- 默认ASC升序
SELECT * FROM tablename ORDER BY columnname ASC;
-- 多列排序
SELECT * FROM tablename ORDER BY columnname ASC,columnname1 DESC;
聚合函数
-- 总数,列信息不为NULL则计数,*所有列不为NULL计数
SELECT COUNT(*) FROM tablename;
SELECT MIN(*) FROM tablename;
SELECT MAX(*) FROM tablename;
SELECT SUM(*) FROM tablename;
SELECT AVG(*) FROM tablename;
-- 组合使用
SELECT COUNT(*),MAX(columnname),AVG(columnname) FROM tablename;
分组
-- 分组前条件用WHERE
-- 分组后条件用HAVING
SELECT 
    columnname,
    columnname1,
    COUNT(*),
    MAX(columnname),
    AVG(columnname2)
FROM
    tablename
WHERE
    columnname IS NOT NULL
GROUP BY columnname , columnname1
HAVING COUNT(*) > 1;
LIMIT方言
-- LIMIT begin,count 6-15
SELECT * FROM tablename LIMT 5,10;

多表查询

合并结果集
- 两个结果集的列相同,结果在同列显示
-- 不去重
SELECT * FROM tablename
UNION ALL
SELECT * FROM tablename1
-- 去除重复
SELECT columnname FROM tablename
UNION
SELECT columnname FROM tablename1
连接查询
  • 内连接
  • 外连接
    • 左外连接
    • 右外连接
    • 全外连接(mysql不支持)
  • 自然连接

内连接

必须满足两张表都有数据

-- 方言版本
SELECT * FROM tablename,tablename1 WHERE tablename.columnname = tablename1.columnname;
-- 标准版本
SELECT * FROM tablename INNER JOIN tablename1 ON tablename.columnname = tablename1.columnname;
-- 自然内连接:自动匹配两个table列名相同的列
SELECT * FROM tablename NATURAL JOIN tablename1;

外连接

外连接一主一次,左外左表为主,主表所有数据都会显示,不满足条件的右表数据为NULL

-- 左外连接
SELECT * FROM tablename LEFT OUTER JOIN tablename1 ON tablename.columnname = tablename1.columnname;
-- 右外连接
SELECT * FROM tablename RIGHT OUTER JOIN tablename1 ON tablename.columnname = tablename1.columnname;
-- 全外链接,左右表结构都在,不符合条件补NULL(mysql不支持)
SELECT * FROM tablename FULL OUTER JOIN tablename1 ON tablename.columnname = tablename1.columnname;
-- 使用合并结果集模拟全外连接
SELECT * FROM tablename LEFT OUTER JOIN tablename1 ON tablename.columnname = tablename1.columnname;
UNION
SELECT * FROM tablename RIGHT OUTER JOIN tablename1 ON tablename.columnname = tablename1.columnname;
-- 自然外连接:同自然内连接
子查询

子查询条件组合

子查询结果集 可嵌套条件
单行单列 =、>、<、>=、<=、!=
多行单列 IN、ALL、ANY
单行多列 多列IN多列
多行多列 当作表连接查询
-- 单行单列
SELECT * FROM tablename WHERE columnname = (SELECT MAX(columnname) FROM tablename);
-- 多行单列
SELECT * FROM tablename WHERE columnname > ANY (SELECT columnname FROM tablename WHERE columnname1 = 'value');
SELECT * FROM tablename WHERE columnname > ALL (SELECT columnname FROM tablename WHERE columnname1 = 'value');
SELECT * FROM tablename WHERE columnname IN (SELECT columnname FROM tablename WHERE columnname1 = 'value');
-- 单行多列
SELECT * FROM tablename WHERE columnname,columnname1 IN (SELECT columnname,columnname1 FROM tablename WHERE columnname1 = 'value');
-- 多行多列 结果集为表要有别名
SELECT MAX(columnname) FROM (SELECT * FROM tablename  WHERE columnname < 'value') alias;

约束

  • 非空约束
  • 唯一约束
  • 检查约束
  • 主键约束
  • 外键约束

主键约束

  • 非空
  • 唯一
  • 被引用(外键)
CREATE TABLE tablename (
    columnname INT PRIMARY KEY AUTO_INCREMENT,-- 自增长,必须整型
    columnname1 VARCHAR(20)
)

CREATE TABLE tablename (
    columnname INT,
    columnname1 VARCHAR(20),
    PRIMARY KEY (columnname)
)

ALTER TABLE tablename ADD PRIMARY KEY(columnname);
ALTER TABLE tablename DROP PRIMARY KEY;

非空约束

CREATE TABLE tablename (
    columnname INT,
    columnname1 VARCHAR(20) NOT NULL -- 非空
)

唯一约束

CREATE TABLE tablename (
    columnname INT,
    columnname1 VARCHAR(20) UNIQUE -- 唯一
)

外键约束

  • 外键必须是另一个表的主键(另一个表也可以是本表)
  • 外键可以重复
  • 外键可以为空
  • 一张表可以有多个外键
CREATE TABLE tablename (
    columnname INT,
    columnname1 VARCHAR(20),
    CONSTRAINT fkname FOREIGN KEY (columnname1)
        REFERENCES tablename1 (colunmname) -- 指定外键,外键名和列名可以不同
)

ALTER TABLE tablename ADD
    CONSTRAINT fkname FOREIGN KEY (columnname1)
        REFERENCES tablename1 (colunmname)

一对一关系

从表的主键既是外键

一对多关系

普通外键表示一对多关系

多对多关系

中间表两个外键映射多对多关系

事务

-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;
-- 查看隔离级别
select @@global.tx_isolation, @@tx_isolation;
select @@autocommit;
-- 查看支持的引擎
show engines;
-- 查看当前引擎
show variables like '%storage_engine%';
-- 查看引擎状态,查询上一次死锁
show engine innodb status;
-- 查看数据信息
status;
-- 查看最近执行语句
select * from information_schema.innodb_trx;
-- 设置隔离级别isolationlevel 4选1
set transaction isolationlevel;
-- 查询正在进行的语句
show processlist;
kill <id>;

视图

视图是由查询结果形成的一张虚拟表,示表通过某种运算得到的一个投影

作用

  • 可以简化查询
  • 可以进行权限控制
    • 表权限关闭,视图中放表的部分数据

创建、修改和删除视图

create view view_name as select.....  
alter view view_name as select.....  
drop view view_name
  • 视图和表同一级别,隶属于数据库
  • 视图可以设定自己的字段名,通常不设置

查询视图

同查询表,可以使用where

查看所有视图

show tables;

查看视图结构

desc view_name

插入视图

  • 视图必须包含表中没有默认值的所有列,才可以进行插入
  • 一般来说,属兔只是用来查询的不应该执行增删改操作。

存储过程

把一段代码封装起来,当要调用这段代码时,可以通过调用储存过程实现。

  • 经过一次编译后再次调用不需要再次编译
  • 权限控制
  • 可复用,配合数据库事务一起使用
show procedure status; --查询现有存储过程

创建、删除存储过程

create procedure p_name(num int)
begin
...
end

drop procedure p_name;

调用存储过程

call p_name(1);

语句结束符

delimiter $
select * from users$

变量

会话变量

在编程环境和非编程环境都可以使用

set @var_name = 'value';
select @var_name;

普通变量

在编程环境使用(存储过程、函数、触发器)

declare var_name type_name default default_value;
set varname = 'value';

变量赋值

set @var_name = 表达式;
set varname = 表达式;
select @var_name := 表达式; -- 赋值并查询结果
select 表达式 into @var_name;

系统变量

以@@开头的都是系统变量

SELECT @@version;

运算符

标识符

begin_label.png

条件

if判断

CREATE PROCEDURE p_name(num INT)
  BEGIN
    IF num = 1
    THEN
      SELECT...
    ELSEIF num = 2
      THEN
        SELECT...
    ELSE
      SELECT...
    END IF;
  END;

case判断

CREATE PROCEDURE p_name(num INT)
  BEGIN
    CASE num
      WHEN 1
      THEN
        SELECT 'spring' AS 'season';
      WHEN 2
      THEN
        SELECT 'summer' AS 'season';
      WHEN 3
      THEN
        SELECT 'autumn' AS 'season';
    ELSE SELECT '' AS 'season';
    END CASE;
  END;

loop循环

CREATE PROCEDURE p_name(num INT)
  BEGIN
    DECLARE current INT DEFAULT 1;
    DECLARE result INT DEFAULT 0;
    operate: LOOP
      SET result = current + result;
      SET current = current + 1;
      IF current > num
      THEN
        LEAVE operate;
      END IF;
    END LOOP;
    SELECT result;
  END;

while循环

CREATE PROCEDURE p_name(num INT)
  BEGIN
    DECLARE current INT DEFAULT 1;
    DECLARE result INT DEFAULT 0;
    WHILE current <= num DO

      SET result = current + result;
      SET current = current + 1;

    END WHILE;

    SELECT result;
  END;

repeat循环

CREATE PROCEDURE p_name(num INT)
  BEGIN
    DECLARE current INT DEFAULT 1;
    DECLARE result INT DEFAULT 0;
    REPEAT

      SET result = current + result;
      SET current = current + 1;

    UNTIL current > num
    END REPEAT;

    SELECT result;
  END;

参数

  • 输入参数(in,默认)
  • 输出参数(out)
  • 输入输出参数(inout)
CREATE PROCEDURE p_name(IN n INT, OUT result INT)
  BEGIN
    SET result = n * n;
  END;

SET @result = 0;
CALL p_name(100, @result);
SELECT @result;
CREATE PROCEDURE p_name(INOUT n INT)
  BEGIN
    SET n = n * n;
  END;

SET @n = 100;
CALL p_name(@n);
SELECT @n;

函数

创建的函数是隶属于库的,只能在创建函数的库中使用

  • 函数内部可以有各种编程语言的元素(变量,流程控制,函数调用)
  • 函数内部可以有增删改等语句
  • 函数内部不可以有select、show、desc这种返回结果集的语句

创建函数

CREATE FUNCTION mySum(n INT, m INT)
  RETURNS INT
  BEGIN
    RETURN m + n;
  END;

SELECT mySum(1, 2);
DROP FUNCTION mySum;

系统函数

数字

SELECT rand();

SELECT *
FROM users
ORDER BY rand()
LIMIT 2; -- 随机取出2个人

SELECT floor(3.9); -- 3
SELECT ceil(3.1); -- 4
SELECT round(3.5); -- 4 四舍五入

字符串

-- 大小写转换
SELECT ucase('Hello');
SELECT lcase('Hello');
SELECT left('abcdef', 3); -- abc
SELECT right('abcdef', 3); -- def
SELECT substr('abcdef', 2, 3); -- bcd,从2开始截取3个,位置从1开始
SELECT concat('abcdef', 3); -- abcdef3
SELECT concat(USERNAME, '-', NICKNAME) FROM users;
SELECT coalesce(NULL, 123); -- 123 如果第一个值为null,就显示第二个值

SELECT length('abcdef'); -- 6
SELECT length('你好'); -- 6 字节个数
SELECT char_length('你好'); -- 2 字符个数
SELECT replace('abc','b','d'); -- adc
SELECT trim(' abc ');

-- 转义
SELECT * FROM BONUS WHERE BONUS.ENAME LIKE '%$%%' ESCAPE '$';-- 指定字符
SELECT * FROM BONUS WHERE BONUS.ENAME LIKE '%\'%';-- 默认转义为\
SELECT * FROM BONUS WHERE BONUS.ENAME LIKE '%''%';-- 是哦那个'转义'

时间

SELECT unix_timestamp();-- 1495179836
SELECT FROM_UNIXTIME(unix_timestamp(),'%y-%m-%d');-- 17-05-19
SELECT FROM_UNIXTIME(unix_timestamp(),'%Y-%m-%d');-- 2017-05-19
SELECT curdate();-- 2017-05-19
SELECT now();-- 2017-05-19 15:44:46
SELECT
  year(now()),
  month(now()),
  day(now()),
  hour(now()),
  minute(now()),
  second(now());
SELECT datediff(now(),'1997-1-1');-- 7443天

SELECT date_sub(curdate(),INTERVAL 1 DAY);-- 2017-05-18
SELECT date_add(curdate(),INTERVAL 1 DAY);-- 2017-05-20
SELECT date_sub(curdate(),INTERVAL 1 HOUR);-- 2017-05-18 23:00:00

SELECT date_format(curdate(),'%Y-%m-%d');
SELECT str_to_date('2017-07-05 17:08:00','%Y-%m-%d %H:%i:%s');
select id from creative where update_time between str_to_date('2017-09-13 19:00:00','%Y-%m-%d %H:%i:%s') and now();

表达式

SELECT concat(10, if(10 % 2 = 0, '偶数', '奇数'));

触发器

  • 是一个特殊的存储过程,在insert、update、delete的时候自动执行的代码块
  • 触发器必须定义在特定的表上
  • 自动执行,不能直接调用

目前mysql不支持多个具有同一动作、同一时间、同一事件、同一地点的触发器

触发器使用

SHOW TRIGGERS;
DROP TRIGGER t_name;

-- 对于新增而言,新增的行用new来表示
CREATE TRIGGER t_name
AFTER INSERT ON orderdetails
FOR EACH ROW
  BEGIN
    UPDATE items
    SET COUNT = COUNT - NEW.COUNT
    WHERE ID = NEW.ITEM_ID;
  END;
-- 对于删除而言,删除的行用old来表示
CREATE TRIGGER t_name
AFTER DELETE ON orderdetails
FOR EACH ROW
  BEGIN
    UPDATE items
    SET COUNT = COUNT + OLD.COUNT
    WHERE ID = OLD.ITEM_ID;
  END;
-- 更新前NEW和更新后OLD
CREATE TRIGGER t_name
AFTER UPDATE ON orderdetails
FOR EACH ROW
  BEGIN
    UPDATE items
    SET COUNT = COUNT + OLD.COUNT
    WHERE ID = OLD.ITEM_ID;
    UPDATE items
    SET COUNT = COUNT - NEW.COUNT
    WHERE ID = NEW.ITEM_ID;
  END;

before和after区别

  • after是先完成数据的增删改,再触发,触发器中的语句晚于监视的增删改,无法影响前面的增删改动作
  • before是完成触发,再做增删改,触发的语句优于监视的增删改发生,我们有机会判断修改即将发生的操作。
CREATE TRIGGER t_name
BEFORE UPDATE ON orderdetails
FOR EACH ROW
  BEGIN
    IF NEW.COUNT > 5
    THEN
      SET NEW.COUNT = 5;
    END IF;
  END;

编码

  • 查询编码
SHOW VARIABLES LIKE 'char%';
变量名 作用 备注
character_set_client 客户端编码 mysql解析客户端发送数据的编码
character_set_results 结果集编码 mysql返回结果集的编码
  • 修改编码
-- 只在当前窗口有效
set character_set_client=gbk;
set character_set_results=gbk;
# 在配置文件中修改永久有效
# 影响三个变量:client、results、connection
[mysql]

default-character-set=utf8

架构

MySQL最重要、最与众不同的特性是它的存储引擎架构,这种架构的设计将查询处理及其它系统服务器和数据的存储/提取相分离。

MySQL逻辑架构

MySQL服务器逻辑架构图:

A-logical-view-of-the-MySQL-server-architecture

  • 最上层不是MySQL所独有的,大多数基于网络的客户端/服务器的工具或者服务都有类似的架构
    • 如连接处理、授权认证、安全等
  • 第二层架构包含MySQL大多数核心服务功能
    • 包括查询解析、分析、优化、缓存及所有的内置函数
    • 所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等
  • 第三层包含了存储引擎
    • 存储引擎负责MySQL中数据的存储和提取
    • 存储引擎由各自的优势和劣势
    • 服务器通过API与存储引擎进行通信,这些接口屏蔽了不同存储引擎之间的差异

连接管理与安全性

每个客户端连接都在服务器进程中获得自己的线程。连接的查询在该单个线程中执行,而该线程又驻留在一个核心或CPU上。 服务器缓存线程,因此不需要为每个新连接创建和销毁线程。

当客户端(应用程序)连接到MySQL服务器时,服务器需要对它们进行身份验证。身份验证基于用户名、原始主机和密码。 如果使用SSL,还会进行X.509证书认证。 连接成功后会继续验证客户端是否具有执行某个特定查询的条件。

优化与执行

MySQL会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括写查询、决定表的读取顺序,以及选择合适的索引等。

用户可以使用特殊的关键字提示(hint)优化器,影响它的决策过程。 也可以请求优化器解释(explain)优化过程的每个因素,使用户可以知道服务器使如何进行优化决策的, 并提供一个参考基准,便于用户重构查询和schema、修改相关配置以提高效率。

优化器并不真正关心特定表使用什么存储引擎,但是存储引擎确实会影响服务器优化查询的方式。 优化器询问存储引擎它的一些功能和某些操作的成本,以及表数据的统计信息。 例如,一些存储引擎支持可能有助于某些查询的索引类型。

对于SELECT语句,在解析查询之前,服务器会先检查查询缓存(Query Cache),如果能找到,就直接返回,不会执行解析、优化和执行的整个过程。

并发控制

只要由多个查询需要在同一时刻修改数据,都会产生并发控制问题。

两个层面的并发控制:

  • 服务层
  • 存储引擎层

读写锁

实现一个两种类型的锁组成的锁系统来解决问题:共享锁和排它锁,也叫读锁和写锁。

读锁使共享的,或者说使互相不阻塞的。写锁使排它的,写锁会阻塞其它的写和读。

锁粒度

一般是在表上施加行级锁,MySQL提供了多种选择。

每种MySQL存储引擎盖都可以实现自己的锁策略和锁粒度。

将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时却会失去对另外一些应用场景的良好支持。

表锁

表锁是MySQL中最基本的锁策略,并且是开销最小的策略。

它会锁定整张表,一个用户在对表进行写操作前,需要获得写锁,这会则色其它用户对该表的所有读写操作。 只有没有写锁是,其它读取的用户才能获得读锁,读锁之间是不相互阻塞的。

写锁也比读锁由更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面,反之不行。

服务器为诸如ALTER TABLE之类的语句使用表锁,而忽略存储引擎的锁机制。

行级锁

行锁可以最大程度的支持并发处理,同时也带来了最大的锁开销。

在InnoDB和XtraDB以及一些其它存储引擎中实现了行级锁。

行级锁只在存储引擎实现,服务层完全不了解存储引擎的锁实现。所有存储引擎都以自己的方式显示实现了锁机制。

事务

ACID表示原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

  • 原子性(Atomicity)
    • 事务被视为一个不可分割的最小工作单元,所有操作要么全部成功,要么全部失败
  • 一致性(Consistency)
    • 数据库总是从一个一致性的状态转换到另一个一致性的状态
  • 隔离性(Isolation)
    • 通常来说,一个事务所作的修改在最终提交之前,对其它事务时不可见的。
  • 持久性(Durability)
    • 一旦事务提交,其做所的修改就会永久保存到数据库中。因此即使系统崩溃也不会丢失。
    • 实际上持久性也分不同级别

即使不支持事务,也可以通过LOCK TABLES语句为应用提供一定程度的保护。

隔离级别

较低级别的隔离通常可以执行更高的并发,系统的开销也更低。

  • READ UNCOMMITTED(读未提交)
    • 事务中的修改在没有提交时,对其它事务也是可见的。
    • 事务可以读取未提交的数据,这也称为脏读
  • READ COMMITTED(读已提交)
    • 大多数数据库系统的隔离级别多是这个级别,但是MySQL不是。
    • 一个事务从开始直到提交前,所作的任何修改对其它事务都是不可见的
    • 这个级别也称为不可重复读
  • REPEATABLE READ(可重复读)
    • MySQL的默认隔离级别
    • 解决了脏读的问题,保证了事务中多次读取同样的记录的结果时一致的
    • 但是不能解决幻读问题
    • InnoDB和XtraDB存储引擎通过多版本并发控制(MVVCC)解决了幻读问题
  • SERIALIZABLE(可串行化)
    • 通过强制事务串行,解决幻读问题
    • 还会再每一行的数据上都加锁,会导致大量的超时和锁争用问题。

隔离级别和问题:

隔离级别 脏读 不可重复度 幻读 加锁读
READ UNCOMMITTED Yes Yes Yes No
READ COMMITTED No Yes Yes No
REPEATABLE READ No No Yes No
SERIALIZABLE No No No Yes
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

死锁

数据库系统实现了各种死锁和死锁超时机制。

InnoDB目前的处理方法是,将持有少量行级排它锁的事务进行回滚。

死锁产生由双重原因:

  • 真正的是数据冲突
  • 存储引擎的实现方式导致

事务日志

事务日志可以提高事务的效率。 使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把修改行为记录到持久在磁盘上的事务日志中, 而不是每次都将修改的数据本身持久到磁盘。

事务日志持久后,内存中被修改的数据在后台可以慢慢刷回到磁盘。通常称之为预写式日志,修改数据需要写两次磁盘。

如果修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此使系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。

事务日志采用追加方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要移动磁头,所以采用事务日志的方式相对来说块的多。

MySQL中的事务

  • MySQL提供了两种事务性的存储引擎:InnoDB和NDBCluster。
  • 第三方存储引擎:XtraDB和PBXT比较知名
自动提交

默认采用自动提交(AUTOCOMMIT)模式。

非事务型引擎默认提交,一些语句如ALTER TABLE还有LOCK TABLES有同样的结果。

SHOW VARIABLES LIKE 'AUTOCOMMIT';
在事务中混合使用存储引擎

混合使用非事务性存储引擎在回滚时会无法回滚非实物型表。

隐式和显示锁定

InnoDB采用两段锁定协议。事务执行过程中随时都可以执行锁定,锁只有在执行COMMIT或者ROLLBACK的时候才会释放,并且所有锁同时释放。 这些锁所锁定都是隐式锁定,InnoDB会根据隔离级别在需要的时候自动加锁。

InnoDB也支持通过特定的语句进行显示锁定,这些语句不属于SQL规范。

SELECT ... LOCK IN SHARE MODE;
SELECT ... FOR UPDATE;

多版本并发控制

基于提升并发性能的考虑,实现的并不是简单的行级锁,而是多版本并发控制(MVCC)。包括Oracle,PostgreSQL都实现了MVCC。

MVCC是行级锁的变种,但它在很多情况下避免了加锁操作,因此开销更低。大都实现了非阻塞的读操作,写操作也只锁定必要的行。

InnoDB的MVCC是通过在每行记录后面保存两个隐藏列来实现的,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。存储的不是时间值,而是系统版本号。

每开始一个事务,系统版本号都会自动递增。事务开始时刻的系统版本号都会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

  • SELECT
    • InnoDb支查找版本早于当前事务版本的数据行,这样保证事务读取的行要么是事务开始之前已经存在的,要么是事务自身插入或修改过的。
    • 行的删除版本要么未定义,要么大于当前事务版本号。确保事务读取到的行,在事务开始之前未被删除。
  • INSERT
    • InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
  • DELETE
    • InnoDB为删除的的每一行保存当前系统版本号作为行版本号。
  • UPDATE
    • InnoDB插入一条记录,保存当前系统版本号作为行版本号;同时保存当前系统版本号到原来的行作为行删除标识。

好处:

使大多数读操作都不需要加锁,读数据操作简单,性能良好,也保证会读取到符合标准的行。

不足:

每行记录需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

存储引擎

.frm文件保存表的定义。

SHOW TABLE STATUS LIKE 'user' \G
  • Name
    • 表名
  • Engine
    • 存储引擎类型,旧版本叫Type
  • Row_format
    • 行的格式
    • 对于MyISAM表,可选值为Dynamic、Fixed或者Compressed
      • Dynamic一般包含可变长度的字段,如VARCHAR或BLOB
      • Fixed的行长度则是固定的,只包含固定长度的列如CHAR、INTEGER
      • Compressed的行只在压缩表中存在
  • Rows
    • 表中的行数
    • InnoDB该值使估值
  • Avg_row_length
    • 平均每行包含的字节数
  • Data_length
    • 表数据的大小,字节为单位
  • Max_data_length
    • 表数据的最大容量
  • Index_length
    • 索引的大小,字节为单位
  • Data_free
    • 对于MyISAM表,表示未使用的空间,包含已删除的空间和未使用的空间
  • Auto_increment
    • 下一个AUTO_INCREMENT的值
  • Create_time
    • 创建时间
  • Update_time
    • 最后修改时间
  • Check_time
    • 使用CHECK TABLE命令或者myisamchk工具的最后时间
  • Collation
    • 表示默认字符集和字符串排序规则
  • Checksum
    • 整个表的实时校验和
  • Create_options
    • 创建表时指定的其它选项
  • Comment
    • 其它信息
    • MyISAM表显示注释
    • InnoDB显示表空间的剩余信息
    • 视图则包含VIEW的文本字样

InnoDB存储引擎

被设计成用来处理大量的短期事务。InnoDB的性能和自动崩溃恢复特性,使得它在非实物型存储的需求中也很流行。

概览
  • 数据存储在表空间中,表空间由InnoDB管理,存储格式是平台独立的
  • 采用MVCC来支持高并发,并实现了四个标准的隔离级别,并通过间隙锁策略防止幻读的出现。
    • 间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入
  • InnoDB表时基于聚簇索引建立的。
    • 聚簇索索引对主键查询由很高的性能。不过它的二级索引必须包含主键列,所以主键应当尽可能小
  • 内部做了很多优化
    • 包括从磁盘读取数据时采用的可预测性读
    • 能够自动在内存中创建hash索引以加速读操作的自适应哈希索引
    • 能够加速插入操作的插入缓冲区等
  • 提供热备份
    • MySql Enterprise Backup
    • XtraBackup

MyISAM存储引擎

提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但不支持事务和行级锁。而且无法自动崩溃恢复。

存储

数据文件.myd和索引文件.myi。根据表定义来决定采用何种行格式。可存储的行记录一般受限于可用的磁盘空间或者操作系统的单个文件最大尺寸。

如果是可变长行,默认配置之恶能处理256TB数据,因为指针长度是6字节。 可以通过MAX_ROWS和AVG_ROW_LENGTH选项的更改来实现更改表指针长度,两者相乘就是表可能达到的最大大小。 修改这两个参数会重建整个表和表的所有索引,可能需要很长时间。

特性
  • 加锁与并发
    • 对整张表加锁而不是行
    • 读取是对需要读到的所有表加共享锁,写入是对表加排它锁
    • 表由读取查询的同时,可以往表中插入新的记录,称为并发插入
  • 修复
    • 可以手工或自动执行检查和修复操作,但与事务恢复和崩溃恢复不同。
    • 执行表修复操作可能导致一些数据丢失,而且修复过程很慢
    • 检查:CHECK TABLE mytable
    • 修复:REPAIR TABLE mytable
    • 如果服务器停止,可以使用myisamchk命令行工具修复
  • 索引特性
    • 可以对BLOB和TEXT的前500个字符创建索引
    • 也支持全文索引
  • 延迟更新索引键
    • 如果指定了DELAY_KEY_WRITE选项,每次修改完成时不会立刻将修改的索引写入磁盘,而是写到内存中的键缓冲区,只有在清理缓冲区或者关闭表的时候才会将对应的索引写入磁盘
    • 这种方式极大地提升写入性能,但是在崩溃时会造成索引损坏,需要进行修复
压缩表

导入后数据不会进行修改的表适合使用压缩表。

myisampack工具对表进行压缩。

压缩表极大的减少空间占用,也减少了I/O,从而提升性能。 压缩表也支持索引,但是时只读的。

解压的性能消耗比I/O的提升小的多,压缩时表中的记录时独立的,所以读取单行的时候不需要去解压缩整个表。

性能

紧密格式存储数据,所以在某些场景下的性能很好。

有一些服务器级别的限制,如索引缓冲区的Mutex锁,基于段的索引缓冲区机制来避免该问题。 但是最典型的性能问题还是表锁。

MySQL内建的其它存储引擎

参考《数据库优化的最佳实践》部分

第三方存储引擎

OLTP类引擎

Percona的XtraDB存储引擎,它包含在Percona Server和MariaDB中,是InnoDB的修改版本。 它的改进是针对性能,可测量性和操作灵活性。它是InnoDB的完全替代,兼容地读写InnoDB的数据文件,并支持InnoDB所有查询。

PBXT是一款社区支持的存储引擎,支持引擎级别的复制、外键约束,并以一种比较复杂的架构对固态存储提供了适当的支持 还对较大值类型如BLOB也做了优化,包含在MariaDB中。

TokuDB引擎使用了一种新的叫做分形树(Fractal Trees)的索引结构,该结构是缓存无关的,因此其大小超过内存性能也不会下降,也就没有内存生命周期和碎片的问题。 它是一个大数据(Big Data)存储引擎,拥有很高的压缩比,可以在很大的数据量上索引。

面向列的存储引擎

默认情况下,MySQL是以面向行的,这意味着每个行的数据都存储在一起,并且服务器以行的单位工作,因为它执行查询。 但是对于非常大的数据量而言,面向列的方法可以更有效;当完全行不需要时,它允许引擎重新生成更少的数据,并且当每个列被单独地存储时,它通常可以更有效地压缩。

领先的面向列的存储引擎是Infobright,它在非常大的数据量(数十TB)下运行良好。它是为分析和数据仓库用例设计的。 它的工作方式是将数据存储在块中,这些块是高度压缩的。它为每个块维护一组元数据,这允许它跳过块,甚至只需查看元数据就可以完成查询。 它没有索引,在如此庞大的规模下,索引是无用的,而块结构是一种准索引。 Infobright需要自定义的服务器版本,因为服务器的部分必须重写以处理面向列的数据。 有些查询无法在面向列的模式下由存储引擎执行,从而导致服务器返回到逐行模式,这是缓慢的。 Infobright有开源社区版本和专有商业版本。

社区存储引擎

谨慎使用。

  • Aria
    • MyISAM的安全替代
  • Groonga
    • 全文检索
  • OQGraph
    • 支持图操作
  • Q4M
    • 队列操作
  • SphinxSE
    • 全文检索
  • Spider
    • 高效透明的实现了分片
  • VPForMySQL
    • 支持垂直分区,可以将表分成不同列的组合,并单独存储

选择合适的存储引擎

参考《数据库优化的最佳实践》部分

除非要使用InnoDB不具备的特性,并且没有其它办法可以替代,否则都应该选择InnoDB引擎。
除非万不得已,否则不要混合使用多种引擎,否则可能带来一系列复杂问题。对一致性备份和服务器参数配置都带来了一些困难。

需要考虑的因素:

  • 事务
    • InnoDB或者XtraDB是目前最稳定的经过验证的事务型引擎
    • 如果不需要事务并且主要是SELECT和INSERT操作,那么MyISAM是不错的选择,一般日志型应用比较适合。
  • 备份
    • 需要热备份则选择InnoDB
  • 崩溃恢复
    • MyISAM崩溃概率大,恢复慢
  • 特有的特性
    • 可以变通解决

InnoDB数据的单台机器上数据量在3-5TB之间或者更大,而不是一个分片的,可以运行良好。 10TB以上,可能需要建立数据仓库,Infobright是最成功的数据仓库方案,也有些应用适合TokuDB。

InnoDB引擎的只读基准测试(数据放入内存):

mysql-readonly-thread-concurrency

转换表的存储引擎

ALTER TABLE
ALTER TABLE mytable ENGINE = InnoDB;

需要执行很长时间。会将按行将数据从原表复制到一张新表中,复制期间会消耗I/O能力,同时原表上会加上读锁。

转换引擎会导致特性丢失,如外键丢失等。

导入和导出

mysqldump工具到处后修改CREATIVE TABLE语句,注意同时修改CREATE TABLE和DROP TABLE语句的表名。

创建(CREATE)与查询(SELECT)

创建新的存储引擎表,然后使用INSERT…SELECT语句导数据:

CREATE TABLE innodb_table LIKE myisam_table;
ALTER TABLE innodb_table ENGINE=InnoDB;
INSERT INTO innodb_table SELECT * FROM myisam_table;
-- 大数据量分批操作
START TRANSACTION;
INSERT INTO innodb_table SELECT * FROM myisam_table WHERE id BETWEEN x AND y;
COMMIT;

执行期间对原表加锁,可确保数据一致。

Percona Tookit提供了一个pt-online-schema-change的工具,基于Facebook的在线schema变更技术。

基准测试

为什么要进行基准测试

  • 验证您对系统的假设,并查看您的假设是否符合实际。
  • 重现系统中的异常行为,以解决这些异常
  • 测量应用程序当前的执行方式。如果你不知道它运行得有多快,你就不能确定你做的任何改变都是有帮助的。您还可以使用历史基准测试结果来诊断您没有预见到的问题。
  • 模拟比生产系统处理的负载更高的负载,以确定您将首先遇到的增长瓶颈。
  • 计划增长。基准测试可以帮助您估计您预计的未来负载需要多少硬件、网络和其它资源。这可以降低升级或主要应用程序更改期间的风险。
  • 测试应用程序容忍环境变化的能力。
    • 例如,可以了解应用程序如何在并发性的零星高峰期间或使用不同的服务器配置执行,或者您可以看到它对不同的数据分布的处理能力。
  • 测试不同的硬件、软件和操作系统配置。
    • RAID 5或RAID 10对您的系统更好吗?
    • 当您从ATA磁盘切换到SAN存储时,随机写入性能如何变化?
    • 2.4Linux内核的规模是否比2.6系列更好?
    • MySQL升级是否有助于性能?
    • 对于数据使用不同的存储引擎如何?
    • 你可以用特殊的基准来回答这些问题。
  • 证明新购买的硬件配置正确。在不对新服务器进行基准测试的情况下,不要将新服务器投入生产是个好主意。如果可能的话,测试总是一个好主意。

基准测试要尽量简单直接,结果之间容易相互比较,成本低且易于执行。只能进行大概的测试,来确定系统大致的余量有多少,这与真是压力测试是不同的。

基准测试的策略

  • 集成式:针对整个系统的整体测试
    • 您正在测试整个应用程序,包括Web服务器、应用程序代码、网络和数据库。这是有用的,因为你不特别关心MySQL的性能;你关心整个应用程序。
    • MySQL并不总是应用程序瓶颈,一个完整的堆栈基准可以揭示这一点。
    • 只有通过测试完整的应用程序,您才能看到每个部件的缓存是如何运行的。
    • 基准测试仅在反映实际应用程序行为的程度上是好的,当您只测试其中一部分时,这是很难做到的。
  • 单组件式:单独测试MySQL
    • 要比较不同的查询或schema的性能
    • 针对应用中某个具体问题的测试
    • 短期的基准测试快速循环验证更改是否有效

整体应用的基准测试很难建立,甚至很难正确设置。
如果可能,可以采用生产环境的数据快照。

测试何种指标

  • 吞吐量
    • 单位时间内事物处理数
    • 标准基准测试如TPC-C
    • 常用的测试单位是每秒事务数(TPS),有些也采用每分钟事务数(TPM)
  • 响应时间或者延迟
    • 任务所需的整体时间
    • 通常使用百分比响应时间来替代最大响应时间衡量
    • 观察长时间测试的趋势
  • 并发性
    • 测试应用在不同并发下的性能
    • 正在工作中的并发操作,或者是同时工作中的线程数或者连接数
    • 并发增长时,需要测量吞吐量是否下降,响应时间是否变长
  • 可扩展性
    • 给系统增加资源,获得更高的吞吐量,性能在可接受范围内

基准测试的方法

常见错误:

  • 使用真是数据的自己而不是全集
  • 使用错误的数据分布
  • 使用不真实的分布参数
  • 在多用户场景中,只做单用户测试
  • 在单服务器上测试分布式应用
  • 与真实用户行为不匹配
  • 反复执行同一个查询
  • 没有检查错误
  • 忽略了系统预热的过程
  • 使用默认的服务器配置
  • 测试时间太短

获取系统性能和状态

需要记录的数包括系统状态和性能指标:CPU使用率、磁盘I/O、网络流量统计、SHOW GLOBAL STATUS计数器等

# 收集MySQL测试数据
#!/bin/sh

INTERVAL=5
PREFIX=$INTERVAL-sec-status
RUNFILE=/home/username/running
mysql -uroot -ppassword -e 'SHOW GLOBAL VARIABLES' >> mysql-variables
while test -e $RUNFILE; do
   file=$(date +%F_%I)
   sleep=$(date +%s.%N | awk "{print $INTERVAL - (\$1 % $INTERVAL)}")
   sleep $sleep
   ts="$(date +"TS %s.%N %F %T")"
   loadavg="$(uptime)"
   echo "$ts $loadavg" >> $PREFIX-${file}-status
   mysql -uroot -ppassword -e 'SHOW GLOBAL STATUS' >> $PREFIX-${file}-status &
   echo "$ts $loadavg" >> $PREFIX-${file}-innodbstatus
   mysql -uroot -ppassword -e 'SHOW ENGINE INNODB STATUS\G' >> $PREFIX-${file}-innodbstatus &
   echo "$ts $loadavg" >> $PREFIX-${file}-processlist
   mysql -uroot -ppassword -e 'SHOW FULL PROCESSLIST\G' >> $PREFIX-${file}-processlist &
   echo $ts
done
echo Exiting because $RUNFILE does not exist.

获得准确的测试结果

  • 是否选择了正确的基准测试
  • 是否问问题收集了相关数据
  • 是否采用了错误的测试标准
  • 确认测试是否可重复
  • 每次测试前用快照还原数据
  • 注意很多因素,包括外部的压力、性能分析和监控系统、详细的日志记录、周期性作业等
  • 每次测试中修改的参数应该尽量少
  • 基于默认配置的测试没有什么意义
  • 不要轻易丢弃异常结果,找到产生这种结果的原因

运行基准测试并分析结果

脚本保存成文件名为analyze:

#!/bin/sh
# This script converts SHOW GLOBAL STATUS into a tabulated format, one line
# per sample in the input, with the metrics divided by the time elapsed
# between samples.
# 第一行列名,第二行数据应该忽略,这是启动时候的数据
# 列:Unix时间戳、日期、时间、系统负载、数据库的QPS
awk '
   BEGIN {
      printf "#ts date time load QPS";
      fmt = " %.2f";
   }
   /^TS/ { # The timestamp lines begin with TS.
      ts      = substr($2, 1, index($2, ".") - 1);
      load    = NF - 2;
      diff    = ts - prev_ts;
      prev_ts = ts;
      printf "\n%s %s %s %s", ts, $3, $4, substr($load, 1, length($load)-1);
   }
   /Queries/ {
      printf fmt, ($2-Queries)/diff;
      Queries=$2
   }
   ' "$@"
./analyze 5-sec-status-2011-03-20

绘图的重要性

利用gnuplot绘制:

# 将文件第五例绘制成折线图
gnuplot> plot "QPS-per-5-seconds" using 5 w lines title "QPS"

基准测试工具

集成测试工具

  • ab
    • Apache的基准测试工具,简单
  • http_load
    • 比ab更加灵活,可定制按时间比率进行测试
  • JMeter
    • 比前两者复杂,有绘图窗口,可以对测试进行记录,然后离线重演。

单组件式测试工具

  • mysqlslap
    • 可以模拟服务器的负载,并输出计时信息
    • 测试时可以执行并发连接数,并制定SQL语句(命令行或文件中),如果没有指定语句自动生成查询schema的SELECT语句。
  • MySQL Benchmark Suite (sql-bench)
    • 主要用于测试服务器执行查询的速度,结果显示那种类型的操作在服务器上执行的更快
    • 包含了大量的预定义测试
  • Super Smack
    • 复杂强大的工具,可模拟多用户访问,可以加载测试数据到数据库,支持随机数据填充数据表。
  • Database Test Suite
    • 类似某些工业标准测试的测试工作集
  • Percona’s TPCC-MySQL Tool
    • TPC-C的基准测试工具集
  • sysbench
    • 多线程系统压测工具,支持Lua脚本,支持操作系统和硬件、MySQL测试

参考监控的《MySQL基准测试套件》部分

基准测试案例

http_load

准备urls.txt:

http://www.mysqlperformanceblog.com/
http://www.mysqlperformanceblog.com/page/2/
http://www.mysqlperformanceblog.com/mysql-patches/
http://www.mysqlperformanceblog.com/mysql-performance-presentations/
http://www.mysqlperformanceblog.com/2006/09/06/slow-query-log-analyzes-tools/
# 5个并发
http_load -parallel 5 -seconds 10 urls.txt
# 每秒5次
http_load -rate 5 -seconds 10 urls.txt
$ http_load -parallel 5 -seconds 10 urls.txt
94 fetches, 5 max parallel, 4.75565e+06 bytes, in 10.0005 seconds
50592 mean bytes/connection
9.39953 fetches/sec, 475541 bytes/sec
msecs/connect: 65.1983 mean, 169.991 max, 38.189 min
msecs/first-response: 245.014 mean, 993.059 max, 99.646 min
HTTP response codes:
  code 200 - 94

MySQL基准测试套件

cd /usr/share/mysql/sql-bench/
./run-all-tests --server=mysql --user=root --log --fast
sql-bench$ tail −5 output/select-mysql_fast-Linux_2.4.18_686_smp_i686
Time for count_distinct_group_on_key (1000:6000):
  34  wallclock secs ( 0.20 usr  0.08 sys +  0.00 cusr  0.00 csys =  0.28 CPU)
Time for count_distinct_group_on_key_parts (1000:100000):
  34  wallclock secs ( 0.57 usr  0.27 sys +  0.00 cusr  0.00 csys =  0.84 CPU)
Time for count_distinct_group (1000:100000):
  34  wallclock secs ( 0.59 usr  0.20 sys +  0.00 cusr  0.00 csys =  0.79 CPU)
Time for count_distinct_big (100:1000000):
  8   wallclock secs ( 4.22 usr  2.20 sys +  0.00 cusr  0.00 csys =  6.42 CPU)
Total time:
  868 wallclock secs (33.24 usr  9.55 sys +  0.00 cusr  0.00 csys = 42.79 CPU)

也可以执行单个测试:

./test-insert

sysbench

sysbench的CPU基准测试
# CPU配置
cat /proc/cpuinfo
sysbench --test=cpu --cpu-max-prime=20000 run
sysbench的I/O基准测试

对于比较不同的硬盘驱动器、不同的RAID卡、不同的RAID模式。

# 创建数据集(测试文件)
sysbench --test=fileio --file-total-size=150G prepare

run:

  • seqwr
    • 顺序写入
  • seqrewr
    • 顺序重写
  • seqrd
    • 顺序读取
  • rndrd
    • 随机读取
  • rndwr
    • 随机写入
  • rndrw
    • 混合随机读/写
# 混合随机读/写测试
sysbench --test=fileio --file-total-size=150G --file-test-mode=rndrw --init-rng=on --max-time=300 --max-requests=0 run
# 清除测试文件
sysbench --test=fileio --file-total-size=150G cleanup
sysbench的OLTP基准测试
# 生成测试表
sysbench --test=oltp --oltp-table-size=1000000 --mysql-db=test --mysql-user=root prepare
# 8个线程只读测试
sysbench --test=oltp --oltp-table-size=1000000 --mysql-db=test --mysql-user=root --max-time=60 --oltp-read-only=on --max-requests=0 --num-threads=8 run

结果包含:

  • 总的事务数
  • 每秒事务数
  • 时间统计信息
  • 线程公平性统计信息,用于表示模拟负载的公平性
sysbench的其它特性

其它测试:

  • 内存
    • 连续读写能力
  • 线程
    • 线程调度器的性能,对于高负载情况下测试线程调度器的行为非常有用
  • 互斥锁
    • 互斥锁的性能,所有县城同一时刻并发运行,并短暂请求互斥锁
  • 顺序写
    • 可以用来测试RAID控制器的告高速缓存的性能

数据库测试套件中的dbt2 TPC-C测试

  1. 准备测试数据
  2. 加载数据到MySQL数据库
  3. 运行测试
# 准备10个仓库的数据,每个仓库大约700MB
src/datagen -w 10 -d /mnt/data/dbt2-w10
# 加载数据到数据库
scripts/mysql/mysql_load_db.sh -d databasename -f /mnt/data/dbt2-w10 -s /var/lib/mysql/mysql.sock
# 运行测试
run_mysql.sh -c 10 -w 10 -t 300 -n databasename -u root -o /var/lib/mysql/mysql.sock-e
  • -c
    • 到数据库的连接数
  • -e
    • 应用零延迟模式,查询之间没有延迟
  • -t
    • 基准测试的持续时间,等待预热

Percona的TPCC-MySQL测试工具

# 创建数据集 server port dbname user pass warehouse(仓库数)
./tpcc_load localhost databasename username p4ssword 5
# 5个线程操作5个数据仓库30秒预热30秒测试 warehouse connection rampup measure
./tpcc_start localhost tpcc5 username p4ssword 5 5 30 30

服务器性能剖析

专注于测量服务器的时间花费在哪里,使用的技术则是性能剖析(profiling)。

性能优化简介

将性能定义为完成某件任务所需要的时间度量,性能即响应时间。
假设性能优化就是在一定的工作负载下尽可能的降低响应时间。

CPU利用率只是一种现象,而不是很好的可度量指标。吞吐量提升可以看作性能优化的副产品。

无法测量就无法有效的优化:第一步应该测量时间花在什么地方。

需要合适的测量范围,有两种不适合的测量:

  • 在错误的时间启动和停止测量
  • 测量的是聚合后的信息,而不是目标活动本身

完成任务所需的时间可以分成两部分:

  • 执行时间
    • 测量定位不同的子任务花费的时间
      • 优化掉一些子任务
      • 降低子任务的执行频率
      • 提升子任务的效率
  • 等待时间
    • 可能由于其它系统或者争用磁盘或者CPU而相互影响

通过性能剖析进行优化

两个步骤:

  • 测量任务所花的时间
  • 然后对结果进行统计和排序,将重要的任务排在前面

性能剖析工具的工作方式基本相同。在任务开始的时启动计时器,在任务结束时停止计时器,然后相减获得时间。

两种类型的性能剖析:

  • 基于时间的
    • 什么任务执行时间最长
  • 基于等待的
    • 任务在什么地方被阻塞的时间最长

性能剖析中缺失的重要信息:

  • 值得优化的查询
    • 不会自动给出哪些查询值得去优化
  • 异常情况
    • 某些任务没有出现在性能剖析输出的前面也需要优化
  • 未知的未知
    • 好的性能剖析工具会显示“丢失的时间”,这些时间可能会导致错过了重要的事情
  • 被掩藏的细节
    • 无法显示所有响应时间的分布,不能只相信平均值

性能瓶颈的影响因素:

  • 外部资源,比如外部系统调用
  • 应用需要大量的处理,比如解析XML
  • 在循环中执行昂贵的操作,比如滥用正则表达式
  • 使用了低效的算法,如暴力搜搜算法

剖析MySQL查询

剖析服务器负载

捕获MySQL的查询到日志文件中

通过long_query_time设置为0捕获所有查询,然后使用Percona Toolkit中的pt-query-digest分析慢查询日志。

慢查询日志是开销最低、精度最高的测量查询时间的工具。

没有权限的替代方法:

  • 使用Percona Toolkit中的pt-query-digest工具,--processlit选项不断查看SHOW FULL PROCESSLIST的输出
  • 使用tcpdump住区TCP网络包,使用pt-query-digest的--type=tcpdump来接续,精度高。
分析查询日志

pt-query-digest可以从慢查询日志中生成一份报告,可以将报告保存在数据库中,以及追踪工作负载随时间的变化。

# V/M为方差均值比,离差指数。
# --explain选项后增加执行计划列
# 可以哦通过--limit和--outlier选项指定工具显示更多详细信息

# Profile
# Rank Query ID           Response time    Calls R/Call V/M   Item
# ==== ================== ================ ===== ====== ===== =======
#    1 0xBFCF8E3F293F6466 11256.3618 68.1% 78069 0.1442  0.21 SELECT InvitesNew?
#    2 0x620B8CAB2B1C76EC  2029.4730 12.3% 14415 0.1408  0.21 SELECT StatusUpdate?
#    3 0xB90978440CC11CC7  1345.3445  8.1%  3520 0.3822  0.00 SHOW STATUS
#    4 0xCB73D6B5B031B4CF  1341.6432  8.1%  3509 0.3823  0.00 SHOW STATUS
# MISC 0xMISC               560.7556  3.4% 23930 0.0234   0.0 <17 ITEMS>

剖析单条查询

SHOW PROFILE
-- MySQL 5.1新增,默认关闭
SET profiling = 1;
-- 查询临时表记录的查询响应时间
SHOW PROFILES;
-- 详情
SHOW PROFILE FOR QUERY 1;
-- 等同于直接查询INFORMATION_SCHEMA表
SET @query_id = 1;
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;
SHOW STATUS

显示的结果大部分都只是一个计数器。并不是性能剖析工具。

SHOW STATUS;
SHOW GLOBAL STATUS;
FLUSH STATUS;
-- SELECT * FROM ...
SHOW STATUS WHERE Variable_name LIKE 'Handler%' OR Variable_name LIKE 'Created%';
检查慢查询日志
使用PERFORMANCE_SCHEMA
-- 查询系统中等待的主要原因
SELECT event_name, count_star, sum_timer_wait
	FROM events_waits_summary_global_by_event_name
	ORDER BY sum_timer_wait DESC LIMIT 5;

诊断间歇性问题

尽量不要通过试错的方式来解决问题。风险很大,而且低效。

间歇性性能问题的案例:

  • 应用通过curl从一个运行的很慢的外部服务器来获得数据
  • 缓存中重要数据国企,导致大量请求落到MySQL以重新生成缓存
  • DNS查询偶尔会有超时现象
  • 由于互斥锁争用,或者内部删除查询缓存的算法效率太低的缘故,MySQL的查询缓存有时候会导致服务器短暂停顿
  • 并发超过某个阈值时,InnoDB的扩展性限制导致查询计划的优化需要很长时间

单条查询问题还是服务器问题

首先要确定这是单条查询的问题还是服务器的问题。

使用SHOW GLOBAL STATUS
# 以较高频率查询某些计数器,将输出结果绘制成图
mysqladmin ext -i1 | awk '/Queries/{q=$4-qp;qp=$4}/Threads_connected/{tc=$4}/Threads_running/{printf "%5d %5d %5d\n", q, tc, $4}'
使用SHOW PROCESSLIST

观察是否有大龄线程处于不正常的状态或者有其它不正常的特征。

mysql -uroot -p4ssword -e 'SHOW PROCESSLIST\G' | grep State: | sort | uniq -c | sort -rn;
使用查询日志

开启慢查询日志并再全局级别设置log_query_time为0,并确认所有连接都采用了新的设置。

# 根据日志统计每秒查询的数量,查看吞吐量变化
awk '/^# Time:/{print $3, $4, c;c=0}/^# User/{c++}' slow-query.log

捕获诊断数据

  • 一个可靠且实时的“触发器”,也就是能区分什么时候出现问题的方法
  • 一个收集诊断数据的工具
诊断触发器

Percona的pt-stalk就是为此情况设计的。可以配置需要监控点变量、阈值、检查的频率等。

需要收集什么样的数据

收集所有可能收集的数据,但只在需要的时间段内收集。 包括系统的状态、CPU利用率、磁盘使用率和空间、ps的输出采样、内存利用率以及可以从MySQL获得的信息,如SHOW STATUS、SHOW PROCESSLIST和SHOW ENGINE INNODB STATUS等。

Percona的pt-collect一般通过pt-stalk调用。因为需要大量重要信息,所以需要root权限。
服务器内部诊断工具oprofile,也可以使用strace,接续查询使用tcpdump抓取查询信息。 使用gdb获取堆转储。

解释结果数据
  • 检查问题是否真的发生了
  • 是否有非常明显的跳跃性变化

查看异常查询或者事务的行为,以及异常的服务器内部行为。

  • 性能低下的SQL查询
  • 使用不当的索引
  • 设计糟糕的数据库逻辑架构等

Percona的pt-sift快速检查收集到的样本数据工具。

一个检查案例

# 查询状态
grep State: processlist.txt | sort | uniq -c | sort -rn
# 查看主要线程状态
# 脏页刷新(Innodb_buffer_pool_pages_dirty,Innodb_buffer_pool_pages_flushed)变化
# 日志序号差距
# 最后检查点差距
# 线程执行数
# 线程等待队列数
SHOW ENGINE INNODB STATUS;
iostat
vmstat
oprofile报表
# 查看文件句柄
lsof
# 观察磁盘可用空间
df -h

其它剖析工具

使用strace

# 会拖慢MySQL,与oprofile相比会显示I/O等待时间,而oprofile只显示CPU时间周期
# pt-ioprofile可以基于这个工具产生结果
strace -cfp $(pidof mysqld)

Schema与数据类型优化

选择优化的数据类型

  • 更小的通常更好
    • 一般情况下,应该尽量使用可以正确存储数据的最小数据类型
    • 占用更少的磁盘、内存和CPU缓存。并且处理时需要的CPU周期也更少,所以更快
    • 在schema中多个地方增加数据类型的范围是一个耗时的操作
  • 简单就好
    • 简单类型的操作通常需要更少的CPU周期
  • 尽量避免NULL
    • 通常情况下最好指定NOT NULL,除非真的需要存储NULL
    • 查询中包含可为NULL的列,更难优化,因为可为NULL的列使索引、索引统计、和值比较都更复杂
    • 可为NULL的列使用更多的存储空间
    • 可为NULL的列改为不可为NULL的列提升不大,但是索引列应该避免设计成为可为NULL的列
    • InnoDB使用单独的位(bit)存储NULL值,以对于稀疏数据有很好的空间利用率。但这一点不适用于MyISAM。

第一步要选择确定合适的大类型:数字、字符串、时间等
下一步是选择具体类型,注意存储的长度和范围、允许的精度,或者需要的物理存储空间,以及一些特殊的行为属性

整数类型

有两种类型的数字:整数(whole number)和实数(real number)

  • 整数类型:TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT
    • 分别是8、16、24、32、64位存储空间,存储范围从-2^(n-1)~2^(n-1)-1
    • 有可选UNSIGN属性,表示不允许负值,大致可以使正数上限提升一倍
    • 整型计算使用64的BIGINT,一些聚合函数除外,它们使用DOUBLE或DECIMAL进行计算
    • 为整型指定宽度不会限制值的合法范围,只是规定了MySQL的一些交互工具用来显示字符串的个数,对存储和计算来说是相同的

实数类型

实数是带有小数部分的数字。然而它们不只是存储小数部分;也可以用来存储比BIGINT还大的整数。

MySQL既支持精确类型,有支持不精确类型。

  • FLOAT和都DOUBLE类型支持使用标准的浮点运算进行近似计算
    • FLOAT占用4个字节,DOUBLE占用8个字节
    • 为整型指定宽度不会限制值的合法范围,只是规定了MySQL的一些交互工具用来显示字符串的个数,对存储和计算来说是相同的
    • MySQL内部使用DOUBLE作为内部浮点计算的类型
    • 比DECIMAL使用更少的存储空间
  • DECIMAL类型用来存储精确的小数
    • 5.0以后的版本中最多允许存储65个数字
    • 可以指定小数点前后允许的最大位数,会影响空间消耗
    • 由于CPU不支持DECIMAL的直接计算,MySQL自身实现了DECIMAL的高精度计算
    • 因为需要额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用DECIMAL
    • 可以使用乘以相应倍数的方式将数据存储在BIGINT中,避免浮点存储计算不精确和DECIMAL精确计算代价高的问题

字符串类型

VARCHAR和CHAR类型

在磁盘和内存中存储的形式与存储引擎相关,以下讨论InnoDB和/或者MyISAM引擎。

  • VARCHAR
    • 存储可变长字符串,是最常见的字符串数据类型
    • 比定长类型更节省空间,因为它仅使用必要的空间,如果设置为ROW_FORMAT=FIXED的话,都使用定长存储,造成浪费
    • 需要1个或2个额外字节记录字符串的长度:如果列的最大长度小于或等于255字节,则只使用1个字节表示,否则使用2个字节
    • 节省了空间,对性能也有帮助,但是在UPDATE时可能使行变长,导致额外的工作
      • MyISAM会将行拆成不同的片段存储
      • InnoDB需要分裂页来使行可以放进页内
    • 适用情形:
      • 字符串列的最大长度比平均长度大的多
      • 更新不频繁,所以碎片不是问题
      • 使用了想UTF-8这样的字符集,每个字符都使用不同的字节数存储
    • 5.0或更高版本,存储和检索时会保留结尾的空格,低版本会去除空格
    • InnoDB更灵活,可以将过长的VARCHAR存储为BLOB
  • CHAR
    • 定长的,总是根据定义的字符串长度分配足够的空间
    • CHAR值末尾空格会被去除(高版本保留),会根据需要采用空格进行填充以方便比较
    • 适合存储很短的字符串或者所有值都接近同一个长度
    • 因为定长所以经常变更也不容易产生碎片
    • 对于非常短的列,CHAR也比VARCHAR更有效率,因为VARCHAR需要额外的记录长度的额外字节

注意:定义是字符数而不是字节数,多字节字符集会需要更多的存储空间
注意:填充和去除空格的行为在不同的存储引擎都是一样的,因为是在MySQL服务器层进行处理的
注意:VARCHAR的长度不影响存储空间开销,但是会影响内存开销,MySQL通常会分配固定大小的内存块来保存内部值,尤其是用内存临时表进行排序或操作时,利用磁盘临时表排序也同样糟糕,所以最好只分配真正需要的空间

与CHAR和VARCHAR类似的类型还有BINARY和VARBINARY,它们存储二进制字符串,与常规字符串相似, 但是存储的是字节码;填充使用\0而不是空格,检索时也不会去掉填充值。 比较时一次按一个字节,比字符串简单的多,也就更快。

BLOB和TEXT类型

都是为存储很大的数据而设计的字符串数据类型,分别采用二进制和字符串方式存储。

  • 字符串类型:TINYTEXT、SMALLTEXT、TEXT、MEDIUMTEXT、LONGTEXT
    • TEXT是SMALLTEXT的同义词
  • 二进制类型:TINYBLOB、SMALLBLOB、BLOB、MEDIUMBLOB、LONGBLOB
    • BLOB是SMALLBLOB的同义词

MySQL把每个BLOB和TEXT值当作一个独立的对象处理,当值太大时,InnoDB会使用专门的“外部”存储区域来进行存储, 此时每个值在行内需要1-4个字节存储一个指针,然后在外部存储区域存储实际的值。

BLOB类型存储的是二进制数据,没有排序规则和字符集,而TEXT类型有字符集和排序规则。 对BLOB和TEXT类型的排序只对每个列的最前max_sort_length字节而不是整个字符串进行排序。 可以减小max_sort_length或者ORDER BY SUSTRING(column,length)来排序一小部分字符串。

使用枚举(ENUM)代替字符串类型

枚举列可以吧一些不重复的字符串存储成一个预定义的集合; 存储时会根据列表值的数量压缩到一个或两个字节中; 内部会将每个值在列表中的位置保存为整数,并且在.frm文件中保存“数字-字符串”映射关系的“查找表”。

CREATE TABLE enum_test(
   e ENUM('fish', 'apple', 'dog') NOT NULL
);
INSERT INTO enum_test(e) VALUES('fish'), ('dog'), ('apple');
SELECT e + 0 FROM enum_test;
+-------+
| e + 0 |
+-------+
|     1 |
|     3 |
|     2 |
+-------+

不要使用数字枚举,双重性会导致混乱;另外,枚举的排序是按照内部存储的整数而不是字符串进行排序的

SELECT e FROM enum_test ORDER BY e;
+-------+
| e     |
+-------+
| fish  |
| apple |
| dog   |
+-------+

可以按照预定的顺序建立枚举或者使用Field函数,但是这会导致无法使用索引消除排序

SELECT e FROM enum_test ORDER BY FIELD(e, 'apple', 'dog', 'fish');

将列换成枚举后,关联变的很快,但是,如果使用VARCHAR列关联ENUM列就会慢的多。
把VARCHAR换为ENUM可以节省空间,如果包含在联合主键中也会减少主键大小。

Test Queries per second
VARCHAR joined to VARCHAR 2.6
VARCHAR joined to ENUM 1.7
ENUM joined to VARCHAR 1.8
ENUM joined to ENUM 3.5

日期和时间类型

MySQL能存储的最小时间粒度为秒,但是MySQL也可以使用微秒级的力度进行临时运算。

MySQL提供两种相似的日期类型:DATATIME和TIMESTAMP

  • DATATIME
    • 能保存大范围的值,从1001年到9999年,精度为秒
    • 它把日期封装为YYYYMMDDHHMMSS的整数中,与时区无关,使用8个字节存储
  • TIMESTAMP
    • 保存了从1970年1月1日午夜以来的秒数,它和UNIX时间戳相同
    • 使用4个字节的存储空间,只能表示1970年到2038年
    • 提供了FROM_UNIXTIME()把Unix时间戳转换为日期,UNIX_TIMESTAMP()函数把日期转换为Unix时间戳
    • TIMESTAMP显示的值依赖于时区,MySQL服务器、操作系统、客户端链接都有时区设置
    • TIMESTAMP默认为NOT NULL,默认会在更新和插入时更新第一个TIMESTAMP列的值,除非明确指定了值。
    • 比DATETIME更高效,因为空间效率更高

不建议将时间存储为整数,不方便处理也没有带来性能收益。除非存储微秒级别的时间戳之类的操作可以选用BIGINT或者DOUBLE存储秒之后的小数部分。

位数据类型

从技术上来说都是字符串类型

  • BIT
    • 可以存储一个或多个true/false值,每个位一个
    • 最大长度64个位
    • 行为因存储引擎而异
      • MyISAM会打包存储BIT列,按位存储到需要的字节中
      • 其它存储引擎如Memory和InnoDB,为每个BIT列使用一个足够存储的最小整数类型来存放,所以不能节省存储空间
    • BIT被当作字符串类型,数字上下文的检索时,结果是位字符串转换成数字,所以应该谨慎使用BIT类型,如可以使用CHAR(0)来存储空串或者NULL来模拟true和false
  • SET
    • 如果需要保存很多的true/false值,可以考虑合并这些列到一个SET数据类型,它在MySQL内部是一系列打包的位的集合来表示的
    • 可以和FIND_IN_SET()和FIELD()函数方便的在查询时使用
    • 缺点是改变列的代价特别高,一般来说也无法通过索引查找
  • 在整数列上执行按位操作
    • 是替代SET的一种方式,使用一个整数包装一系列的位
    • 好处是可以不用ALTER TABLE去修改所代表的枚举值,缺点是更难读写
CREATE TABLE bittest(a bit(8));
INSERT INTO bittest VALUES(b'00111001');
SELECT a, a + 0 FROM bittest;
+------+-------+
| a    | a + 0 |
+------+-------+
| 9    |    57 |
+------+-------+
CREATE TABLE acl (
  perms SET('CAN_READ', 'CAN_WRITE', 'CAN_DELETE') NOT NULL
);
INSERT INTO acl(perms) VALUES ('CAN_READ,CAN_DELETE');
SELECT perms FROM acl WHERE FIND_IN_SET('CAN_READ', perms);
+---------------------+
| perms               |
+---------------------+
| CAN_READ,CAN_DELETE |
+---------------------+
-- 变量也可以是程序中的常量
SET @CAN_READ   := 1 << 0,
@CAN_WRITE  := 1 << 1,
@CAN_DELETE := 1 << 2;
CREATE TABLE acl (
  perms TINYINT UNSIGNED NOT NULL DEFAULT 0
);
INSERT INTO acl(perms) VALUES(@CAN_READ + @CAN_DELETE);
SELECT perms FROM acl WHERE perms & @CAN_READ;
+-------+
| perms |
+-------+
|     5 |
+-------+

选择标识符

为标识列选择合适的数据类型非常重要。与相关联的列的数据类型应该保持一致。包括UNSIGNED等选项

选择时不仅需要考虑存储类型,还需要考虑MySQL对这种卡类型怎么执行计算和比较。

在可以满足值的范围需求,同时预留未来增长空间的前提下,应该选择最小的数据类型。

  • 整数类型
    • 通常是最好的选择,因为它们很快并且可以AUTO_INCREMENT
  • ENUM和SET类型
    • 是一个糟糕的选择,除非是存储这个字段的“术语表”,如作为查找表并增加一些有意义的描述文本或者为下拉菜单提供有意义的标签
  • 字符串类型
    • 应该尽量避免,因为它们很消耗空间,并且通常比数字慢。在MyISAM表中由于默认对字符串压缩所以更慢。
    • MD5()、SHA1()、UUID产生的字符串,生成的新值会任意分布在很大空间内,会导致INSERT以及SELECT语句变慢,但也是消除热点的方法
      • 因为插入值会随机的写到索引的不同位置,使INSERT更慢,会导致页分裂、磁盘随机访问以及对于聚簇存储和引擎产生聚簇索引碎片
      • 逻辑上相邻的行会分布在磁盘和内存的不同地方,SELECT会变慢
      • 这会导致缓存会有很多的刷新和不命中,因为会消除热点

如果存储UUID值,应该移除“-”符号,或者用UNHEX()函数转换为16字节的数字,存储在BINARY(16)列中,检索时通过HEX()函数来格式化为16进制形式。

警惕ORM框架自动生成的schema没有使用最优的数据类型来存储,最好总是在真实的数据集上做测试,这样不会太晚才发现性能问题。

特殊数据类型

人们常用VARCHAR(15)来存储IP地址,它们实际上是一个32为无符号整数,不是字符串。MySQL提供了INET_ATON()和INET_NTOA()函数在这两种表示法之间转换

-- 互相转换
select INET_ATON('127.0.0.1') from dual;
select INET_NTOA(2130706433) from dual;

MySQL schema设计中的陷阱

  • 太多的列
    • 存储引擎的API工作时在服务器层和存储引擎之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列,这个操作代价依赖于列的数量
  • 太多的关联
    • MySQL限制了每个关联操作最多只能有61张表,关联多的情况下解析和优化查询的代价会成为MySQL的问题
    • 单个查询最好在12个表以内做关联
  • 全能的枚举
    • 防止过度使用枚举,会导致设计非常混乱
  • 变相的枚举
    • 集合值不同时出现时应该使用枚举替代集合
  • 非此发明的NULL
    • 可以使用0、某个特殊值、或者空串来代替NULL,但是应该避免这使代码变的复杂,因为处理-1比处理NULL更难。MySQL索引中存储NULL值,而Oracle不会。

范式和反范式

在范式化的数据库中,每个事实数据会出现并且只出现一次。 在反范式化的数据库中,信息是冗余的,可能会存储在多个地方。

为了提升速度,经常会建立一些额外的索引,增加冗余列,甚至是创建缓存表和汇总表。 虽然写操作变得慢了,但是更显著地提高了读操作的性能。同事也增加了读操作和写操作了开发难度。

范式的优点和缺点

优点:

  • 范式的更新操作比反范式更新快
  • 当数据较好地范式化是,就只有很少或者没有重复数据,所以只需要修改更少的数据
  • 范式化的表通常非常小,可以更好的放在内存里,所以执行操作会更快
  • 很少有多余的数据意味着检索列表数据时更少需要DISTINCT或者GROUPBY语句

缺点:

  • 通常需要关联。这不但代价昂贵,也可能使一些索引无效

反范式的优点和缺点

优点:

  • 可以很好的避免关联
  • 如果不需要关联表,即使进行全表扫描也避免了随机I/O(基本上是顺序I/O,依赖于存储引擎)
  • 单独的表也能使用更有效的索引策略

混用范式化和反范式化

最常见的反范式化数据的方法是复制或者缓存,在不同的表中存储相同的列,使用触发器更新换出使这种方式更简单。

另一个从父表冗余一些数据到子表的理由是排序的需要。

缓存衍生值也是有用的,如一些统计信息。

缓存表和汇总表

在同一张表中保存衍生的冗余数据。 或者完全独立的创建一张汇总表或缓存表。

汇总表降低了实时统计计算统计值的昂贵操作。 缓存表对优化搜索和检索语句很有效。如可能会需要很多不同索引组合来加速各种类型的查询

使用缓存表和汇总表时需要考虑是实时维护还是定期重建。定期重建不只是节省资源,可以个保持表不会有很多碎片,以及完全顺序组织的索引。 重建时可以使用建表切换的方式,保持数据依然可以操作。

DROP TABLE IF EXISTS my_summary_new, my_summary_old;
CREATE TABLE my_summary_new LIKE my_summary;
-- populate my_summary_new as desired
RENAME TABLE my_summary TO my_summary_old, my_summary_new TO my_summary;

物化视图

实际上是预先计算并存储在磁盘上的表。

使用Flexviews tools实现MySQL的物化视图。

  • 变更数据抓取功能,读取服务器的二进制日志并解析相关行的变更
  • 一系列可以帮助创建和管理视图定义的存储过程
  • 一些可以应用变更到数据库中的物化视图工具

通过提取对源表的更改,增量的重新计算物化视图的内容,不需要通过查询原始数据来更新视图。

计数器表

-- 使用多个行获得高并发性
CREATE TABLE hit_counter (
   slot tinyint unsigned not null primary key,
   cnt int unsigned not null
) ENGINE=InnoDB;
UPDATE hit_counter SET cnt = cnt + 1 WHERE slot = RAND() * 100;
SELECT SUM(cnt) FROM hit_counter;
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;
-- 插入或更新而不用预先生成行
INSERT INTO daily_hit_counter(day, slot, cnt)
   VALUES(CURRENT_DATE, RAND() * 100, 1)
   ON DUPLICATE KEY UPDATE cnt = cnt + 1;
-- 防止表行数过多,周期性合并所有结果到0号槽
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);
DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt = 0;

加快ALTER TABLE的操作速度

MySQL的ALTER TABLE操作的性能对达标来说是个大问题。

MySQL执行大部分修改表结构的操作方式是用新的结构创建一个空表,从旧表中查出所有数据插入新表,然后删除旧表。 这个操作需要很长的时间,如果内存不足而表又很大,而且还有很多索引的情况下尤其如此。

最新版本的InnoDB支持通过排序来重建索引,这使得索引重建更快并且有一个紧凑的布局。还有一些不需要锁表的在线操作支持。

大部分的ALTER TABLE操作会导致服务中断,能使用的技巧有两种:

只修改.frm文件

所有的MODIFY COLUMN操作都将导致表重建。而ALTER COLUMN操作直接修改.frm文件,是非常快的。

-- 会重建表,很慢
ALTER TABLE sakila.film MODIFY COLUMN rental_duration TINYINT(3) NOT NULL DEFAULT 5;
-- 直接操作索引,很快
ALTER TABLE sakila.film ALTER COLUMN rental_duration SET DEFAULT 5;

ALTER COLUMN允许使用ALTER COLUMN、MODIFY COLUMN、CHANGE COLUMN语句修改列,这三种操作都是不同的。

下面这些操作有可能不需要重建表:

  • 移除一个列的AUTO_INCREMENT属性
  • 增加、移除或更改ENUM和SET常量。如果移除的是已经有行数据用到其值的常量,查询将会返回一个空字符串

基本的技术是为想要的表结构创建一个新的.frm文件,然后替换已经存在的.frm文件。

-- 创建信标并修改列
SHOW COLUMNS FROM sakila.film LIKE 'rating';
CREATE TABLE sakila.film_new LIKE sakila.film;
ALTER TABLE sakila.film_new
MODIFY COLUMN rating ENUM('G','PG','PG-13','R','NC-17', 'PG-14')
DEFAULT 'G';
-- 锁定表
FLUSH TABLES WITH READ LOCK;
# 替换.frm文件
/var/lib/mysql/sakila# mv film.frm film_tmp.frm
/var/lib/mysql/sakila# mv film_new.frm film.frm
/var/lib/mysql/sakila# mv film_tmp.frm film_new.frm
-- 解锁表
UNLOCK TABLES;
SHOW COLUMNS FROM sakila.film LIKE 'rating'\G
DROP TABLE sakila.film_new;

快速创建MyISAM索引

有一个常用的技巧是先禁用索引、载入数据,然后重新启用索引。

ALTER TABLE test.load_data DISABLE KEYS;
-- load the data
ALTER TABLE test.load_data ENABLE KEYS;

因为构建索引的工作被延迟到数据完全载入以后,这时已经可以通过排序来构建索引了。这样更快也使所引述的碎片更少、更紧凑。 但是对唯一索引无效,MyISAM会在内存中构造唯一索引,并检查唯一性。 InnoDB也可以利用类似的技巧。

对大表快的多的操作,但是要确定索引的有效性而跳过校验:

  1. 用需要的表结构创建一张新表,不包括索引。
  2. 载入数据到表中以构建.myd文件
  3. 按照需要的结构创建另外一张空表,包含索引。这会创建.frm文件和.myi文件
  4. 获取读锁并刷新表
  5. 重命名第二张表的.frm和.myi文件,让MySQL认为是第一张表的文件
  6. 释放读锁
  7. 使用REPAIR TABLE来重建表的索引,该操作会通过排序来创建所有索引,包括唯一索引

创建高性能索引

当数据量逐渐增大时,不恰当的索引性能会急剧下降。 索引优化应该是对查询性能优化的最有效的手段,能够将查询性能提高几个量级,最优的索引会比一个好的索引要好两个数量级。

在选择索引和编写查询时,有如下三个原则:

  • 单行访问是很慢的。特别是机械硬盘存储中。
    • 如果服务器从存储中读取一个数据块只是为了获取其中一行,那么浪费了很多的工作。
    • 最好读取的块中能包含尽可能多所需要的行。使用索引可以创建位置引用以提升性能。
  • 按顺序访问范围数据是很快的
    • 顺序I/O不需要多次磁盘寻道,所以比随机I/O快得多
    • 如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且GROUP BY查询也无序在做排序和将行按组进行聚合计算了
  • 覆盖索引查询是很快的
    • 如果一个索引包含了查询需要的所有列,那么存储引擎就不再回表查找行。这避免了大量的单行访问。

总的来说,编写查询语句时应该尽可能选择合适的索引尽可能避免单行查找、尽可能的使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。

必须有所取舍的创建合适的索引,或者寻求替代策略(反范式化,或者提前计算汇总表等)。

按响应时间排序查询,找出最差查询或最大压力查询,检查这些查询的schema、SQL和索引结构, 判断是否查询扫描了太多的行,是否做了很多额外的排序或者使用了临时表,是否使用了随机I/O访问数据或者太多回表查询那些不在索引中的列的操作。

可以创建一个更合适的索引或者重写查询利用索引来提升性能。

索引基础

存储引擎首先在索引中找到对应的值,然后根据匹配的索引记录找到对应的数据行。 如果索引中包含多个列,那么列的顺序十分重要,因为MySQL只能高效的使用索引的最左前缀列。 创建包含一个列的索引和创建两个只包含一列的索引是大不同的。

索引的类型

索引是在存储引擎层而不是服务器层实现的。不同存储引擎的索引的工作方式不一样,也不是所有的存储引擎都支持所有类型的索引。都支持的底层实现也可能不同。

B-Tree索引

如果没有特殊指明类型,那么多半说的事B-Tree索引,它使用B-Tree数据结构来存储数据。 实际上很多存储引擎使用的是B+Tree,即每个叶子节点包含一个指向下一个叶子节点的指针,从而方便叶子节点的范围便利。 大多是MySQL引擎都支持这种索引。

底层存储引擎也肯能使用不同的数据结构,如NDB使用T-Tree结构存储这种索引,即使其名字是BTREE;InnoDB则使用B+Tree。

存储引擎以不同的方式使用B-Tree索引,性能也各有不同,各有优劣。 如MyISAM使用前缀压缩技术使得索引更小,但InnoDB按照原数据格式进行存储; MyISAM通过数据的物理位置引用被索引的行,而InnoDB根据主键引用被索引的行。

B-Tree通常意味着所有的值都是按照顺序存储的,并且每一个叶子页到根的距离相同。

建立在B-Tree(从技术上来说是B+Tree)结构上的索引:

建立在B-Tree(从技术上来说是B+Tree)结构上的索引

B-Tree索引能够加快访问数据的速度,因为存储引擎不在需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索。

树的深度和表的大小直接相关。

B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据。

B-Tree(从技术上来说是B+Tree)索引树中的部分条目示例:

CREATE TABLE People (
   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)
);

B-Tree(从技术上来说是B+Tree)索引树中的部分条目示例

索引对多个值进行排序的依据是CREATE TABLE语句中定义索引时列的顺序。

可以使用B-Tree索引的查询类型。B-Tree索引适用于全键值、键值范围或键前缀查找。 其中键前缀查找只适用于根据最左前缀的查找。

  • 全值匹配
    • 和索引中的所有列进行匹配
    • 如姓名为Cuba Allen、出生于1960-01-01的人
  • 匹配最左前缀
    • 只使用索引的第一列
    • 查找所有姓Allen的人
  • 匹配列前缀
    • 只匹配某一列的值的开头部分。这里也只使用了索引的第一列
    • 查找以J开头的姓的人
  • 匹配范围值
    • 使用索引的第一列匹配范围
    • 超找姓在Allen和Barrymore之间的人
  • 精确匹配某一列并范围匹配另外一列
    • 第一列全匹配,第二列范围匹配
    • 姓Allen,名字以K开头的人
  • 只访问索引的查询
    • 查询只需要访问索引,而无需访问数据行

因为节点是有序的,所以还可以用于查询中的ORDER BY操作。 一般来说,如果B-Tree可以按照某种方式查找到值,那么也可以按照这种方式用于排序。所以ORDER BY子句满足前面几种查询类型,则这个索引也满足对应的排序需求。

B-Tree索引的限制:

  • 如果不是按照最左列开始查找,则无法使用索引。
    • 无法使用索引查找名字为Bill的人,也无法查找某个生日,也无法查找姓以某个字母结尾的人
  • 不能跳过索引中的列。如果中间列不指定,则MySQL只能使用索引的第一列。
    • 无法用于查找姓为Smith并在某个日期出生的人
  • 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。
    • WHERE last_name=’Smith’ AND first_name LIKE ‘J%’ AND dob=’1976-12-23’,查询只能用索引前两列,因为LIKE是一个范围条件。
    • 如果范围查询列值的数量有限,可以通过使用多个等于条件代替范围查询。

这些限制都和索引列的顺序有关。在优化性能的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。

哈希索引

哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。 对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,将所有的哈希码存储在索引中,同时哈希表中保存指向每个数据行的指针。

在MySQL中,只有Memory引擎显式支持哈希索引,也是它的默认索引类型,同时Memory引擎同时也支持B-Tree索引。
Memory引擎时支持非唯一哈希索引的。如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中。

因为索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。然而,哈希索引也有它的限制:

  • 哈希索引只包含哈希值和行指针,不能使用索引中的值来避免读取行。而访问内存中的行速度很快
  • 哈希索引数据并不是按照索引值顺序存储的,所以无法进行排序
  • 不支持部分索引匹配查找,因为使用索引列的全部内容来计算哈希值
  • 哈希索引只支持等值比较查询不支持范围查询
  • 访问哈希索引的数据非常快,除非有很多哈西冲突,当出现冲突时,存储引擎必须遍历链表中的所有行指针,逐行比较
  • 哈希冲突很多的话,一些索引维护操作的代价也会很高。如删除等操作需要遍历链表

NDB引擎也支持唯一哈希索引,单应用场景有限。

InnoDB引擎有一个特殊的功能叫做“自适应哈希索引”。 当InnoDB注意到某些索引值被使用的非常频繁时,它会在内存中基于B-Tree索引之上再创建一个哈希索引,这就让B-Tree索引也具有哈希索引的一些优点,这是一个内部行为。

自定义哈希索引

在B-Tree基础上创建一个伪哈希表,这和真正的哈希索引不是一回事, 因为还是使用B-Tree查找,但是它使用哈希值而不是键本身进行索引查找。 你需要做的就是在查询的WHERE子句中执行哈希函数。

实例:

如果索引大量url,则索引非常大:

SELECT id FROM url WHERE url='http://www.mysql.com';

新增一个哈希列,使用哈希列查找,这样性能非常高,因为优化器会使用选择性很高而体积很小的url_crc列索引来完成查找。 即使有哈希冲突,查找也非常快,一一比较返回对应的行。

SELECT id FROM url WHERE url='http://www.mysql.com' AND url_crc=CRC32('http://www.mysql.com');

这样做的缺陷是要维护哈希值,可以手动维护,也可以使用触发器实现。

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)
);
-- 转义终结字符
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 ;

这种方式不要使用SHA1()和MD5()作为哈希函数。这两个函数计算出的值是非常长的字符串,会浪费大量的空间,比较时也会更慢。 简单哈希函数的冲突在一个可以接受的范围,同时又能提供更好的性能。 但是如果数据表非常大,CRC32()就会出现大量哈希冲突,则可以考虑自己实现一个64位哈希函数返回整数。 可以使用MD5()函数返回值的一部分作为自定义哈希函数。

SELECT CONV(RIGHT(MD5('http://www.mysql.com/'), 16), 16, 10) AS HASH64;
-- HASH64:9761173720318281581

处理哈希冲突

出现哈希冲突的概率的增长速率可能比想象的要快得多。 要避免冲突,就必须在WHERE条件中带入哈希值和对应列值。 如果不想查询具体值,则可以不带入列值,返回多个结果。

SELECT id FROM url WHERE url_crc=CRC32('http://www.mysql.com')  AND url='http://www.mysql.com';
SELECT id FROM url WHERE url_crc=CRC32('http://www.mysql.com');
空间数据索引(R-Tree)

MyISAM表支持空间索引,可以用作地理数据存储。

和B-Tree索引不同,这类索引无需前缀查询,空间索引会从所有维度来索引数据。查询时可以使用任意维度来组合查询。

必须使用MySQL的GIS相关函数如MBRCONTAINS()等来维护数据。

MySQL的GIS支持并不完善,GIS做的比较好的事PostgreSQL的PostGIS。

全文索引

全文索引是一种特殊类型的索引,它查找的事文本中的关键词,而不是直接比较索引中的值。

在相同的列上同时创建全文索引和基于值的B-Tree索引不会有冲突,全文索引适用于MATCH AGAINST操作,而不是不同的WHERE条件操作。

其它索引类别

很多第三方存储引擎使用不同类型的数据结构来存储索引。

TokuDB使用分形树索引,这是一类较新开发的数据结构,既有B-Tree的很多优点,也避免了B-Tree的一些缺点。

更多InnoDB的主题:聚簇索引、覆盖索引等。

索引的优点

索引可以快速地定位到表的指定位置。根据创建索引的数据结构不同,索引也有一些其它的附加作用。

B-Tree索引,按照顺序存储数据,所以MySQL可以用来做ORDER BY和GROUP BY操作。 因为数据有序,所以也会将相关的列值都存储在一起。因为存储了实际的列值,所以某些查询只使用索引就能够完成全部查询。

  • 索引大大减少了服务器要扫描的数据数量
  • 索引可以帮助服务器避免排序和临时表
  • 索引可以将随机I/O变为顺序I/O

只有索引帮助存储引擎快速查找到记录带来的好处大于其带来的额外工作时,索引才是有效的。 对于非常小的表,大部分情况下全表扫描更高效;对于中到大型表,索引就非常高效;对于特大型表,建立和使用索引的代价将随之增长, 这种情况下,则需要一种直接区分出查询需要的一组数据,而不是一条记录一条记录的匹配。使用分区或者块级别元数据来代替索引。

高性能的索引策略

独立的列

“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。

-- 无法使用actor_id索引
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
-- 函数参数,无法使用索引
SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;

前缀索引和索引选择性

索引很长的字符列,这会让索引变得更大且慢,一个策略是模拟哈希索引, 或者索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但是这样也会降低索引的选择性。

一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于BLOB、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引。

诀窍在于选择足够长的前缀保证较高的选择性,同时有不能太长以便节省空间。

-- 实验后发现前缀长度7比较合适
SELECT COUNT(*) AS cnt, LEFT(city, 7) AS pref FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
+-----+---------+
| cnt | pref    |
+-----+---------+
|  70 | Santiag |
|  68 | San Fel |
|  65 | London  |
|  61 | Valle d |
|  49 | Hiroshi |
|  48 | Teboksa |
|  48 | Pak Kre |
|  48 | Yaound  |
|  47 | Tel Avi |
|  47 | Shimoga |
+-----+---------+
-- 计算完整列的选择性,7以后提升幅度很小
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 sakila.city_demo;
+--------+--------+--------+--------+--------+
| sel3   | sel4   | sel5   | sel6   | sel7   |
+--------+--------+--------+--------+--------+
| 0.0239 | 0.0293 | 0.0305 | 0.0309 | 0.0310 |
+--------+--------+--------+--------+--------+

如果是4,最常出现的城市出现次数要大得多。

SELECT COUNT(*) AS cnt, LEFT(city, 4) AS pref FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 5;
+-----+------+
| cnt | pref |
+-----+------+
| 205 | San  |
| 200 | Sant |
| 135 | Sout |
| 104 | Chan |
|  91 | Toul |
+-----+------+

前缀索引是一种能是索引更小、更快的有效方法,但也有缺点:无法使用前缀索引锁ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描。

-- 添加前缀索引
ALTER TABLE sakila.city_demo ADD KEY city_prefix_key (city(7));

可以倒置存储字符串然后使用前缀索引来实现后缀索引的功能。

多列索引

在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。 MySQL5.0和更新版本引入了一种叫“索引合并”的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。

5.0以后,查询能够同时使用两个单列索引进行扫描,并将结果进行合并,这种算法有三个变种:

  • OR条件的联合(union)
  • AND条件的相交(intersection)
  • 组合两种情况的联合及相交
-- 早期版本两个索引都不是好的选择
SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id = 1 OR film_id = 1;
-- 改写成下面语句可以使用索引
-- 默认地,UNION 操作符选取不同的值。如果允许重复的值,请使用 UNION ALL。
SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id = 1
UNION ALL
SELECT film_id, actor_id FROM sakila.film_actor WHERE film_id = 1
   AND actor_id <> 1;
-- 两个联合索引扫描
EXPLAIN SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id = 1 OR film_id = 1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: film_actor
         type: index_merge
possible_keys: PRIMARY,idx_fk_film_id
          key: PRIMARY,idx_fk_film_id
      key_len: 2,2
          ref: NULL
         rows: 29
        Extra: Using union(PRIMARY,idx_fk_film_id); Using where

索引合并策略有时候是一种优化策略,但实际上更多说明了表上的索引建的很糟糕:

  • 当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一个包含所有相关列的所列索引,而不是多个独立的单列索引
  • 当服务器需要对多个索引做联合操作时(通常有多个OR条件),通常需要消耗大量CPU和内存资源在算法的缓存、排序和合并操作上。特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候
  • 更重要的是,优化器不会把这些计算到查询成本中,优化器只关心随机页面读取。这会使得查询的成本被低估,导致该执行计划还不如直接走全表查询。这样做不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性。通常来说还不如写成UNION的方式更好

如果在EXPLAIN时查看到有合并索引,应该好好检查一下查询的表的结构,看是不是已经是最优的。 也可以通过optimizer_switch来关闭索引合并功能。也可以使用IGNORE INDEX提示让优化器忽略掉某些索引。

选择合适的索引列顺序

正确的顺序依赖于使用该索引的查询的,并且同时需要考虑如何更好的满足排序和分组的需要(适用于B-Tree索引)。

在一个B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列。 所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY、GROUP BY和DISTINCT等子句的查询需求。

如何选择索引的列顺序有一个经验法则:

将选择性最高的列放到索引最前列。但通常不如避免随机I/O和排序那么重要。

当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。 然而性能不只是依赖于所有索引列的选择性,也可能和值的分布有关。可能需要根据那些运行频率最高的查询来调整索引列的顺序,这样索引的选择性最高。

优化最差查询:

SELECT * FROM payment WHERE staff_id = 2 AND customer_id = 584;
SELECT SUM(staff_id = 2), SUM(customer_id = 584) FROM payment\G
*************************** 1. row ***************************
     SUM(staff_id = 2): 7992
SUM(customer_id = 584): 30

对于这个最差查询使用customer_id在前面的索引选择性更好

查看customer_id对于staff_id的选择性:

SELECT SUM(staff_id = 2) FROM payment WHERE customer_id = 584\G
*************************** 1. row ***************************
SUM(staff_id = 2): 17

注意:这个结果非常依赖于选定的具体值,可能对一些其它查询不公平,服务器整体性能可能更糟或者其它查询运行的不如预期

但是如果没有最差查询,还是需要使用经验法则来优化:

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(*): 16049

经验法则考虑全局基数和选择性,而不是具体查询,所以答案是customer_id的选择性更高。

ALTER TABLE payment ADD KEY(customer_id, staff_id);

经验法则和推论适合多数情况,但是不要假设平均情况下的性能也能代表特殊情况下的性能,特殊情况可能摧毁整个系统。 并且,别忘了WHERE子句的排序、分组和范围条件等其它因素,这些因素可能对查询的性能造成非常大的影响。

聚簇索引

聚簇索引不是一种单独的索引类型,而是一种数据存储方式。InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。

当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中。术语聚簇表示数据行和相邻的键值紧凑地存储在一起。 因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引(覆盖索引可以模拟多个聚簇索引的情况)。

聚簇索引的数据分布:

聚簇索引的数据分布

叶子页包含了行的全部数据,但是节点页只包含了索引列。

InnoDB通过主键聚集数据。如果没有主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,会隐式定义一个主键来作为聚簇索引。 InnoDB只聚集在同一个页面中的记录,包含相邻键值的页面可能会相距甚远。

聚簇主键可能对性能有帮助,但也可能导致严重的性能问题。所以需要仔细考虑聚簇索引,尤其是在变更存储引擎的时候。

聚集的数据有一些重要的优点:

  • 可以把相关数据保存在一起。这样只需要从磁盘读取少数的数据页就能获取相关数据。如果没有使用聚簇索引,每次都可能导致一次I/O。
  • 数据访问更快。索引和数据保存在同一个B-Tree中,因此从聚簇索引中获取数据通常比非聚簇索引中查找更快。
  • 使用覆盖扫描的查询可以直接使用叶节点中的主键值。

设计表和查询时能能够充分利用上面的优点,那就能极大地提升性能。

缺点:

  • 最大限度的提高了I/O密集型应用的性能,但是如果数据全部存放在内存中,就没有什么优势了
  • 插入速度严重依赖于插入顺序。按主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但是如果不是按照主键顺序加载数据,那么在加载完成后最好执行OPTIMIZE TABLE重新组织一下表
  • 更新聚簇索引的代价非常高,因为会强制InnoDB将每个被更新的行移动到新的位置
  • 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临“页分裂”的问题。当主键值要求必须将这一行插入一个已满的页中时,存储引擎将会分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的存储空间
  • 可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候
  • 二级索引(非聚簇索引)可能比想象的要更大。因为二级索引的叶子结点包含了引用行的主键列
  • 二级索引访问需要两次索引查找,而不是一次
    • 二级索引叶子结点保存的是行的主键值,而不是行的物理位置的指针。存储引擎需要找到二级索引的叶子结点获取对应的主键值,然后根据这个值去聚簇索引中查找对应的行。InnoDB的自适应哈希索引能够减少这样的重复操作。
InnoDB和MyISAM的数据分布对比

聚簇索引的数据分布有区别,以及对应的主键索引和二级索引的数据分布也有区别,通常让人感到困惑和意外。

-- 主键取值1~10000,随机数序插入并使用OPTIMIZE TABLE命令优化
-- 磁盘存储方式已经最优,单行的顺序是随机的
-- 列col2是从1~100之间随机赋值,有很多重复值
CREATE TABLE layout_test (
   col1 int NOT NULL,
   col2 int NOT NULL,
   PRIMARY KEY(col1),
   KEY(col2)
);
MyISAM的数据分布

按照插入的顺序存储在磁盘上。

MyISAM表的layout_test的数据分布:

MyISAM表的layout_test的数据分布

MyISAM表的layout_test的主键分布:

MyISAM表的layout_test的主键分布

MyISAM表的layout_test的col2列索引的分布:

MyISAM表的layout_test的col2列索引的分布

MyISAM不总是使用“行号”,根据定长还是边长有不用的策略。

MyISAM的主键列索引和其它索引没什么不同。就是一个名为PRIMARY的唯一非空索引。

InooDB的数据分布

因为支持聚簇索引,所以使用非常不同的方式存储同样的数据。

InnoDB表layout_test的主键分布:

InnoDB表layout_test的主键分布

存储了整个表而不只是索引。每一个叶子节点都包含了主键值、事务ID、用于事务和MVCC的混滚指针及所有剩余列。如果主键是一个前缀索引,也会包含完整的主键列和剩余列。

InnoDB表layout_test的二级索引分布:

二级索引的叶子节点存储的是主键值,而不是“行指针”。这样的策略减少了行移动时二级索引的维护操作。

InnoDB表layout_test的二级索引分布

图中隐藏了细节:非叶子节点包含了索引列和一个指向下级节点的指针。这对聚簇索引和二级索引都适用。

聚簇和非聚簇对比

聚簇和非聚簇对比

在InnoDB表中按主键顺序插入行

如果正在使用InnoDB表并且没什么数据需要聚集,那么可以定义一个代理键作为主键,这种主键的数据应该和应用无关, 最简单的方式是使用AUTO_INCREMENT自增列。这样保证主键是顺序写入的,根据主键的关联操作性能也会更好。

最好避免随机的(不连续且值的分布范围非常大—)聚簇索引,特别是I/O密集型应用。 如使用UUID,它使得聚簇索引的插入变得完全随机,它是最坏的情况,使得数据没有任何聚集特性。 向UUID主键插入行不仅插入行不仅花费的时间更长,而且索引占用的空间也更大。 一方面是由于主键字段更长;另一方面是由于页分裂和碎片导致的。

向聚簇索引插入顺序的索引值:

向聚簇索引插入顺序的索引值

因为主键值是顺序的,所以InnoDB把每一条记录都存储在上一条记录的后面, 当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,流出部分空间用于以后修改),下一条记录就会写入新的页中。

顺序的主键对于高并发的负载,可能造成明显的争用:

  • 主键的上届成为热点,因为所有插入都发生在这里,所以并发插入可能导致间隙锁竞争
  • 另一个热点是AUTO_INCREMENT锁机制。可以考虑重新设计表或应用或者更改innodb_autoinc_lock_mode参数。
    • innodb_autoinc_lock_mode
      • 0 tradition,一直持有锁直至插入完成,能保证值分配的可预见性,与连续性,可重复性
      • 1 consecutive,锁不要一直保持到语句的结束,只要语句得到了相应的值后就可以提前释放锁
      • 2 interleaved,这个模式下已经没有了auto_inc锁,对于同一个语句来说它所得到的auto_incremant值可能不是连续的。性能最好
      • 如果复制模式是mixed或者row,都是安全的

向聚簇索引插入无序的值:

向聚簇索引插入无序的值

因为新行的主键值不一定比之前插入的大,需要寻找合适的位置,通常是中间位置并分配空间,这会导致很多额外的工作,并导致数据分布不够优化。

缺点:

  • 写入的目标页可能已经刷新到磁盘上并存缓存中移除,或者还没有被加载到内存中,InnoDB在插入之前不得不先找到并从磁盘中读取目标页到内存中。这将导致大量的随机I/O。
  • 因为写入是乱序的,InnoDB不得不频繁的做页分裂操作,以便为新行分配空间,页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页。
  • 由于频繁的页分裂,页会变得稀疏并不规则的填充,所以最终数据会有碎片。

将随机值载入到聚簇索引以后,也需要做一次OPTOMIZE TABLE来重建表并优化页的填充。

覆盖索引

如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。

覆盖索引能够极大的提高性能。如果查询只需要扫描索引而无需回表,会带来很多好处:

  • 索引条目通常个远小于数据行的大小,所以如果只需要读取索引,那么MySQL就会极大的减少数据访问量
    • 这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上
    • 覆盖索引对I/O密集型应用也有帮助,因为索引比数据更小,更容易全部放入内存中,这对MyISAM尤其正确,它能压缩索引
  • 因为索引是按照列值顺序存储的(至少在单个页内是),所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少得多
    • 对于能使用OPTIMIZE命令使得索引完全顺序排列的存储引擎,这让范围查询使用完全顺序的索引访问。如MyISAM和XtraDB
  • 一些存储引擎如MyISAM只在内存中缓存索引,数据则依赖于操作系统来缓存,因此访问数据需要一次系统调用,这可能导致严重的性能问题,尤其是那些系统调用占了数据访问中的最大开销的场景。
  • 由于InnoDB的聚簇索引,覆盖索引对InnnoDB表特别有用。二级索引叶子结点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。

在索引中满足查询的成本比一般比查询行要小得多。

不是所有类型的索引都可以成为覆盖索引,覆盖索引必须存储索引列的值。MySQL只能使用B-Tree索引做覆盖索引。

当发起一个被索引覆盖的查询(也叫索引覆盖查询)时,Explain的Extra列可以看到“Using index”的信息。

mysql> EXPLAIN SELECT store_id, film_id FROM sakila.inventory\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: inventory
         type: index
possible_keys: NULL
          key: idx_store_id_film_id
      key_len: 3
          ref: NULL
         rows: 4673
        Extra: 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,IX_PROD_ACTOR
          key: ACTOR
      key_len: 52
          ref: const
         rows: 10
        Extra: Using where

这里索引无法覆盖查询,有两个原因:

  • 没有任何索引可以覆盖这个查询,因为查询从列表中选择了所有的列,而没有任何索引覆盖了所有列。
    • 但是WHERE条件中的列是有索引可以覆盖的,索引根据索引找到actor并过滤title
  • MySQL不能在索引上执行LIKE操作。这是底层存储引擎API的限制,5.5或更早的版本只允许做简单比较操作。
    • MySQL能在索引中做最左前缀匹配的LIKE比较,因为该操作可以转换为简单的比较操作,但是通配符开头的LIKE查询,就无法做比较匹配。
    • 这种情况下,只能取数据行而不是索引值来作比较。

解决上面两个问题需要重写查询并巧妙的设计索引。先将索引扩展至覆盖三个数据列(actor、title、prod_id),然后重写查询。

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)\G
*************************** 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
possible_keys: ACTOR,ACTOR_2,IX_PROD_ACTOR
          key: ACTOR_2
      key_len: 52
          ref:
         rows: 11
        Extra: Using where; Using index

把这种方式叫做延迟关联,因为延迟了对列的访问。 在查询的第一阶段MySQL可以使用覆盖索引,在FROM子句的子查询中找到匹配的prod_id,然后根据这些prdod_id在外围查询获取需要的所有列

InnoDB由于覆盖索引,二级索引可以使用保存的主键列完成覆盖查询。

5.6以后存储引擎API上的一个重要改进“索引条件推送”允许将过滤条件传到存储引擎层,将大大改善查询的执行方式,一些技巧也不在需要了。

使用索引扫描来做排序

MySQL有两种方式可以生成过有序的结果:通过排序操作或者通过索引顺序扫描。

如果EXPLAIN出来的type列为index,则通过索引顺序扫描来做排序(这与Extra的信息表达不同的意思)。

MySQL可以使用同一个索引既满足排序,有用于查找行。因此如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好的。

  • 只有索引的列顺序和ORDER BY子句的顺序完全一致,并且所有的列排序方向(倒序或正序)完全一致的时候,MySQL才能够使用索引来对结果做排序。
    • 如果使用不同的方向做排序,一个技巧是存储该列值的反转串或者相反数。
  • 如果关联多张表,只有当ORDER BY语句引用的字段全部为第一个表时,才能使用索引做排序。
  • ORDER BY子句和查找型查询的限制是一样的,都需要满足索引的最左前缀要求;否则MySQL都需要执行排序操作。
    • 一种情况是前导列为常量时,可以不满足最左前缀的要求。
(rental_date, inventory_id, customer_id):
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> EXPLAIN SELECT rental_id, staff_id FROM sakila.rental
    -> WHERE rental_date = '2005-05-25'
    -> ORDER BY inventory_id, customer_id\G
*************************** 1. row ***************************
         type: ref
possible_keys: rental_date
          key: rental_date
         rows: 1
        Extra: Using where

rental_date被制定为常量,所以虽然不满足最左前缀的要求,也可以用于查询排序。

第一列提供常量条件,第二列排序,组合就形成了最左前缀:

... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;

使用前两列就是最左前缀:

... WHERE rental_date > '2005-05-25' ORDER BY rental_date, inventory_id;

不能使用索引做排序的查询:

指定了两种不同的排序方向:

... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC, customer_id ASC;

引用了一个不在索引中的列:

... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, staff_id;

WHERE和ORDER BY组合无法合成最左前缀:

... WHERE rental_date = '2005-05-25' ORDER BY customer_id;

第一列使用范围条件,无法使用后面的索引列:

... WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id;

多个等于条件,对于排序来说这也是一种范围查询:

... WHERE rental_date = '2005-05-25' AND inventory_id IN(1,2) ORDER BY customer_id;

压缩(前缀压缩)索引

MyISAM使用压缩索引来减少索引的大小,从而让更多的索引可以放入内存中,这可以在某些情况极大的提升性能。 默认压缩字符串,通过参数可以压缩整数。

MyISAM每个索引块压缩的方法是:先保存索引块中的第一个值,然后将其它值和第一个值进行比较得到相同的前缀字节数和剩余的不同后缀部分,把这部分存储起来即可。 如第一个值是“perform”,第二个是“performance”,那么第二个值压缩有类似“7,ance”。MyISAM对行指针也采用类似的前缀压缩方式。

压缩块使用更少的空间,代价是某些操作可能更慢。因为每个值的压缩前缀都依赖前面的值,所以无法在索引块使用二分查找而只能从头开始扫描。 正序的扫描速度还不错,但是如果倒序扫描就不是很好了。所有在块中查找某一行的操作平均都需要扫描半个索引块。

对于CPU密集型应用,因为扫描需要随机查找,压缩索引使得MyISAM在索引查找上要慢好几倍。倒序扫描就更慢了。 压缩索引可能只需要十分之一大小的磁盘空间,乳沟是I/O密集型应用,对某些查询带来的好处会比成本多很多。 压缩索引需要在CPU内存资源和磁盘之间做权衡。

冗余和重复索引

MySQL允许在相同列上创建多个索引。这乔单独维护重复的索引,并且优化器在优化查询的时候也需要逐个的进行考虑,这会影响性能。

重复索引

重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样创建索引。

-- 索引重复,应该删除
CREATE TABLE test (
   ID INT NOT NULL PRIMARY KEY,
   A  INT NOT NULL,
   B  INT NOT NULL,
   UNIQUE(ID),
   INDEX(ID)
) ENGINE=InnoDB;

唯一限制和主键限制都是通过索引实现的,所以上面的语句创建了三个重复索引。

通常没有理由这样做,除非在同一列上创建不同类型的索引来满足不同的查询需求,这样不算重复索引。

冗余索引

冗余索引和重复索引有一些不同。 如果创建了索引(A,B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引。索引(A,B)可以当作索引(A)来使用。(对于B-Tree索引来说的)。 还有一种情况是如果创建(A,ID),因为主键列已经包含在二级索引中,所以也是冗余索引。但是如果索引被扩展成(A,B,ID),那么这个索引就不是冗余索引。所以扩展也应小心。 如果再创建索引(B,A),则不是冗余索引,索引(B)也不是。 创建其它的非B-Tree索引也不是冗余索引。

大多数情况都不需要冗余索引,应该尽量扩展已有的索引而不是创建新索引。 有时候出于性能考虑需要冗余索引,因为扩展已有索引会导致其变得太大,从而影响其它使用该索引的查询的性能

例如,如果在整列上有一个索引,现在需要额外增加一个很长的VARCHAR列来扩展该索引,那么性能可能会急剧下降。 特别是有查询把这个索引当作覆盖索引,或者这是MyISAM表并且有很多范围查询(前缀压缩)的时候。

userinfo表有1000000行,对每个state_id值大概有20000条记录,在state_id列有一个索引,下面查询Q1:

-- QPS大概115
SELECT count(*) FROM userinfo WHERE state_id=5;

相关查询Q2:

-- QPS小于10
SELECT state_id, city, address FROM userinfo WHERE state_id=5;

提升性能的方法是扩展索引为(state_id,city,address),让索引能覆盖查询:

ALTER TABLE userinfo DROP KEY state_id, ADD KEY state_id_2 (state_id, city, address);

索引扩招后Q2快了,但是Q1却变慢了。如果让两个查询都快,则需要冗余这两个索引。

使用不同索引策略的SELECT查询的QPS测试结果:

引擎,查询语句 state_id only state_id_2 only Both state_id and state_id_2
MyISAM, Q1 114.96 25.40 112.19
MyISAM, Q2 9.97 16.34 16.37
InnoDB, Q1 108.55 100.33 107.97
InnoDB, Q2 12.12 28.04 28.06

InnoDB引擎上Q1的性能下降并不明显,因为InnoDB没有使用索引压缩。

有两个索引的缺点是索引成本更高。

使用不同索引策略时插入100万行数据的速度:

存储引擎 state_id only Both state_id and state_id_2
InnoDB, enough memory for both indexes 80 seconds 136 seconds
MyISAM, enough memory for only one index 72 seconds 470 seconds

表中的索引越多插入速度会越慢。一般来说,增加新索引将会导致INSERT、UPDATE、DELETE等操作速度变慢,特别是当新增索引后达到了内存瓶颈的时候。

解决:

找到并删除这样的索引。

可以通过写一些复杂的访问INFORMATION_SCHEMA的表查询来找(如果有大量数据或者大量的表会导致性能问题), 或者使用common-schema或者Percona Toolkit中的pt-duplicate-key-checker分析表结构来找出冗余和重复的索引。

未使用的索引

有一些索引永远不会使用,建议删除。除非是用来避免重复的索引。

Percona Toolkit中的pt-index-usage,该工具可以读取查询日志,并对日志中的每条查询进行EXPLAIN操作,然后打印出关悦索引和查询的报告。 该工具可以找出哪些未使用的索引,还可以了解查询的执行计划,例如在某些情况有些类似的查询的执行方式不一样,可以帮助你定位那些偶尔服务质量差的查询来优化。 该工具还可以将结果写到MySQL表中。

索引和锁

索引可以让查询不访问那些不需要的行,锁定更少的行。

  • InnoDB只有在访问行的时候进行加锁,InnoDB的行锁定效率很高,内存使用也很少,但是锁定行的时候仍然会带来额外的开销
  • 锁定超过需要的行会增加锁竞争并减少并发性

如果索引无法过滤掉无效的行,那么InnoDB检索到数据并返回给服务器层后,MySQL服务器才能执行WHERE语句 这时已经无法避免锁定行了:InnoDB已经锁住行,到适当的时候才释放。5.1以后的版本会在服务器过滤掉行后就释放,早期版本需要事务提交释放。5.6以后可能对这个问题有很大改进(传递WHERE条件)。

EXPLAIN SELECT actor_id FROM sakila.actor WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE;
+----+-------------+-------+-------+---------+--------------------------+
| id | select_type | table | type  | key     | Extra                    |
+----+-------------+-------+-------+---------+--------------------------+
|  1 | SIMPLE      | actor | range | PRIMARY | Using where; Using index |
+----+-------------+-------+-------+---------+--------------------------+

这里会range查询<5的数据并锁定,所以会锁定1。Using where表示服务器将存储引擎返回行以后再引用WHERE过滤条件。

索引案例

在线约会网站,用户信息表包含很多列,国家、地区、城市、性别、眼睛颜色等。

支持多种过滤条件

现在需要看看哪些列有很多不同值,哪些列在WHERE子句中出现的最频繁。

  • country列选择性不高,但很多查询都会用到
  • sex列选择性很低,但很多查询中用到

考虑使用的频率,还是建议在不同组合索引的时候将(sex、country)列座位前缀。

  • 如前所述的业务场景,几乎所有查询都会用到sex列
  • 还可以使用AND SEX IN (‘m’,’f’)来让MySQL选择这个索引以匹配索引的最左前缀。但是列值太多就会使IN()语句太长。

基本原则:考虑表上所有的选项。当设计索引时,不要只为现有的查询考虑哪些索引,还要考虑对索引进行优化。同时优化索引和查询以找到最佳的平衡。

接下来需要考虑其它常见WHERE条件的组合,并需要了解那些组合在没有合适索引的情况下会变慢。

(sex,country,age)、(sex,country,region,age)、(sex,country,region,cityage)都有可能。 这可能需要大量的索引,可以使用上面提到的IN()的技巧来避免同时需要多个类似的索引。 这将需要一些全部国家、国家的全部地区的列表来确保索引前缀有同样的约束。组合起来可能是一个非常大的条件。 这些索引将满足大部分常见的搜索查询。

对于生僻列,可以忽略它们让MySQL多扫描一些列或者将它们的索引建立在age列的前面并使用IN()语句的方法来处理没有指定内容的场景。

为了总是尽可能让MySQL使用更多的索引列,因为查询只能使用最左前缀,直到遇到第一个范围列, age多半是范围查询,所以放到索引最后。当然也可以用IN()代替,但是并不是总能转换。

注意事项:IN()方法不能滥用,因为每增加一个条件,优化器要做的组合都将以指数形式增加,最终可能会极大的降低性能

-- 4x3x2=24种组合,并不是很夸张,在组合数达到上千个则需要特别小心
WHERE eye_color   IN('brown','blue','hazel')
   AND hair_color IN('black','red','blonde','brown')
   AND sex        IN('M','F')

避免多个范围条件

EXPLAIN语句无法区分是列表还是范围查询,都适用range来描述。但是二者的查询效率是不同的,因为范围查询后无法时候后面的索引,多个等值条件查询则没有这个限制。

-- 查询过去几周上线过的用户
WHERE  eye_color   IN('brown','blue','hazel')
   AND hair_color  IN('black','red','blonde','brown')
   AND sex         IN('M','F')
   AND last_online > DATE_SUB(NOW(), INTERVAL 7 DAY)
   AND age         BETWEEN 18 AND 25

上面的查询有两个范围条件,last_online和age列,可以使用last_online列索引或者age列索引,但是无法同时使用它们。

如果只哟last_online而没有age,那么我们可能考虑在索引后面加上这个列。

这里考虑如果无法把age转换成一个IN()的列表,将无法同时有两个维度的范围查询速度很快。 但是我们能够将其中一个范围查询转换成一个简单比较。可以由定时任务来维护一个active列,登录是设置为1,定时检查是否超如7天来置0。 这个方法可以让MySQL使用(active,sex,country,age)索引。但active列只能满足不是十分精确的请求。 如果需要精确数据,可以吧last_online放到WHERE子句,但不加入索引,因为这个条件的过滤性不高,对查询影响也不是很明显。

优化排序

对于选择性非常低的列,可以增加一些特殊的索引来做排序,如可以创建(sex,rating)索引用于下面查询:

-- 同时使用了ORDER BY和LIMIT,没有索引会很慢
SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 10;
-- 即使有索引,翻页的skip值很大时也会非常慢
SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 100000, 10;
  • 一个优化措施是限制用户的翻页数量。
  • 另一个比较好的策略是使用延迟关联
    • 通过使用覆盖索引查询返回需要的主键,在根据这些逐渐关联原表获得需要的行。这样可以减少MySQL扫描那些需要丢弃的行数。
-- 高效的使用(sex,rating)索引进行分页
SELECT <cols> FROM profiles INNER JOIN (
   SELECT <primary key cols> FROM profiles
   WHERE x.sex='M' ORDER BY rating LIMIT 100000, 10
) AS x USING(<primary key cols>);

维护索引和表

使用正确的类型创建了表并加上了合适的索引,还需要维护表和索引来确保它们能正常工作。

维护表有三个主要目的:找到并修复损坏的表,维护准确的索引统计信息,较少碎片

找到并修复损坏的表

  • MyISAM引擎表损坏通常是系统崩溃导致的
  • 其它引擎由于硬件问题、MySQL本身的缺陷或者操作系统问题导致索引损坏。

损坏的索引会导致查询返回错误的结果或莫须有的主键冲突等问题。严重时还会导致数据库崩溃。

可以运行CHECK TABLE来检查是否发生了表损坏(有些引擎不支持或者支持更多的选项)。 CHECK TABLE通常能够找出大多数的表和索引的错误。

可以使用REPAIR TABLE命令来修复损坏的表。如果存储引擎不支持,可以通过一个空的ALTER操作来重建表。

ALTER TABLE innodb_tbl ENGINE=INNODB;

此外,也可以使用一些存储引擎相关的离线工具,如myisamchk。

如果是表的行数据区域损坏,可以从备份中恢复表,或者阐释从损坏的数据文件中尽可能的恢复数据。

如果InnoDB引擎的表出现了损坏,需要立刻调查一下。InnoDB的色剂保证了它并不容易被损坏。 如果发生损坏,一般约么是数据库的硬件问题(内存、磁盘)要么是数据库管理员的错误(外部操作了数据文件),抑或是InnoDB本身缺陷。 常见错误通常是由于尝试使用rsync备份InnoDB导致的。 可以使用innodb_force_recory参数进入InnoDB导致的。也可以使用InnoDB Data Recory Toolkit直接从数据文件恢复数据。

更新索引统计信息

MySQL的查询优化器会通过两个API来了解存储引擎的索引值的分布信息,以决定如何使用索引。

  • 第一个API是records_in_range(),通过向存储引擎传入两个边界值获取在这个范围大概有多少条就。
    • 对于某些存储引擎返回精确值,如MyISAM
    • 某些存储引擎返回估算值,如InnoDB
  • 第二个API是info(),该接口返回各种类新的数据,包括索引的基数(每个键有多少条记录)。
    • 如果信息不准确,优化器会使用索引统计信息来估算扫描行数。

MySQL优化器使用的是基于成本的模型,而衡量成本的主要制表就是一个查询需要扫描多少行。 如果没有统计信息或者统计信息不准确,优化器就很有可能做出错误的决定。可以通过ANALYZE TABLE来重新生成统计信息解决这个问题。

每种存储引擎实现统计信息的方式不同,所以需要执行ANALYZE TABLE的频率也因不同的存储引擎而不同,每次运行的成本也不同。

  • Memory引擎不存储索引统计信息
  • MyISAM将索引统计信息存储在磁盘中,ANALYZE TABLE需要进行一次全索引扫描来计算索引基数。整个过程需要锁表。
  • 直到5.5,InnoDB也不在磁盘中存储统计信息,而是通过随机索引访问呢进行评估并将其存储在内存中。

可以使用SHOW INDEX FROM命令来查看索引的基数(Cardinality)。 5.0或更新的版本中,可以通过INFOMATION_SCHEMA.STATISTICS表很方便的查询到这些信息。

InnoDB可以通过innodb_stats_sample_pages来设置样本页的数量,默认是8。
InnoDB会在首次打开表,或者执行ANALYZE TABLE或者表的大小发成非常大的变化的时候计算索引的统计信息。
InnoDB在打开某些INFOMATION_SCHEMA表,或者使用SHOW TABLE STATUS和SHOW INDEX,抑或在MySQL客户端开启自动补全功能的时候会触发索引统计信息的更新。 关闭innodb_stats_on_metadata来避免SHOW INDEX查看统计信息触发更新而导致大量加锁造成的启动时间长、服务器压力大的问题。一旦关闭自动更新,需要周期性的使用ANALYZE TABLE来手动更新。

减少索引和数据的碎片

B-Tree索引可能会碎片化,这会降低查询的效率。碎片化的索引可能会以很差或者无序的方式存储在磁盘上。

根据设计,B-Tree需要随机磁盘访问才能定位到叶子页,所以随机访问是不可避免的。 然而如果叶子页的物理分布上是顺序且紧密的,那么查询的性能就会更好。 否则,对于范围查询、索引覆盖扫描等操作来说,速度可能会降低很多倍,对于索引覆盖扫描这一点更加明显。

表的数据存储也可能碎片化:

  • 行碎片
    • 数据行被存储为多个地方的多个片段中。
    • 即使查询只从索引中访问一行记录,行碎片也会导致性能下降
  • 行间碎片
    • 逻辑上的顺序页,或者行在磁盘上不是顺序存储的。
    • 对诸如全表扫描和聚簇索引扫描之类的操作有很大影响,因为这些操作原本能够从磁盘上顺序存储的数据中获益。
  • 剩余空间碎片
    • 数据页中有大量的空余空间。这会导致服务器读取大量不需要的数据,从而造成浪费。

对于MyISAM表,三种碎片都可能发生。对于InnoDB不会出现短小的行碎片,InnoDB会移动短小的行并重写到一个片段中。

可以通过OPTIMIZE TABLE或者导出再倒入的方式来重新整理数据。

  • 对于一些引擎如MyISAM,可以通过排序算重建索引的方式消除碎片。
  • 新版本的InnoDB增加了“在线”添加和删除索引的功能,可以通过先删除,然后再重新创建索引的方式来消除索引的碎片化。
  • 对于不支持OPTIMIZE TABLE的操作,可以通过ALTER TABLE重建表。
    • 这种方式对于InnoDB只会消除表的碎片化,可以用先删除所有索引然后重建表然后重建索引的方式。
ALTER TABLE <table> ENGINE=<engine>;

XtraBackup有一个–stats参数以非备份的方式运行,只是打印索引和表的统计情况,包括页中的数据量和空间剩余。这可以用来确定数据的碎片化程度。 另外也可以考虑数据是否到达稳定状态,如果进行碎片整理将数据压缩到一起,可能反而会导致后续的更新操作触发一系列的页分裂和重组,对性能造成不良影响。

查询性能优化

为什么查询速度会慢

要优化查询,实际上是优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行的更快。

查询的生命周期:从客户端,到服务器,然后在服务器上进行解析,生成执行计划,执行,并返回给客户端。

执行可以认为是整个生命周期中最重要的阶段,其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。

完成这些任务的时候,查询需要在不同的地方花费时间,包括网络,CPU计算,生成统计信息和执行计划、锁等待等操作, 尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作和内存不足时导致的I/O操作上消耗时间。 根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。

慢查询基础:优化数据访问

查询性能低下的最基本原因是访问的数据太多。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。

对于低效的查询,有两个步骤来分析:

  • 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行。但有时候也可能是访问了太多的列。
  • 确认MySQL服务器层是否在分析大量超过需要的数据行。

是否数据库请求了不需要的数据

有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担,并增加网络开销。同时也消耗应用服务器的CPU和内存资源。

  • 查询不需要的记录
    • MySQL会先返回全部数据然后进行计算
  • 多表关联时返回全部的列
    • 只返回需要的表的列
  • 总是取出全部列
    • 谨慎使用SELECT *,这会让优化器无法完成索引覆盖扫描这类优化。还会给服务器带来额外的I/O、内存、和CPU消耗。
    • 还会带来修改列带来的一些问题。
    • 但是这也可能由于复用和缓存简化开发并抵消一些效率问题。
  • 重复查询相同的数据
    • 适当的使用缓存而不是每次从数据中查询。

MySQL是否在扫描额外的记录

最简单的衡量查询开销的三个指标如下:

  • 响应时间
  • 扫描的行数
  • 返回的行数

这三个指标都会记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描行数过多的查询的好办法。

响应时间

服务时间和排队时间两个部分之和。

  • 服务时间是指数据库处理这个查询真正花了多长时间
  • 排队时间是指服务器因为等待某些资源而没有真正执行查询的时间
    • 可能是等待I/O操作完成,也可能是等待行锁等

快速上限估计法来估算查询的响应时间。了解查询需要哪些索引以及它的执行计划是什么,然后计算大概需要杜少个顺序和随机I/O, 再用其乘以在具体硬件条件下一次I/O的消耗时间。最后把这些消耗都加起来,就可以获得一个大概参考值来判断当前响应时间是不是一个合理的值。

扫描的行数和返回的行数

分析查询时,查看查询扫描的行数是非常有帮助的。

理想状态下扫描的行数和返回的行数应该是相同的。但如关联查询,服务器必须要扫描多行才能生成结果集中的一行。 扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间,有时候这个值可能非常大。

扫描的行数和访问类型

在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。

MySQL有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问不需要扫描就能返回结果。

EXPLAIN语句中type列反映了访问类型。访问类型有和多种:

全表扫描、索引扫描、范围扫描、唯一索引查询、常数索引等。

速度从慢到快,扫描的行数从多到少。

如果查询无法找到一个合适的访问类型,那么解决的最好办法通常是增加一个合适的索引。 索引让MySQL以最高效、扫描行数最少的方式找到需要的记录。

MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为:

  • 在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。
  • 使用索引覆盖扫描(Using index)来返回记录,直接从索引中过滤过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,但无需在回表查询记录。
  • 从数据表中返回数据,然后过滤不满足条件的记录(Using where)。这在MySQL服务器层完成,MySQL需要先从数据表读取记录然后过滤。

如果发现查询需要扫描大量的数据但只返回少数的行,那么通常可以尝试下面的技巧:

  • 使用索引覆盖扫描,将所有需要用的列都放到索引中去
  • 改变库表结构。例如使用汇总表
  • 重写这个复杂查询,让MySQL优化器能够以更优化的方式执行这个查询

重构查询的方式

有时候可以查询转换一种写法返回一样的结果,但是性能更好。也可以通过修改程序代码,用另一种方式完成查询,最终达到一样的目的。

一个复杂查询还是多个简单查询

有时候将一个大查询分解为多个小查询时很有必要的。但是如果一个查询能够胜任时还写成多个独立查询时不明智的。

以前总是认为网络通信、查询解析和优化是一件代价很高的事情。但是MySQL从设计上让连接和断开连接都很轻量级,在返回一个小的查询结果方面很高效。 即使一个千兆网卡也能轻松满足每秒超过2000次查询。在一个通用服务器上可以运行每秒超过10万的查询。

切分查询

将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果。

删除旧数据是一个很好的例子。定期地清楚大量数据时,如果用一个大的语句一次性完成的话, 则可能需要一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。

切分成多个较小的查询可以尽可能小的影响MySQL性能,同时还可以减少复制的延迟。

DELETE FROM messages WHERE created < DATE_SUB(NOW(),INTERVAL 3 MONTH);
rows_affected = 0
do {
   rows_affected = do_query(
      "DELETE FROM messages WHERE created < DATE_SUB(NOW(),INTERVAL 3 MONTH)
      LIMIT 10000")
} while rows_affected > 0

还可以分散执行时间,大大降低对服务器的影响,还可以减少删除时锁的持有时间。

分解关联查询

很多高性能的应用都会对关联查询进行分解。

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';
SELECT * FROM  tag WHERE tag='mysql';
SELECT * FROM  tag_post WHERE tag_id=1234;
SELECT * FROM  post WHERE  post.id in (123,456,567,9098,8904);

分解关联查询有如下优势:

  • 让缓存的效率更高效。
    • 许多应用程序可以方便的缓存单表查询对应的结果对象。
    • 对于MySQL的Query Cache来说,关联表发生变化就无法使用缓存,而拆分可以使用那部分不变的缓存。
  • 执行单个查询可以减少锁的竞争
  • 在应用层做关联,可以更容易对数据进行拆分,更容易做到高性能和可扩展。
  • 查询本身效率也可能会有所提升。
  • 可以减少冗余记录的查询。避免了MySQL做关联查询可能重复的访问一部分数据的问题,这也缓解了带宽压力。
  • 这样做相当于在应用中实现了哈希关联,而不是MySQL的嵌套循环关联。

应用场景:

  • 当应用能够方便的缓存单个查询的结果的时候
  • 当可以将数据分布到不同的MySQL服务器上的时候
  • 当能够使用IN()的方式替代关联查询的时候
  • 当查询中使用同一个数据表的时候

查询执行的基础

MySQL执行一个查询的过程,查询执行路径:

查询执行路径

  1. 客户端将SQL语句发送到服务器。
  2. 服务器检查查询缓存。如果有命中, 它将从缓存返回存储的结果;否则, 它将SQL语句传递到下一步骤。
  3. 服务器分析、预处理和优化SQL到查询执行中计划。
  4. 查询执行引擎通过调用存储引擎来执行计划Api。
  5. 服务器将结果发送到客户端。

MySQL客户端/服务器通信协议

MySQL客户端和服务器端是半双工的。任一时刻,要么是有服务器向客户端发送数据,要么是客户端向服务器端发送数据,不能同时发生。

这个协议让MySQL通信简单快速,但也从很多方面限制了MySQl。一个明显的限制是,这意味着没法进行流量控制。 一旦开始发送消息,只有接收完整个消息才能响应它。

客户端用一个单独的数据包将查询发送给服务器,语句很长超过max_allowed_packet的配置时,会拒绝接受更多的数据并抛出错误。

多数连接MySQL的库函数(JDBC)都可以获得全部结果集并缓存到内存里,还可以逐行获取数据。 默认一般是获得全部结果集并缓存在内存中,MySQL通常需要等所有的谁都已发送给客户端才能释放这条查询所占用的资源,所以接受全部结果并缓存通常可以减少服务器压力,让查询早点结束并释放资源。

如果结果集很大,这样会好消耗内存,这种情况可以指定不使用缓存来处理数据,但是会一直占用服务器资源。

查询状态

最简单的是使用SHOW PROCESSLIST

  • Sleep
    • 线程正在等待客户端发送新的数据
  • Query
    • 线程正在执行查询或者将结果发送给客户端
  • Locked
    • 在MySQL服务层,该线程正在等待表锁。对于MyISAM这是一个比较典型的状态
    • 在存储引擎级别实现的锁,如行锁不会体现在线程状态中。
  • Analyzing and statistics
    • 线程正在收集存储引擎的统计信息,并生成查询的执行计划。
  • Copying to tmp table [on disk]
    • 线程正在执行查询,并且将其结果都复制到一个临时表中,这种状态要么是在做GROUP BY操作,要么是在做文件排序操作,或者是UNION操作。
    • 如果有on disk标记,表明正在将内存临时表放到磁盘上。
  • Sorting result
    • 线程正在对结果集进行排序。
  • Sending data
    • 可能在多个状态之间传送数据,或者在生成结果集,或者在向客户端返回数据。

这个状态可以帮助你很快了解“谁在持球”。在一台繁忙服务器,可能会看到大量的不正常状态。

查询缓存

查询缓存如果打开,MySQL会优先检查查询是否命中查询缓存中的数据。

这个检查是通过一个对大小写敏感的哈希查找实现的(Query Cache)。

在命中后,会检查一次权限,然后返回数据。这种情况,查询不会被解析,不会生成查询计划,不会被执行。

查询优化处理

下一步是将一个SQL转换成一个执行计划,MySQL再依照这个计划和存储引擎交互。这包含多个子阶段:

解析SQL、预处理、优化SQL执行计划。

这个过程中任何错误都可能终止查询。

语法解析器和预处理
  • 首先,MySQL通过关键字将SQL语句进行解析,并生成一棵对应的“解析树”。MySQL解析器将使用MySQL语法规则验证过和解析查询。
    • 如它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等。再或者还会验证引号是否能前后正确匹配。
  • 预处理则根据一些MySQL规则进一步检查解析树是否合法。
    • 如检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义
  • 下一步预处理器会验证权限。这通常很快,服务器上有非常多的权限配置。
查询优化器

语法树被认为是合法的了,并且由优化器将其转化成执行计划。

优化器的作用是找出多种执行方式中最好的执行计划。

最初,成本的最小单位是随机读取一个4K数据页的成本,后来成本计算公式变得更复杂,并且引入了一些“因子”来估算某些操作的代价。

可以通过查询会话的Last_query_cost的值来得知MySQL计算的当前查询的成本。

SELECT SQL_NO_CACHE COUNT(*) FROM sakila.film_actor;
SHOW STATUS LIKE 'Last_query_cost';
# 结果表示大概需要做1040个数据页的随机查找才能完成查询
+-----------------+-------------+
| Variable_name   | Value       |
+-----------------+-------------+
| Last_query_cost | 1040.599000 |
+-----------------+-------------+

这是根据一系列的统计信息算出来的:

每个表或者索引的页面个数、索引的基数(索引中不同值的数量)、索引和数据行的长度、索引分布情况。

很多种原因会导致MySQL优化器选择错误的执行计划:

  • 统计信息不正确
    • 如InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息,只能估算。
  • 执行计划中的成本估算不等同于实际执行的成本
    • 如某个计划需要读取很多的页面,但是成本很小(顺序读取或在内存中)
  • MySQL的最有可能与你想的最优不一样
    • MySQL只是基于成本模型选择最优的执行计划,所以不一定是真实的最优执行计划
  • MySQL从不考虑其它并发执行的查询,这可能会影响到当前查询的速度
  • MySQL也并不是任何时候都是基于成本的优化
    • 有时也会基于一些固定的规则,如全文搜索的MATCH()子句,即使有时候使用别的索引和WHERE条件可以远比这种方式要快,MySQL也仍然会使用对应的全文索引
  • MySQL不考虑不受其控制的操作的成本,如执行存储过程或者用户自定义函数的成本
  • 优化器有时候无法估算所有可能的执行计划,所以可能错过最优的执行计划

MySQL的查询优化器使用了很多优化策略来生成一个最优的执行计划。优化策略可以简单的分为两种:

  • 静态优化
    • 直接对解析树进行分析,并完成优化
    • 可以通过一些简单的代数变换将WHERE转换成另一种等价形式。不依赖于数值。
    • 在第一次完成后就一直有效,即使使用不同的参数重复执行查询也不会发生变化,可以认为是一种编译时优化
  • 动态优化
    • 优化规则和查询的上下文有关,也可能和其它因素有关。
    • 例如WHERE条件中的取值、索引中条目对应的数据行数等。这需要在每次查询的时候重新评估,可以认为是运行时优化

静态优化只执行一次,动态优化则在每次执行时都要重新评估。有时候甚至在查询的执行过程中也会重新优化。 如范围查询的执行计划会针对每一行重新评估索引。可以通过EXPLAIN执行计划中的Extra列是否有“range checked for each record”来确认。 该执行计划还会增加select_full_range_join这个服务器变量的值。

MySQL能够处理的优化类型:

  • 重新定义关联表的顺序
    • 数据表的关联并不总是按照查询中指定的顺序执行。
  • 将外连接转化成内连接
    • 并不是所有的OUTER JOIN语句都必须以外连接方式执行。
    • 如WHERE条件、库表结构都可能会让外连接等价于一个内连接。
  • 使用等价变换规则
    • 使用等价变换来简化规范表达式,可以合并和减少一些比较,还可以移除一些恒成立和恒不成立的判断。
    • (a<b AND b=c) AND a=5会被改写成b>5 AND b=c AND a=5,这些规则对于我们编写条件语句很有用。
  • 优化COUNT()、MIN()、MAX()
    • 索引列是否可为空通常可以帮助MySQL优化这类表达式。
    • 如查询B-Tree的最左和最右记录获得最小和最大值。可以在EXPLAIN语句中看到“Select tables optimized away”证明使用了这个优化
  • 预估并转化为常数表达式
    • 检测到一个表达式可以转换为常数的时候,就会一直把该表达式作为常数进行优化处理。
    • 如不变的用户变量,MIN()函数的返回值,或者恒等WHERE中的条件等
    • 如通过索引信息就知道返回多少行数据,或者WHERE条件中带值所以表访问类型是const
  • 覆盖索引扫描
    • 当索引中包含所有查询中需要使用的列的时候使用索引返回所有的数据
  • 子查询优化
    • 将子查询转换一种效率更高的形式,从而减少多个查询多次对数据进行访问。
  • 提前终止查询
    • 发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。
    • 如LIMIT子句、不成立的条件直接返回空
    • 类似于不同值/不存在的优化可以用于DISTINCT、NOT EXIST()、LEFT JOIN类型的查询
  • 等值传播
    • 如果两个列的值通过WHERE关联,那么MySQL能够把其中的一个列的WHERE条件传递到另外一个上
  • 列表IN()的比较
    • MySQL将IN()列表中的数据先进行排序,然后通过二分查找的方式确定列表中的值是否满足条件,这是一个O(log n)复杂度的操作。
    • 等价的转换为OR的操作的复杂度为O(n),对于IN()列表中有大量的取值的时候,MySQL的处理速度将会更快
数据和索引的统计信息

存储引擎提供给优化器对应的统计信息:

  • 每个表或者索引有多少个页面
  • 每个表每个索引的基数是多少
  • 数据行和索引长度
  • 索引分布信息等

优化器根据这些信息来选择一个最优的执行计划。

MySQL如何执行关联查询

每一个查询,每一个片段(包括子查询,甚至基于单表的SELECT)都可能是关联。

MySQL关联执行的策略很简单(嵌套循环关联):

MySQL对于任何关联都执行嵌套循环关联操作,即MySQL先在一个表中取出单条语句,然后再嵌套循环下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。 然后根据各个表匹配的行,返回查询中需要的列。 MySQL会尝试在最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行以后,MySQL返回到上一层次关联表,看是否能够找到更多的匹配记录,一次类推迭代执行。

通过泳道图展示MySQL如何完成关联查询:

通过泳道图展示MySQL如何完成关联查询

从本质上来说,MySQL对所有的类型和查询都以同样的方式运行。

如FROM语句遇到子查询时,先执行子查询并将其结果放到一个临时表中,然后将这个临时表当作一个普通表对待(正如其名“派生表”);
在执行UNION查询时也使用类似的临时表,在遇到右外连接的时候,MySQL将其改写成等价的左外连接。

注意事项:临时表是没有任何索引的,在编写任何复杂的子查询和关联查询的时候需要注意这一点,对UNION查询也一样。
Tips:MySQL 5.6中有了重大改变,引入了更加复杂的执行计划。

执行计划

MySQL不会生成查询字节码来执行查询。MySQL生成查询的一棵指令树,然通过存储引擎执行完成这棵树指令并返回结果。最终的执行计划包含了重构查询的全部信息。
如果对某个查询执行EXPLAIN EXTENDED后,再执行SHOW WARNINGS就可以看到重构出的查询。

MySQL如何实现多表关联(左侧深度优先的树):

左侧深度优先的树

关联查询优化器

它决定了多个表关联时的顺序。通过评估不同顺序时的成本来选择一个代价最小的关联顺序。

使用STRAIGHT_JOIN关键字重写查询,让优化器按照你认为的最优的关联顺序执行。

糟糕的是,如果有超过n个表的关联,那么需要检查n的阶乘种关联顺序。搜索空间的增长速度非常快。 当需要关联的表超过optimizer_search_depth的限制的时候,就会选择“贪婪”搜索模式了。

有时查询的顺序并不能随意安排,这是关联优化器可以根据这些规则大大减少搜索空间。如左连接、相关子查询。

排序优化

无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。

当不能使用索引生成排序结果的时候,MySQL需要自己进行排序。如果数据量小则在内存中进行,如果数据量大则需要使用磁盘。这个过程统一称为“文件排序(filesort)”。

  • 如果需要排序的数据量小于“排序缓冲区”,使用内存进行“快速排序”操作
  • 如果内存不够排序,那么会先将数据分块,对每个独立的块使用“快速排序”进行排序,并将各个块的排序结果放到磁盘上,然后将各个排好序的块进行合并(merge),最后返回排序结果。

MySQL有两种排序算法:

  • 两次传输排序(旧版本使用)
    • 读取行指针和需要排序的字段,对其进行排序,然后根据排序结果读取所需要的数据行
    • 两次数据传输,从数据表中读取两次数据,第二次读取数据的时候,因为是读取排序列进行排序后的所有记录,会产生大量的随机I/O。
    • 有点是排序的时候存储尽可能少的数据,这就让“排序缓冲区”中可能容纳尽可能多的行数进行排序
  • 单次传输排序(新版本使用)
    • 先获取查询所需的所有列,然后再根据给定列进行排序,最后直接返回排序结果。
    • 4.1以后才引入
    • 对于I/O密集型应用,这样做的效率高了很多。而且不需要两次I/O,无需随机I/O。
    • 缺点是如果需要返回的列非常多、非常大,会额外占用大量的空间。因为单条排序记录很大,可能会有更多的排序块进行合并。

不超过max_lenogth_for_sort_data参数限制,使用单次传输排序。

MySQL在进行文件排序的时候需要使用的临时存储空间可能比想象的大得多。 原因在于在排序时,对每一个排序记录都会分配一个足够长的定长空间来存放,这个长度必须可以容纳其中最长的字符串(如使用UTF-8字符集,那么会为每个字符预留三个字节)。

MySQL会使用两种情况来处理文件排序:

  • 如果ORDER BY子句中所有的列都来自关联的第一个表,那么MySQL在关联处理第一个表的时候就进行文件排序。
    • EXPLAIN显示Using filesort
  • 除此之外,MySQL都会先将关联结果放到一个临时表中,然后在所有的关联都结束后,再进行文件排序。
    • EXPLAIN显示Using temporary;Using filesort
  • 如果有LIMIT的话,LIMIT也会在排序后应用,所以即使需要返回较少的数据,临时表和需要排序的数量仍然会非常大

5.6在这里很多重要的改进,当只需要返回部分排序结果的时候,不再对所有结果进行排序,而是根据实际情况,选择抛弃不满足条件的结果,然后再进行排序。

查询执行引擎

在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个计划来完成整个查询。这里执行计划是一个数据结构,而不是其它关系型数据库那样会生成过对应的字节码。

MySQL只是简单地根据执行计划给出的指令逐步执行。在根据计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口来完成 这些接口也就是称为“handler API”的接口。查询中的每一个表由一个handler的实例表示。

实际上,MySQL在优化阶段就为每个表创建了一个handler实例,优化器根据这些实例的接口可以获取表的相关信息,包括所有的列名、统计信息等。

返回结果给客户端

最后一个阶段返回结果集和一些信息,如影响到的行数等。

如果查询可以被缓存,那么这个阶段也会将结果放到查询缓存中。

返回结果集是一个增量、逐步返回的过程。在开始生成第一条结果时,就可以开始向客户端逐步返回结果集了。这样做有两个好处:

  • 服务器无须存储太多的结果,也就不会因为要返回太多结果而消耗太多内存
  • 可以让客户端第一时间获得结果

结果集中的米一行都会以一个满足MySQL客户端/服务器通信协议的封包发送,再巩固TCP协议进行传输,在TCP传输过程中,可能对MySQL的封包进行缓存然后批量传输。

MySQL优化器的局限性

嵌套循环只对少部分查询不适用,而且往往可以通过改写查询让MySQL高效的完成工作。5.6以后会消除很多MySQL原本的限制。

关联子查询

MySQL的子查询实现的非常糟糕。最糟糕的一类查询是WHERE条件中包含IN()的子查询语句。

SELECT * FROM sakila.film
WHERE film_id IN(
   SELECT film_id FROM sakila.film_actor WHERE actor_id = 1);
-- 认为的执行方式
-- 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,25,106,140,166,277,361,438,499,506,509,605,635,749,832,939,970,980);
-- 实际的执行方式,将相关的外层表压到子查询中
SELECT * FROM sakila.film
WHERE EXISTS (
   SELECT * FROM sakila.film_actor WHERE actor_id = 1
   AND film_actor.film_id = film.film_id);
-- 根据file表全表扫描,根据film_id逐个进行子查询
EXPLAIN SELECT * FROM sakila.film ...;
+----+--------------------+------------+--------+------------------------+
| id | select_type        | table      | type   | possible_keys          |
+----+--------------------+------------+--------+------------------------+
|  1 | PRIMARY            | film       | ALL    | NULL                   |
|  2 | DEPENDENT SUBQUERY | film_actor | eq_ref | PRIMARY,idx_fk_film_id |
+----+--------------------+------------+--------+------------------------+

使用JOIN来改写查询:

SELECT film.* FROM sakila.film
   INNER JOIN sakila.film_actor USING(film_id)
WHERE actor_id = 1;

另一个优化办法是使用函数GROUP_CONCAT在IN中构造另一个都好分隔的列表,有时这比关联查询更快。

还有一种使用EXIST等效的改写来获取更好的效率。

SELECT * FROM sakila.film
WHERE EXISTS(
   SELECT * FROM sakila.film_actor WHERE actor_id = 1
      AND film_actor.film_id = film.film_id);
如何用好关联子查询

并不是所有的关联子查询性能都会很差。先测试,然后做出自己的判断。

  • 不要听从关于子查询的“绝对真理”
  • 应该用测试来验证对子查询的执行计划和响应时间的假设。

UNION的限制

如果希望UNION的各个子句能够根据LIMIT只去部分结果集,或者希望能够先排好序再合并结果集的话,就需要在UNION的各个子句中分别使用这些子句。

(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;

这会查询虽有结果到临时表然后再取出20条。使用下面方式限制临时表大小

(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;

但是这是无序的,如果想要获得正确的顺序,还要获得全局的ORDER BY和LIMIT。

索引合并优化

当WHERE子句中包含多个复杂条件的时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行。

等值传递

等值传递有时候会遇到意想不到的消耗,例如IN()列表非常大的时候。

MySQL优化器发现存在WHERE、ON或者USING的子句,将这个列表的值和另一个表的某个列相关联,那么优化器会将IN()列表都复制应用到关联的各个表中。

并行执行

MySQL无法利用多核特性来并行执行查询。

哈希关联

MySQL并不支持哈希关联,所有的关联都是嵌套循环关联。

可以通过建立一个哈希索引来曲线地实现哈希关联。

如果是Memory存储引擎,则索引是哈希索引,关联也是哈希关联。

松散索引扫描

不支持松散索引扫描(不连续的方式扫描一个索引)。

MySQL的索引扫描需要先定义一个起点和终点,即使需要的数据只是这段索引中的很少数几个,MySQL仍需要扫描这段索引的每个条目。

5.0以后的版本中,有些场景可以使用松散索引扫描。

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
         type: range
possible_keys: NULL
          key: PRIMARY
      key_len: 2
          ref: NULL
         rows: 396
        Extra: Using index for group-by

Using index for group-by表示这里将使用松散索引扫描。

5.6以后松散索引扫描的一些限制将会通过“索引条件下推”的方式来解决。

最大值和最小值优化

SELECT MIN(actor_id) FROM sakila.actor WHERE first_name = 'PENELOPE';

由于first_name上没有索引,所以MySQL会做全表扫描。

移除MIN()使用LIMIT重写查询:

-- 告诉MySQL如何执行,但是SQL并不能一眼看出是要最小值
SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) WHERE first_name = 'PENELOPE' LIMIT 1;

同一个表上查询和更新

不允许对同一张表同时进行查询和更新。

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 cant specify target table 'outer_tbl' for update in FROM clause

通过临时表绕过:

UPDATE tbl
   INNER JOIN(
      SELECT type, count(*) AS cnt
      FROM tbl
      GROUP BY type
   ) AS der USING(type)
SET tbl.cnt = der.cnt;

实际上执行了两个查询:一个是子查询的SELECT语句,另一个是多表关联UPDATE,只是关联的表示一个临时表。 子查询会在UPDATE语句打开表之前就完成。

查询优化器的提示(hint)

如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终的执行计划。

  • HIGH_PRIORITY and LOW_PRIORITY
    • 语句的优先级
    • HIGH_PRIORITY用于SELECT的时候,会将SELECT语句放在表队列的最前面,在表修改数据的语句之前。
    • HIGH_PRIORITY用于INSERT语句,抵消了全局的LOW_PRIORITY的影响
    • LOW_PRIORITY使语句一直处于等待状态,只要队列中还有需要访问同一个表的语句,即使接收到的更晚。
    • LOW_PRIORITY可以对SELECT、INSERT、UPDATE和DELETE语句使用。
    • 这两个语句对支持表锁的存储引擎有效。
    • 不要再InnoDB或者其它有细粒度锁机制和并发控制的引擎使用,即使MyISAM中也要注意。因为会导致并发插入被禁用,可能会严重降低性能。
    • 只是简单的控制了队列的顺序。
  • DELAYED
    • 对INSERT和REPLACE有效。
    • 提示的语句立即返回给客户端,并将插入的行数据放入到缓冲区,然后在表空闲时批量将数据写入。对I/O密集型应用很有效。
    • 并不是所有的存储引擎都支持这样的做法,会导致LAST_INSERT_ID()无法正常工作。
  • STRAIGHT_JOIN
    • 放在SELECT语句的SELECT关键字之后,也可以放置在任何两个关联表的名字之间。
    • 第一个用法是让查询中所有的表按照在语句中出现的顺序进行关联。
    • 第二个用法则是固定其前后两个表的关联顺序。
    • 当MySQL没能选择正确的关联顺序的时候,或者由于可能的顺序太多导致MySQL无法评估所有的关联顺序的时候(长时间statistics状态)很有用。
    • 先EXPLAIN查看关联顺序,然后按照顺序改写语句,加上关键字
  • SQL_SMALL_RESULT and SQL_BIG_RESULT
    • 只对SELECT语句有效
    • 告诉优化器对GROUP BY或者DISTINCT查询如何使用临时表及排序。
    • SQL_SMALL_RESULT表示结果很小,使用内存的索引临时表
    • SQL_BIG_RESULT表示结果可能会非常大,使用磁盘临时表做排序
  • SQL_BUFFER_RESULT
    • 提示将查询结果放入到一张临时表,然后尽可能快的释放表锁。
    • 当无法使用客户端缓存的时候,使用服务器端缓存更有效。
  • SQL_CACHE and SQL_NO_CACHE
    • 结果集是否应该缓存在查询缓存中。
  • SQL_CALC_FOUND_ROWS
    • 返回除去LIMIT子句后这个查询需要返回的结果集的总数,而实际上返回的只有LIMIT要求的子集。
    • 可以通过FOUND_ROW()获得这个值。
  • FOR UPDATE and LOCK IN SHARE MODE
    • 控制SELECT语句的锁机制,但只对实现了行级锁的存储引擎有效。
    • INSERT..SELECT语句不需要,会自动加锁
    • 唯一内置支持这两个提示的存储引擎是INNODB
    • 会导致覆盖索引等优化无法正常使用,滥用会导致服务器锁争用问题。
  • USE INDEX, IGNORE INDEX, and FORCE INDEX
    • 提示使用或者不使用哪些索引来查询记录
    • 5.1以后可以通过新增选项FOR ORDER BY和FOR GROUP BY来指定是否对排序和分组有效
    • FORCE INDEX会告诉优化器全表扫描的成本会远远高于索引扫描

5.0以后一些服务器变量影响优化:

  • optimizer_search_depth
    • 控制优化器在穷举执行计划的限度,如果长时间处于Statistics状态,那么可以考虑调低此参数
  • optimizer_prune_level
    • 默认打开,根据需要扫描的行数来判断是否跳过某些执行计划
  • optimizer_switch
    • 包含了一些开启/关闭优化器特性的标志位。如索引合并特性。

控制优化器是不好的,收益甚小却给维护带来了额外的工作量,在版本升级的时候导致新的优化策略失效。

Percona Toolkit中的pt-upgrade工具,可以检查在新版本中运行的SQL是否与老版本一样,返回相同的结果。

优化特定类型的查询

随着版本升级,优化器自己会实现更更多的优化技巧。

优化COUN()查询

  • 统计某个列值的数量(要求非空)
    • 在括号中指定了列或者列的表达式,则统计的是表达式有值的结果数
  • 统计行数

COUNT(*)不会扩展成所有的列,而是直接统计行数。如果统计行数,直接用这个表达式会更好,避免歧义,性能也更好

MyISAM

只有COUNT()函数不带任何条件的时候可以避免查询直接利用存储引擎特性获取结果。否则和其它引擎没哟区别。

利用这个特性可以减少扫描的行数量:

SELECT COUNT(*) FROM world.City WHERE ID > 5;
-- 反转并做减法减少扫描行数
SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*) FROM world.City WHERE ID <= 5;

简单的优化

互斥条件的COUNT():

-- 使用SUM
SELECT SUM(IF(color = 'blue', 1, 0)) AS blue,SUM(IF(color = 'red', 1, 0)) AS red FROM items;
-- 满足条件为真
SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color = 'red' OR NULL) AS red FROM items;

使用近似值

有时候业务场景并不要求完全精确的COUNT值,此时可以用近似值代替。 EXPLAIN的估算行数是一个不错的近似值,这不需要真正去执行。

更复杂的优化

通常COUNT()都需要扫描大量的行才能获得精确的结果,因此是很难优化的。

在MySQL还能做的就只有覆盖索引扫描了。如果还不够,可以考虑修改应用架构增加汇总表。或者增加外部缓存系统

优化关联查询

  • 确保ON和USING子句中的列上有索引,创建表的时候就要考虑到关联的顺序。
    • 没有用的索引也会带来负担,一般来说除非有其它理由,否则只要在关联顺序中的第二个表的相应列上创建索引。
  • 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列。
    • MySQL有可能使用索引优化这个过程。
  • 升级MySQL需要注意,关联语法、运算符优先级等其它可能会发生变化的地方。
    • 因为普通关联可能变成笛卡尔积,不同关联会产生不用结果。

优化子查询

尽可能的使用关联查询代替子查询,至少当前版本MySQL需要这样。5.6以后的版本可能可以忽略这些建议。

优化GROUP BY和DISTINCT

MySQL都使用同样的办法优化这两种查询,事实上,MySQL优化器会在内部处理的时候相互转化这两类查询。

无法使用索引的时候,GROUP BY使用两种方式完成:使用临时表或则文件排序来做分组

如果需要对关联查询做分组(GROUP BY),并且是按照表中的某个列进行分组,那么通常采用查找表的标识列分组的效率会比其它列效率高。

-- 不好
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;
-- 更好
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列分组的效率会更好

-- 满足关系理论,但是效率不高,临时表没有索引
SELECT actor.first_name, actor.last_name, c.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) ;

如果没有显示的指定排序列,当查询使用GROUP BY子句的时候,结果集会自动按照分组的字段进行排序。 如果不关心结果集的顺序,使用默认排序又导致了需要文件排序,则可以使用ORDER BY NULL,让MySQL不在进行文件排序。

优化GROUP BY WHTI ROLLUP

WITH ROLLUP对分组结果再做一次超级聚合,获得总的统计信息,这是通过文件排序或者临时表实现的。最好将这些功能放到应用程序处理。

优化LIMIT分页

如果有索引,偏移量加LIMIT语句的方式,配合ORDER BY效率会不错,否则MySQL需要大量的文件排序操作。

如果偏移量较大,将会查询前面的前部结果然后舍弃分页外的部分数据。 要优化这种查询,要么限制分页的数量,要么是优化大偏移量的性能。

一个最简单的方式就是尽可能的使用覆盖索引,而不是查询所有的列。然后使用延迟关联查询所需的列。

SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50, 5;
-- 大数据量的改写:
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);

有时候也可以将LIMIT查询转换为已知位置的查询,通过范围扫描来获得结果:

SELECT film_id, description FROM sakila.film WHERE position BETWEEN 50 AND 54 ORDER BY position;

如果可以记录上次翻页的位置,可以避免使用OFFSET:

-- 无论翻页到多么后面,性能都很好
SELECT * FROM sakila.rental
WHERE rental_id < 16030
ORDER BY rental_id DESC LIMIT 20;

其它优化还包括汇总表和关联的冗余表等。

优化SQL_CALC_FOUND_ROWS

增加了这个提示后,会扫描所有满足条件的行,然后抛弃不需要的行,所以代价可能很高。

  • 一个更好的设计是不显示总数,每次查询的数据多一条,如果每次多出一条就有下一页,否则没有
  • 先缓存并获取较多的数据,然后增加一个获取结果之外的1000条之类的按钮
  • 考虑使用类似EXPLAIN语句的近似值,当需要时,再使用COUNT(*)来满足需求

优化UNION查询

MySQL总是通过填充临时表的方式来执行UNION查询。经常需要手动指定多个相同条件到不同的子查询中。

除非确实需要消除重复行,否则一定要使用UNION ALL,否则服务器会在临时表上加上DISTINCT选项,导致整个临时表的数据唯一性检查。

静态查询分析

Percona Toolkit中的pt-query-advisor能够分析慢查询日志、分析查询模式,然后给出所有可能有潜在问题的查询,并给出足够详细的建议。

使用用户自定义变量

定义:

SET @one       := 1;
SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
SET @last_week := CURRENT_DATE-INTERVAL 1 WEEK;

使用:

SELECT ... WHERE col <= @last_week;

属性和限制:

  • 使用自定义变量的查询,无法使用查询缓存
  • 不能在常量或者标识符的地方使用自定义变量,例如表名、列名和LIMIT子句中
  • 用户自定义变量的生命周期实在一个连接中有效,所以不能用它们来做连接间的通信
  • 如果使用连接池或者持久化连接,自定义变量可能让看起来毫无关系的代码发生交互,通常是BUG
  • 5.0之前大小写敏感,要考虑兼容问题
  • 不能显示的声明变量类型
    • 最好在初始化时赋初值,
    • 是一个动态类型
  • 优化器某些场景下可能将这些变量优化掉,导致代码运行结果过不可预想
  • 赋值的顺序和赋值的时间点并不总是固定的,依赖于优化器的决定
  • 赋值号:=的优先级非常低,赋值表达式需要使用明确的括号
  • 使用未定义变量不会产生任何语法错误

优化排名语句

可以在使用时改变变量的值,拥有左值特性。

SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS rownum
FROM sakila.actor LIMIT 3;
+----------+--------+
| actor_id | rownum |
+----------+--------+
|        1 |      1 |
|        2 |      2 |
|        3 |      3 |
+----------+--------+

复杂查询:

SET @curr_cnt := 0, @prev_cnt := 0, @rank := 0;
SELECT actor_id,
   @curr_cnt := cnt AS cnt,
   @rank     := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) 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 |
|      106 |  35 |    7 |    35 |
|       60 |  35 |    7 |    35 |
|       13 |  35 |    7 |    35 |
|      158 |  35 |    7 |    35 |
+----------+-----+------+-------+

避免重复查询刚刚更新的数据

UPDATE t1 SET lastUpdated = NOW() WHERE id = 1;
SELECT lastUpdated FROM t1 WHERE id = 1;
-- 使用变量
UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW();
SELECT @now;

统计更新和插入的数量

存在则更新,同时维护一个变量的递增。乘以0以不影响插入值。

INSERT INTO t1(c1, c2) VALUES(4, 4), (2, 1), (3, 1)
ON DUPLICATE KEY UPDATE
   c1 = VALUES(c1) + ( 0 * ( @x := @x +1 ) );

确定取值的顺序

由于执行时机不同,所以在查询的不同阶段使用变量会造成意想不到的结果。

SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS cnt
FROM sakila.actor
WHERE @rownum <= 1;
+----------+------+
| actor_id | cnt  |
+----------+------+
|        1 |    1 |
|        2 |    2 |
+----------+------+

解决问题的方法是让变量的赋值和取值发生在执行查询的同一阶段:

SET @rownum := 0;
SELECT actor_id, @rownum AS rownum
FROM sakila.actor
WHERE (@rownum := @rownum + 1) <= 1;
+----------+--------+
| actor_id | rownum |
+----------+--------+
|        1 | 1      |
+----------+--------+

如果加上ORDER BY语句,会出现更意外的结果。原因可以在EXPLAIN的Using where、Using temporary、Using filesort中找到。

SET @rownum := 0;
SELECT actor_id, first_name, @rownum AS rownum
FROM sakila.actor
WHERE @rownum <= 1
ORDER BY first_name, LEAST(0, @rownum := @rownum + 1);

LEAST函数在不影响排序的时候完成复制操作。

编写偷懒的UNION

SELECT id FROM users WHERE id = 123
UNION ALL
SELECT id FROM users_archived WHERE id = 123;

上面的查询可以正常工作,但是users查询到结果后还会继续查询。

-- 如果查到记录就在结果列中做一次赋值,将赋值放到函数GREATEST中来避免返回额外的数据。
-- 在查询的末尾将变量重置为NULL,保证遍历时不干扰后面的结果
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;

用户自定义变量的其它用处

  • 查询运行时计算总数和平均值
  • 模拟GROUP语句中的函数FIRST()和LAST()
  • 对大量数据做一些数据计算
  • 计算一个大表的MD5散列值
  • 编写一个样本处理函数,当样本中的数值超过某个边界值的时候将其变成0
  • 模拟读/写游标
  • 在SHOW语句的WHERE子句中加入变量值

常用的优化策略

优化通常需要三管齐下:不做、少做、尽快的做。

  • 尽量少做事,可以的话就不要做任何事。除非不得已,否则不要使用轮询,因为会增加负载。
  • 尽可能快的完成需要做的事情。尽量使用UPDATE代替SELECT FOR UPDATE然后再UPDATE的写法。
    • 因为事务提交的越快,持有锁的时间就越短,可以大大减少竞争和加速串行高执行效率。
  • 将已处理和未处理数据分开,保证数据集足够小。
  • 某些查询时无法优化的,考虑使用不同的查询或者不同的策略去实现相同的目的。
  • 需要的时候尽可能让应用程序完成一些计算。

MySQL高级特性

  • 分区表
    • 是一种粗粒度的、简易的索引策略,适用于大数据量的过滤场景。
    • 最适合在没有合适的索引时对分区进行扫描或者只有一个分区和索引是热点,而且可以放入内存中。
    • 限制单个表分区不要超过150个,并且注意某些导致无法分区的细节
    • 分区表对于单条记录的查询并没有什么优势
  • 视图
    • 对多个表的复杂查询,使用视图有时候会大大简化问题。
    • 当视图使用临时表时,无法将WHERE条件下推到具体的表,也不能使用索引,要注意这种情况的查询性能。
    • 为了便利,使用视图是很合适的。
  • 外键
    • 将约束放进MySQL中,对于必须维护外键的场景,性能会很高。
    • 也带来很多额外的复杂性和索引消耗,还会增多表之间的交互,导致更多的锁和竞争。
    • 在意系统性能的时候不使用外键,而是使用应用程序来维护
  • 存储过程
    • 对于基于语句的复制有很多问题。
    • 可以节省网络开销。
  • 绑定变量
    • 减少解析和执行计划生成的开销。适合大量重复执行语句的时候。
    • 使用二进制协议比普通方式更快
  • 插件
    • 最大程度的扩展MySQL
  • 字符集
    • 使用UTF-8会消耗更多的磁盘和内存空间,因为总是按照三个字节的最大占用空间来分配
    • 字符集应该匹配,否则可能导致索引无法使用
  • 全文索引
    • 使用外部方案解决
  • XA事务
    • XA会带来性能问题
    • XA在内部的二进制日志和存储引擎之间发挥作用
  • 查询缓存
    • 高并发压力环境中查询缓存会导致系统性能的下降,甚至僵死
    • 如果一定要使用,不要使用太大内存,而且要明确收益
    • 建议使用外部替代方案

分区表

对于用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成。实现分区的代码实际上是对一组底层表的句柄对象的封装。
对分区表的请求,都会通过句柄对象转化成对存储引擎的接口调用。所以分区对于SQL层来说是一个完全封装底层实现的黑盒子,对应用是透明的, 但是从底层文件系统就很容易发现,每一个分区表都有一个使用#分隔命名的表文件。

MySQL实现分区表的方式:对底层表的封装,意味着索引也是按照分区的子表定义的,而没有全局索引。

MySQL使用PARTITION BY子句定义每个分区存放的数据。

在执行查询的时候,优化器会根据分区定义过滤那些没有需要的数据的分区。

分区的一个主要目的是将数据按照一个较粗的粒度分在不同的表中。这样可以将相关数据放在一起。另外,如果想一次批量删除整个分区的数据也变得很方便。

适用场景:

  • 表非常大以至于无法全部存放在内存中,或者只在表的最后部分有热点数据,其它均是历史数据
  • 分区表的数据更容易维护。
    • 如批量删除分区数据
    • 对独立分区进行优化、检查、修复等操作
  • 分区表的数据可以分布在不同物理设备上,从而高效的利用多个硬件设备。
  • 可以使用分区表来避免特殊的瓶颈,如InnoDB的单个索引的互斥访问、ext3文件系统的inode锁竞争等
  • 如果需要,还可以备份和恢复独立的分区,这样在非常大的数据集场景下效果非常好。

限制:

  • 一个表最多有1024个分区
  • 在5.1中,分区表达式必须是整数或者是返回整数的表达式。在5.5中,某些场景中可以直接使用列来分区
  • 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来
  • 分区表中无法使用外键约束

分区表的原理

存储引擎管理分区表和普通表一样,分区表的索引只是在各个底层表上各自加上一个完全相同的索引。

分区表上的操作逻辑:

  • SELECT
    • 分区层先打开并锁住所有的底层表
    • 优化器先判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据
  • INSERT
    • 分区层先打开并锁住所有的底层表
    • 然后确定那个分区接收这条记录,再将记录写入对应底层表
  • DELETE
    • 分区层先打开并锁住所有的底层表
    • 然后确定数据对应的分区,最后对相应底层表进行删除操作
  • UPDATE
    • 分区层先打开并锁住所有的底层表
    • MySQL先确定需要更新的记录在哪个分区,然后取出数据并更新
    • 再判断更新后的数据应该放在那个分区,最后对底层表进行写入操作,并对原数据所在的底层表进行删除操作。

有些操作是支持过滤的,如DELETE和UPDATE的WHERE条件恰好和分区表达式匹配,就可以将所有不包含记录的分区都过滤掉。INSERT本身就对应一个分区。 MySQL先确定这条记录属于哪个分区,再将记录写入对应的底层分区表,无须对任何其它分区进行操作。

虽然每个操作都会先打开并锁住所有的底层表,但这并不是说所有分区表的处理过程中是锁住全表的, 如果存储引擎能够自己实现行级锁,如InnoDB,则会在分区层释放对应表锁。 这个加锁和解锁过程与普通InnoDB上的查询类似。

分区表的类型

MySQL支持多种分区表,使用最多的是根据范围进行分区。 玫瑰阁分区存储落在某个范围的记录,分区表达式可以是列,也可以是包含列的表达式。

CREATE TABLE sales (
   order_date DATETIME NOT NULL,
   -- Other columns omitted
) ENGINE=InnoDB PARTITION BY RANGE(YEAR(order_date)) (
    PARTITION p_2010 VALUES LESS THAN (2010),
    PARTITION p_2011 VALUES LESS THAN (2011),
    PARTITION p_2012 VALUES LESS THAN (2012),
    PARTITION p_catchall VALUES LESS THAN MAXVALUE );

PARTITION分区子句可以使用各种函数,但是返回值要是一个确定的整数,且不能是一个常数。

MySQL还支持键值、哈希和列表分区中,这其中还支持子分区(哈希子分区将数据切分成多个小片,大大降低互斥量的竞争),不过在生产环境中很少见到。 5.5中还支持RANGE COLUMNS分区,这样使基于时间的分区也无需再将其转化成一个整数。

其它分区技术:

  • 根据键值进行分区,来减少InnoDB的互斥量竞争
  • 使用数学模函数来进行分区,然后将数据轮询放入不同的分区。
  • 有自增主键id,希望根据时间最近热点数据集中存放。分区表达式(HASH id DIV 1000000),为100万数据建立一个分区。实现了分区的目的,比起时间分区还避免了超过一定阈值需要建立一个新增分区的问题。

如何使用分区表

当数据量非常大的时候,B-Tree索引就无法起作用了。除非是索引覆盖查询,否则数据库服务器需要根据索引扫描的结果回表, 查询所有符合条件的记录,如果数据量巨大,将产生随机I/O,随之数据库的响应时间将大到不可接受的程度。
另外,索引维护(磁盘空间、I/O操作)的代价也非常高。所以如Infobright完全放弃了B-Tree索引,而选择了一些更粗粒度的但消耗更小的方式检索数据,如在大数据量上只索引对应的一小块元数据。

分区所做的事就是以代价非常小的方式定位到需要的数据在哪一片区域。在这片区域内,可以做顺序扫描,可以建索引,还可以将数据缓存到内存等等。

分区不需精确定位每条数据的位置,也就无需额外的数据结构记录每个分区有哪些数据,所以代价非常低。只需要一个简单的表达式就你可以表达每个分区存放的是什么数据。

为了保证大数据量的可扩展性,一般有下面两个策略:

  • 全量扫描数据,不要任何索引
    • 简单的分区方式存放表,不要任何索引,根据分区的规则大致定位需要的数据位置。
    • 只要能够使用WHERE条件,将需要的数据限制在少数分区内,则效率是很高的
    • 使用该策略假设不用将数据完全装入内存,同时还假设需要的数据全部都在磁盘上,因为内存很小,数据很快会被挤出内存,所以缓存起不了任何作用。
    • 适用于以正常方式访问大量数据的时候。
  • 索引数据,并分离热点
    • 将热点数据单独放在一个分区中,让这个分区的数据能够有机会都缓存在内存中。
    • 这样查询就可以只访问一个很小的分区表,能够使用索引,也能够有效的使用缓存。

什么情况下会出问题

上面的两种分区策略都基于两个非常重要的假设:

  • 查询都能过滤掉很多额外的分区
  • 分区本身不会带来很多额外的代价。

这两个假设在某些场景下会有问题:

  • NULL值会使分区过滤无效
    • 第一个分区是一个特殊分区。如果NULL值或非法值,则记录被存放到第一个分区。
    • MySQL会检查第一个分区和目标分区,因为如YEAR()函数在接收非法值的时候会返回NULL值。如果第一个分区非常大,特别是全量扫描数据,不要任何索引的策略时,代价会非常大。
    • 扫描两个分区来查找列不是我们使用分区表的初衷,可以创建一个无用的第一个分区,如PARTITION p_nulls VALUES LESS THAN (0)来创建第一个分区。即使检查第一个分区,代价也非常小。
    • 5.5中不需要上面的优化技巧,可以直接PARTITION BY RANGE COLUMNS(order_date)。
  • 分区列和索引列不匹配
    • 会导致无法进行分区过滤。
    • 应该避免建立和分区列不匹配的索引,除非查询中还同时包含了可以过滤分区的条件。
  • 选择分区的成本可能很高
    • 服务器需要扫描所有的分区定义的列表来找到正确的答案。类似这样的线性搜索的效率不高,所以随着分区数的增长,成本会越来越高。
    • 按行写入大量数据的时候,这个代价会跟高。可以通过限制分区的数量来缓解此问题,根据时间经验,100个左右的分区是没问题的。
    • 其它类型的分区,如键分区和哈希分区,则没有这样的问题
  • 打开并锁住所有底层表的成本可能很高
    • 这个操作发生在分区过滤之前。
    • 会影响所有查询,对一些本身操作非常快的查询会带来明显的额外开销。
    • 可以使用批量操作的方式降低单个操作的此类开销。
    • 同时还是需要限制分区个数。
  • 维护分区的成本可能很高
    • 某些维护操作如创建和删除非常快
    • 重组或者类似ALTER语句的操作,需要复制数据会很慢

其它限制:

  • 所有分区必须使用相同的存储引擎
  • 分区函数中可以使用的函数和表达式也有一些限制
  • 某些存储引擎不支持分区
  • 对于MyISAM分区表,不能再使用LOAD INDEX INTO CACHE操作
  • 对于MyISAM表,使用分区表时需要打开更多的文件描述符。分区表只占用一个表缓存条目,文件描述符还是需要多个,会出现超过文件描述符限制问题。

5.6中对分区表做了很多增强,如ALTER TABLE EXCHANGE PARTITION。

查询优化

分区的最大优点就是优化器可以根据分区函数来过滤一些分区。根据粗粒度索引的优势,通过分区过滤通常可以让查询扫描更少的数据。

所以对于访问分区表来说,很重要的一点是要在WHERE条件中带入分区列,这样可以让优化器过滤掉无需访问的分区。 如果没有这些条件,MySQL就需要让对应的存储引擎访问这个表的所有分区,如果表非常大的话,就可能非常慢。

通过语句查看优化器是否执行了分区过滤:

EXPLAIN PARTITIONS SELECT * FROM sales \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: sales_by_day
   partitions: p_2010,p_2011,p_2012
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 3
        Extra:
EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE day > '2011-01-01'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: sales_by_day
   partitions: p_2011,p_2012

MySQL可以将范围条件转化为离散的值列表,并根据列表中的每个值过滤分区。 然而,优化器也不是万能的:

EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE YEAR(day) = 2010\G

MySQL只能使用分区函数的列本身进行比较时才能过滤分区,而不能根据表达式的值过滤分区。

改写查询:

-- 带入分区列,而不是分区表达式
EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE day BETWEEN '2010-01-01' AND '2010-12-31'\G

即便在创建分区时可以使用表达式,但在查询时却只能根据列来过滤分区。

优化器在处理查询的过程中尽可能聪明的去过滤分区。如分区表是关联操作中的第二张表,且关联条件是分区键,MySQL就只会在对应的分区里匹配行。 EXPAIN语句无法显示这种情况下的分区过滤,因为这是运行时分区过滤,而不是查询优化阶段的。

合并表

合并表是一种早期的、简单的分区实现,和分区表相比有一些不同的限制,并且缺乏优化。 分区表严格来说是一个逻辑上的概念,用户无法访问底层的各个分区,对用户来说是透明的。但是合并表余怒许用户单独访问各个子表。 分区表和优化器的结合更紧密,这也是未来的发展趋势,合并表则是一种将被淘汰的技术。

视图

视图本身是一个虚表,不存放任何数据。 在使用SQL语句访问视图的时候,它返回的数据是MySQL从其它表中生成的。

MySQL在很多地方对于视图和表示同样对待的。不过视图和表也有不同,例如不能对视图创建触发器。

-- 创建视图
CREATE VIEW Oceania AS
   SELECT * FROM Country WHERE Continent = 'Oceania'
   WITH CHECK OPTION;
-- 使用临时表模拟视图
CREATE TEMPORARY TABLE TMP_Oceania_123 AS
   SELECT * FROM Country WHERE Continent = 'Oceania';
SELECT Code, Name FROM TMP_Oceania_123 WHERE Name = 'Australia';

临时表有很明显的性能问题,优化器也很难优化这个临时表上的查询。实现视图的更好方法是,重写含有视图的查询,将视图的定义SQL直接包含进查询的SQL中。

SELECT Code, Name FROM Country
WHERE Continent = 'Oceania' AND Name = 'Australia';

MySQL可以是用这两种办法中的任何一种来处理视图,这两种算法分别称为合并算法(MERGE)和临时表算法(TEMPTABLE),如果可能,会尽可能的使用合并算法。

MySQL可以嵌套定义视图,在EXPALIN EXTENDED之后,SHOW WARNINGS来查看使用视图的查询重写后的结果。

视图的两种实现

如果视图中包含GROUP BY、DISTINCT、任何聚合函数、UNION、子查询等,只要无法在原表记录和视图中建立一一映射的场景中,MySQL都将使用临时表算法来实现视图。

如果想确定使用的是哪种算法,可以EXPLAIN一条针对视图的简单查询:

-- DERIVED表示为临时表算法实现的
-- 5.5和更老的版本中,EXPLAIN是需要实际执行并产生该派生表
EXPLAIN SELECT * FROM <view_name>;
+----+-------------+
| id | select_type |
+----+-------------+
|  1 | PRIMARY     |
|  2 | DERIVED     |
+----+-------------+

视图的算法是视图本身的属性,和作用在视图上的查询语句无关。

-- 指定算法,真正查询的时候需要临时表,创建语句不需要
CREATE ALGORITHM=TEMPTABLE VIEW v1 AS SELECT * FROM sakila.actor;

可更新视图

如果指定了合适的条件,就可以跟新、删除甚至向视图写入数据。如果视图定义中包含了GROUP BY、UNION、聚合函数以及一些特殊情况,就不能被更新了。 被更新的列必须来自同一个表,另外,所有使用临时表算法实现的视图都无法被更新。 通过视图更新的行必须符合视图本身的WHERE条件。所以不能更新视图定义列以外的列。

mysql> UPDATE Oceania SET Continent = 'Atlantis';
ERROR 1369 (HY000): CHECK OPTION failed 'world.Oceania'

视图对性能的影响

在某些情况下视图可以帮助提升性能,而且视图还可以和其它提升性能的方式叠加使用。

在重构schema的时候,可以通过视图维持应用程序不变的运行。

外层查询的WHERE条件无法下推到构建视图的临时表的查询中,临时表也无法创建索引。

如果需要用视图来提升性能,需要做比较详细的测试。即使合并算法实现的视图,也会有额外的开销,而且视图的性能很难预测。

视图的限制

不会保存创建视图的原始语句。可以通过.frm文件的最后一行获得一些信息。

-- 如果有权限,LOAD_FILE命令显示结果
SELECT
   REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
   REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
      SUBSTRING_INDEX(LOAD_FILE('/var/lib/mysql/world/Oceania.frm'),
      '\nsource=', 1),
   '\\_','\_'), '\\%','\%'), '\\\\','\\'), '\\Z','\Z'), '\\t','\t'),
   '\\r','\r'), '\\n','\n'), '\\b','\b'), '\\\"','\"'), '\\\'','\''),
   '\\0','\0')
AS source;

外键约束

InnoDB是目前MySQL中唯一支持外键的内置存储引擎。

使用外键是有成本的,比如外键通常都要求每次在修改数据时都要在另外一张表中多执行一次查找操作。虽然InnoDB强制外键使用索引,但是还是无法完全消除开销。

如果外键选择性很低,会导致一个非常大的选择性很低的索引。

在某些情况下,外键会提升一些性能。如数据一致性检查和外键相关数据删除、更新。 外键维护操作是逐行进行的,所以比批量删除和更新慢一些。

外键约束使得查询需要访问一些别的表,这也意味着需要额外的锁。如果向子表中写入一条记录,外键约束会让InnoDB检查对应的父表的记录, 也就需要对父表对应记录进行加锁操作,来确保这条记录不会再这个事务完成之时就被删除了。 这会导致额外的锁等待,甚至会导致死锁。这种死锁往往很难排查。

有时可以通过触发器来代替外键,对于相关数据的同时更新外键更合适,但如果外键只是用作数值约束,那么触发器或者显式地限制取值会更好些。 也可以使用ENUM类型。

如果只是做约束,那通常在应用程序中实现改约束会更好。外键会带来很大的额外消耗。

MySQL内部存储代码

可以通过触发器、存储过程、函数的形式来存储代码。5.1开始,还可以在定时任务中存储代码,也被称为事件。

存储代码的优点:

  • 节省带宽和网络延迟
  • 是一种代码重用,方便的统一业务规则,也提供一定的安全性
  • 简化代码的维护和版本更新
  • 提升安全,如细粒度的权限控制
  • 可以缓存存储过程执行计划
  • 存储程序的维护工作会很简单
  • 实现开发人员的良好分工

存储代码的缺点:

  • MySQL本身没有提供好的开发和调试工具
  • 存储代码的效率稍微差一些。如存储代码中可以使用的函数非常有限
  • 会给应用程序代码的部署带来额外的复杂性
  • 可能有安全隐患,如加密算法和数据同时泄露
  • 给数据库服务器带来额外压力
  • 没有什么选项可以控制存储过程的消耗
  • 存储代码实现也有很多限制,功能还非常弱
  • 调试存储过程是一件很难的事
  • 基于语句的二进制日志复制合作的并不好

存储过程和函数

限制:

  • 优化器无法使用DETERMINISTIC关键字来优化单个查询中多次调用存储过程的情况
  • 优化器无法评估存储函数的执行成本
  • 每个连接都有独立的存储函数的执行计划缓存,这将导致浪费缓存空间来反复缓存同样的执行计划
  • 存储程序和复制是一组诡异的组合,如果可以直接复制数据而不是调用更好

通常希望存储过程越小、越简单越好。特别是一个调用替代很多小查询的时候,相比查询执行的成本,解析和网络的开销就会变得很明显,存储过程就会显得快很多。

DROP PROCEDURE IF EXISTS insert_many_rows;

delimiter //

CREATE PROCEDURE insert_many_rows (IN loops INT)
BEGIN
   DECLARE v1 INT;
   SET v1=loops;
   WHILE v1 > 0 DO
     INSERT INTO test_table values(NULL,0,
               'qqqqqqqqqqwwwwwwwwwweeeeeeeeeerrrrrrrrrrtttttttttt',
               'qqqqqqqqqqwwwwwwwwwweeeeeeeeeerrrrrrrrrrtttttttttt');
     SET v1 = v1 - 1;
   END WHILE;
END;
//

delimiter ;

插入一百万数据的执行时间;

Method Total time
Stored procedure 101 sec
Client application 279 sec
Client application with MySQL Proxy 307 sec

触发器

可以让你在执行INSERT、UPDATE或者DELETE的之前或者之后,执行一些特定的操作。

触发器本身没有返回值,但是可以读取或者改变SQL的受影响行数。

触发器可以减少客户端和服务器之间通信,所以触发器可以简化应用程序逻辑,还可以提高性能。 还可以用于自动更新范式化数据和汇总表数据。

限制:

  • 对于每一个表的每一个事件,最多只能定义一个触发器
  • 只支持基于行的触发,始终是针对一条记录的,而不是针对整个SQL语句。如果变更的数据集非常大的话,效率会很低。
  • 会掩盖服务器背后的工作,会使SQL影响的记录书翻一倍
  • 问题很难排查,很难分析和定位
  • 可能导致死锁和等待。触发器失败则SQL也失败

从性能考虑,限制最大的就是基于行的触发设计。因为性能原因,很多时候无法使用触发器来维护汇总表和缓存表。 但是触发器可以保证数据总是一致的。

触发器并不能保证更新的原子性。如在MyISAM上更新,触发器失败不会导致执行语句回滚。 在InnoDB表上触发器是在一个事务中完成的。所以操作是原子的。

在InnoDB表上使用触发器检查数据一致性的时候,需要特标小心MVCC,可能会得到错误的结果。 如没有使用SELECT FOR UPDATE语句检查,并发的修改语句同时更新,会导致数据不一致。

事件

5.1引入。指定MySQL在某个时候执行一段SQL代码,或者每隔一段时间执行一段SQL代码(通常调用存储过程)。

事件在一个独立事件调度线程中呗初始化,这个线程和处理连接的线程没有任何管理,不接收任何参数,也没有任何返回值。

可以在INFORMATION_SCHEMA.EVENTS表中查看各个事件状态,如最后一次执行时间等。

在和基于语句的复制一起工作时,也可能触发同样的问题。创建事件意味着给服务器增加额外工作。

典型应用包括定期地维护任务、重建缓存、构建汇总表来模拟物化视图,或者存储用于监控和诊断的状态。

CREATE EVENT optimize_somedb ON SCHEDULE EVERY 1 WEEK
DO
CALL optimize_tables('somedb');

可以指定事件是否被复制。

防止未执行完又执行:

CREATE EVENT optimize_somedb ON SCHEDULE EVERY 1 WEEK
DO
BEGIN
   DECLARE CONTINUE HANLDER FOR SQLEXCEPTION
      BEGIN END;
   IF GET_LOCK('somedb', 0) THEN
      DO CALL optimize_tables('somedb');
   END IF;
   DO RELEASE_LOCK('somedb');
END

GET_LOCK()来确保当前总是只有一个事件在执行。
CONTINUE HANLDER用来确保当事件执行出现了异常,仍然会释放持有的锁。

事件的执行是和连接无关的,但是它仍然是线程级别的。

事件调度线程在配置文件或者使用命令配置:

SET GLOBAL event_scheduler := 1;

事件进程是可以并行执行的,MySQL会创建一个新的进程用于执行事件。 线程的生命周期就是事件的执行过程。通过SHOW PROCESSLIST中的Command列来查看,值总是Connect。

在存储过程中保留注释

使用版本相关的注释,这些注释可能会被服务器执行,所以不会被删除。 为了避免被执行,使用一个非常大的版本号。

CREATE TRIGGER fake_statement_trigger
BEFORE INSERT ON sometable
FOR EACH ROW
BEGIN
   DECLARE v_row_count INT DEFAULT ROW_COUNT();
   /*!99999      ROW_COUNT() is 1 except for the first row, so this executes
      only once per statement.   */
   IF v_row_count <> 1 THEN
      -- Your code here
   END IF;
END;

游标

MySQL服务器提供只读的、单向的游标,且只能在存储过程或更底层的客户端API中使用。 因为游标中指向的对象都是存储在临时表中而不是实际查询到的数据,所以总是只读的。

在存储过程中,可以有多个游标,也可以在循环中嵌套的使用游标。

当打开一个游标的时候需要执行整个查询。

如果在关闭游标的时候只是扫描一个大结果集的一小部分,那么存储过程可能不仅没有减少开销,相反带来了大量的额外开销。 这时,需要考虑使用LIMIT限制结果集。

游标会让MySQL执行一些额外的I/O操作,这些操作的效率可能非常低。 如游标返回列中包含BLOB和TEXT类型,或者超过了temp_table_size的时候,会创建磁盘表来存放。

绑定变量(Prepare Statement)

当创建一个绑定变量SQL时,客户端向服务器发送了一个SQL语句的原型。 服务器端收到这个SQL语句框架后,解析并存储这个SQL语句的部分执行计划,返回给客户端一个SQL语句的句柄。 以后每次执行这类查询,客户端都指定使用这个句柄。

INSERT INTO tbl(col1, col2, col3) VALUES (?, ?, ?);

可以通过向服务器发送各个问号的取值和这个SQL的句柄来执行一个具体的查询。

MySQL在使用绑定变量的时候可以更高效的执行大量的重复语句:

  • 服务器端只需要解析一次SQL语句
  • 在服务器端某些优化器的工作只需要执行一次,因为它会缓存一部分的执行计划
  • 以二进制的方式只发送参数和句柄,比起每次都发送ASCII码文本效率更高,一个二进制的日期字段只需要三个字节,而ASCII码需要十个字节。
    • 最大的节省来自于BLOB和TEXT字段,绑定变量的形式可以分块传输,而无序一次性传输。
    • 二进制协议在客户端也可以节省很多内存,减少了网络开销
    • 还节省了将数据从存储原始格式转换成文本格式的开销
  • 仅仅是参数而不是整个查询语句发送到客户端,节省了网络开销
  • MySQL在存储参数的时候,直接将其放到缓存中,不再需要在内存中多次复制。

绑定变量也相对安全,无序应用程序中处理转义。

有些客户端模拟的绑定变量,最终还是转换成SQL。

绑定变量(Prepare Statement)的优化

根据优化时机,可以将优化分为三类:

  • 在准备阶段
    • 解析SQL语句,移除不可能的条件,并重写子查询
  • 在第一次执行的时候
    • 如果可能的话,服务器先简化嵌套循环关联,并将外关联转化成内关联
  • 在每次SQL语句执行时
    • 过滤分区
    • 尽量移除COUNT()、MIN()、MAX()
    • 移除常量表达式
    • 检测常量表
    • 做必要的等值传播
    • 分析和优化ref、range和索引优化等访问数据的方法
    • 优化关联顺序

SQL接口的绑定变量

4.1以后的版本中,支持了SQL接口的绑定变量。不使用二进制传输协议也可以直接以SQL的方式使用绑定变量。

SET @sql := 'SELECT actor_id, first_name, last_name
FROM sakila.actor WHERE first_name = ?';
PREPARE stmt_fetch_actor FROM @sql;
SET @actor_name := 'Penelope';
EXECUTE stmt_fetch_actor USING @actor_name;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
|        1 | PENELOPE   | GUINESS   |
|       54 | PENELOPE   | PINKETT   |
|      104 | PENELOPE   | CRONYN    |
|      120 | PENELOPE   | MONROE    |
+----------+------------+-----------+
DEALLOCATE PREPARE stmt_fetch_actor;

最主要的用途就是在存储过程中使用。

REPEAT
   FETCH c INTO t;
   IF NOT done THEN
      SET @stmt_text := CONCAT("OPTIMIZE TABLE ", db_name, ".", t);
      PREPARE stmt FROM @stmt_text;
      EXECUTE stmt;
      DEALLOCATE PREPARE stmt;
   END IF;
UNTIL done END REPEAT;

另一个常见的参数是动态设置就是LIMIT子句,因为二进制协议中无法将这个值参数化。

绑定变量(Prepare Statement)的限制

限制和注意事项:

  • 绑定变量时会话级别的,所以链接之间不能共用绑定变量句柄。一旦连接断开,则原来的句柄也不能使用了
    • 连接池和持久化连接可以在一定程度上缓解这个问题
  • 5.1之前,绑定变量的SQL是不能使用查询缓存的
  • 并不是所有的时候使用绑定变量都能获得更好的性能。如果只执行一次SQL,返回会增加开销
  • 当前版本还不能在存储函数中使用绑定变量
  • 如果总是忘记释放绑定变量资源,会反生资源泄漏。绑定变量SQL总数是一个全局限制。
  • 有些操作,如BEGIN无法在绑定变量中完成。

三种绑定变量的区别:

  • 客户端模拟的绑定变量
    • 客户端的驱动程序接收一个带参数的SQL,再将指定的值带入其中,最后将完整的查询发送到客户端
  • 服务器端的绑定变量
    • 客户端使用特殊的二进制协议将带参数的字符串发送到服务器端,然后使用二进制协议将具体的参数值发送给服务器端并执行。
  • SQL接口的绑定变量
    • 客户端先发送一个带参数的字符串到服务器端,这类似于使用PREPARE的SQL语句,然后发送设置参数的SQL,然后使用EXECUTE来执行SQL。所有这些都适用普通的文本传输协议。

用户自定义函数

自定义函数(UDF)可以使用支持C语言调用约定的任何编程语言来实现。

UDF必须事先编译好并动态链接到服务器上,这种平台相关性使得UDF在很多方面都很强大。速度非常快,可以访问大量的操作系统功能,还可以使用大量库函数。

UEF中的一个错误可能会让服务器崩溃,甚至扰乱服务器的内存或者数据。

需要确保UEF是线程安全的,MySQL是一个纯粹的多线程环境。

UDF仓库:http://www.mysqludf.org

插件

插件可以在MySQL中新增启动选项和状态值,还可以新增INFORMATION_SCHEMA表,或者在MySQL后台执行任务等。

一个简单的插件接口列表:

  • 存储过程插件
    • 在存储过程运行后再处理一次运行结果。
  • 后台插件
    • 让你的程序在MySQL中运行,可以实现自己的网络监听、执行自己的定期任务。
  • INFORMATION_SCHEMA插件
    • 提供一个新的内存INFORMATION_SCHEMA表
  • 全文解析插件
    • 提供一种文本处理的功能,根据自己的需求来对一个文档进行分词。
  • 审计插件
    • 在查询执行的过程中的某些固定点被调用,所以它可以用作记录MySQL的事件日志
  • 认证插件
    • 可以既在客户端可以在服务器端,可以使用这类插件扩展MySQL的认证功能。

字符集和校对

字符集指从二进制编码到某类字符符号的映射。 校对是指一组用于某个字符集的排序规则。

每一类编码字符都有与其对应的字符集和校对规则。

MySQL如何使用字符集

只有基于字符的值才真正有字符集的概念。

MySQL的设置可以分为两类:创建对象时的默认设置、服务器和客户端通信时的设置。

创建对象时的默认设置

MySQL服务器有默认的字符集和校对规则,每个数据库也有自己的默认值,每个表也有自己的默认值。 这是一个逐层继承的默认设置,最终最靠底层的默认设置将影响你创建的对象。这些默认设置,自上而下地告诉MySQL应该使用什么字符集来存储某个列。

每一层,都可以指定一个特定的字符集或者让服务器使用它的默认值:

  • 创建数据库的时候,将根据服务器上的character_set_server设置来设定该数据库的默认字符集
  • 创建表的时候,将根据数据库的字符集设置指定这个表的字符集设置
  • 创建列的时候,将根据表的设置指定列的字符集设置

真正存放数据的是列,所以更高阶梯的设置只是指定默认值。只有创建列时没有指定字符集,表的默认字符集才有作用。

服务器和客户端通信时的设置

当服务器和客户端通信的时候,可能使用不同的字符集,这是服务器端必须要进行翻译转换共组。

  • 服务器端总是假设客户端是按照character_set_client设置的字符来传输数据和SQL语句的
  • 当我父亲接收到客户端的SQL语句时,它现将其转换成character_set_connection。它还使用这个设置来决定如何将数据转换成字符串。
  • 当服务器端返回数据或者错误信息给客户端时,它会将其转换成character_set_result。

客户端和服务器的字符集:

客户端和服务器的字符集

根据需要,使用SET NAMES或者SET CHARACTER SET语句来改变上面的服务器端设置。

SHOW VARIABLES LIKE 'char%';
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | utf8   |
| character_set_connection | utf8   |
| character_set_database   | utf8   |
| character_set_filesystem | binary |
| character_set_results    | utf8   |
| character_set_server     | utf8   |
| character_set_system     | utf8   |
| character_sets_dir       |        |
+--------------------------+--------+
MySQL如何比较两个字符串的大小

如果两个字符串的字符集不同,MySQL会现将其转换成相同的字符集再进行比较。如果两个字符集不兼容就会抛错。 这时需要显式使用CONVERT()函数将其中一个字符集转换成一个兼容的字符集。

还可以使用COLLATE子句来指定字符串的字符集或者校对字符集。

-- 指定了前缀来指定utf8字符集,还是用COLLATE子句指定了使用二进制校对规则
SELECT _utf8 'hello world' COLLATE utf8_bin;
+--------------------------------------+
| _utf8 'hello world' COLLATE utf8_bin |
+--------------------------------------+
| hello world                          |
+--------------------------------------+
一些特殊情况
  • 诡异的character_set_database设置
    • 不指定时默认和character_set_server相同
  • LOAD DATA INFILE
    • 总是按照character_set_database来解析
    • 在加载数据的时候总是以同样的字符集处理所有数据,而不管表中的列是否有不同的字符集设定
  • SELECT INTO OUTFILE
    • 将结果不做任何转码的写入文件,除了使用CONVERT()函数进行转码外没有好办法
  • 嵌入式转义序列
    • 根据character_set_client的设置来解析转义序列,即使包含前缀或者COLLATE子句也一样。
    • 因为解析器在处理字符串中的转义字符时,完全不关心校对规则,前缀只是一个关键字而已。

选择字符集和校对规则

使用命令SHOW CHARACTER SET和SHOW COLLATION来查看支持的字符集和校对规则。

最好使用相同的字符集,然后根据需要在列上指定不同的字符集。

对于校对规则通常要考虑的一个问题是,是否以大小写敏感的方式比较字符串,或者以字符串编码的二进制值来比较大小。 他们对应的校对规则的前缀分别是_cs、_ci和_bin。

二进制校对规则直接使用字符的字节进行比较,而大小写敏感的校对规则在多字节字符集时,有更复杂的比较规则。

MySQL如何选择字符集和校对规则:

If you specify Resulting character set Resulting collation
Both character set and collation As specified As specified
Character set only As specified Character set’s default collation
Collation only Character set to which collation belongs As specified
Neither Applicable default Applicable default
-- 在创建数据库、表、列的时候显式指定字符集和校对规则
CREATE DATABASE d CHARSET latin1;
CREATE TABLE d.t(
   col1 CHAR(1),
   col2 CHAR(1) CHARSET utf8,
   col3 CHAR(1) COLLATE latin1_bin
) DEFAULT CHARSET=cp1251;
SHOW FULL COLUMNS FROM d.t;
+------+---------+-------------------+
|Field | Type    | Collation         |
+------+---------+-------------------+
|col1  | char(1) | cp1251_general_ci |
|col2  | char(1) | utf8_general_ci   |
|col3  | char(1) | latin1_bin        |
+------+---------+-------------------+

字符集和校对规则如何影响查询

某些字符集和校对规则可能会需要更多的CPU操作,可能会消耗更多的内存和存储空间,甚至还会影响索引的正常使用。

不同字符集和校对规则之间转换可能会带来额外的系统开销。

只有排序查询要求的字符集与服务器数据的字符集相同的时候,才能使用索引进行排序。需要使用别的字符集进行排序,那么需要使用文件排序。

EXPLAIN SELECT title, release_year FROM sakila.film ORDER BY title\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: film
         type: index
possible_keys: NULL
          key: idx_title
      key_len: 767
          ref: NULL
         rows: 953
        Extra:
EXPLAIN SELECT title, release_year
FROM sakila.film ORDER BY title COLLATE utf8_bin\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: film
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 953
        Extra: Using filesort

为了能够适应各种字符集,包括客户端字符集、在查询中指定的字符集,MySQL会在需要的时候进行字符集转换。 可以在EXPLAIN EXTENDED后使用SHOW WARNINGS来查看MySQL是如何处理的。

UTF-8一种多字节编码,他存储一个字符会使用变长的字节数(一到三个字节)。 在MySQL内部,通常使用一个定长的空间来存储字符串,在进行相关操作,这样做的目的是希望总是保证缓存中有足够的空间来存储字符串。 变长的字段类型(VARCHAR TEXT)存储在磁盘上时不会有这个困扰,但当它存储在临时表中用来处理或者排序时,也总是会分配最大可能的长度。

MySQL中有两个函数LENGTH()、CHAR_LENGTH()来结算字符串的长度,在多字节字符集中,两个函数的返回结果不同。

如果要索引一个UTF-8字符集的列,MySQL会假设每一个字符都是三个字节,所以最长索引前缀的限制一下缩短到原来的三分之一了。

mysql> CREATE TABLE big_string(str VARCHAR(500), KEY(str)) DEFAULT CHARSET=utf8;
Query OK, 0 rows affected, 1 warning (0.06 sec)
mysql> SHOW WARNINGS;
+---------+------+---------------------------------------------------------+
| Level   | Code | Message                                                 |
+---------+------+---------------------------------------------------------+
| Warning | 1071 | Specified key was too long; max key length is 999 bytes |
+---------+------+---------------------------------------------------------+
mysql> SHOW CREATE TABLE big_string\G
*************************** 1. row ***************************
       Table: big_string
Create Table: CREATE TABLE `big_string` (
  `str` varchar(500) default NULL,
  KEY `str` (`str`(333))
) ENGINE=MyISAM DEFAULT CHARSET=utf8

这里仅仅是在列的前缀上建立了索引。这会对MySQL使用索引造成一些影响,如无法使用索引覆盖扫描。

使用UTF-8字符集会消耗更多的磁盘空间,而有些应用并不真的需要这样做。

在考虑使用哪种字符集的时候,需要根据存储的具体内容来决定。

当从某个具体语种转换为UTF-8时,存储空间的使用会相应增加。 如果使用的是InnoDB表,那么字符集的改变可能导致数据段的大小超过可以在页内存储的临界值,需要额外的外部存储区,这会导致很严重的空间浪费,还会带来很多空间碎片。

很多时候不需要任何字符集,只有在做大小写无关的比较、排序、字符串操作(SUBSTRING())的时候才需要使用字符集。

如果数据不关心字符集,可以直接存储二进制信息,然后增加一个记录编码集的列。但是忘记多字节是一个字符时会出现问题,所以不建议这样做。

全文索引

没有索引也可以工作,没有索引效率更高。

5.6中,InnoDB实验性的实现了全文索引。MyISAM对全文索引的支持有很多限制,如表级锁对性能的影响、数据文件的崩溃、崩溃后的恢复等,使得MyISAM的全文索引应用并不广。

MyISAM的全文索引作用对象是一个“全文集合”,这可能是某个数据表的一列,也可能是多个列。 具体的,对数据表的某一条记录,MySQL会将需要索引的列全部拼接成一个字符串,然后进行索引。

MyISAM的全文索引是一类特殊的B-Tree索引,共有两层。 第一层是所有关键字,然后对于每一个关键字的第二层,包含的是一组相关的文档指针。

全文索引根据下面规则过滤词语:

  • 停用词不会索引。通过ft_stopword_file指定
  • 对于长度大于ft_min_word_len和ft_max_word_len的词语,都不会被索引

全文索引不会存储关键字具体匹配哪一列,如果需要根据不同的列来进行组合查询,那么不需要针对每一个列建立多个索引。这也意味着不能指定列的相关性。

自然语言的全文索引

和普通查询不同,这类查询自动按照相似度进行排序。在使用全文索引进行排序的时候,MySQL无法再使用索引排序。所以如果不想使用文件排序,那么久不要再查询中使用ORDER BY子句。

mysql> SHOW INDEX FROM sakila.film_text;
+-----------+-----------------------+-------------+------------+
| Table     | Key_name              | Column_name | Index_type |
+-----------+-----------------------+-------------+------------+
| ...
| film_text | idx_title_description | title       | FULLTEXT   |
| film_text | idx_title_description | description | FULLTEXT   |
+-----------+-----------------------+-------------+------------+
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.4692449569702 |
|     126 | CASUALTIES ENCINO     | Face a Boy in A Monastery | 5.2615661621094 |
|     193 | CROSSROADS CASUALTIES | a Composer in The Outback | 5.2072987556458 |
|     369 | GOODFELLAS SALUTE     | d Cow in A Baloon Factory | 3.1522686481476 |
|     451 | IGBY MAKER            | a Dog in A Baloon Factory | 3.1522686481476 |

在一个查询中使用两次MATCH()不会有额外消耗,MySQL会自动识别并只进行一次搜素。

MATCH()函数中指定的列必须和在全文索引中指定的列完全相同,否则就无法使用全文索引。这是因为全文索引蛇记录关键字是来自哪一列的。 这也意味着无法使用全文索引来查询某个关键是是否在某一列中存在。

解决:

根据关键词在多个不同列的全文索引上的相关度来算出排名值,然后依此来排序。

ALTER TABLE film_text ADD FULLTEXT KEY(title);
-- 因为需要文件排序,所以不高效
SELECT film_id, RIGHT(description, 25),
ROUND(MATCH(title, description) AGAINST('factory 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'))
   + MATCH(title, description) AGAINST('factory casualties') DESC;
+---------+---------------------------+----------+-----------+
| film_id | RIGHT(description, 25)    | full_rel | title_rel |
+---------+-------------- ------------+----------+-----------+
|     831 | a Car in A Baloon Factory |    8.469 |     5.676 |
|     126 | Face a Boy in A Monastery |    5.262 |     5.676 |
|     299 | jack in The Sahara Desert |    3.056 |     6.751 |
|     193 | a Composer in The Outback |    5.207 |     5.676 |
|     369 | d Cow in A Baloon Factory |    3.152 |     0.000 |
|     451 | a Dog in A Baloon Factory |    3.152 |     0.000 |
|     595 | a Cat in A Baloon Factory |    3.152 |     0.000 |
|     649 | nizer in A Baloon Factory |    3.152 |     0.000 |

布尔全文索引

可以在查询中自定义某个被搜索的词语的相关性。返回结果时未经排序的。

布尔全文索引的通用修饰符(dinosaur,恐龙,指关键词):

Example Meaning
dinosaur Rows containing “dinosaur” rank higher.
~dinosaur Rows containing “dinosaur” rank lower.
+dinosaur Rows must contain “dinosaur”.
-dinosaur Rows must not contain “dinosaur”.
dino* Rows containing words that begin with “dino” rank higher.

必须同时包含:

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 |
+---------+---------------------+---------------------------+

进行短语搜索:

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 |
+---------+---------------------+---------------------------+

短语搜索速度会比较慢。只查询索引无法确定是否精确匹配,通常需要查询原文。

只有MyISAM引擎才能使用布尔全文索引。而没有全文索引的时候,MySQL通过全表扫描实现。

MySQL 5.1中全文索引的变化

增加了插件,如可以增强分词功能。

全文索引的限制和替代方案

MySQL的全文索引只有一种判断相关性的方法:词频。
数据量大小也是问题,只有全部在内存中的时候,性能才很好。

相比其它索引,INSERT、UPDATE、和DELETE操作运行时,全文索引的操作代价都很大:

  • 修改一段文本中的100个单词,则需要100次索引操作,而不是一次
  • 一般来收列长度不会太影响其它的索引类型,但是全文索引的长度会引起数量级的差异的性能变化
  • 会有更多的碎片,需要更多的优化操作

还会影响优化器的工作,索引选择、WHERE子句、ORDER BY都有可能不是按照你所预想的方式来工作。

  • 如果使用了MATCH AGAINST子句,又有可用的全文索引,那么就一定会使用全文索引。
  • 全文索引只能用作全文搜索匹配。其它任何操作,如WHERE都要等待完成全搜索返回后才能进行。
  • 不存储实际值,就不可能做索引覆盖扫描
  • 除了相关性排序,不能做其它排序,所以其它排序都只能做文件排序。

可以使用复制库的不同引擎来实现全文索引。或者使用垂直拆分将列放到单独的表中。冗余也是一个好办法。

全文索引的配置和优化

  • 日常维护通常能大大提升性能,因为有更多的碎片问题。如果是I/O密集型应用,定期重建也会提升很多性能。
  • 如果想高效使用,还需要所有足够大的内存,保证所有的全文索引都能够缓存在内存中,通常可以为全文索引设置单独的键缓存,抱枕不会被其它的索引缓存挤出内存。
  • 提供一个好的停用词列表也很重要。
  • 忽略太短的单词也可以提升全文索引的效率。
  • 停用词表和允许最小词长都可以减少索引词语来提升全文索引的效率,也会降低精度。(调整词长需要OPTIMIZE TABLE才生效)
  • 导入大量数据最好使用DISABLE KEYS来禁用全文索引,然后再导入后使用ENABLE KEYS来建立全文索引,这样可以节省时间,同时为索引做了一次碎片整理

分布式(XA)事务

分布式事务让存储引擎级别的ACID可以扩展到数据库层面,甚至可以扩展到多个数据库之间,这需要通过两段提交实现。

XA事务中需要有一个事务协调器来保证所有的事务参与者都完成了准备工作(第一阶段)。 如果协调器收到所有的参与者都准备好的消息,就会告诉所有的事务可以提交了,这是第二阶段。

MySQL在XA事务中扮演一个参与者的角色,而不是协调者。

MySQL有两种XA事务,一方面,MySQL可以参与到外部的分布式事务中;另一方面,还可以通过XA事务来协调存储引擎和二进制日志。

内部XA事务

MySQL本身的插件式架构导致在内部需要使用XA事务。MySQL中各个存储引擎都是完全独立的,彼此壁纸刀对方的存在,所以一个跨存储引擎的事务就需要一个外部的协调者。 如果不使用XA协议,就会在各个引擎处理过程中崩溃导致事务不一致。

二进制日志被看作XA事务中的一个参与者。

外部XA事务

MySQL能够作为参与者完成一个外部的分布式事务。

因为通信延迟和参与者本身可能失败,所以外部XA事务比内部消耗会更大。

通常可以使用别的方式实现高性能的分布式事务。如可以写入本地数据,并将其放入队列,然后在一个更小、更快的事务中自动分发。 还可以使用MySQL本事的复制机制来发送数据。

由于某些原因不能使用MySQL本身的复制,或者性能不是瓶颈的时候,可以尝试使用。

查询缓存

MySQL在某些场景下可以缓存执行计划,对于相同类型的SQL就可以跳过SQL解析和执行计划生成阶段。 MySQL还有另一种不同的缓存类型:缓存完整的SELECT查询结果,也就是查询缓存。

查询缓存保存查询返回的完整结果。当查询命中该缓存,MySQL会立即返回结果,跳过了解析、优化和执行阶段。

查询缓存系统会跟踪查询中涉及的每个表,如果这些表发生变化,那么和这个表相关的所有查询缓存数据都将失效。 这看起来效率很低,但是代价很小,在很繁忙的系统中非常重要。

查询缓存对应用来说是透明的。

随着通用服务器越来越强大,查询缓存被发现是一个影响服务器扩展的因素。他可能成为整个服务器的资源竞争单点,在多核服务器上还可能导致服务器僵死。 很多时候还是认为应该默认关闭查询缓存,如果查询缓存作用很大,就配置一个很小的查询缓存空间(几十兆)。

MySQL如何判断缓存命中

缓存存放在一个引用表中,通过一个哈希值引用,这个哈希值包括了如下因素,即查询本身、当前要查询的数据库、客户端协议的版本等一些其它可能影响返回结果的信息。

当判断缓存是否命中时,不会解析、标准化或者参数化查询语句,而是直接使用SQL语句和客户端发送过来的其它原始信息。 任何字符上的不同,如空格、注释等都会导致缓存的不命中。

当查询语句中有一些不确定的数据时,则不命中缓存。 如包含函数NOW()或者CURRENT_DATE()的查询不会被缓存。 CURRENT_USER()或者CONNECTION_ID()的查询语句因为会根据不同的用户返回不同的结果,所以也不会被缓存。 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表或者任何包含列级别权限的表都不会被缓存。

检查查询缓存之前,MySQL只做一件事情,就是用大小写不敏感的检查看看SQL语句是不是以SEL开头。

不确定的查询在查询结果之后不会放入到查询缓存中。

所以希望缓存一个带日期的查询,那么最好将日期提前计算好,而不是直接使用函数。

... DATE_SUB(CURRENT_DATE, INTERVAL 1 DAY) -- Not cacheable!
... DATE_SUB('2007-07-14', INTERVAL 1 DAY) -- Cacheable

查询缓存建立在完整的SELECT语句基础上的,而且只是刚收到SQL语句的时候才检查,所以子查询和存储过程无法使用查询缓存。

打开查询缓存对读和写操作都会带来额外消耗:

  • 读取查询在开始之前必须先检查是否命中缓存
  • 如果过这个读查询可以被缓存,那么当完成执行后,MySQL若发现查询缓存中没有这个查询,会将结果存如查询缓存,会带来额外的系统消耗。
  • 向某个表写入数据的时候,MySQL必须将对应表的所有缓存都设置失效。如果查询缓存非常大或者碎片很多,这个操作就可能会带来很大系统消耗。

但是查询缓存仍然可能给系统带来性能提升。但是,这些额外消耗也可能不断增加,再加上对查询缓存操作是一个加锁排他操作,这个消耗可能不容小觑。

对于InnoDB,事务的一些特性会限制查询缓存的使用。 当一个语句在事务中修改了某个表,MySQL会将这个表的对应的查询缓存都设置失效。多版本特性会暂时将这个修改对其他事务屏蔽。 这这个事务提交之前,这个表的相关查询是无法被缓存的。因此长时间运行的事务,会大大降低查询缓存的命中率。

如果查询缓存使用了大量的内存,缓存失效操作可能成为一个非常严重的问题瓶颈。 如果缓存中存放了大量的查询结果,那么缓存失效操作时整个系统都可能会僵死一会。 因为这个操作是靠一个全局锁操作保护的,所有需要做该操作的查询都需要等待这个锁。 无论是检测是否命中缓存、还是缓存失效检测都需要等待这个全局锁。

查询缓存如何使用内存

和文件系统有些类似:

需要一些内存专门用来确定哪些内存目前是可用的、哪些是已经用掉的、哪些用来存储数据表和查询结果之间的映射、哪些涌过来存储查询字符串和查询结果。

基本的管理维护数据结构大概需要40KB的内存资源,除此之外,MySQL用于查询缓存的内存被分成一个个的数据块,数据库是变长的。 每一个数据块中,存储了自己的类型、大小和存储的数据本身,还外加一个向前和向后的数据库指针。

数据块的类型有:存储查询结果、存储查询和数据表的映射、存储查询文本等等。

系统启动的时候先初始化查询缓存需要的内存,内存池起初是一个完整的空间块,空闲块的大小就是配置查询缓存的大小减去维护原数据的数据结构消耗的内存空间。

因为一开始就要申请空间,所以无法为每一个查询结果精确分配大小恰好匹配的空间。最小使用query_cache_min_res_unit的配置空间。

因为需要先锁住空间块,然后找到合适的数据块,所以相对来说分配内存块是一个非常慢的操作。MySQL尽量避免这个操作的次数。

查询缓存如何分配内存来存储结果数据:

当需要缓存的一个查询结果的时候,它先选择一个尽可能小的内存块,然后将结果存入起哄。如果数据块全部用完,但仍有剩余数据需要存储,那么会申请一个新数据库,仍然是尽可能小,继续存储结果数据。 当查询完成时,如果申请的内存空间还有剩余,MySQL会将其释放,并放入空闲内存部分。

查询缓存如何分配内存来存储结果数据

这里的内存是MySQL自己管理的,不依赖操作系统的内存管理。

如果回收的数据小于query_cache_min_res_unit,所以不能直接在后续的内存块分配中使用,所以数据块的分配更复杂些。

查询缓存中存储查询结果后剩余的碎片

空隙问题被称为碎片,这个是一个经典问题。

什么情况下查询缓存能发挥作用

与服务器的压力模型有关,当缓存带来的资源节约大于其本身的资源消耗时才带来性能提升。可以通过观察打开和关闭的差异判断是否需要开启查询缓存。

很多时候,全局平均不能反映实际情况。有时候如果能够让某些关建的查询速度更快,稍微降低一下其他查询的速度也是值得的。

对于那些需要消耗大量资源的查询通常都是非常适合缓存的。如COUNT()或者复杂的SELECT中多表JOIN后排序分页。但要求涉及的表上UPDATE、DELETE、INSERT操作相比SELECT来说要非常少才行。

查询缓存命中率公式:

Qcache_hits/(Qcache_hists+Com_select)

缓存未命中的原因:

  • 查询中包含一个不确定的函数,或者返回结果太大。会导致Qcache_not_cached增加
  • 从未处理过这个查询
  • 缓存被挤出或被失效

如果有大量缓存未命中,但是实际上绝大多数查询都被缓存了,那么一定是以下情况:

  • 查询缓存还没有完成预热,也就是MySQL还没有机会将查询结果都缓存起来
  • 查询语句之前从未执行过
  • 缓存失效操作太多了

缓存碎片、内存不足、数据修改都会造成缓存失效。 如果设置了足够的缓存空间和合理的query_cache_min_res_unit,那么缓存失效应该主要死数据修改导致的。 可以通过Com_*(Com_update、Com_delete)来查看数据修改的情况。 还可以通过Qcache_lowmem_prunes查看多少次失效是由于内存不足导致的。

如果更新操作和带缓存的读操作混合,那么查询缓存带来的好处很难衡量。

如果Qcache_inserts和Com_select值相当,那么每次查询操作都是缓存未命中,并将结果放入查询缓存,这有可能造成缓存浪费。 在缓存完成预热后,我们总是希望看到Com_select远大于Qcache_inserts。

另一个直观的办法反映查询缓存是否对系统有好处:

命中(Qcache_hits)和写入(Qcache_inserts)的比率。

经验值这个比率大于3:1是有效的,不过这个比率最好能达到10:1。

由于有缓存失效的存在,所以预想的缓存大小通常比实际的大小大。通过观察实际情况来调整缓存大小。不过要注意如果超过了几十兆这样的数量级,是有潜在危险的。

还要考虑一些其他的缓存,如InnoDB的缓存池、或者MyISAM的索引缓存。

最有效办法还是查看某类查询时间消耗是否增大或者减小来判断。

如何配置和维护查询缓存

  • query_cache_type
    • 是否打开查询缓存,取值ON\OFF\DEMAND,DEMAND表示在语句中写明SQL_CACHE的语句才放入缓存。
  • query_cache_size
    • 缓存总内存,单位字节。必须是1024的整数倍。
  • query_cache_min_res_unit
    • 分配内存块的最小单位
  • query_cache_limit
    • 能够缓存的最大查询结果。超出会增加Qcache_not_cached。建议在SQL中增加SQL_NO_CACHE来避免查询缓存带来的额外消耗。
  • query_cache_wlock_invalidate
    • 如果某个数据表被其它的连接锁住,是否仍然从查询缓存中返回结果。默认是OFF。
减少碎片

选择合适的query_cache_min_res_unit可以减少由碎片导致的内存空间浪费。

太小则浪费空间更少,但是导致更频繁的内存块申请操作。如果太大,碎片更多。实际是在平衡内储存浪费和CPU消耗。

合适大小应该与查询结果的平均大小直接相关。

通过内存实际消耗(query_cache_size-Qcache_free_memory)/Qcache_queries_in_cache计算单个查询的平均缓存大小。 如果数据大小很不均匀,碎片和反复的内存块分配可能无法避免。如果缓存一个非常大的结果没有意义,可以通过query_cache_limit限制来调整减少内存碎片的发生。

Qcache_free_blocks反映了剩余空闲块的多少。最糟糕的情况是,任何两个存储结果的数据块之间都有一个非常小的空闲块。 所以如果Qcache_free_blocks大小恰好达到Qcache_total_blocks/2,那么查询缓存就有严重的碎片问题。 如果有很多空闲块,而Qcache_lowmem_prunes还在不断增加,那么说明碎片导致了过早的在删除查询缓存结果。

使用FLUSH QUERY CACHE完成碎片整理。使用命令RESET QUERY CACHE完成清空。 FLUSH时期会访问所有查询缓存,导致服务器僵死,建议保持查询缓存非常小,以便维护时可以将服务器僵死控制在非常短的时间内。

提高利用率

如果没有碎片问题,命中率还是很低,还可能是查询缓存的内存空间太小导致的。因为无法缓存新的结果时会删除老的缓存。这回增加Qcache_lowmem_prunes。

Qcache_lowmem_prunes增加的很快有两个原因:

  • 有很多空闲块,则是碎片导致的
  • 如果这是没什么空闲块了,则是缓存空间不够大可以通过检查Qcache_free_memory来查看有多少没有使用的内存。

如果空闲块很多,碎片很少,没有什么由于内存导致的缓存失效,但是缓存命中率非常低,那么可能说明在你的系统压力下,查询缓存并没有什么好处。 一定是什么原因导致缓存无法提供服务,如有大量更新或者查询语句本身不能被缓存。

query_cache_size设置为0来关闭查询缓存。(query_cache_type不会影响已经打开的连接,也不会将查询缓存的内存释放给系统)

如何分析和配置查询缓存:

如何分析和配置查询缓存

InnoDB和查询缓存

由于MVCC,InnoDB和查询缓存的交互更滑复杂。

事务是否可以访问查询缓存取决于当前的事务ID,以及对应的数据表上是否有锁。每一个InnoDB表的内存数据字典都保存了一个事务ID号,如果当前事务ID小于该事务ID,则无法访问查询缓存。

如果表上有任何锁,那么对这个表的任何查询语句都是无法被缓存的。

  • 所有大于表计数器的事务才可以使用查询缓存。
  • 该表的计数器并不是直接更新为对该表珍惜ing加锁的事务ID,而是被更新成一个系统事务ID。所以事务自身的后续操作也可能无法读取查询缓存。

查询缓存由MySQL层面完成,InnoDB可以在事务中显示地告诉MySQL合适应该让某个表的查询缓存都失效。

让加锁的表不能使用查询缓存是MVCC的是一个简单实现,并不是必须的。

通用查询缓存优化

库表结构设计、查询语句、应用程序设计都可能会影响到查询缓存的效率。

  • 用多个小表代替一个大表对查询缓存有好处。因为可以更细粒度的控制失效。
  • 批量写入只做一次缓存失效。
  • 控制缓存空间大小,防止缓存失效导致僵死
  • 无法在数据级别或者表级别控制查询缓存,但是可以通过SQL_CACHE和SQL_NO_CACHE来控制单个SELECT。还可以使用回话级别变量query_cache_type来控制查询缓存
  • 对于写密集型程序,直接禁用查询缓存可能提高系统性能
  • 因为互斥量竞争,关闭查询缓存对读密集型程序也有好处

查询缓存的替代方案

使用应用层缓存。

优化服务器设置

服务器的配置应该符合它的工作负载、数据,以及应用需求,并不仅仅看硬件的情况。

在正确的配置了基本配置项后,应该将更多的时间花在schema的优化、索引及查询设计上。再花力气去改其他配置项的收益通常就比较小了。

使用InnoDB最重要的两个选项:

  • innodb_buffer_pool_size
  • innodb_log_file_size

不要做:

  • 不要“调优”服务器
  • 不要使用比率、公式或者调优脚本作为设置配置变量的基础
  • 不要相信来自互联网上的非权威信息
  • 不要为了看起来很糟糕的事情去不断刷新SHOW STATUS,先检查索引和查询语句

如果有些设置其实是错误的,在剖析服务器性能时也会展现出来。

MySQL配置的工作原理

MySQL从命令行参数和配置文件获取配置信息。

类UNIX系统中,配置文件一般在/etc/my.cnf或者/etc/mysql/my.cnf。或者安装时指定。

# 查看使用的配置文件位置
$ which mysqld
/usr/sbin/mysqld
$ /usr/sbin/mysqld --verbose --help | grep -A 1 'Default options'
Default options are read from the following files in the given order:
/etc/mysql/my.cnf ~/.my.cnf /usr/etc/my.cnf

服务器通常读取配置文件的[mysqld]段。

语法、作用域和动态性

# 小写下划线分隔
/usr/sbin/mysqld --auto-increment-offset=5
/usr/sbin/mysqld --auto_increment_offset=5

变量可以是服务器级(全局作用域)、对每个连接(回话作用域)、和一些对象级的。

使用SHOW GLOBAL VARIABLES查看全局变量。

许多变量可以通过后缀指定单位,如1M表示一百万字节。

有个特殊值可以通过SET命令赋值给变量:DEFAULT

设置变量的副作用

动态设置变量可能导致意外的副作用,如从缓冲中刷新脏块。

变量和动态修改它们的效果:

  • key_buffer_size
    • 为键缓存分配所有指定的空间。在使用时分配
    • 如果对一个已经存在的键缓存设置非零值,会导致刷新该键缓存。会阻塞所有尝试访问该键缓存的操作。
  • table_cache_size
    • 会延迟到下次有线程打开表才有效。
    • 可能会删除缓存中不常使用的表
  • thread_cache_size
    • 下次有链接被关闭时生效。
    • 在关闭连接时在缓存中增加线程,在创建新连接时才从缓存中删除线程。
  • query_cache_size
    • 启动时一次性初始化该缓存。
    • 修改会清除所有的查询缓存
  • read_buffer_size
    • 在需要使用时一次性分配
  • read_rnd_buffer_size
    • 在需要时分配需要的大小
  • sort_buffer_size
    • 需要排序时分配,立刻分配全部大小。
    • 设置这个值的代价非常高

可以在连接级别调大值,防止对全局影响造成资源浪费。

SET @@session.sort_buffer_size := <value>;
-- Execute the query...
SET @@session.sort_buffer_size := DEFAULT;
-- 保存原来值
SET @saved_<unique_variable_name> := @@session.sort_buffer_size;
SET @@session.sort_buffer_size := <value>;
-- Execute the query...
SET @@session.sort_buffer_size := @saved_<unique_variable_name>;

入门

过大的配置容易导致服务器发生内存交换或者超过地址空间。

应该始终通过服务器监控来确认生产环境中的变量修改。

将配置文件置于版本控制中,是一个可以回滚配置的好办法。

在修改配置之前,应该先优化查询和schema。或者其他明显该做的事情,如添加索引。

通过基准测试迭代优化

不建议,因为需要做非常多的工作和研究。而且不能衡量一切,不能检验长时间运行的稳定性,如一些周期性抖动或者周期性慢查询无法发觉。

如果必须这样做,建议使用定制的基准测试包或者在实际的数据上重放工作负载。

每次改动一两个变量,然后运行长时间的基准测试来确认性能是否稳定。

什么情况下基准测试是好的建议:

  • 对服务器的容量规划来测试以确定硬件要求。
    • 对InnoDB缓冲池进行基准测试,有助于制定一个内存曲线,以展示真正需要多少内存,不同的内存容量如何影响存储系统的要求。
  • 了解InnoDB从崩溃中恢复需要多久时间,可以反复设置一个备库,故意让他崩溃,然后测试重启中需要花费多久恢复。这是做高可用性规划。
  • 以读为主的应用程序,在慢查询日志中捕捉所有的查询(或者用pt-query-digest分析TCP流量),或者使用pt-log-player重放所有慢查询,用pt-query-digest来分析输出报告。

什么不该做

  • 首先,不要根据一些“比率”来调优。
    • 如缓存命中率应该高于某个百分比。缓存命中率和缓存是否过大或者过小没有关系。首先, 命中率取决于工作负载。其次,缓存命中率没有什么意义。
  • 不建议使用调优脚本
  • 应该是配置或者优化,而不是调优的手段。
  • 在互联网搜索如何配置不总是一个好主意。
  • 不要相信内存消耗公式

创建MySQL配置文件

配置文件不要使用发行版本中的实例文件,最好从头开始。

  • 设置数据的位置
  • 指定默认存储引擎
    • 最好显示指定表的存储引擎
  • InnoDB的基础配置
    • 大小合适的缓冲池和日志文件

设置缓冲池大小:

  1. 从服务器内存总量开始
  2. 减去操作系统的内存占用,如果不是MySQL独占,则需要扣掉其它应用的内存占用
  3. 减去MySQL自身需要的一些内存,如为每个查询操作分配的一些缓冲。
  4. 减去足够让操作系统缓存InnoDB日志文件的内存,至少是足够缓存最近经常访问的部分。
  5. 减去其它配置的MySQL缓冲和缓存需要的内存,如MyISAM的键缓存,或者查询缓存
  6. 除以105%,这差不多接近InnoDB管理缓冲池增加的自身管理开销。
  7. 把结果四舍五入,向下取一个合理的值。向下舍入不会太影响结果,但是如果分配太多可能就会是件很糟糕的事情。

一开始就获得精确的设置并不是关建。从一个比默认值大一点但不是大得很离谱的安全值开始是比较好的,运行一段时间后看看真实情况需要多少内存。

配置内存缓冲区的时候,宁可谨慎而不是把他们配置得过大。过大可能造成内存交换、磁盘抖动甚至内存耗尽和硬件死机。

检查MySQL服务器状态变量

使用SHOW GLOBAL STATUS的输出,作为配置的输入,以更好的通过工作负载来自定义配置。 既要看绝对值,又要看值是如果随时间而改变的,最好为高峰和非高峰时间的值最几个快照。

# 60s间隔来查看状态变量的变化
mysqladmin extended-status -ri60

pt-mext活pt-mysql-summary可以简洁的显示状态计数器的变化。

配置内存使用

  1. 确定可以使用的内存上限
  2. 确定每个连接MySQL需要使用多少内存,例如排序缓冲和临时表
  3. 确定操作系统需要多少内存才够用
  4. 把剩下的都给MySQL缓存,例如InnoDB的缓冲池,这样很有意义

MySQL可以使用多少内存

考虑物理内存大小和操作系统限制。或者如glibc的限制。或者MySQL自身的配置上限。

每个连接需要多少内存

MySQL保持一个连接(线程)只需要少量的内存。还要求一个基本量的内存来执行任何给定查询。要为高峰期执行大量查询预留好足够的内储存。

为操作系统保留内存

至少应该为操作系统给保留1-2G的内存。建议2GB或者总内存的5%为基准,以较大者为准。

为缓存分配内存

大部分情况来说最重要的缓存:

  • InnoDB缓冲池
  • InnoDB日志文件和MyISAM数据的操作系统缓存
  • MyISAM键缓存
  • 查询缓存
  • 无法手工配置的缓存,如二进制日志和表定义文件的操作系统缓存。

InnoDB缓冲池

缓冲池对于InnoDB表比其他任何缓存都重要。

InnoDB缓存的缓存内容:

  • 索引
  • 行的数据结构
  • 自适应哈希索引
  • 插入缓冲
  • 其它内部数据结构

还是用缓冲池帮助延迟写入。

很大的缓冲池内存也带来很多挑战,如预热和关闭都会花很长时间。可以预先调小innodb_max_dirty_page_pct变量来等待刷新线程清理缓冲池,然后在脏页数量较小时关闭(观察Innodb_buffer_pool_pages_dirty变量)。

MyISAM键缓存

默认只有一个,可以指定创建多个,只缓存索引,不缓存数据(依赖操作系统缓存数据)。

最重要的配置项是key_buffer_size。任何没有分配给它的内存都可以被操作系统利用。

查询INDEX_LENGTH字段,把他们的值都想加,就可以得到索引存储占用的空间:

SELECT SUM(INDEX_LENGTH) FROM INFORMATION_SCHEMA.TABLES WHERE ENGINE='MYISAM';

类UNIX系统可以使用下面的命令:

du -sch `find /path/to/mysql/data/directory/ -name "*.MYI"`

不要超过索引总大小或者不超过为操作系统缓存保留内存的25%~50%,以更小为准。

多个键缓冲来利用大于4GB的内存。

key_buffer_1.key_buffer_size = 1G
key_buffer_2.key_buffer_size = 1G
-- 指定索引使用的缓冲区,未指定则使用默认缓冲区
CACHE INDEX t1, t2 IN key_buffer_1;
-- 预载入索引
LOAD INDEX INTO CACHE t1, t2;

键缓存使用率:

100 - ( (Key_blocks_unused * key_cache_block_size) * 100 / key_buffer_size )

每秒缓存命中次数:

Key_reads / Uptime
-- 查看Key_reads
mysqladmin extended-status -r -i 10 | grep Key_reads

将内存更多的保留给操作系统缓存而不是键缓存是有意义的,MyISAM使用操作系统缓存来缓存数据文件,通常数据文件比索引要大。

即使没有任何MyISAM表,依然需要将key_buffer_size设置为较小的值,如32M。 因为服务器有时在内部使用MyISAM表。例如GROUP BY语句可能会使用MyISAM做临时表。

键缓存块大小

块大小影响了MyISAM、操作系统缓存以及文件系统之间的交互,尤其是写密集型负载。

如果块缓存太小了,可能会遇到写时读取(写操作之前必须先总磁盘上读取一些数据)。

如果在读取数据后修改前,由于块缓存不足导致被挤出内存空间,在写入时就需要重新读取块来进行写入更改。

myisam_block_size变量控制着索引块大小。在CREATE TABLE或者CREATE INDEX语句中指定KEY_BLOCK_SIZE选项可以分别指定。

表索引块的大小应该等于操作系统的块大小,才能避免由边界对其导致的写时读取。

线程缓存

保存哪些为新连接主内连接服务的线程。thread_cache_size指定了MySQL可以保持在缓存中的线程数。

要检查是否足够大,可以查看Threads_created状态变量。也可以通过Thread_connected变量来观察大小是否能满足处理业务波动。 如连接保持在100~200,则20的缓存大小就可以处理波动,如果连接在500-700,则200就足以处理业务波动。

表缓存

包含表.frm文件的解析结果,加上一些其他数据(依赖于存储引擎,如MyISAM表示表的数据和索引的文件描述符,Merge表则可能是多个文件描述符)。

表缓存可以重用资源,但是这个开销并不大。真正的好处可以让服务器避免修改MyISAM文件头来标记表“正在使用中”。InnoDB有自己的表缓存版本,但是也可以从缓存解析的.frm文件中获益。

在5.1中,表缓存被分离成两部分:一个是打开表的缓存,一个是表定义的缓存。通过table_open_cache和table_definition_cache变量来配置。

如果Opened_tables状态变量很大或者在增长时,可能是因为表缓存不够大,那么可以增加table_cache系统变量(5.1中table_open_cache变量)。

表缓存非常大的时候,如果有很多的MyISAM表,则可能导致关闭时间较长。

如果无法打开更多的文件而导致错误,调整open_files_limit服务器变量来调整。

线程缓存和表缓存消耗的资源并不多,然而这些代是累加的。

InnoDB数据字典

InnoDB自己的表缓存,可以称为表定义缓存或者数据字典。

innodb_stats_on_metadata选项来避免耗时的表统计信息刷新。

innodb_file_per_table选项限制ibd文件打开数量。这由InnoDB引擎负载,而不是由MySQL服务器管理,并由Innodb_open_files来控制。将它调整的更大来保持所有的.ibd文件同时打开。

配置MySQL的I/O行为

影响MySQL怎样同步数据到磁盘以及如何做恢复操作。这些操作对性能的影响非常大,因为都涉及到昂贵的I/O操作。也体现了性能与数据安全之间的权衡。

通常立即并且一致地写到磁盘是很昂贵的。如果可以冒一些磁盘写可能没有真正持久化到磁盘的风险,就可以增加并发性和减少I/O等待,但是必须决定可以容忍多大风险。

InnoDB的I/O设置

允许控制怎么恢复、怎么打开和刷新数据文件,这会对恢复和整体性能产生巨大的影响。

尽管可以影响他的行为,InnoDB的恢复流程实际上是自动的,并且经常在InnoDB启动时运行。

InnoDB有一系列复杂的缓存和文件设计可以提升性能,以及保证ACID的特性,并且每一部分都是可配置的:

InnoDB’s buffers and files

最重要的一小部分是InnoDB的日志文件大小、InnoDB怎样刷新它的日志缓冲,以及InnoDB怎样执行I/O。

InnoDB事务日志

InnoDB使用日志来减少提交事务时的开销。因为日志中已经记录了事务,就无需在每个事务提交时把缓冲池的脏块数据刷新到磁盘中。

事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些变更到磁盘需要很多随机I/O。 InnoDB用日志吧随机I/O变成顺序I/O。一旦日志安全写到磁盘,事务就持久化了,即使没有写到数据文件,InnoDB可以重放日志并且恢复已经提交的事务。

因为日志有固定的大小。InnoDB的日志是环形方式写入的:当写到日志的尾部,会重新跳转到开头继续写,但不会覆盖还没有应用到数据文件的日志记录。

后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。 事务日志把数据文件的随机I/O转换为几乎顺序的日志文件和数据文件I/O,把刷新操作转移到后台使查询可以更快完成,并且缓和查询高峰时I/O系统的压力。

整体的日志文件大小受控于innodb_log_file_size和innodb_log_files_in_group两个参数,这对写性能非常重要。日志文件的总大小是每个日志文件的大小之和。 默认只有两个5MB的文件。对于高性能工作来说太小了,至少要几百兆或者上GB的日志文件。

InnoDB使用多个文件作为一组循环日志,通常不需要修改默认的日志数量,只修改每个文件的大小即可。

修改时需要完全关闭MySQL并备份旧日志文件。

要想确定理想的日志文件大小,必须权衡正常数据变更的开销和崩溃恢复需要的时间。

日志缓冲区:

当InnoDB变更任何数据时,会写一条变更记录到内存日志缓冲区。在缓冲满的时候、事务提交的时候或者每一秒,InnoDB都会刷写缓冲区的内容到磁盘日志文件。 如果有大事务,增加日志缓冲区大小(默认1MB)可以帮助减少I/O。变量innodb_log_buffer_size可以控制日志缓冲区的大小。 通常不需要把日志缓冲区设置的非常大,推荐范围是1MB~8MB。除非要写很多相当大的BLOB记录。

较大的日志缓冲区在某些情况下也是有好处的:可以减少缓冲区中空间分配的争用,有时简单的分配32MB~128MB的日志缓冲,可以帮助避免压力瓶颈。 如果有问题,瓶颈一般会表现为日志缓冲Mutex的竞争。

Innodb_os_log_written状态变量来查看对日志文件写了多少数据。

查看10~100秒间隔的数据,然后记录峰值。可以用来判断日志缓冲和日志文件是否设置的正好。 如果看到峰值是每秒100KB写入数据到日志,那么1MB的日志缓存就足够了。 如果峰值是100KB/S,那么256MB的日志文件足够存储至少2560秒的日志记录,这看起来足够了。 作为一个经验法则,日志文件的全部大小,应该满足服务器一小时的活动内容。

InnoDB怎样刷新日志缓冲:

当InnoDB吧日志缓冲刷新到磁盘日志文件时,先会使用一个Mutex锁住缓冲区,刷新到所需要的位置,然后移动剩下的条目到缓冲区的前面。 当Mutex释放时,可能有多个事务已经准备好刷新其日志记录,InnoDB有一个Group Commit功能,可以在一个I/O操作中提交多个事务,但是在5.0中打开了二进制日志就不能用了。

日志缓冲必须被刷新到持久化存储,以确保提交的事务被完全持久化了。

如果更在乎性能,修改innodb_flush_log_at_trx_commit变量来控制缓冲刷新的频繁程度:

  • 0
    • 把日志缓冲写到日志文件,并且每秒钟刷新一次,但是事务提交时不做任何事
  • 1
    • 将日志缓冲写到日志文件中,并且每次事务提交都刷新到持久化存储。
    • 这是默认的并且是最安全的设置,保证不丢失任何已提交事务,除非磁盘或者操作系统是伪刷新
    • 因为写数据到磁盘比较慢,所以会明显降低InnoDB每秒提交的事务数。
  • 2
    • 每次提交时把日志缓冲写到日志文件,但是并不刷新。InnoDB每秒钟做一次刷新。
    • 0和2的区别是如果MySQL进程挂了,则不会丢失任何事务,如果整个服务器挂了或者断电了,则可能丢失一个事务
    • 所以2是比0更合适的设置

把日志缓冲写到日志文件和把日志刷新到持久化存储是不同的:

  • 在大部分操作系统中,把缓冲写到日志只是简单的把数据从InnoDB的内存缓冲转义到了操作系统的缓存,也是在内存里,并没有写到持久化存储。
  • 把日志刷新到持久化存储意味着InnoDB请求操作系统把数据刷出缓存,并且确认写到磁盘了。
    • 这是一个阻塞I/O调用,直到数据被完全协会才会完成。

操作系统或者硬盘伪刷新可能导致数据损坏,而不仅仅是丢失事务。

高性能事务处理需要的最佳配置时把innodb_flush_log_at_trx_commit设置为1且把日志文件放到一个有电池保护的写缓存的RAID卷中。这兼顾了安全和速度。

InnoDB怎样打开和刷新日志以及数据文件

innodb_flush_method配置InnoDB如何跟文件系统相互作用。影像InnoDB如何读写数据。

Windows操作系统对这个选项的值是互斥的:async_unbuffered、unbuffered和normal。其它操作系统都是fdatasync。

既影响日志文件,又影响数据文件。

可能的值:

  • fdatasync
    • 在非Windows系上是默认值:InnoDB调用fsync()来刷新数据和日志文件
    • InnoDB通常用fsync()来代替fdatasync(),fdatasync()只刷新文件的数据,不包含元数据(最后修改时间等)。但在某些情况下会造成数据损坏。
    • fsync()的缺点是操作系统至少会在自己的缓存中缓冲一些数据。理论上这种双重缓冲是浪费的。InnoDB自己管理缓冲比操作系统能做的更加智能。
    • innodb_file_per_table会导致每个文件独立的做fsync(),意味着多个表不能合并到一个I/O操作,这可能导致InnoDB执行更多的fsync()操作
  • 0_DIRECT
    • InnoDB对数据文件使用0_DIRECT标记或者directio()函数,这依赖于操作系统。
    • 依然调用fsync()函数来刷新文件到磁盘,但是会通知操作系统不要缓存数据,也不要用预读。
    • 完全关闭了操作系统缓存,并且使所有的读写都直接通过存储设备,避免了双重缓存。不会关闭RAID卡的预读。
    • 这个设置不影响日志文件并且不是所有的类UNIX系统上都有效。GUN/Linux、FreeBSD、以及Solaris是支持的。同时影响读写。
    • 关闭系统缓存意味着可能导致服务器的预热时间过长,特别是操作系统的缓存很大的时候。也可能导致小容量的缓冲池比缓冲I/O的方式慢的多。
    • 需要打开innodb_file_per_table防止一些文件系统的互斥锁造成性能损失
  • ALL_0_DIRECT
    • Percona Server和MariaDB中可用。
    • 使打开日志文件时也能使用0_DIRECT方式
  • 0_DSYNC
    • 使日志文件调用open()函数时设置0_SYNC标记。使得所有的写同步,数据写入到磁盘后才会返回。
    • 不影响数据文件
    • 0_SYNC和0_DIRECT标记的不同之处在于0_SYNC没有禁用操作系统层的缓存。操作系统告诉设备不使用缓存。0_SYNC每个write()或pwrite()操作都会在函数完成之前把数据同步到磁盘。不用0_SYNC标记的写入调用fsync()允许写操作积累在缓存,使每个写更快,然后一次刷新所有数据。
    • 0_SYNC同步数据和元数据。0_DSYNC只同步数据。
  • async_unbuffered
    • Windows下的默认值,让InnoDB对大部分的写使用没有缓冲的I/O。但当innodb_flush_log_at_trx_commit=2时对日志文件使用缓冲I/O。
  • unbuffered
    • 只对Windows有效。
    • 与async_unbuffered雷士,但是不使用原生异步I/O,使用InnoDB多线程模拟的异步I/O。
  • normal
    • 只对Windows有效。
    • 让InnoDB不要使用原生异步I/O或者无缓冲I/O。
  • Nosync和littlesync
    • 只为开发使用

如果是类UNIX操作系统并且RAID控制器带有电池保护的写缓存,建议使用0_DIRECT,否则使用默认值或者0_DIRECT都可能是最好的选择,具体要看应用类型。

InnoDB表空间

InnoDB把数据保存在表空间内,本质上是一个由一个或多个磁盘文件组成的虚拟文件系统。

InnoDB表空间有很多功能,并不只是存储表和索引。还保存了会滚日志(旧版本行)、插入缓冲、双写缓冲,以及其他内部数据结构。

innodb_data_home_dir  = /var/lib/mysql/
innodb_data_file_path = ibdata1:1G;ibdata2:1G;ibdata3:1G

InnoDB先填满第一个文件,当第一个文件填满了在用第二个,如此循环。用RAID控制器是分布负载的好方式。

# 自动扩展,并限制上限
...ibdata3:1G:autoextend:max:2G

回收空间的唯一方式是导出数据,关闭MySQL并删除所有文件,修改配置,重启,生成新的文件后倒入数据。

innodb_file_per_table选项使每张表使用一个文件。在数据字段存储为“表明.ibd”的数据。使得删除一张表时回收空间简单多了,并且可以容易的将表分散到不同的磁盘上。而且方便管理。

innodb_file_per_table不好的一面:更差的DROP TABLE性能,这可能导致服务器阻塞

  • 删除要从文件系统层删除文件,可能在某些文件系统上很慢。
  • 每张表都在使用自己的表空间,删除表空间实际上需要InnoDB锁定和扫描缓冲池,查找属于这个表空间的页面,在一个有庞大缓冲池的服务器上做这个是非常慢的。

建议使用innodb_file_per_table并且给共享表空间设置大小范围,这样不用处理空间回收的。

行的旧版本和表空间:

在一个写压力大的情况下,InnoDB的表空间可能增长的非常大。如果事务打开状态很久,而且使用默认的REPEATABLE READ事务隔离级别,InnoDB将不能删除旧的行版本。

有时这个问题并非是没提交的事务的原因,也可能是工作负载的原因:清理过程只有一个线程处理,导致清理速度跟不上旧版本行数增加的速度。

为了控制写入速度,可以设置innodb_max_purge_lag为一个大于0的值,这是一个有争议的做法。 如事务平均影响1KB的行,并且可以容许表空间里有100MB的未清理行,那么可以设置这个值问1000000。

双写缓冲(Doublewrite Buffer)

双写缓冲来避免页没写完整导致数据损坏。当一个磁盘写操作不能完整的完成时,不完整的页写入就可能发生,16KB的页可能只有一部分被写到磁盘上。 有多种原因,如崩溃、BUG等。双写缓冲可以保证这种情况发生时的数据完整性。

双写缓冲是表空间中一个特殊的保留区域,在一些连续的块中足够保存100个页,本质上是最近写回的页面的备份拷贝。 当InnoDB从缓冲池刷新页面到硬盘时,首先把他们写到双写缓冲,然后再把它们写到其所属的数据区域中。这样可以保证每个页的写入都是原子并持久化的。

这意味着每个页都要写两遍,但是实际的性能冲击只有几个百分点,在SSD上开销更明显。

如果一个不完整的页写到了双写缓冲,原始的页依然会在磁盘上它的真实位置。当InnoDB恢复时,它将用原始页面替换掉双写缓冲中的损坏页面。 如果双写缓冲成功写入,但写到页的这真实位置失败了,InnoDB在恢复时将使用双写缓冲中的拷贝开替换。 因为每个页面结尾有校验值,所以InnoDB可以辨别是否损坏。

有些场景如在备库上禁用双写缓冲,或者一些文件系统做了同样的事,可以通过设置innodb_doublewrite为0来关闭。

其他的I/O配置项

sync_binlog选项控制MySQL怎么刷新二进制日志到磁盘。默认值是0,有操作系统控制何时刷新。 如果这个值比0大,指定了两次刷新到磁盘的动作之间间隔多少次二进制日志写操作。常用值为1。

设置为1可以获得安全保障。这样要求MySQL同步把二进制日志和事务日志这两个文件刷新到两个不同的位置。相对来说是个很慢的操作。

像InnoDB日志文件一样,把二进制日志放到一个带有电池保护的写缓存的RAID卷,可极大的提升性能。 事实上,写二进制日志缓存其实比InnoDB事务日志要昂贵多了。 因为每次写二进制日志都会增加它们的大小,这需要每次 写入文件系统都要更新元信息。所以sync_binlog=1可能比innodb_flush_log_at_trx_commit=1对性能的损害大得多。

如果使用expire_logs_days选项来自动清理二进制日志,就不要用rm命令去删。服务器会感到困惑并且拒绝自动删除它们,并且PURGE MASTER LOGS也将停止工作。 解决办法是手动重新同步“主机名-bin.index”文件,可以用磁盘上现有日志文件的列表来更新。

MyISAM的I/O设置

MyISAM通常每次写操作后就把索引变更刷新到磁盘。如果打算在一张表上做很多修改,批量操作会更快一些。

一种办法是用LOCK TABLES延迟写入,直到解锁这些表。这个性能提升是很有价值的技巧。 因为可以精确控制哪些写被延迟,以及什么时候把他们刷新到磁盘,可以精确延迟那些希望延迟的语句。

通过设置delay_key_write变量,也可以延迟索引的写入。如果这么做,修改的键缓冲块知道表被关闭(表缓存没空间或FLUSH TABLES等)才会刷新。

可能的配置如下:

  • OFF
    • MyISAM每次写操作后刷新键缓冲中的脏块到磁盘
  • ON
    • 打开延迟键写入,但是只对DELAY_KEY_WRITE选项创建的表有效
  • ALL
    • 所有的MyISAM表都延迟键写入

当键缓冲的读命中很好但写命中不好时,数据又比较小可能很有用,但是通常不会带来很大的性能提升。

缺点:

  • 如果服务器缓存并且块没有被刷新到磁盘,索引可能会损坏。
  • 如果很多写被延迟了,MySQL可能需要花费更长时间去关闭表,因为必须等待缓冲刷新到磁盘。这可能引起很长的表缓存锁。
  • 由于上面提到的原因,FLUSH TABLES可能需要很长时间。如果为了做逻辑卷快照或者其他备份操作,而执行FLUSH TABLES WITH READ LOCK,那可能增加操作的时间
  • 键缓冲中没有刷回去的脏块可能占用空间,导致从磁盘上读取的新块没有空间存放。因此查询语句可能需要等待释放一些键的缓存空间。

myisam_recover_options选项控制MyISAM怎样寻找和修复错误。在配置或者启动项中设置,只能查看不能修改。

SHOW VARIABLES LIKE 'myisam_recover_options';

打开这个选项通知MySQL在表打开时,检查是否损坏,并且在找到问题的时候自动修复。

  • DEFAULT或者不设置
    • 尝试修复任何被标记为崩溃或者没有标记为完全关闭的表。
  • BACKUP
    • 将数据文件备份写到.BAK文件,以便随后进行检查
  • FORCE
    • 即使.MYD文件中丢失的数据可能超过一行,也让恢复继续
  • QUICK
    • 除非有删除块否则跳过恢复。块中已经删除的行也会占用空间,但会被后来的INSERT语句重用。
      • 这个可能比较有用,因为MyISAM的大表恢复可能花费相当长的时间。

可以配置多个值:BACKUP,FORCE

建议打开这个选项,因为表损坏可能导致更多的数据损坏甚至服务器崩溃。 然而如果有很大大表,会导致服务器打开所有的MyISAM表时都会检查和恢复,这是低效的做法。比较好的主意是启动后使用CHECK TABLE和REPAIR TABLE命令来做。

打开myisam_use_mmap选项来启用数据文件的内存映射(MMAP)访问。使得MyISAM直接通过操作系统的页面缓存访问.MYD文件,避免系统调用的开销。

配置MySQL并发

InnoDB并发配置

InnoDB有自己的线程调度器控制线程怎么进入内核访问数据,以及他们在内核中一次可以做哪些事情。

最基本的限制并发的方式是使用innodb_thread_concurrency变量,它会限制一次性可以由多少线程进入内核,0表示不限制。

并发值 = CPU数量 * 磁盘数量 * 2

在实践中,使用更好的值更好一点。

InnoDB使用两段处理来尝试让线程尽可能高效的进入内核,两端策略减少了因操作系统调度引起的上下文切换。 线程第一次休眠innodb_thread_sleep_delay微秒(默认10000),然后再重试。 如果它依然不能进入内核,则放入一个等待线程队列,让操作系统处理。

当CPU有大量的线程处在“进入队列前的休眠”状态,因而没有充分利用时,改变这个值在高并发环境里可能有帮助。如果有大量的小查询,默认值可能太大了,因为增加了10毫秒的查询延迟。

一旦进入内核,他会有一定数量的票据,可以让它自由返回内核,不用再做并发检查。这限制了一个线程回到其它等待线程之前可以做多少事。 innodb_concurrency_tickets选项控制票据的数量。票据是按查询授权的,不是按事务。

innodb_commit_concurrency变量控制有多少个线程可以在同一时间提交。如果这个值太低也可能导致大量的线程冲突。

MyISAM并发配置

删除操作不会重新整理整个表,只是把行标记为删除,在表中留下空洞。在插入时重新利用这些空间。如果没有空洞了,就直接插入到队尾。

尽管MyISAM是表级锁,他依然可以一边读取,一边并发追加新行。这种情况下只能读取到查询开始时的所有数据,新插入的数据是不可见的,可以避免不一致读。 然而若表中间的某些数据变动了的话,还是难以提供一致读。

MVCC是解决这个问题最流行的方法:一旦修改者创建了新版本,他就让读取者读数据的旧版本。但是MyISAM不支持MVCC。所以除非插入操作在表的末尾,否则不能支持并发插入。

concurrent_insert这个变量,可以配置很好MyISAM打开并发插入:

  • 0
    • MyISAM不允许并发插入,所有插入都会对表加互斥锁。
  • 1
    • 默认值。只要没有空洞,就允许并发插入。
  • 2
    • 5.0以后有效。强制并发插入到表的末尾,即使表中有空洞。如果没线程从表中读取数据,MySQL将把新行放在空洞里。使用这个设置通常是表更加碎片化

通过delay_key_write变量延迟写索引,需要平衡性能和安全。 也设置low_priority_updates让INSERT、REPLACE、DELETE以及UPDATE语句的优先级比SELECT语句更低。这让SELECT获得很好的并发度。

键缓存的每个缓冲区持有一个Mutex。当一个线程从键缓冲复制键数据块到本地磁盘时会有竞争。从磁盘上读取时就没有这个问题。有时可以围绕这个问题把键缓冲分成多个区。

基于工作负载的配置

使用innotop工具和pt-query-digest来创建查询报告(慢查询日志分析)了解服务器正在做什么,哪些工作花费了大量时间。

优化BLOB和TEXT的场景

服务器不恩能够在内存临时表中存储BLOB值,因此如果一个查询设计BLOB值,有需要使用临时表,它都会立即在磁盘上创建临时表。这样效率很低。

两种方法减轻这个情况:SUBSTRING()函数把值转换为VARCHAR或者让临时表更快,如放在内存文件系统中。

服务器里设置临时表文件目录的是tempdir。可以指定多个临时表存放位置,MySQL将会轮询使用。

如果BLOB列非常大,也可以调大InnoDB的日志缓冲大小。

对于很长的列,InnoDB存储一个768字节的前缀在行内,然后会在行外分配扩展存储空间来存储剩下的部分。 它会分配一个完成的16KB的页,像其他所有InnoDB的页一样,每个列都有自己的页面。InnoDB一次只为一个列分配一个页的扩展存储空间,直到使用了超过32个页以后,就会一次性分配64个页面。
如果行的总长比InnoDB的最大行长度限制要短(比8kb小一些),InnoDB将不会分配扩展存储空间,即使大字段的长度超过了前缀长度。

当InnoDB更新存储在扩展存储空间中的大字段时,将不会在原来的位置更新。而是在扩展空间中写一个新的值到一个新的位置,并且不会删除旧的值。

这些因素带来下面影响:

  • 大字段在InnoDB里可能浪费大量空间。
  • 扩展存储仅用了自适应哈希,因为需要完整的比较列的整个长度,才能发现不是正确的数据。
    • 因为自适应哈希是完全的内存结构,并且直接指向Buffer Pool中访问最频繁的页面,但对于扩展存储空间却无法使用自适应哈希。
  • 太长的值可能使得在查询中作为WHERE条件不能使用索引,因为执行很慢。
    • 可以尝试使用覆盖索引解决
  • 如果一张表里有很多大字段,最好是把他们组合起来单独存到一个列里面,比如XML格式。这让大字段共享一个扩展存储空间,比每个字段用自己的页好
  • 有时候可以吧大字段用COMPRESS()压缩后再村委BLOB,或者发送到MySQL之前在应用程序中进行压缩,这可以获得显著的空间和性能优势。

优化排序

如果需要ORDER BY的列超过max_length_for_sort_data字节,采用two-pass算法。或者任何需要的返回列是BLOB或者TEXT,也会采用这个算法。可以使用SUBSTRING()转换后就可以使用single-pass算法了。

max_length_for_sort_data可以影响选择哪种排序算法。因为single-pass算法为每行需要排序的数据创建一个大小固定的缓冲,对于VARCHAR列,在和max_length_for_sort_data比较的时候会使用定义的长度。

当必须排序BLOB或TEXT字段时,只会使用前缀,然后忽略剩下部分的值。因为缓冲只能分配固定大小的结构体来保存需要排序的值,然后从扩展缓存中复制前缀到这个结构体中。使用max_sort_length来指定这个前缀有多大。

完成基本配置

  • tmp_table_size和max_heap_table_size
    • 控制使用Memory引擎的内存临时表能够使用多大内存。
    • 如果隐性内存临时表超过这两个设置的值,就会被转换为磁盘MyISAM表,所以它的大小可以继续增长
    • 应该设置这两个变量的值相同,但是不要太大。同时也可以利用较大的值防止使用磁盘临时表。
    • SHOW STATUS检查created_tmp_disk_tables和create_tmp_tables变量来监测创建临时表的频率。
  • max_connections
    • 保证服务器不会因为应用程序激增的连接而不堪重负。这是一种快速而代价最小的失败方式。
    • 观察max_used_connections随时间变化来确定一个合理值。这是一个峰值。如正常有200个连接可以设置为500或更高。
  • thread_cache_size
    • 观察thread_connected状态变量并且找到它在一般情况下的最大值和最小值。
    • 如果thread_connected从150变化到175,则缓存可以设置为75,上限为250是个不错的选择
    • 如果这个值一直在增长,则可能需要增大thread_cache_size
    • slow_launch_threads变量可能因为某些原因延迟了连接分配新线程
  • table_cache_size(5.1以后被拆分成两个缓存区)
    • 这个值应该设置的足够大,防止总是重新打开和重新解析表的定义
    • 通过观察open_tables值的变化来调整,隐式临时表也会导致这个值增长
    • 太大会有反作用
      • MySQL没有一个有效的方法检查缓存如果太大可能效率会下降。不应该超过10000
      • 有些类型的工作负载无法缓存,这种情况关闭缓存更好。如几万张表被很均匀的使用,就不可能全部缓存了。

安全和稳定的设置

一些对服务器配置有帮助的项:

  • expire_logs_days
    • 如果启用了二进制日志,建议打开
    • 建议设置的足够两个备份之前恢复,在最近的备份失败的情况下
    • 即使每天做备份,建议保留7-14天的日志。
  • max_allowed_packet
    • 防止服务器发送过大的包,也会控制多大的包可以被接收。
    • 如果备库不恩呢该接收主库发过来的复制数据,也需要增加这个设置到16MB或者更大。
  • max_connect_errors
    • 网络问题或者权限问题短暂时间内不断的尝试连接,可能被列入黑名单
    • 太小会导致问题。如果服务器可以抵御攻击,可以设置的大点
  • skip_name_resolve
    • 建议关闭DNS查找,改为使用IP地址、通配符,基于主机名的账号会被禁用。
  • sql_mode
    • 可以改变服务器行为。
    • 一些有用的值:STRICT_TRANS_TABLES, ERROR_FOR_DIVISION_BY_ZERO, NO_AUTO_CREATE_USER, NO_AUTO_VALUE_ON_ZERO, NO_ENGINE_SUBSTITUTION, NO_ZERO_DATE, NO_ZERO_IN_DATE, and ONLY_FULL_GROUP_BY
  • sysdate_is_now
    • 避免sysdate()的不确定行为使其等于now()

控制复制行为,对备库出问题很有帮助:

  • read_only
    • 只接受从主库传输过来的变更
  • skip_slave_start
    • 阻止MySQL视图自动启动复制,因为崩溃或者其他问题后启动复制是不安全的,需要手动检查
  • slave_net_timeout
    • 备库发现跟主库的连接已经失败并且需要重连的等待时间。默认一个小时,应该设置为一分钟或更短
  • sync_master_info, sync_relay_log, and sync_relay_log_info
    • 5.5以后可用,默认关闭
    • 这些选项使得更容易从崩溃中恢复,建议打开

高级InnoDB设置

  • innodb
    • 在InnoDB可以启动的时候才启动MySQL,建议设置为FORCE
  • innodb_autoinc_lock_mode
    • 控制InnoDB如何生成自增主键值,在高并发场景自增主键可能是瓶颈。
    • 通过SHOW ENGING INNODB STATUS里看到有很多自增锁,应该检查这个变量
  • innodb_buffer_pool_instances
    • 把缓冲池切分为多段,这可嫩是高负载的多核机器上提升MySQL可扩展性最重要的一个方式了
    • 分散了压力,所以一些全局Mutex竞争就没有那么大了
  • innodb_io_capacity
    • 告诉InnoDB服务器有多大的I/O能力,如果是SSD,需要设置成上万才能稳定的刷新脏页
  • innodb_read_io_threads and innodb_write_io_threads
    • 后台有多少线程可以被I/O操作使用默认是4个读线程和4个写线程
    • 如果有很多硬盘工作可以增加这个线程数或者把这个值设置为可以听I/O能力的磁盘数量
  • innodb_strict_mode
    • 在某些情况下把警告改成抛错,尤其是无效的或者可能有风险的CREATE TABLE选项。
  • innodb_old_blocks_time
    • InnoDB有个两段缓冲池LRU链表,设计目的是防止换出长期使用很多次的页面。
    • 这个变量指定一个页面从链表的young转移到old部分之前必须经过的毫秒数

高可用和可扩展

为了保证站点可响应和可用,需要三样东西:数据备份,系统冗余和响应性。

  • 备份可以将节点恢复到崩溃前的状态
  • 即使一个或多个节点停止运行,冗余也可以使站点继续运行
  • 响应能力使系统在实践生产中可用

什么是可扩展性

从高层次看,可扩展性就是通过增加资源来提升容量的能力。

产生负载的不同角度:

  • 数据量
    • 应用所能积累的数据量是可扩展性最普遍的挑战,特别是现在的互联网应用不删除任何数据。
  • 用户量
    • 即使每个用户只有少量的数据,在累积到一定数量的用户后,数据量也会开始不成比例的增长且速度快过用户的增长。
  • 用户活跃度
    • 新特性导致用户突然活跃或者部分用户更活跃
  • 相关数据集的大小
    • 如果用户间存在关系,应用可能需要整个管理用户群体上执行查询和计算。这比处理单个用户数据复杂的多。

通用可扩展定律(USL)说的是现行扩展的偏差可以通过两个因素来建立模型:无法并发执行的一部分工作以及需要交互的另外一部分工作(内部节点或者线程间通信)。 为第一个因素建模就有了Amdahl定律,它会导致吞吐量趋于平缓。 增加第二个因素就有了USL。

大多数系统看起来更像是USL曲线。

Comparison-of-linear-scalability,-Amdahl-scalability,-and-the-Universal-Scalability-Law

USL揭示了构建一个高扩展性系统的重要原则:在系统内尽量避免串行化和交互。

规划可扩展

以下问题帮助规划可扩展:

  • 应用程序功能完成了多少?许多可扩展建议将使得完成功能的难度增加。
  • 预期的最大负载时多少?应用应当在最大负载下也能正常工作。
  • 如果系统某个部分失效怎么办?是否应该准备一些空闲容量来防范这种问题

扩展准备工作:

  • 优化性能
    • 修复大多主要问题后达到一个收益递减点
  • 购买性能更强的硬件
    • 更多的服务器或者更多的内存通常是个好办法。

向上扩展

也成为垂直扩展,意味着购买更多性能强悍的硬件。

当前合理的收益递减点是256GB RAM,32核CPU和一个PCIe flash驱动器。 也可以通过多个小的MySQL实例来代替单个大实例。

如果应用变的非常庞大,向上扩展可能就没有办法了。在大多数共有云中都无法获得性能非常强的服务器。

向外扩展

也称水平扩展。策略分为三个部分:复制、拆分以及数据分片。

  • 以读为主的应用使用复制将数据分发到不同的服务器上,然后将备库用于读查询。
  • 另一个比较常见的方法是将工作负载分不到多个节点。
    • 节点可能是下面的某一种
      • 一个主-主复制的双机结构,拥有一个主动服务器和被动服务器
      • 一个主库和多个备库
      • 一个主动服务器,并使用分布式复制块设备作为备用服务器
      • 一个基于存储区域网络的集群
      • 大多数情况下,一个节点内的所有服务器应该拥有相同的数据

按功能拆分

按功能拆分或者说按职能拆分,意味着不同的节点执行不同的任务。将独立的服务器或节点分配给不同的应用。这样每个节点只包含它的特定应用所需要的服务器。

一个门户网站以及专用与不同功能区域的节点:

A-portal-and-nodes-dedicated-to-functional-areas

每个应用还可以有专用web服务器,但是没有专用数据库服务器那么常见。

另一个可能按功能划分的方法是对单个服务器的数据进行划分,并确保划分的表集合之间不会执行关联操作。 每种类型的数据只能在单个节点上找到,这并不是一种通用的分布数据的方法,因为很难做到高效,并且相比其它方案没有优势。

归根结底,还是不能通过功能划分来无限地进行扩展,因为如果一个功能区域被捆绑到单个MySQL几点,就只能进行垂直扩展。 一个功能区域最终增长到非常庞大时,都会迫使你去寻求一个不同的策略,如果进行了太多的功能划分,就很难采用更具扩展性的设计了。

数据分片

参考《数据分片》部分。

在扩展大型MySQL应用的方案中,数据分片是最通用且最成功的方法。它将数据分隔成一小片,或者说一块,然后存储到不同的节点中。

数据分片在和某些类型的按功能划分联合使用时非常有用。

全局的不分片的数据存储在单个节点上,并通常保存进memcached等缓存。

事实上大多数应用只会对需要的数据做分片,通常是那些增长的非常庞大的数据。

从单个实例到按功能划分的数据存储:

从单个实例到按功能划分的数据存储

最后根据用户id进行分片,将用户信息保留在单个节点上:

一个全局节点和六个主-主结构节点的数据存储方式

分片应用常会有一个数据块访问抽象层,用以降低应用和分片数据存储之间通信的复杂度,但无法完全隐藏分片。

多实例扩展(向上和向外组合)

可以让数据分片足够小,以使每台机器上都能放置多个分片,每台服务器上运行多个实例,然后划分服务器的硬件资源。 也可以不用分片,但是分片对于在大型服务器上的联合扩展有天然的适应性。

通过集群扩展

NDB Cluster是一个完全分布式、非共享高性能、自动分片并且不存在单点故障的事务性数据库服务器。由MySQL、NDB集群存储引擎和NDB组成。

MySQL Cluster是两项技术的结合:NDB数据库,以及作为SQL前端的MySQL存储引擎。NDB是一个分布式、具备容错性、非共享的数据库,提供同步复制以及节点间的数据自动分片。
NDB Cluset存储引擎将SQL转换为NDB API调用,但遇到NDB不支持的操作时,就会在MySQL服务器上执行。NDB是一个键值数据存储,无法执行连接或聚合的复杂操作。
NDB的两点包括非常高的写入和按键查询吞吐量。可以基于哈希自动决定哪个节点应该存储给定的数据。

NDB对于复杂查询支持的还不是很好。所以不要指望它来做数据仓库。NDB是一个事务型系统,但是不支持MVCC,所以读操作也要加锁,恶不作任何死锁检测,如果死锁就会返回超时。

类似产品: Clustrix, Percona XtraDB Cluster, Galera, Schooner Active Cluster, Continuent Tungsten, ScaleBase, ScaleArc, dbShards, Xeround, Akiban, VoltDB, and GenieDB.

向内扩展

对不在需要的数据进行归档和清理。具体取决于工作负载和数据特性。这种做法并不用来代替其它策略,可以作为争取时间的短期策略,也可以作为

设计归档策略需要考虑如下几点:

  • 对应用的影响
    • 一个设计良好的归档系统能够在不影响事务处理的情况下,从一个高负载的OLTP服务器上移除数据。
    • 这里的关建是能高效的找到需要删除的行,然后小块的移除。通常需要平衡一次归档的 行数和事务的大小,以找到一个锁竞争和事务负载量的平衡。
  • 要归档的行
    • 当知道某些数据不再使用后,就可以立即清理或者归档它们。
    • 可以设计应用去归档那些几乎不使用的数据。
  • 维护数据一致性
    • 当数据间存在联系时,会使归档和清理工作更加复杂。
    • 一个设计良好的归档任务能够保证数据的逻辑一致性,或至少在应用需要时能够保证一致,而无需在大量的事务中包含多个表。
    • 当表之间存在关系时,哪个表首先归档是个问题。在归档时需要考虑孤立行的影响。可以选择使用SET FOREIGN_KEY_CHECKS=0进制InnoDB的外键约束,或者把“悬空指针”记录放到一边。
  • 避免数据丢失
    • 如果是在服务器间归档,归档期间可能就无法做分布式事务处理,也有可能将数据归档到MyISAM或其他非事务型存储引擎中。
    • 因此为了避免数据丢失,在从源表中删除时,要保证数据在目标机器上保存。
    • 将归档数据写到一个文件里是个好主意。
    • 可以将归档任务设计为能够随时关闭或重启,并且不会引起不一致或索引冲突之类的错误。
  • 解除归档
    • 可以通过一些解除归档策略来减少归档的数据量。可以帮助你归档那些不确定是否需要的数据。并且在以后可以通过选项进行回退。如果可以设置一些检查确定是否可以归档数据就很容易做到。

保持活跃数据独立

如果不把老数据转移到别的服务器,许多应用也能受益于活跃数据和非活跃数据的隔离。 这有助于高效利用缓存,并为活跃和不活跃数据使用不同的硬件或应用架构。

  • 将表划分为几个部分
    • 分表是一种比较明智的办法,特别是整张表无法完全加载到内存时。拆分表可以明显改善缓存利用率
    • 如users表可以划分为active_users和inactive_users表。
  • MySQL分区
    • 分区表可以吧最近的数据保留在内存中
  • 基于时间的数据分区
    • 如果不断有新数据进来,那么新数据总是比旧数据更加活跃。
    • 例如:在两个节点的分片上存储用户数据,新数据总是进入“活跃”节点,该节点使用更大的内存和快速硬盘,另外一个节点使用更大的硬盘存储旧数据。
    • 可以用动态分片来轻松实现这种策略
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)
);

通过一个归档脚本将旧数据从活跃节点转移到归档节点,当移动用户数据到归档节点时,更新archie_timestamp列的值。shard_new和shard_archive列记录存储数据的分片号。

负载均衡

在一个服务器集群内尽可能的平均负载量。

负载均衡的五个常见目的:

  • 可扩展性
    • 负载均衡对某些扩展策略有所帮助,例如读写分离时从备库读取数据
  • 高效性
    • 负载均衡有助于更有效的使用资源,因为他能够控制请求被路由到何处。如果服务器处理能力不同,可以把更多的工作分配给性能更好的机器
  • 可用性
    • 一个灵活的负载均衡方案能使用保持可用的服务器
  • 透明性
    • 客户端无须知道是否存在负载均衡设置,也不需要关心在负载均衡背后有多少机器,负载均衡器给客户端看到的只是一个虚拟服务器
  • 一致性
    • 如果应用是有状态的(数据库事务、回话等),那么负载均衡器就应该将相关的查询指向同一个服务器,以防止状态丢失。应用无须去耿总到底连接的是哪一台服务器,

直接连接

复制上的读/写分离

通常需要修改应用以适应这种分离请求,然后应用就可以使用主库来进行写操作,并将读操作分配到主库和备库上;如果不太关心数据是否是脏的,可以使用备库,否则对即使 数据的请求使用主库。 这称为读写分离。

主动-被动复制或者主从复制都需要考虑的最大的问题是如何避免由于读了脏数据引起的奇怪问题。

比较常见的读/写分离的方法如下:

  • 基于查询分离
    • 将不能容忍脏数据的读和写查询分配到主动或主库服务器上。其它查询分配到备库或被动服务器上。
    • 容易实现,但是不能有效利用备库,因为实际上很多查询不能容忍脏数据
  • 基于脏数据分离
    • 基于查询分离的改进,让应用检查复制延迟,以确定备库数据是否太旧。
    • 许多报表应用都使用这个策略,只要晚上加载的数据复制到备库即可,并不关心是否100%跟上了主库。
  • 基于会话分离
    • 通过判断用户是否修改了数据来决定是否能从备库读。用户只需要看到自己的最新数据,而不是他人的。
    • 可以在回话层做一个标记,表明做了更新,就将该用户的查询在一段时间内总是指向主库。可以与复制延迟监控结合起来,始终选择一个备库是防止备库之间进度不同步问题。
    • 这是推荐的策略,简单与有效性之间的一种很好的妥协。
  • 基于版本分离
    • 跟踪对象的本版号以及/或者时间戳,通过从备库读取对象的版本或者时间戳来判断数据是否够新。
    • 如果备库的数据太旧,可以从主库获取最新的数据。即使对象本身没有变化,但如果是顶层对象,只要下面的任何对象变化,也可以增加版本号,这简化了脏数据检查,只需要检查顶层对象一处就能判断是否有更新。
    • 如用户发表了一篇文章后,可以更新用户的版本,这样就会从主库去读取数据了。
  • 基于全局版本/回话分离
    • 基于版本和回话分离的变种。在提交事务后执行一次SHOW MASTER STATUS操作。然后缓存中存储主库日志坐标,作为被修改对象以及/或者会话版本号。
    • 当连接到备库时,使用SHOW SLAVE STATUS并将备库的左边和缓存中的版本号做对比。如果备库相比记录点更新,就可以安全的读取备库数据。

大多数读/写分离解决方案都需要监控复制延迟来决策读查询的分配,不管是通过复制或负载均衡器,或者是一个中间系统。

修改应用配置

每台机器配置成连接到不同的MySQL备库,并为第N个用户或网站生成报表。

可以在每台服务器上修改硬编码或者在一个中心服务器上修改,然后通过文件副本或代码控制更新命令发布到其他服务器上。

修改DNS域名

比较粗糙的负载均衡技术,对于简单应用实用。可以为不同的服务器起不同的名字,如果出现延迟,就把DNS名指定为主库,如果备库能跟上主库,就把DNS名指定给备库。

最大的缺点是无法完全控制DNS:

  • 修改DNS不是立刻生效的,也不是原子的。DNS的变化传递到整个网络或在网络间传播都需要比较长的时间。
  • DNS数据会在各个地方缓存下来,这个过期时间是建议性的
  • 可能需要应用或服务器重启才能使修改后的DNS完全失效
  • 多个IP地址公用一个DNS名并依赖于轮询行为来均衡请求,这并不是一个好主意。因为轮询行为并不总是可预知的。
  • DBA可能没有权限修改DNS

修改HOSTS文件而非DNS来改善对系统的控制,降低延迟。

转移IP地址

一些负载均衡解决方案依赖于在服务器间转移虚拟地址,一般能够很好的工作。 根据指定的IP地址去监听流量,所以转移IP地址允许DNS保持不变。可以通过ARP(地址解析协议)命令强制IP地址的更改快速而且原子性的通知到网络上。

使用最普遍的技术是Pacemaker。类似工具包包括LVS和Wackmole。

可以使用虚拟IP对应物理IP的方式使这个操作更加容易。

引入中间件

中间件可以是硬件设备或者软件。

负载均衡器
  • 除非负载均衡器知道MySQL的真实负载,否则再发恩发请求时可能无法做到很好的负载均衡。因为均衡器对所有请求一视同仁
  • 许多均衡器可以对HTTP请求固定到一台服务器上,但是无法把所有从单个HTTP会话发送的连接请求固定到一个MySQL服务器,这将降低缓存效率
  • 连接池和长连接可能阻碍负载均衡器分发连接请求。连接池方案只有他们本身能够处理负载均衡器时才能工作的很好。
  • 许多多用途负载均衡器只会针对HTTP服务器做健康和负载检查。因此需要自己构建MySQL健康检查方法。
负载均衡算法
  • 随机
    • 随机选择一个服务器处理请求
  • 轮询
    • 循环顺序发送请求到服务器
  • 最少连接数
    • 又有最少活跃连接的服务器
  • 最快响应
    • 能够最快处理请求的服务器接收下一个连接。
    • 在服务器响应速度或者不同的缓存命中导致相应速度不同时很有用。
  • 哈希
    • 对源IP进行哈希,将其映射到同一个服务器上。
  • 权重
    • 结合上述算法,根据权重分发。

需要根据业务的工作负载决定使用哪种负载均衡。有时候使用排队算法可能更有效。有的连接池也支持队列算法。

在服务器池中增加/移除服务器

有时候需要缓慢增加一台新增服务器的负载以预热数据。或者在加入前对热机器的请求进行重放预热。

连接池要留有足够的容量应对维护或者失效。

一主多备间的负载均衡

  • 功能分区
    • 对特定的目的可以通过配置备库或一组备库来极大的提升容量。
    • 如报表、分析、数据仓库,以及全文索引
  • 过滤和数据分区
    • 可以使用过滤技术在相似的备库上做分区。
  • 将部分写操作转移到备库
    • 将写操作分解,一部分在备库上执行
  • 保证备库跟上主库
    • 使用MASER_POS_WAIT()函数阻塞直到备库赶上了设置的同步点。
    • 另一种替代方案是使用复制心跳来检查延迟
  • 同步写操作
    • 使用MASER_POS_WAIT()函数等待
    • 使用半同步复制

复制

复制的两种最常见的用途是:

  • 创建主服务器的备份,以避免主服务器崩溃时丢失任何数据
  • 让主服务器的副本执行报表和分析工作,而不会影响其它业务

复制可以做的更多:

  • 支持多个机房
  • 有服务器停机时高可用
  • 灾备
  • 错误保护
    • slave比master落后一个周期,在master上发生错误时,找到出错的语句,在slave执行前删除它

在横向扩展场景中使用复制时,重要的是要明白,MySQL复制传统上是异步的,因为事务首先在主服务器上提交, 然后复制到从服务器并在此处应用。这意味着master和slave可能不一致,如果复制持续运行,则slave将落后于master。

使用异步复制的优点是它比同步复制更快、更有效,但是在需要有实时数据的情况下,必须处理不同步的问题以确保信息的时效性。

MySQL复制原理

  • 通过热备份达到高可用性
  • 产生报表
    • 创建一个额外的服务器来运行大量的后台作业
  • 调试和审计

复制的基本步骤

系统创建一个能访问关键文件的shell用户。

# master
[mysqld]
user            = mysql
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
port            = 3306
basedir         = /usr
datadir         = /var/lib/mysql
tmpdir          = /tmp
# 默认hostname-bin,来自pid-file选项
# 使用默认值有一个问题就是hostname一旦改变会找不到文件,下同
log-bin         = master-bin
# 默认与上面同名
log-bin-index   = master-bin.index
server-id       = 1
# slave
[mysqld]
user            = mysql
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
port            = 3306
basedir         = /usr
datadir         = /var/lib/mysql
tmpdir          = /tmp
# 与master不重复
server-id       = 2
relay-log-index = slave-relay-bin.index
relay-log       = slave-relay-bin
-- master server创建一个复制用户
CREATE USER repl_user;
GRANT REPLICATION SLAVE ON *.* TO repl_user IDENTIFIED BY 'password';
-- 配置master和slave的用户,slave上执行
-- 需要FLUSH LOGS、SHOW MASTER\SLAVE STAUS、CHANGE MASTER TO 、等命令的权限
GRANT REPLICATION SLAVE, RELOAD, CREATE USER, SUPER ON *.* TO mats@'192.168.2.%' WITH GRANT OPTION;
-- slave上使用mats执行
CHANGE MASTER TO MASTER_HOST = 'master-1',MASTER_PORT = 3306,MASTER_USER = 'repl_user',MASTER_PASSWORD = 'password';
-- 删除并清空二进制文件,确保没有slave链接到master
RESET MASTER;
-- 删除复制用的所有文件
STOP SLAVE;-- 首先执行确保没有活动的复制
RESET SLAVE;

建立新slave(增加)

自举slave,而不是从头开始复制。参考《二进制日志》部分

  1. 配置新的slave
  2. 备份master(或者slave)
  3. 记录备份的binlog位置
  4. 从新的slave上恢复
  5. 配置slave从binlog位置开始恢复

方式1:克隆master

这种方式需要离线master

手动过程:

-- 刷新所有表并锁定
FLUSH TABLES WITH READ LOCK;
SHOW MASTER STATUS\G
# binlog下一个写入位置为456552
*************************** 1. row ***************************
             File: mysql-bin.0000042
         Position: 456552
     Binlog_Do_DB: 
 Binlog_Ignore_DB: 
# 备份master
mysqldump --all-databases --host=master-1 >backup.sql
UNLOCK TABLES;
# 导入slave
mysql --host=slave-1 <backup.sql
CHANGE MASTER TO MASTER_HOST = 'master-1', MASTER_PORT = 3306, MASTER_USER = 'slave-1', MASTER_PASSWORD = 'password', MASTER_LOG_FILE = 'master-bin.000042', MASTER_LOG_POS = 456552;
START SLAVE;

自动过程:

# 自动生成CHANGE MASTER TO语句
mysqldump --host=master -all-databases --master-data=1 >backup-source.sql
mysql --host=slave-1 <backup-source.sql

方式2:克隆slave

STOP SLAVE;
SHOW SLAVE STATUS \G

执行后可以创建备份,参考《克隆master》部分

...
Relay_Master_Log_File: master-bin.000042
...
Exec_Master_Log_Pos: 546632
START SLAVE;

FLUSH TABLES WITH READ LOCK在InnoDB中使用是不安全的,后台仍有一些阻止不了的活动在运行,下面方法可安全的创建InnoDB数据表的备份:

  • 关闭服务器并复制文件。 如果数据库很大,这可能是一个优势,因为使用mysqldump恢复数据可能会很慢。
  • 在执行FLUSH TABLES WITH READ LOCK之后使用mysqldump(如上面的操作过程)。
    • 读取锁定可防止读取数据时发生更改。
    • 如果要读取大量数据,数据库可能会长时间处于锁定状态。
    • 可以使用–single-transaction选项获取一致的快照,但只有在使用InnoDB表时才可能。
  • 在使用FLUSH TABLES WITH READ LOCK锁定数据库的同时,使用LVM(在Linux上)或ZFS(在Solaris上)等快照解决方案。
  • 使用MySQL企业备份(或XtraBackup)进行MySQL的联机备份。

执行常见的复制任务

横向扩展、热备份等

-- 查看二进制文件名称
SHOW BINARY LOGS;
# 停止slave后,指定binlog文件名称并指定binlog同步的日期区间来解析binlog内容
mysqlbinlog --force --read-from-remote-server --host=reporting.bigcorp.com --start-datetime='2009-09-25 23:55:00' --stop-datetime='2009-09-25 23:59:59' capulet-bin.000004
# 解析结果:
	.
	.
# at 2495
#090929 23:58:36 server id 1  end_log_pos 2650  Query   thread_id=27    exe...
SET TIMESTAMP=1254213690/*!*/;
SET /*!*/;
INSERT INTO message_board(user, message)
     VALUES ('mats@sun.com', 'Midnight, and I'm bored')
/*!*/;
-- 开启slave直到指定位置停止
START SLAVE UNTIL MASTER_LOG_POS='capulet-bin.000004', MASTER_LOG_POS=2650;
-- 阻塞检查直到同步完成,就可以进行其它操作了
SELECT MASTER_POS_WAIT('capulet-bin.000004',  2650);

任务调度:

# reporttab file content
# stop reporting slave five minutes before midnight, every day
55 23 * * * $HOME/mysql_control/stop_slave
# Run reporting script five minutes after midnight, every day
5 0 * * * $HOME/mysql_control/daily_report
crontab reporttab

二进制日志

参考《复制》部分二进制日志的配置

复制过程中需要binlog,它记录了服务器数据库上的所有变更,对于不改变数据的语句不会写入二进制日志。
二进制日志按照master上事务提交的顺序记录它们,每个事务在日志中是连续记录的,取决于事务提交的时间。

Tips:master和slave上下文环境不完全一致的话,可能导致执行结果不同。MySQL还提供了基于行的复制

文件记录

-- 刷新binlog
FLUSH LOGS;
-- \G字段换行输出,只显示第一个
SHOW BINLOG EVENTS\G
SHOW BINLOG EVENTS IN 'master-bin.000002'\G
SHOW BINLOG EVENTS FROM 238\G
-- 查看正在写入的文件名称
SHOW MASTER STATUS\G
*************************** 1. row ***************************
   Log_name: mysql-bin.000001
        Pos: 4
 Event_type: Format_desc
  Server_id: 1
End_log_pos: 107
       Info: Server ver: 5.5.34-0ubuntu0.12.04.1-log, Binlog ver: 4
*************************** 2. row ***************************
   Log_name: mysql-bin.000001
        Pos: 107
 Event_type: Query
  Server_id: 1
End_log_pos: 198
       Info: use `test`; CREATE TABLE tbl (text TEXT)
*************************** 3. row ***************************
...
*************************** 5. row ***************************
   Log_name: mysql-bin.000001
        Pos: 374
 Event_type: Xid
  Server_id: 1
End_log_pos: 401
       Info: COMMIT /* xid=188 */
*************************** 6. row ***************************
   Log_name: mysql-bin.000001
        Pos: 401
 Event_type: Rotate
  Server_id: 1
End_log_pos: 444
       Info: mysql-bin.000002;pos=4
6 rows in set (0.00 sec)
  • Event_type:事件类型
  • Server_id:事件服务器id
  • Log_name:存储事件的文件名
  • Pos:事件在文件中的开始位置
  • End_log_pos:事件在文件中的结束位置
  • Info:关于事件信息的可读文本

结构和内容

二进制日志由一组包含真实内容的binlog文件和一个跟踪binlog文件存储位置的索引文件组成。
有一个二进制文件是活动二进制文件,即当前被写入的文件。

二进制日志的构成

二进制日志文件都以格式描述时间开始,以日志轮换事件结束。

注意事项:如果服务器突然停止或司机,binlog文件末尾可能不是轮换事件

binlog文件中的事件组要么是不属于事务的单个语句,要么是由多条语句组成的事务。每个组要么全都执行,要么都不执行。

包含多个事件组的单个binlog文件

binlog事件的结构
  • 通用头
    • 事件的基本信息,事件类型和事件大小
  • 提交头
    • 与特定的事件类型有关
  • 事件体
    • 存储事件的主要数据
  • 校验和
    • 版本5.6开始可以产生,是一个32位整数,用于检查事件写入后是否有损坏
    • 默认开启,使用CRC32校验和
# 验证校验和
mysqlbinlog --verify-binlog-checksum master-bin.000001

将语句写入日志

在事件组写二进制日志之前,二进制日志将获得一个互斥锁LOCK_log,然后在事件组写完成后释放。这个锁常常会阻塞某些会话线程。

写入DML语句

数据操作语言(DML)语句通常是指DELETE,INSERT和UPDATE语句。
为了以一致的方式支持日志记录更改,MySQL在获取了事务级锁的同时写入二进制日志,并在二进制日志写入后释放它们。

为了确保二进制日志与语句修改的表一致地更新,每个语句在语句提交期间都被记录到二进制日志中,就在表锁释放之前。 如果没有将日志记录作为语句的一部分,则可以在语句向数据库引入的更改和对二进制日志的语句的日志记录之间“注入”另一个语句。 这意味着语句将以不同的顺序记录,而不是在数据库中执行的顺序,这可能会导致manster和slave之间的不一致。

写入DDL语句

数据定义语言(DDL)语句影响数据模式(schema),例如CREATE TABLE和ALTER TABLE语句。 DDL语句会在文件系统中创建或改变对戏那个,例如表定义存储在.frm文件中,而数据表现为文件系统中的目录。因此服务洗需要将这些信息保存在内部数据结构中。
为了保护这个内部数据结构的更新,在修改表定义之前需要先获得一个内部锁(称为LOCK_open)。

因为只有一个锁保护这些数据结构,所以数据库对象的创建、更新和销毁都可能带来性能问题。例如创建和销毁临时表。

写入查询

无论那种情况,时间都在不同的上下文(context)中执行,上下文是指服务器执行语句时必须知道的隐式(implicit)信息,以保证语句能够正确执行

  • 当前数据库
    • 向QUERY事件添加一个特殊字段,记录当前数据库
  • 当前时间
    • SYSDATE()操作系统取时间,用于复制不安全,慎用;
    • NOW()开始执行语句的时间
    • 事件存储一个时间戳,表明事件何时开始执行。
  • 上下文事件
    • 用户自定义变量的值
      • 事件:User_var
      • 记录变量名及值
    • RAND函数的种子
      • 事件:Rand
      • 记录RAND随机函数的种子,种子取自会话内部状态。
    • AUTO_INCREMENT字段的插入值
      • 事件:Intvar
      • 记录增量计数器的值
    • 调用LAST_INSERT_ID的返回值
      • 事件:Intvar
      • 记录返回值
  • 线程ID
    • 如CONNECTION_ID就必须知道线程ID,线程ID对临时表处理尤为重要
    • 临时表名称由服务器的进程ID,创建表的进程ID和一个线程计数器组成,计数器用来区分同一个线程中不同的临时表实例。
    • 线程ID作为一个独立字段存储在每个QUERY事件,因此可以用线程ID字段来计算线程特定的数据,并正确处理临时表
*************************** 1. row ***************************
   Log_name: mysqld1-bin.000001
        Pos: 238
 Event_type: Query
  Server_id: 1
End_log_pos: 306
       Info: BEGIN
*************************** 2. row ***************************
   Log_name: mysqld1-bin.000001
        Pos: 306
 Event_type: Intvar
  Server_id: 1
End_log_pos: 334
       Info: INSERT_ID=1
*************************** 3. row ***************************
   Log_name: mysqld1-bin.000001
        Pos: 334
 Event_type: RAND
 Server_id: 1
End_log_pos: 369
       Info: rand_seed1=952494611,rand_seed2=949641547
*************************** 4. row ***************************
   Log_name: mysqld1-bin.000001
        Pos: 369
 Event_type: User var
  Server_id: 1
End_log_pos: 413
       Info: @`foo`=12
*************************** 5. row ***************************
...
*************************** 9. row ***************************
   Log_name: mysqld1-bin.000001
        Pos: 681
 Event_type: Intvar
  Server_id: 1
End_log_pos: 709
       Info: LAST_INSERT_ID=1
LOAD DATA INFILE语句
  • Begin_load_query
    • 这个事件标志着文件中数据传输的开始。
  • Append_block
    • 一个或多个这些事件的序列遵循Begin_load_query事件,以包含文件的其余数据,如果文件大于连接上允许的最大数据包大小。
  • Execute_load_query
    • 该事件是QUERY事件的一种特殊变体,它包含在master服务器上执行的LOAD DATA INFILE语句。
    • 即使该事件中包含的语句包含了master服务器上使用的文件的名称,但这个文件将不会被slave找到。相反,使用前面的Begin_load_query和Append_block事件获取文件的内容。
SHOW BINLOG EVENTS IN 'master-bin.000042' FROM 269\G
*************************** 1. row ***************************
   Log_name: master-bin.000042
        Pos: 269
 Event_type: Begin_load_query
  Server_id: 1
End_log_pos: 16676
       Info: ;file_id=1;block_len=16384
*************************** 2. row ***************************
   Log_name: master-bin.000042
        Pos: 16676
 Event_type: Append_block
  Server_id: 1
End_log_pos: 33083
       Info: ;file_id=1;block_len=16384
*************************** 3. row ***************************
   Log_name: master-bin.000042
        Pos: 33083
 Event_type: Append_block
  Server_id: 1
End_log_pos: 33633
       Info: ;file_id=1;block_len=527
*************************** 4. row ***************************
   Log_name: master-bin.000042
        Pos: 33633
 Event_type: Execute_load_query
  Server_id: 1
End_log_pos: 33756
       Info: use `test`; LOAD DATA INFILE 'foo.dat' INTO...;file_id=1
4 rows in set (0.00 sec)
Binary Log Filters

将事务写进日志

以下情况开启事务:

  • 用户发出START TRANSACTION或BEGIN命令时
  • 当AUTOCOMMIT=1,且开始执行访问事务型表的语句时(事务型表)
  • 当AUTOCOMMIT=0,且上一个事务已经隐式或显示地被提交或终止时

非事务型语句之后执行的事务型语句仍然属于当前活动的事务。

隐式提交的语句:

  • 写文件的语句
    • 大多数的DDL语句
  • 修改MySQL数据表的语句
    • 所有创建、删除或修改用户账户或用户权限的语句都是隐式提交的,而且不是事务的一部分。
  • 出于实践原因要求隐式提交的语句
    • 锁定表、用于管理的语句和LOAD DATA INFILE语句都会导致隐式提交,这是具体实现的需要。
事务缓存

二进制日志包含所有会话的事务信息,按照它们提交的顺序保存,就好像它们都是顺序执行的。

为了确保每个事务都是作为二进制日志的一个单元来编写的,服务器必须在不同的线程中分别执行不同的语句。 在提交事务时,服务器将所有作为事务部分的语句写入到二进制日志中,作为单个单元。 为此,服务器为每个线程保留一个事务缓存(transaction cache)。为事务执行的每个语句都放在事务缓存中,事务缓存的内容随后被复制到二进制日志中,并在事务提交时清空。

transaction-cache

非事务型语句如果同时影响事务型表和非事务型表,情况更加复杂,下面是记录时采取的一些方法:

如何写入非事务型语句:

当没有事务打开时,非事务型语句被写入到语句执行结束时的二进制日志中,并且在结束二进制日志之前不会在事务缓存中“中转”。但是,如果事务是打开的,规则如下:

  • 如果该语句被标记为事务型的,那么它将被写入到事务缓存中。
  • 如果语句没有被标记为事务型的,并且在事务缓存中没有语句,那么该语句将直接写入到二进制日志中。
  • 如果语句没有被标记为事务型的,但是在事务缓存中有语句,那么语句将写到事务缓存中。
    • 避免了事务型操作和非事务型操作顺序乱序问题
    • 会引起非事务型数据表操作顺序与写入binlog的顺序不同

如何避免非事务型语句的复制问题:

  • 不使用非事务型表
  • 确保事务中影响非事务型表的语句先写入日志。这样就先写入日志。

binlog_direct_non_transactional_updates选项强制非事务型语句直接写入二进制日志。 这要保证这些语句之间没有依赖关系,否则就要使用基于行的复制。

使用XA进行分布式事务处理

MySQL版本5.0允许使用X/Open DT模型即XA来协调涉及不同资源的事务。 在5.0版本中,服务器在内部使用XA来协调二进制日志和存储引擎。

XA包括一个事务管理器,该事务管理器协调一组资源管理器,以便它们以原子单元的形式提交全局事务。 每个事务分配一个唯一的XID,由事务管理器和资源管理器使用。当在MySQL服务器内部使用时,事务管理器通常是二进制日志,资源管理器是存储引擎。

提交XA事务的过程

  • 在阶段1中,每个存储引擎被要求准备提交。在准备过程中,存储引擎会写入任何需要正确提交到安全存储的信息,然后返回一个OK消息。如果任何存储引擎的回答是否定的,即它不能提交事务,那么提交将被中止,所有的引擎都被指示回滚事务。
    • 在所有存储引擎都报告了它们已经准备好了没有错误的情况下,在第2阶段开始之前,事务缓存被写入到二进制日志中。与正常的事务不同,正常的事务以一个提交的普通查询事件结束,XA事务以包含Xid的Xid事件终止。
  • 在第2阶段中,在第1阶段准备的所有存储引擎都被要求提交事务。当提交时,每个存储引擎将报告它已经提交了在稳定存储中的事务。
    • 重要的是要理解commit不能失败:一旦阶段1通过,存储引擎保证事务可以提交,因此不允许在第2阶段报告失败。当然,硬件故障可能导致崩溃,但由于存储引擎存储了持久存储中的信息,因此当服务器重启时,它们将能够正确地恢复。
  • 在第2阶段之后,事务管理器就可以丢弃共享资源。二进制日志不需要做任何清理操作,因此在这一步中它对XA没有任何特别的作用。

如果提交XA事务的时候发生系统崩溃,服务器重启后进入恢复过程。

启动后,服务器打开上一个二进制文件并检查Format_description事件。如果binlog_in_use标记被设置,说明服务器发生了崩溃,需要进行XA恢复。

XA恢复过程

服务器首先检查二进制日志,通过读取Xid事件确定所有事务的XID,然后服务器中加载的存储引擎将根据这个清单来提交事务。 对于列表中的每个XID,存储引擎将判断是否准备了与XID的事务,但没有提交,如果是这样,则提交它。如果存储引擎已经准备了一个没有在这个列表中的XID的事务,那么在服务器崩溃之前,XID显然没有写入二进制日志,因此事务应该回滚。

阶段提交协议为了保证事务的一致性,不管是事务管理器还是各个资源管理器,每执行一步操作,都会记录日志,为出现故障后的恢复准备依据。

二阶段提交协议的存在的弊端是阻塞,因为事务管理器要收集各个资源管理器的响应消息,如果其中一个或多个一直不返回消息,则事务管理器一直等待,应用程序也被阻塞,甚至可能永久阻塞。

二进制日志的组提交

由于数据库系统必须能够安全应对崩溃情况,所以需要在事务提交的时候,强制将数据写回磁盘。 如果每个事务都要写磁盘会带来性能问题,为了避免这个问题,多个独立事务可以按组的形式,一起写入磁盘,这就是组提交(group commit)。
不仅要考虑存储引擎提交事务数据的效率,还好考虑写二进制日志的效率。为此MySQL 5.6增加了二进制日志组提交(binary log group commit)。

每个事务完全提交之前增加一些步骤,每个步骤引入了一个互斥对象,确保每个步骤最多只有一个线程。

二进制日志的组提交架构

各个步骤负责处理一部分提交过程。

  • 第一步将线程的事务缓存到文件页
  • 第二步执行一个同步操作将文件写到磁盘
  • 最后一步提交所有事务

为了以有序的方式在各个阶段之间移动会话,每个阶段都有一个相关的队列,其中会话可以排队等待处理。每个阶段队列都由在操作队列时短暂持有的互斥锁保护。

Binlog组提交阶段和互斥对象:

Stage Stage mutex Stage queue mutex
Flush LOCK_log LOCK_flush_queue
Sync LOCK_sync LOCK_sync_queue
Commit LOCK_commit LOCK_commit_queue

通常一个会话对应一个线程,在flush步骤,任何想要提交事务的会话线程会进入队列:

  1. 如果会话线程排队到一个非空的队列,则它是一个follower,并将等待它的事务由其它会话线程提交。
  2. 如果会话线程排队到一个空的队列,它是一个leader,并注册所有进入该队列的会话。
  3. leader在一个步骤中清空队列的所有会话。会话的顺序将被维护,新的会话可以进入到队列。
  4. 阶段处理完成如下:
    • 对于flush,每个会话的事务按照它们进入flush队列的顺序被flush到二进制日志。
      • 做了优化,每次“搬”一个会话,flush它。只要还有会话进入队列,就没必要处理整个队列,逐个处理能让更多会话入队。
      • binlog_max_flush_queue_time参数控制leader线程从flush队列“搬”会话的时间。
    • 对于sync,执行一个fsync调用。
    • 对于commit,事务在存储引擎中按照注册的顺序提交。
  5. 这些会话将按照与此阶段注册的相同顺序排队等待下一个阶段的队列。
    • 如果队列是非空的,leader就变成了follower,但是follower不能变成leader
    • 新的leader线程会将提交过程进行到一半的旧线程合并到会话队列中,从而系统可以动态适应各种情况。

注意事项:binlog_order_commits控制事务是否按顺序提交,如果为OFF,则平行提交事务,线程本身以任意顺序提交,而不用等待leader。

基于行的复制

基于语句的复制仍然无法正确处理:

  • 如果UPDATE、DELETE或INSERT语句包含一个LIMIT子句,那么在执行过程中数据库崩溃可能会导致问题。
  • 如果在执行非事务性语句时出现错误,则不能保证对主和从器的效果是相同的。
  • 如果一个语句包含对UDF的调用,那么就没有办法确保对这个slave使用相同的值。
  • 如果该语句包含任何不确定的功能——比如USER、CURRENT_USER或connection_id——结果可能会在master和slave之间有所不同。
  • 如果一个语句更新了两个带有自动递增列的表,那么它将不能正常工作,因为只有一个最后的插入ID可以被复制,这将被用于两个表上的从属,而在主服务器上,每个表的插入ID将被单独使用。

上述情况下,最好复制插入表中的真实数据,这就是基于行的复制。
基于行的复制不复制产生变更的语句,而是复制每个被插入、删除、更新的行。发给slave的行与发给存储引擎的行是一样的,包含插入表的真实数据。

基于行与基于语句复制的选择:

  • 语句是否会更新大量的行,还是只做少量行的更新或插入
    • 更新大量的行,那么基于语句的复制更快,但并不总是这样,如果语句的优化和执行计划很复杂,可能基于行的复制更快,因为寻找行的逻辑快的多。
    • 如果只更新或插入少量的行,则基于行的复制更快,因为不需要解析直接交给存储引擎处理。
  • 是否需要知道执行了哪些语句
    • 基于行的复制中,事件难以解码
    • 基于语句的复制中,语句被写入二进制日志中,因此可以直接读取
      • 5.6后支持产生行的语句和行一起写入日志
启用基于行的复制

可以通过binlog-format选项控制使用那种格式。该参数可以作为全局变量也可以作为会话变量。 此选项可以使用值STATEMENT, MIXED, 或者 ROW。

[mysqld]
user            = mysql
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
port            = 3306
basedir         = /usr
datadir         = /var/lib/mysql
tmpdir          = /tmp
log-bin         = master-bin
log-bin-index   = master-bin.index
server-id       = 1
# 基于行复制的参数
binlog-format   = ROW
使用混合模式

混合模式:正常情况下使用基于语句的复制,而对不安全的语句则切换到基于行的复制。

切换到行复制的情况:

  • 语句调用了
    • UUID函数
    • 用户自定义函数
    • CURRENT_USER或USER函数
    • LOAD_FILE函数
  • 一个语句同时更新了两个或两个以上含有AUTO_INCREMENT列的表
  • 语句中使用了服务器变量
  • 存储引擎不允许使用基于语句的复制,如MySQL Cluster引擎
  • 后续增加的其它影响安全的因素

二进制日志管理

二进制文件有多个文件组成,将它们分割成适当的组,构成一个binlog文件序列。 为了操作安全,向二进制日志添加一些特殊事件(轮换事件)。

二进制日志和系统崩溃安全

如果更新没有写入二进制日志,就不会提交到存储引擎,反之亦然。

非事务引擎情况下,存储引擎在语句写入日志之前,早就完成了所有变更;
为了解决此问题,事件写入二进制日志的时机是表上的锁释放之前,所有变更提交给存储引擎之后。 因此如果在存储引擎释放锁之前系统崩溃,服务器必须确保写入二进制日志的变更实际存在于磁盘上的表中,然后才能提交语句或事务。 这些需要与标准文件系统之间的同步与协调。

操作系统将文件的一部分缓存在内存的某个特殊位置,通常称之为页面缓存。
XA第一阶段后,所有数据被写入磁盘,以正确的应对崩溃。每次提交事务事,页面缓存都会被写入磁盘(实际由组提交控制)。

sync-binlog选项可以控制写入磁盘的频率,数值表示提交固定次数后写入磁盘。默认为0,由操作系统控制。

对于支持XA的存储引擎,例如InnoDB,设置sync-binlog=1,一般的系统崩溃情况下,都不会丢失任何数据。
而不支持XA的引擎,可能会至少丢失一个事务。

binlog文件轮换

4种行为导致轮换:

  • 服务器停止
  • binlog文件大小达到最大限制
    • binlog-cache-size来控制文件大小
  • 二进制日志被显式刷新
    • FLUSH LOGS命令
    • 建议使用binlog文件进行恢复前,强制执行显示刷新而不是使用活动文件
  • 服务器上发生事故

Format_desc事件:

  • binlog-in-use标记
    • 写入轮换事件后设置该标记
  • binlog文件格式版本
  • 服务器版本

为了应对崩溃时候也能安全地轮换二进制日志,服务器采用预写策略,把这个意图存到一个名为清除索引文件的临时文件中,这个文件也被用于清除binlog文件。

master-bin.index对应master-bin.~rec~

事故

指在服务器上不会产生数据变更但是必须写入二进制日志的时间,因为它们潜在的影响了复制。

二进制日志中有以下两种incident事件:

  • Stop
    • 表示服务器正常停止
    • slave上重放二进制文件,将忽略任何Stop事件
  • Incident
    • 表示通用的事故事件,from 5.1
    • 该事件包含一个标识符指定发生的事故类型。表明服务器被迫执行了一些操作,可能导致二进制日志变更丢失。
    • 如数据库重新装载或者非事务型事件由于过大无法写入binlog文件。
    • slave上重放二进制日志时,遇到Incident时间,就会因错误而停止。
    • 如果在集群重载时发现Incident事件,表明需要重新同步集群,很可能还要找到丢失的事件。
清除binlog文件

expire-logs-days选项,服务器可以自动清除旧的binlog文件。

手动清除:

  • PURGE BINARY LOGS BEFORE datetime
  • PURGE BINARY LOGS TO ‘filename’

如果发生崩溃,服务器通过对比清除索引文件和索引文件内容来继续清除(参考轮换部分),并删除哪些因系统崩溃而没有被删除的文件。
清除索引文件也会在轮换时使用,因此在索引文件正确更新之前发生崩溃,新的binlog文件将被删除,然后在每次轮换时重新创建binlog文件。

mysqlbinlog实用工具

基本用法
SHOW BINARY LOGS;
sudo mysqlbinlog --short-form --force-if-open --base64-output=never /var/lib/mysql1/mysqld1-bin.000038

文件可以指定多个

  • short-form
    • 只输出发出的sql语句信息,忽略注释
  • force-if-open
    • 禁止输出警告
  • base64-output=never
    • 阻止输出base64编码的事件
  • start-position=bytepos
    • 转储的第一个事件的字节位置,多个文件中的第一个
  • stop-position=bytepos
    • 最后输出的事件的字节位置,多个文件中的最后一个
  • start-datetime=datetime
  • stop-datetime=datetime

读取远程文件

只需要服务器上有一个用于REPLICATION SLAVE权限的用户即可

使用read-from-remote-server选项读取binlog文件,参数包括歍的主机和用户、可选的端口号和密码,以及binlog文件名称

$ sudo mysqlbinlog
>    --read-from-remote-server
>    --host=master.example.com
>    --base64-output=never
>    --user=repl_user --password
>    --start-position=294
>    mysqld1-bin.000038

读取日志文件的原始二进制文件

mysqlbinlog工具不仅可以用于审查二进制日志,还可以获取binlog文件的备份。

# 文件会存储与当前目录
mysqlbinlog --raw --read-from-remote-server \
   --host=master.example.com --user=repl_user \
   master-bin.000012 master-bin.000013 ...
  • –result-file=prefix
    • 创建写入文件的前缀,可以是目录名(反斜杠)或任何其它前缀
  • –to-last-log
    • 给定开始的文件,会传送剩余的文件
  • –stop-never
    • 达到一个日志文件末尾也不停止,等待更多输入。
解释事件

hexdump选项告诉mysqlbinlog去写事件的市集字节。

$ sudo mysqlbinlog                       \
   >     --force-if-open                    \
   >     --hexdump                          \
   >     --base64-output=never              \
   >     /var/lib/mysql1/mysqld1-bin.000038

二进制日志的选项和变量

  • expire-log-days=days
    • 文件保留天数,重启或轮换时删除
    • 默认0,永不删除
  • log-bin[=basename]
    • 开启二进制日志,及指定binlog文件的文件名
  • log-bin-index[=filename]
    • 索引文件名
  • log-bin-trust-function-creators
    • 废除创建存储函数时需要SPUER权限的要求
  • binlog-cache-size=bytes
    • 事务缓存在内存中的部分的大小,以字节数计
    • 大事务中增加这个值可以提高性能
  • max-binlog-cache-size=bytes
    • 日志文件中每个事务的大小,如果事务大小超过这个值,将出错终止,防止长时间阻塞二进制日志
  • max-binlog-size=bytes
    • 每个binlog文件的大小,超过则轮换
  • sync-binlog=period
    • 事务调用次数需写入磁盘的阈值,0表示由操作系统控制
  • read-only
    • 阻止任何客户端进程(除了SUPER权限的slave线程和用户)更改服务器上的任何数据
    • 对于slave服务器的复制工作非常有用,可以保证slave客户端不破坏数据
基于行的复制参数

binlog-format参数可以设置为以下几种模式:

  • STATEMENT
    • 基于语句的复制
  • ROW
    • 基于行的复制
    • DDL语句还是基于语句的复制
  • MIXED
    • 以语句方式写入二进制文件,如果语句不安全,切换为基于行的复制

binlog-max-row-event-size:

指定何时开始下一个包含行的事件。由于事件在处理时被完全读入内存,该参数粗略控制那些包含行的事件的大小,保证处理行的时候不会消耗过多的内存。

binlog-rows-query-log-events (new in MySQL 5.6.2以后的版本支持,否则导致slave停止复制):

在行事件值钱向二进制日志添加一个信息事件,这个信息事件包含产生这些行的原始查询。

Tips:5.6.2以后的版本会忽略不支持的事件

面向高可用性的复制

确保高可用的三件事:

  • 冗余
    • 如果一个组件出现故障,必须有一个替代品;替代品可以使闲置的,也可以是系统中的一部分。
  • 应急计划
    • 故障后应该做什么。取决于那个组件出现故障,以及为何出现故障
  • 程序
    • 必须能检测出故障原因并迅速解决

如果系统中单个组件的故障导致整个系统瘫痪,成为单点故障。如果系统中存在单点故障,就会严重限制我们实现高可用性的能力。 因此,首要目标是找到这些单点故障,确保我们做了冗余处理。

冗余

一旦确定了哪里需要冗余,我们需要从两个基本方案选择:

  1. 为每个组件保留副本,一旦原先的组件发生故障,副本马上接管
    • 切换时不会影响性能
    • 速度快与系统故障恢复
  2. 确保系统有额外的处理能力,一旦组件出现故障时,依然可以处理负载
    • 需要更多的处理能力,如果在故障时负载满载,也就失去了继续冗余处理故障的能力
  3. 这不是二选一,你可以将两者相结合

服务器发生故障的概率:

单点故障概率 1 2 3
1.00% 100.00% 49.50% 16.17%
0.50% 50.00% 12.38% 2.02%
0.10% 10.00% 0.50% 0.02%

计划

  • slave故障怎么处理已经存在的链接
    • 通常由应用层向另一个服务器重新尝试查询
  • master故障
    • 如果有冗余的master,需要将所有的slave都移到master上
slave故障

负载均衡将新的查询定位到正常工作的slave,由于失去连接报错后,应用重新递交给正常工作的slave。

master故障

迅速替换,防止写操作中断时间过长。
所有slave上存在过时的数据

relay故障

对中继服务器(relay)的故障,需要特殊处理。

剩余的slave必须重定向到其它中继服务器或master,由于添加中继服务器就是为了减轻master的负载,有可能出现master无法处理某个中继服务器上的所有slave的负载

灾难恢复

不可抗拒力,多种故障同时发生。

将数据保存在另一个物理位置。

方法

准备工作:

  • 添加新的slave
    • 创建现有slave的快照,恢复后从合适的位置启动复制
    • 方法
      • 使用mysqldump
        • 不用关闭服务器,安全,速度慢
        • 参数可以直接获取快照位置
      • 复制数据库文件
        • 先将服务器离线,速度快
        • 需要mysqldump获取正确启动复制的位置
      • 使用在线备份方法
        • MySQL Enterprise Backup和XtraBackup
      • 使用LVM获取快照
        • Linux逻辑卷管理器(LVM)得到快照。
        • 自己管理复制位置
      • 使用文件系统快照的方法
        • 操作系统支持的快照功能
        • 自己管理复制位置
  • 从拓扑结构中删除slave
    • 在负载均衡中剔除slave,然后删除它
  • 切换master
    • 将连接在master上的所有slave切换到副master,然后通知负载均衡剔除原来的master
    • 另一种方法是使用slave提升
    • 热备份
  • slave故障处理
    • 检测到slave不存在了就在负载均衡池中剔除
  • master故障处理
    • 把slave转移到奥一个备用master上或者选择一个slave提升为master
  • 升级slave
    • 需要在负载均衡中剔除后升级
  • 升级master
    • 首先要升级所有的slave,这样才能读取master全部的复制事件
    • 通常升级时使用备用master或者使用slave提升的master
热备份

做服务器副本最简单的拓扑结构就是热备份(hot standby)。

热备份是一个专用服务器,它是主master的副本,以slave方式连接到master,以读取和应用更新。

这种配置通常称为主-备份配置(primary-backup configuraion),可以有多个热备份。

具有一个热备份的master

热备份为我们提供了修复master的机会,修复master后,需要让它重新工作,要么将它设置为热备份,要么把所有的slave再重定向回来。

主master还在运行的时候,切换到热备份

处理切换:

slave从standby上开始复制的位置,同它在master上停止复制的位置,要完全一致。通常位置是不同的。

在完全相同的位置停止运行slave和standby,然后把slave重定向到standby,由于standby停止后位置没有发生变动,只需确定 standby的binlog位置,然后让slave从那个位置启动。这个任务必须手动执行,因为简单的停止无法使它们之间是同步的。

-- 检查standby和slave的状态
SHOW SLAVE STATUS;
-- 使用该命令使slave同步到相同位置
START SLAVE UNTIL
       MASTER_LOG_FILE = 'master-bin.000096',
       MASTER_LOG_POS =  756648;
-- 等待slave同步完成
SELECT MASTER_POS_WAIT('master-bin.000096',  756648);
-- standby上运行查看停止节点
SHOW MASTER STATUS;
-- 调整slave重定向到standby
CHANGE MASTER TO
       MASTER_HOST = 'standby.example.com',
       MASTER_PORT = 3306,
	   MASTER_USER = 'repl_user',
       MASTER_PASSWORD = 'xyzzy',
       MASTER_LOG_FILE = 'standby-bin.000019',
       MASTER_LOG_POS = 56447;
-- 如果slave在standby前面,则把上面某些步骤的slave和standby对调
# 使用python处理切换
from mysql.replicant.commands import (
    fetch_slave_position,
    fetch_master_position,
    change_master,
)
def replicate_to_position(server, pos):
    server.sql("START SLAVE UNTIL MASTER_LOG_FILE=%s, MASTER_LOG_POS=%s",
               (pos.file, pos.pos))
    server.sql("SELECT MASTER_POS_WAIT(%s,%s)", (pos.file, pos.pos))
def switch_to_master(server, standby, master_pos=None):
    server.sql("STOP SLAVE")
    server.sql("STOP SLAVE")
    if master_pos is None:
        server_pos = fetch_slave_position(server)
        standby_pos = fetch_slave_position(standby)
        if server_pos < standby_pos:
            replicate_to_position(server, standby_pos)
        elif server_pos > standby_pos:
            replicate_to_position(standby, server_pos)
        master_pos = fetch_master_position(standby)
    change_master(server, standby, master_pos)
	standby.sql("START SLAVE")
    server.sql("START SLAVE")
双主结构

两个master互相复制,保持同步。双主结构是对称的,用起来非常简单。 将故障切换到备份master上不需要重新配置主master,备用master故障时切换回来也非常简单。

服务器可以是主动的(active),也可以是被动的(passive):
如果是主动的,是指服务器接受写操作,这些写操作可以通过复制传播到其它地方;
如果是被动的,只是跟随主动的master,一旦主动master发生故障可以替代它。

根据目的不同,有两种不同的配置:

  • 主动-主动
    • 写操作同时到达两个服务器,然后将变更发送给对方
    • 用于不同地区的用户集同构访问地理位置较近的服务器
    • 由于事务在本地被提交,系统响应更快,这也意味着两个master不是一致的。一个master上提交的变更最终会传播到另一个master,在此之前,两个master上的数据是不一致的
  • 主动-被动
    • 负责写操作的成为主动master,另外一个为被动master,与主动master保持同步
    • 和热备份差不多,很容易在两个master之间进行切换
    • 然而不需要被动master响应查询,有些方案中被动master实际是一个冷备份。

主主不同步造成两个后果需要注意:

  • 如果两个master都更新了同样的信息,这两个变更之间将会产生冲突,可能会导致复制停止。
  • 如果两个master不一致的时候发生了系统崩溃,有些事务将丢失。

只允许写一个master可以从一定程度上避免变更冲突的问题,从而使另一个master成为被动maaster,即主动-被动模式。

使用异步复制不可避免的结果是服务器崩溃时会丢失事务,MySQL 5.5新功能半同步复制,可以限制事务丢失的数量。
原理是:提交线程的事务会被阻塞,直到至少一个slave确认收到这个事务。由于事务提交到存储引擎后事件才会发给slave,所以事务丢失数量可以控制到最多每个线程1个。

主动-被动配置的一个重要问题是,解决两台服务器同时成为主master的风险问题,有称为脑裂综合症。 如果网络连接丢失,被动master将自己提升为主动,后来主动master又重新联机,这时就会产生这个问题。
为了阻止这个问题发生,通过一种称为STONITH的技术实现,实现有很多种,如连接到服务器然后使用kill -9(如果服务器可达), 关闭网卡隔离服务器,或者关掉机器电源等。如果服务器真的不可达,下次服务器又能访问的时候要使用“毒丸”让它自杀。
处理脑裂综合症为题依赖于使用共享磁盘解决方案,如SCSI支持服务器预留磁盘,服务器发现磁盘被另一个服务器预留,意识到自己不再是primary,就把自己离线。

共享磁盘

使用共享磁盘的双主结构

忧点:不用切换binlog的位置,master切换速度很快,只需要记住slave停止的位置,执行CHANGE MASTER命令,然后再次启动复制。

问题:要确保两个master不会同时写文件,在被动master上执行任务时必须小心,重写配置文件,哪怕时失误,都可能时灾难性的。只读模式仍然不够,因为InnoDB处于只读模式还是会写文件。

使用DRBD(分布式复制块设备)复制磁盘

行为和外观上和正常磁盘一样,不需要mysql做特殊配置。

Using-DRBD-to-replicate-disks

只能在主动-被动配置中使用DRBD技术,被动磁盘完全不能访问,被动master也不能访问。
切换速度比共享磁盘方案慢。
和共享磁盘相同,需要在服务器联机之前恢复数据库文件,所以建议使用恢复性能搞的事务性引擎,MyISAM表的恢复成本相当高,InnoDB是一个好选择。

相对于共享磁盘方案的优点:

  • 避免了磁盘的单点故障。
  • DRBD还内置了脑裂综合症问题的解决方案,可以配置为自动恢复

双向复制

双向复制可以邮主动-主动配置,也可以使用在主动-被动结构中。

Bidirectional-replication

配置双向复制的步骤:

  1. 确保两台服务器拥有不同的服务器Id
  2. 确保两台服务器具有不同的数据(并且在复制启动之前量个系统没有变更)
    • 确定复制的数据没有冲突
  3. 创建一个复制用户,在两台服务器上准备复制,参考《复制》部分
  4. 在两台服务器上准备复制

如果要把一个slave连接到其中一个服务器,要启用log-slave-updates选项。(使用服务器ID跳过自己发出的事件被重复传播回来的问题)

主动-主动配置的唯一推荐的方法,是保证不同的主动服务器写不同的区域

方案之一是为不同的master分配不同的数据库(或者不同的表),如使用视图连接不同的表。以下问题使管理变的复杂:

  • 对不同的表进行读写
    • 应用程序将读写分离,从表写入,从表或视图读取
  • 准确数据和当前数据
    • 快照数据不准确,要求准确信息要依赖于应用程序
  • 优化视图
    • 利用创建视图和结果集有两个方法,MERGE和TEMPTABLE
    • 对视图做仔细设计是获得良好性能的因素

如果更新同一张表,MySQL服务器设了两个变量用于处理这种情况(会话的或者是全局的)。

  • auto_increment_offset
  • aotu_increment_increment
value = auto_increment_offset + N * aotu_increment_increment
-- The common table can be created on either server
CREATE TABLE Employee (
   uid INT AUTO_INCREMENT PRIMARY KEY,
   name VARCHAR(20),
   office VARCHAR(20)
);
-- Setting for first master
SET GLOBAL AUTO_INCREMENT_INCREMENT = 2;
SET GLOBAL AUTO_INCREMENT_OFFSET = 1;
-- Setting for second master
SET GLOBAL AUTO_INCREMENT_INCREMENT = 2;
SET GLOBAL AUTO_INCREMENT_OFFSET = 2;

注意事项:使用这种方法应该控制对应序列Id的处理发送给正确的服务器,否则会导致不一致问题,一种可行方案是划分表权限,但并不总是可行

提升slave

如果备用服务器之后与任何一个slave,就不能使用备用服务器作为master

提升slave处理master故障的方法:不是保持一个专门的备用服务器(当然也就没有最佳候选备用), 而是确保任何一个连接到master的slave都能被提升为master,并且从master故障的位置接管。 选择“知道最多”的slave成为master,将它们连接到新的master,然后从新的master上读取事件。

提升slave的传统方法

提升slave替代故障的master

要求:

  • 每个可提升的slave必须有一个复制用户账户
  • 每个可提升的slave运行时必须使用log-bin选项,即启动二进制日志
  • 每个可提升的slave运行时必须不适用log-slave-updates选项

步骤:

  1. 使用STOP SLAVE停止slave
  2. 使用RESET MASTER重置迹象成为新master的slave。slave将以master身份启动,其它连接的slave从提升的那个时刻开始读取事件
  3. 使用CHANGE MASTER将其它slave连接到新的master上。由于重置了新的master,直接从二进制的七点开始复制,不需要额外的位置参数。

每个slave都需要获取丢失的事务,如果slave没有启动二进制日志,可以复制整个库或使用类似mysqldbcompare的工具获取变更。

注意事项:大多数情况下,传统方法并不适用,因为slave往往落后于master

提升slave的修正方法

Binary-log-positions-of-the-master-and-the-connected-slaves

即使知道最多的那个slave,也没有从故障master那里获得全部变更。没有复制到新master的变更将丢失。5.6引入了GTID就不存在这个问题,或者实现一个类似GTID的机制。

环形复制

环形复制:所有用户数据被复制到所有站点,所有的数据中心都可以进行数据更新。

由于slave只能连接一个master,所以两个以上master互相复制,只能以环形的方式搭建。

MySQL 5.6引入了全局事务ID后,很多不推荐环形复制的原因都失效了,其中一个主要原因是一旦发生故障系统将无法运行。

Circular-replication-setup

某台服务器出现故障,其它服务器重新连接至上游服务器,使得复制继续。有三个问题:

  • 下游服务器需要连接上游服务器,并且从最近的位置开始复制,怎么确定位置
  • 故障服务器崩溃前发出了一些事件,这些事件怎么处理
  • 怎样把故障服务器重新接入拓扑结构,以及写入日志而为发出的事件丢失问题或者在接入时被重新发送

Changing-topology-in-response-to-a-failing-server

所有问题可以通过全局事务标识符解决,使用CHANGE MASTER命令加上MASTER_AUTO_POSITION=1选项,将下游服务器连接到上游服务器。

CHANGE MASTER TO MASTER_HOST='stockholm.example.com', MASTER_AUTO_POSITION = 1;

由于每个服务器都有事务记录,故障服务器发出的任何事务都会在剩余的每个服务器上执行一次,问题2和3丢失问题自动解决了。
将服务器恢复到环中的方式是,从环中任意一台服务器恢复,然后接入环中,防止重新发送。

面向横向扩展的MySQL复制

当负载开始增加,有两种解决办法:

  • 第一种方法时购买更大的服务器来应对增加的负载,成为纵向扩展(或向上扩展,scale up)
  • 第二种方法时添加更多的服务器,成为横向扩展(或向外扩展,scale out)
    • 更常用,只需购买低成本的标准服务器,更具有成本效益
    • 添加服务器不仅可以处理增加的负载,还可以支持高可用性及其它商业要求。如果有效使用,可以综合并利用所有的服务器资源

横向扩展和复制的常见用途:

  • 读操作的负载均衡
    • master忙于更新数据,所以将响应查询的服务器分离出来
  • 写操作的负载均衡
    • 高流量的部署将处理分发到很多计算机上,复制在分发信息的过程中起着关键作用。
      • 基于信息角色的分发。很少更新的表在一个服务器上,频繁更新的表分割到多个服务器上
      • 按地理区域分割,这样流量可以直接定向到最近的服务器
  • 通过热备份进行灾难避免
    • 通过slave热备份防止master单点故障
  • 通过远程复制进行灾难避免
    • 远程数据中心之间进行数据传输
  • 制作备份
    • 备份服务器离线,然后备份
  • 生成报表
    • 离线一个slave产生报表
  • 过滤或分区数据
    • 如果网络连接很慢,或者有些数据对某些客户端不可用,可以添加一个服务器进行数据过滤
    • 同样适用于将数据区分到独立的服务器

横向扩展读操作

横向扩展只能扩展读操作,而不是写操作。写操作使用分片技术扩展

单个服务器每秒有10000个事务,master每秒的写负载为4000个事务,而每秒的读事务为6000个:

average-load-before

添加三个slave,每秒的总负载量就增加到40000个,由于写操作也会被复制,每个写操作都会执行4次(1次master,3个slave 3次),读取负载被分发到各个slave,总的读负载没有增加:

average-load-before

异步复制的价值

异步复制比同步复制快的多,同步需要额外的同步机制来保持一致性,一般通过两段提交协议来实现。 两端提交协议保证了master和slave之间的一致性,但却需要它们之间有额外的通信消息传递。工作流程如下:

  1. 当执行commit语句时,事务被发送给slave,而slave被要求准备提交。
  2. 每个slave都准备事务,以便提交,然后向master发送一个OK(或ABORT)消息,表示事务已经准备好(或者是不能准备的)。
  3. master等待所有的slave发送OK或ABORT消息:
    • 如果master从所有的slave那里收到了一个OK的信息,它会向所有的slave发送一个提交信息,要求它们提交交易。
    • 如果master收到来自任何一个slave的中止消息,它会向所有的slave发送一个中止消息,请求它们中止交易。
  4. 然后,每个slave都在等待master的一个OK或ABORT消息。
    • 如果slave收到提交请求,它们就提交事务并向master发送确认该事务是提交的。
    • 如果slave收到中止请求,它们会取消任何更改并释放它们所持有的任何资源,从而中止事务,然后向master发送确认该事务被中止。
  5. 当master服务器收到来自所有slave的确认时,它会将事务报告为提交(或中止)并继续处理下一个事务。

这个协议之所以慢,是因为它一共需要4次消息传递,准备及确认和终止或提交及确认。 主要问题不在于处理同步的网络流量,而是由于网络和slave提交产生的延迟,而且master的提交会被阻塞直到所有的slave确认事务。
而异步复制只需要一条消息即可,master不需要等待slave,就可以立即报告事务的提交,从而极大的提高了性能。

网络延迟 (ms) 事务提交时间 (ms) 每秒提交的事务数量 示例
0.01 0.14 ~7,100 Same computer
0.1 0.5 ~2,000 Small LAN
1 4.1 ~240 Bigger LAN
10 40.1 ~25 Metropolitan network
100 400.1 ~2 Satellite

而异步就像没有slave一样。是以一致性为代价换取性能。

  • 如果master出现故障,事务就会消失
  • slave上执行的查询可能会返回旧数据

管理复制拓扑

简单拓扑、树形拓扑、双主拓扑和环形拓扑:

Simple-tree-dual-master-and-circular-replication-topologies

  • 双主拓扑用来处理故障转义,环形复制和双主结构允许各个站点在本地运行的同时还能将变更复制到其它站点
  • 简单拓扑和树形拓扑用于横向扩展

复制的使用导致读取的数量大大超过写入的数量,这种部署有两种特殊要求:

  • 需要负载均衡
    • 写操作交给master
    • 读操作交给slave
    • 特定的查询发给特定的slave
  • 需要管理拓扑
    • 应对master或者slave崩溃的情况

为了处理负载均衡更加高效,服务器要保留空闲处理能力:

  • 峰值负载的处理
    • 要有余力处理峰值负载,需要密切监控应用以确定什么时候响应时间变慢
  • 分布成本
    • 要有空闲处理能力来运行复制,包括管理分布式系统所需的额外查询
    • 每个slave上的写操作和master一样,slave需要一些处理能力来完成复制
  • 管理性任务
    • 重新建立复制需要有空闲处理能力,如在服务器之间移动数据的时候重建复制

两种情况处理负载均衡:

  • 应用程序根据查询类型请求服务器
  • 中间层(通常指代理)分析查询,然后发给适当的服务器

使用中间层分析和分发查询是目前最灵活的方式,但有两点不足:

  • 代理导致性能下降
    • 分析查询消耗资源
    • 查询多了一次节点转移,查询被分析两次一次是代理,一次是MySQL
  • 正确的查询分析很难实现,有时甚至不可能实现

Using-a-proxy-to-distribute-queries

应用层的负载均衡

应用程序根据要发出的查询类型向负载均衡服务器请求链接。

应用层的负载均衡需要一个中心存储,存储服务器信息以及这些服务器能够进行哪些查询。

Load-balancing-on-the-application-level

级联复制

从实践上说,一个master可以处理70个slave,但很大程度上取决于应用程序,master无响应永远是个问题。
这时候需要添加一个或多个额外的slave作为中继slave(或简称中继服务器,relay),其目的事通过管理一群slave来减轻slave上的复制负载。这种使用中继的方式成为级联复制。

包括一个master、一个relay和若干个连接到relay的slave

Hierarchical-topology-with-master-relay-and-slaves

默认情况下,slave从master那里得到的变更不会写入slave的二进制日志中,如果出现问题,总是可以通过克隆master或另一个slave来恢复它
另一方面,relay需要进行二进制日志记录所有变更,因为relay需要把变更传给其它slave。与slave不同的是,relay不需要应用这些变更,因为它不响应任何查询。

为此,建立一个Blackhole的存储引擎,它接受所有语句,总是报告语句执行成功,单丢弃任何数据变更。

relay引入了额外延迟,slave滞后master的程度比直接连接master的时候还多。

配置relay
  1. 将slave配置成发送任何slave线程执行的事件,并将这些事件写入relay的binlog
    • 主配置文件中配置log-slave-updates选项
  2. 将relay上所有表的存储引擎都改成BLACKHOLE存储引擎,保留空间并提高性能
    • SET SQL_LOG_BIN=0;
    • ALTER TABLE user_data ENGINE=’BLACKHOLE’;
    • SET SQL_LOG_BIN=1;
  3. 保证relay上的所有新表都使用BALCKHOLLE引擎
    • 主配置文件中配置default-storage-engine更改默认存储引擎
    • 通过命令SET STORAGE_ENGINE=’BLACKHOLE’可暂时修改,重启无效

引入relay:

  1. 将relay连接到master,并将其角色配置为relay
  2. 一次将slave的连接切换到relay

专用slave

将访问很少的数据放到每个slave上是一种资源浪费。为此需要在复制的之后分离表,MySQL通过过滤事件实现,在事件离开master或到达slave的时候过滤它们。

master和专用slave的复制拓扑:

Replication-topology-with-master-and-specialized-slaves

过滤复制事件
  1. 在master上过滤事件,称为master过滤器
    • 控制哪些被写入二进制日志以及哪些被发送给slave
  2. 在slave上过滤事件,称为slave过滤器
    • 控制哪些被执行

如果使用master过滤,意味着无法使用PITR正确的恢复数据库,备份可以恢复,而之后的变更无法恢复,因为二进制日志中没有记录这些变更。
如果使用slave过滤器,所有变更都会通过网络传输,浪费带宽。

master过滤器

创建master过滤器需要两个选项,不推荐同时使用。不接受多个参数但是可以重复使用。

  • binlog-do-db=db
    • 如果当前库是db,则写入binlog
  • binlog-ignore-db=db
    • 如果当前库是db,则忽略

slave过滤器

可以基于数据库的过滤,还可以过滤单个表,甚至使用通配符过滤一组表。

  • replicate-do-db=db
  • replicate-ignore-db=db
  • replicate-do-table=db_name.tbl_name
  • replicate-wild-do-table=db_pattern.tbl_pattern
  • replicate-ignore-table=db_name.tbl_name
  • replicate-wild-ignore-table=db_pattern.tbl_pattern

pattern可以使用_%匹配

使用过滤将时间分配给slave

master过滤的问题:

  • 因为事件是从二进制日志中过滤出来的,而且只有一个二进制日志,所以不可能“切分”更改,并将数据库的不同部分发送到不同的服务器。
  • 二进制日志也用于PITR,因此如果服务器存在任何问题,就不可能恢复所有内容。
  • 如果由于某种原因,需要以不同的方式分割数据,那么它将不再可能,因为二进制日志已经被过滤,不能“撤销过滤”。

如果担心流量问题,可以在master上配置一个relay,保留master二进制日志的过滤后的版本。

Filtering-by-putting-master-and-relay-on-the-same-machine

数据一致性管理

为了避免数据过于陈旧,要保证slave提供的是有用的最新数据。如果还要添加relay,问题就更加棘手。
解决的基本思路是在master上提交的事务做个标记,等slave取到这个事务的时候(或更晚),才在slave上执行查询

MySQL 5.6引入了全局事务标识符(GTID),slave和客户端的故障转移变得简单多了,因为大多数问题都可以自动处理了。
5.6之前,是否存在relay服务器,有不同的解决办法。

非级联部署的一致性

使用SHOW MASTER STATUS获取master的binlog位置,然后在slave上调用MASTER_POS_WAIT函数等待slave到达位置。

级联部署的一致性

由于每个中间的中继服务器都会更改binlog位置,所以无法等待master位置到达最终slave。

  • 利用自定义全局事务标识符来提升slave,并反复轮询slave有没有处理过这个事务。
    • 与5.6的全局事务标识符不同,这里没有wait函数,需要轮询
    • 如果master和slave基本同步,时间较短,只需关心最终slave即可
  • 用MASTER_POS_WAIT函数将从master到最终slave路径上的所有relay都连接起来,保证所有变化都能传递到slave。
    • 如果slave大多是滞后的,等待复制树向下扩散,然后执行查询。避免轮询间隔过大导致响应性问题。
    • 应用程序代码需要访问relay
    • 应用程序需要知道复制架构

Synchronizing-with-all-servers-in-a-relay-chain

5.6以后,使用WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS函数代替MASTER_POS_WAIT

SELECT WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS($gtids)

数据分片

将数据库分割有两种方法:

  • 将某些表分别放在不同的机器上,又称为功能分割
  • 将某些表分割成不同的行分别存储在不同的机器上,成为水平分割

什么是数据分片

双主结构并不能扩展写操作,因为所有的写操作都会被复制并执行两次。
最直接的办法就是服务器之间不再有复制机制,这样它们就是完全分离的。 这样就可以将数据分成两个完全独立的子集,然后将客户端定向到它试图变更的数据所在的分区,从而实现扩展写操作。
此次更新处理并不需要其它的分片消耗资源。这种分割数据的方式通常称为分片(sharding,又称水平分割)。每个分区成为一个分片。

为什么分片

分片的原因取决于应用程序的巨大压力。分片的好处(原因):

  • 将数据放在地理位置接近用户的位置。
    • 减少延迟,提升性能
  • 减少工作集(working set)的大小。
    • 如果表比较小,大部分数据甚至整张表可以装入内存
    • 检索表的算法在表较小的时候更有效
  • 分发工作
    • 将工作并行化

并不是所有数据都需要分片:

  • 可以将某些大表附近进行拆分,然后在每个分片上对小表做全量副本(这些通常是全局表)
  • 可以同时使用分片和功能分割,对庞大的数据做分片(文章、评论),而将索引数据(如用户和目录)放在不分片的中心存储

带有中心数据库的分片:

Shards-with-a-centralized-database

分片的局限性

分片可以提高性能,也有一些局限性

挑战是确保所有查询在向未分片的数据库和分片数据库执行时给出相同的结果。 如果您的查询访问多个表(通常是这样的情况),必须确保对未分片的数据库和分片数据库得到相同的结果。 这意必须选择一个分片索引,以确保查询在共享或不共享的数据库中得到相同的结果。

跨分片连接
  • 以map-reduce的方式执行查询,即将查询发送到所有分片,然后将查询结果收集到单个结果集中
    • 需要占用一定的服务资源,需要监控资源占用
  • 将所有分片复制到某个单独的报表服务器,然后在报表服务器上运行查询
    • 简单的复制,一般采用这种方法完成报表
使用AUTO_INCREMENT

分片不同步AUTO_INCREMENT标识符。

如果需要一个唯一标识符,有两种方法:

  • 生成一个唯一的UUID
    • 缺点是占用128个比特位,也存在极小的可能重复
    • 不连续不适合做InnoDB主键
  • 使用复合标识符(composite identifier),前半部分是分片标识符,后半部分是本地生成的标识符(比如AUTO_INCREMENT生成的)
    • 除了AUTO_INCREMENT列以外,还要增加一列用于维护分片标识符
  • auto_increment_increment and auto_increment_offset
    • 是一种比较普遍的方法,但是需要非常仔细的配置服务器,很容易因为配置错误生成重复数字,特别是当增加服务器需要改变其角色或进行灾难恢复时。
  • 全局节点中创建表
    • 在一个全局数据库节点中创建一个包含AUTO_INCREMENT列的表,应用可以通过这个表来生成唯一数字
  • 使用memcached
    • 使用incr()函数,另外也可以使用redis
  • 批量分配数字
    • 从一个全局节点请求一批数字,用完后申请

复合关键字:

A-composite-key

分片方案的要素

如何分布数据以及如何有效地对数据进行重新分片:

  • 确定如何为应用数据分区
    • 哪些表应该分割
    • 哪些表在所有分片上都应该有
    • 应该在什么列上进行分片
  • 确定需要什么分片元数据以及如何管理元数据
    • 如何将分片分配到MySQL服务器
    • 如果将分片关键字映射到分片
    • 分片数据库需要存储哪些数据
  • 确定如何分发查询
    • 如何获取分片关键字将查询和事务定向到正确的分片
  • 创建分片管理的模式
    • 如何监控分片负载
    • 如何迁移分片
    • 如何通过数据分割和合并分片使系统重新负载均衡
高级分片架构

查询来自应用程序,并由broker接收。broker决定发送查询到哪里,可能需要根据一个记录分片信息的分片数据库来决定。 然后将查询发送到应用程序数据库的一个或多个分片并执行。执行的结果集由broker收集,有可能对结果集进行处理,然后再发送回应用程序。

High-level-sharding-architecture

数据分区

高效的数据检索同样重要,为此,需要将相关数据放在一起。因此,高效分片的最大挑战是创建一个高效的分片索引(sharding index),使经常一起访问的数据落在一个分片上。

分片索引是定义在多个表的多个列之上的,通常每个表只用一个列(一般为主键,也便于做再次分割),也可以每个表多个列(不便于维护)。分片索引决定哪些表需要分片,以及怎样分片。

数据库表:

Table Rows
departments 9
dept_emp 331603
dept_manager 24
employees 300024
salaries 2844047
titles 443308

Employee schema

带有分片表和全局表的模式:

Schema-with-sharded-and-global-tables

分片索引取决于你要做怎样的查询,以及表之间的依赖关系和记录行数。

使用MySQL的information_schema模式,能够计算出相关列上所有可能的分片索引。

USE information_schema;
SELECT
   GROUP_CONCAT(
     CONCAT_WS('.', table_schema, table_name, column_name)
   ) AS indexes
FROM
   key_column_usage JOIN table_constraints
      USING (table_schema, table_name, constraint_name)
WHERE
   constraint_type = 'FOREIGN KEY'
GROUP BY
   referenced_table_schema,
   referenced_table_name,
   referenced_column_name
ORDER BY
   table_schema, table_name, column_name;

在上述结构中使用查询得到两个分片索引:

Candidate #1 Candidate #2
salaries.emp_no dept_manager.dept_no
dept_manager.emp_no dept_emp.dept_no
dept_emp.emp_no  
titles.emp_no  

向结构中添加出版物表,与员工为多对多关系。

Publication-schema-added-to-employees-schema

department表仍然在所有节点上,也有新增表的外键参照,可以对新增表进行分片。这样就存在多个独立的分片索引了。

这也引入了一个问题,可以独立的分别查询两个分片索引的多表查询,而不能执行跨分片索引的连接查询。

分片索引及列:

Index name Sharding column set
si_emps employees.emp_no, dept_emp.emp_no, salaries.emp_no, dept_manager.emp_no, titles.emp_no
si_pubs publications.pub_id, dept_pub.pub_id
分配分片

要有效地使用shards,您需要以一种加速物理访问的方式存储它们。 最直接的方法是每个服务器保留一个shard,但是也可以在每个服务器上保留多个虚拟分片。

怎样分配分片:

  • 应用程序使用跨schema的查询吗
    • 单个schema比较容易,可以在一个服务器上存储多个分片,每个分片保存一个schema,也不需要重写查询
  • 查询根据分片方案调整吗
    • 可以要求开发者在写查询的时候考虑分片方案,就仍然能够在一个服务器上存储多个分片
    • 这样就可以以一种可控的方式重写查询,比如可以在所有数据名字后面加上分片号作为后缀
  • 需要常常重新分片吗
    • 如果重写查询并不容易,或是要求开发者以某种特定的方式写查询,就必须一个服务器保存一个分片,因为这些查询可能是跨schema的
    • 如果要常常重新分片,一个服务器一个分片可能带来性能瓶颈,但是一个服务器保存多个分片也是可能的,能够在服务器之间迁移分片以达到负载均衡,但是如果某个分片成为热点,可能还是需要继续分片
  • 如何备份分片
    • 用于备份和迁移
    • 多数备份方法都是对整个服务器或一个(多个)模式创建备份,所以要谨慎确保某个模式整体保存在同一个分片中(但是一个分片中可能有多个模式)。
每个服务器一个分片

最直接的方法是每个服务器上保存一个分片,这种情况允许跨模式查询,所以不需要重写查询。

缺点:

  1. 多个表可能超出服务器的主存大小,从而影响性能
    • 打破了小表可以装入内存的性能优势
  2. 如果需要对做这些表进行重新分片的话,那么服务器之间负载均衡操作更加昂贵

如果某个服务器过载需要减轻负载,方案是:

分割分片,要么用一个备用服务器创建一个新的分片,要么把那些不相干的行迁移到另一个分片

如果把行迁移到另一个分片,而且每个服务器只有一个分片,迁移过来的行必须与分片上已有的行合并,合并很难联机完成,在一个服务器一个分片的情况下,分割和重新合并操作很昂贵。

每个服务器多个分片(虚拟分片)

如果能够在单个机器上保存多个分片,数据就能够更加高效的在服务器之间迁移,因为数据已经是分片的。 但是,这么做就要区分一台服务器上的不同分片。

常用的方法是在模式名上附加分片标识符,如employees_1,表也可以增加分片名后缀employees_1.dept_emp_1。
如果不跨schema,可以通过USE employee_1来解决模式名后缀的问题而不重写查询,但是跨模式就需要重写查询。重写可以借助代理中间件实现。

由于每个模式存储在不同目录下,大多数备份都可以多模式进行备份,但是备份单个表就会有问题。 只需将不同分片的表分别存放在不同的目录,就很容易应对分片备份。 可以将replicate-do-db限定为服务器上的某个模式,把变更复制到单个分片上,这在服务器之间迁移分片非常有用。

在一个服务器上保存多个分片,就可以通过迁移分片减轻负载。并且不需要合并分片。

在节点上部署分片的方式组合

常用办法:

  • 每个分片使用单一的数据库,并且数据名要相同。典型的应用场景是需要每个分片都能镜像到原生应用的结构
  • 将多个分片的表放到一个数据块中,在每个表名上包含分片号(bookclub.comments_23),这种配置单库可以支持多分片。
  • 为每个分片使用一个数据库,并在数据库中包含所有应用需要的表。在数据库名中包含分片号(bookclub_23.comments)。优点是不用针对分片写查询,便于对只是用单个数据库的应用进行分片。
  • 每个分片使用一个数据库,并且数据库名和表明都带分片号(bookclub.comments_23)
  • 每个节点上运行多个MySQL实例,每个实例上有一个或多个分片,可以使用上面提到的方式的任意组合来安排分片。

表名中有分片号,就需要在查询模板里插入分片号。常用方法是使用特殊的占位符。

对于新应用推荐使用每个分片一个数据块的方式,把分片号写到数据库名和表明中。这回增加如ALTER TABLE这类操作的复杂度。但有如下优点:

  • 如果分片全部在一个数据库中,转移分片比较容易。
  • 数据库本事是文件系统中的一个目录,可以方便的管理一个分片的文件。
  • 如果分片互不关联,可以方便的查看分片的大小
  • 全局唯一表明可以避免错误的数据操作。

对已有应用增加分片支持的结果往往是一个节点对应一个分片。

映射分片关键字

计算正确的分片需要哪些分片元数据,以及怎样将已分片的表映射到实际分片上。

目标:对那些重要且频繁访问的数据减少分片。可扩展性法则的其中一条就是要避免不同节点间的交互。

分片方案

分区函数可以通过静态分片方案或者动态分片方案来实现。

  • 静态分片方案
    • 在静态分片方案中,通常通过固定不变的分配方法将分片关键字映射到分片标识符
    • 计算通常由连接器或应用程序完成,非常高效
  • 动态分片方案
    • 分片关键字通过字典查询,该字典表明哪个分片包含数据。
    • 这种方案比静态分片更加灵活,但是需要一个中心存储,称为分片数据库
静态分片方案

如果查询分布不均,静态分片方案会遇到问题。如果哈希分布不好,也会产生这种问题。
所以选择合适的分区关键字和区分函数非常重要。

缺点:

  • 如果分片很大且数量不多,就很难平分不同分片间的负载
  • 固定分片的方式无法自定义数据放在哪个分片上,这对于那些分片间负载不均衡的应用来说很重要。如热点数据的变化
  • 修改分片策略很困难,需要重新分配已有的数据。
动态分片方案

这是推荐的方式。

动态分片方案非常灵活,不仅允许更改分片位置,如果需要迁移数据,也很容易实现。
动态方案计算分片的时候需要一些额外的查询,增加了复杂度,也会影响性能。增加缓存以缓解。 出于效率方面的考虑,这种架构常常需要更多的分层。 动态分配以及灵活的应用分片亲和性有助于减轻规模扩大而带来的跨分片查询问题。

将分片数据库以一组表的形式保存在一个分片服务器上的MySQL数据库中。

  • 包含每个分片信息的locations表
  • 包含每个分区函数信息的partition_function表
CREATE TABLE locations (
       shard_id INT AUTO_INCREMENT,
       host VARCHAR(64),
       port INT UNSIGNED DEFAULT 3306,
       PRIMARY KEY (shard_id)
);

CREATE TABLE partition_functions (
       func_id INT AUTO_INCREMENT,
       sharding_type ENUM('RANGE','HASH','LIST'),
       PRIMARY KEY (func_id)
);

或者:

CREATE TABLE user_to_shard (
   user_id INT NOT NULL,
   shard_id INT NOT NULL,
   PRIMARY KEY (user_id)
);

表本身就是分区函数,给定分区键就能找到分片号。如果该行不存在,就从目标分区中找到并将其加入表中。也可以推迟更新–这就是动态分配的含义。

最大好处是可以对数据存储位置做细粒度的控制。这使得均衡分配数据到分区更加容易,并且可以提供适应未知改变的灵活性。

混合动态分配和固定分配

目录映射不太大时,动态分配可以很好胜任,但如果分片单元太多,效果就会变差。

使用类似哈希环的方式。

显式分配

在应用插入新的数据行时,显示的指定目标分片。这种策略在已有的数据上很难做到。但在某些情况下是有用的。

这个方法是把分片号码编码到ID中。

如有一个用户3,分配到第11个分片中,使用BIGINT的高八位来 保存分片号,最终id为(11«56)+3,即792633534417207299。

SELECT (792633534417207299 >> 56) AS shard_id,
    792633534417207299 & ~(11 << 56) AS user_id;
+----------+---------+
| shard_id | user_id |
+----------+---------+
|       11 |       3 |
+----------+---------+

这种方式的好处是每个对象的ID同时包含了分区键,而其他方法通常需要一次关联或者查找来确定分区键。

另一个解决方案是将分区键存储在一个单独的列里。这不违背第一范式。然而额外的列会增加开销、编码,以及其他不便之处。

显示分配的缺点是分片方式是固定的,很难做到分片间的负载均衡。但是结合固定分配和动态分配,该方法就能很好的工作。 不再像之前那样哈希到固定的桶里并将其映射到节点,而是将桶作为对象的一部分进行编码。这样应用就能控制数据的存储位置,因此可以将先关联的数据一起放到同样的分片中。

选择分区键

分区键决定了每一行分配到哪一个分片中国,如果知道一个对象的分区键,就可以回答两个问题:

  • 应该在哪里存储数据
  • 应该从哪里得到数据

主键哈希简化了判断数据存储在何处的操作,但却可能增加了获取数据的难度。 跨多个分片的查询比单个分片上的查询性能要差。但是只要不涉及太多的分片,也不会太糟糕。最糟糕的是不知道数据存放在哪里而进行分片扫描。

一个好的分区键常常是数据库中一个非常重要的实体的主键。这些键值决定了分片单元。

确定分区键的一个比较好的方法是用实体-关系图,同时也要考虑查询(避免虽然有关但是不一起查询的情况)。

两种数据模型,一种易于分片,另一种难于分片:

Two data models, one easy to shard and the other difficult

选择分区键的时候,尽量选择那些能够避免跨分片查询的,同时也要让分片足够小,以免过大的数据片导致问题。足够小的分片在为不同数量的分片进行分组时能够很容易平衡。

多个分区键

许多应用有多分区键,特别是存在两个或更多个维度的时候,换句话说,应用需要从不同的角度看到有效且连贯的数据视图。这意味着某些数据在系统内至少要存储两份。

可以冗余两份不同的分片数据或者将某些关联数据(或冗余)存储在一起。

分片映射函数
  • 列表映射
    • 根据分片列中的一组不同的值,将行分布在分片上。例如,这个列表可以是一个国家列表。
    • 容易实现,不能有效分摊负载,适合分区域
  • 区间映射
    • 根据分片列在一个范围内的位置,行分布在分片上。当您在ID列、日期或其它信息上很方便地进入范围时,这是很方便的。
    • 消除了某些分摊负载问题,但是很难达到负载均衡
  • 散列映射
    • 根据分片键值的散列值,将行分布在分片上。这在理论上提供了最均匀的数据分布。
    • 最有效负载均衡的,但是最难有效实现

每个分片映射都要考虑两个问题:如何添加新的分片,以及如何根据分片关键字选择正确的分片。

区间映射

虽然容易实现,问题是区间可能变的零碎,并且要求数据有效支持区间。

创建索引表:包含区间和映射信息的表,并将这些区间映射到分片标识符

CREATE TABLE ranges (
       shard_id INT,
       func_id INT,
       lower_bound INT,
       UNIQUE INDEX (lower_bound),
       FOREIGN KEY (shard_id)
           REFERENCES locations(shard_id),
       FOREIGN KEY (func_id)
           REFERENCES partition_functions(func_id)
)

区间映射表区间:

Lower bound Key ID Shard ID
0 0 1
1000 0 2
5500 0 4
7000 0 3

添加新的分片:向ranges表和locations表分别插入一行

INSERT INTO locations(host) VALUES ('shard-1.example.com');
SET @shard_id = LAST_INSERT_ID();
INSERT INTO ranges VALUES (@shard_id, @func_id, 1000);

获取分片:使用查询获取分片

-- ?替换为分区关键字,另一种选择是存储上边界,但是更新分片数据库就会变得复杂
SELECT shard_id, hostname, port
  FROM ranges JOIN locations USING (shard_id)
 WHERE func_id = 0 AND ? >= ranges.lower_bound
ORDER BY ranges.lower_bound DESC
LIMIT 1;

哈希映射与一致性哈希

适合应对数据热点无法分散,某个分片过载而导致需要分割分片问题。

Hash-ring-used-for-consistent-hashing

常用的密码哈希函数:

提供一个包含大量比特位的哈希值,以及将输入字符串平均分布到输出区间。

Hash function Output size (bits)
MD5 128
SHA-1 160
SHA-256 256
SHA-512 512

创建索引表:

-- 添加索引加速检索哈希值
CREATE TABLE hashes (
    shard_id INT,
    func_id INT,
    hash BINARY(32),
    UNIQUE INDEX (hash)
    FOREIGN KEY (shard_id)
        REFERENCES locations(shard_id),
    FOREIGN KEY (func_id)
        REFERENCES partition_functions(func_id)
)

table hashes:

Key ID Shard ID Hash
1 0 dfd59508d347f5e4ba41defcb973d9de
2 0 2e7d453c8d2f9d2b75a421569f758da0
3 0 468934ac4c69302a77cbe5e7fa7dcb13
4 0 47a9ae8f8b8d5127fc6cc46b730f4f22

添加分片:向locations和hashes各插入一行

INSERT INTO locations(host) VALUES ('shard-1.example.com');
SET @shard_id = LAST_INSERT_ID();
INSERT INTO hashes VALUES (@shard_id, @func_id, MD5('shard-1.example.com'));

获取分片:根据分片关键字查找分片位置,计算分片关键字的哈希值,然后招待小小于这个哈希值的最大哈希值对应的分片标识符,如果没有,就选最大的哈希值。

(
  SELECT shard_id FROM hashes 
  WHERE MD5(sharding key) > hash
  ORDER BY hash DESC
) UNION ALL (
  SELECT shard_id FROM shard_hashes 
  WHERE hash = (SELECT MAX(hash) from hashes)
) LIMIT 1

处理查询和事务调度

分片工具(数据库中间件)

这个抽象层需要完成以下任务:

  • 连接到正确的分片并执行查询
  • 分布式一致性校验
  • 跨分片的结果集聚合
  • 跨分片关联操作
  • 锁和事务管理
  • 创建新的分片并重新平衡分片

事务相关:

  • 如何将事务分配到合适的分片
  • 如何获取事务的分片关键字
  • 如何使用缓存提高性能

broker可以是中间代理,或者由连接器实现,看上去是一个透明的方案,但是实际不是,在处理事务时,使用代理需要扩展协议,而且(或者)会限制应用程序。

Hibernate Shards是一个支持分片的数据库抽象层

处理事务

broker需要知道待处理事务的参数。

从应用程序的角度看,每个事务包含一个查询序列或语句序列,其中最后一个语句时提交或中止。

-- 开启事务
START TRANSACTION; 
-- 读写事务体
SELECT salary INTO @s FROM salaries WHERE emp_no = 20101; 
SET @s = 1.1 * @s; 
INSERT INTO salaries(emp_no, salary) VALUES (20101, @s); 
-- 提交
COMMIT; 
START TRANSACTION; 
INSERT INTO ;
COMMIT;

代理需要处理以下几个问题:

  • 为了把事务事务发送到正确的分片,broker必须在看到事务的第一个语句的时候就知道分片关键字
    • 可以约定在第一句中暴漏关键字,但是容易出错
    • 事务的第一个语句显示提供分片关键字,特定的注释或者允许broker接收频带外的分片关键字(即不作为查询的一部分)
  • 必须在第一个语句发送到服务器之前,知道事务事读事务还是写事务
    • 将事务标记为读写事务或者只读事务,在查询中加入特殊的注释或者将这个信息带外发送给broker
  • 能够推断是否处于某个事务内部,而且同一个事务的下一个语句应该使用相同的链接
    • SERVER_STATUS_IN_TRANS和SERVER_STATUS_AUTOCOMMIT在5.6中被增加。
    • 如果事务通过START TRANSACTION开始,第一个标记为真;如果AUTOCOMMIT=0;就不设置标记。
    • 如果设置了自动提交,就设置SERVER_STATUS_AUTOCOMMIT;否则清空标记
    • 联合使用两个标记,就能知道某个语句是否是事务的一部分,以及下一个语句是否应该使用相同的连接。
    • 但是连接器不支持,只能在broker中跟踪
  • 能够看到上一个语句是否提交了某个事务,从而确定是否切换到另一个连接
    • 需要监控,还要考虑语句隐式提交事务问题
  • 确定如何处理会话特定的状态信息,比如用户自定义变量、临时表以及服务器变量的特定设置等。

1、2两个问题需要检测用户是否发送出错。MySQL 5.6添加了START TRANSACTION READONLY,保证程序不会接受更新语句。
检查分片如果使用分片名区分则很容易,否则需要引入类似断言的功能。

分配查询

在分片环境中处理事务是极不透明的,应用程序必须考虑是否使用分片的数据库。考虑分配查询帮助应用开发者使用

如果查询中给出了需要访问的表,就可以推导出函数标识符,而不需要开发者提供。 此外,还可以检查这个查询是否真的只访问基于该分区函数分区的表,可能需要参考一些全局表的信息。

需要引入一个新的表,保存从表到分区函数的映射。

-- 记录各个表及其分区函数的表
CREATE TABLE columns (
       schema_name VARCHAR(64),
       table_name VARCHAR(64),
       func_id INT,
       PRIMARY KEY (schema_name, table_name),
       FOREIGN KEY (func_id) REFERENCES partition_functions(func_id)
)

分片管理

分片迁移或者在分片之间迁移数据。保持分片很小有助于数据的备份、恢复和转移。

将分片迁移到其它的节点

尽可能少的宕机时间,主要思想是为分片做备份,在目标节点上恢复备份,然后使用复制重新执行这期间发生的变更

  1. 在源节点上创建模式的备份。在线或离线备份方法都可以
  2. 记录某个特定的binlog文职
  3. 停止服务器,将目标节点离线
  4. 服务器停止过程中
    • 将replicate-do-db=schema_1选项设置为需要迁移的那个分片
    • 按需要从源节点恢复备份
  5. 将目标节点恢复运行
  6. 将复制配置从第2步的位置开始,然后在目标服务器上启动复制。从源服务器上读取事件,并将变更应用到要迁移的分片上
    • 保证目标节点有足够的处理能力
  7. 如果目标节点与源节点差距较大,锁定源节点的分片模式,阻止变更。不需要停止目标节点上的分片变更,因为还没有写操作访问它。
    • LOCK TABLES命令
  8. 检查源服务器上的日志位置,因为没有继续变更,这就是需要的最高日志位置
  9. 等待目标服务器同步到这个位置,使用START SLAVE UNTIL和MASTER_POS_WAIT。
  10. 在目标服务器上通过RESET SLAVE关闭复制
    • 这会删除所有复制信息
  11. 将目标服务器离线,删除replicate-do-db选项,然后在恢复服务器,这是可选的
  12. 更新分片信息,使得请求被定向到新的分片位置
  13. 将模式解锁,重新启动分片的写操作
  14. 删除源服务器的分片模式,取决于分片是如何锁定的。

借助Replicant库自动化实现这个过程。

分割分片

如果分片太热,可以分割分片后迁移

  1. 对分片中的所有模式使用在线备份方法,如MEB、XtraDB或系统文件快照等
  2. 记下备份对应的binlog位置
  3. 在目标节点上恢复备份
  4. 启动从源节点到目标节点的复制,使用binlog-do-db或replication-do-db选项只复制要迁移的模式的变更。
  5. 等待复制使目标节点跟上源节点,然后锁定源分片,即不能读也不能写
  6. 等待目标主机完成与源主机的同步,这是分片的所有数据不可用
  7. 更新分片数据库,使所有请求都被定向到新分片
  8. 解锁源分片。这是所有数据都有了,但是两个分片上有数据冗余,但是冗余数据不会被访问
  9. 使用LIMIT语句删除冗余数据,防止占用过多资源
重新均衡分片数据

一个较好的策略是使用动态分片策略,并将新数据随机分配到分片中, 当一个分片块写满时,可以设置一个标志位,告诉应用不要再往这里方数据了。如果未来需要向分片中放入更多的数据,可以直接把标志位清除。

深入复制

  • 如何更加安全的将slave提升为master
  • 崩溃后避免数据库损坏
  • 多源复制
  • 基于行的复制
  • 全局事务标识符
  • 多线程复制

复制架构基础

master和若干slave的内部结构:

Master-and-several-slaves-with-internal-architecture

事件通过复制系统从master到slave,以如下方式:

  • 会话接受来自客户机的语句,执行该语句,并与其它会话同步,以确保每个事务执行,而不与其它会话所做的其它更改相冲突。
  • 在语句完成执行之前,一个包含一个或多个事件的条目被写入到二进制日志中。
  • 在将事件写入到二进制日志之后,主服务器中的一个转储线程接管,从二进制日志中读取事件,并将它们发送到slave的I/O线程。
  • 当slave的I/O线程接收事件时,它将其写入到中继日志的末尾。
  • 在中继日志中,一个slave的SQL线程从中继日志读取事件,并执行该事件,将更改应用到slave的数据库中。

如果丢失了master的链接,slave的I/O线程将试图重连服务器

中继日志的结构

中继日志结构:

Structure-of-the-relay-log

除了二进制日志的内容未见和索引文件以外,中继日志还维护两个文件来跟踪复制的进度,即中继日志信息文件和master日志信息文件。名称可有my.cnf配置。

# 默认为relay-log.info
relay-log-info-file=filename
# 默认为master.info
# 这个文件信息优于my.cnf,推荐直接使用CHANGE MASTER TO命令配置复制
master-info-file=filename

master.info文件包含了master读取位置以及连接到master服务器并开始复制所需的所有信息。当slave的I/O线程启动时,该文件如果可用就从中读取信息。

23                                         1   Number of lines in the file
master-bin.000001                          2   Current binlog file being read (Master_Log_File)
151                                        3   Last binlog position read (Read_Master_Log_Pos)
localhost                                  4   Master host connected to (Master_Host)
root                                       5   Replication user (Master_User)
                                           6   Replication password
13000                                      7   Master port used (Master_Port)
60                                         8   Number of times slave will try to  reconnect (Connect_Retry)
0                                          9   1 if SSL is enabled, otherwise 0
                                           10  SSL Certification Authority (CA)
                                           11  SSL CA Path
                                           12  SSL Certificate
                                           13  SSL Cipher
                                           14  SSL Key
0                                          15  SSL Verify Server Certificate 
60.000                                     16  Heartbeat 
                                           17  Bind Address 
0                                          18  Ignore Server IDs 
8c6d027e-cf38-11e2-84c7-0021cc6850ca       19  Master UUID
10                                         20  Retry Count 
                                           21  SSL CRL 
                                           22  SSL CRL Path 
0                                          23  Auto Position

info文件跟踪复制的进度,并由SQL线程更新。

./slave-relay-bin.000003     Relay log file (Relay_Log_File)
380                          Relay log position (Relay_Log_Pos)
master1-bin.000001           Master log file (Relay_Master_Log_File)
234                          Master log position (Exec_Master_Log_Pos)

如果有任何文件不可用,在slave启动的时候将从my.cnf文件中的信息及CHANGE MASTER TO命令的参数重建这些文件。需要START SLAVE。

复制线程
  • master转储线程
    • 当一个slave I/O线程连接的时候,这个线程被创建在master服务器上。转储线程负责从master服务器上读取条目并将其发送给slave。
    • 每个连接的slave有一个转储线程。
  • slave I/O线程
    • 该线程连接到master服务器,请求转储发生的所有更改,并将它们写入到中继日志中,以供SQL线程进一步处理。
    • 每个slave上都有一个I/O线程。一旦连接建立起来,它就会被保持打开,这样master的任何变化都会立即被slave接收。
  • slave SQL线程
    • 该线程从中继日志读取更改并将其应用到slave数据库。该线程负责协调其它MySQL线程,以确保更改不会影响MySQL服务器上正在进行的其它活动。

从master的角度来看,I/O线程只是另一个客户机线程,它可以同时执行转储请求和master服务器上的SQL语句。 这意味着客户端可以连接到服务器,并假装是一个slave,以便让master服务器从二进制日志中转储更改。这就是mysqlbinlog程序的操作方式。

SQL线程在处理数据库时就像一个会话。但是它还需要处理一些context信息保证复制的正确性。

I/O线程的速度比SQL线程快得多,因此,在复制期间,在中继日志中通常会缓冲几个事件。如果master服务器崩溃,您必须在连接到新master之前处理这些问题。
为了避免丢失这些事件,在尝试重新连接到另一个master服务器之前,等待SQL线程处理完这些事件。

启动和终止slave线程
  • slave I/O现场称从master.info文件读取最后读位置进行恢复
    • 中继日志文件也存在轮换事件
  • slave SQL线程从relay-log.info文件读取中继日志位置进行恢复。

停止和启动slave线程的命令:

  • START SLAVE和STOP SLAVE
    • 两个线程
  • START SLAVE TO_THREAD和STOP SLAVE TO_THREAD
    • I/O线程
  • START SLAVE SQL_THREAD和STOP SLAVE SQL_THREAD
    • SQL线程

通过Internet进行复制

保护数据,基本都需要用到SSL:

  • 使用服务器内置的加密支持,对master到slave的复制进行加密
    • 权威认证机构的证书(CA)
    • 服务器的(共有)的证书
    • 服务器的(私有)的证书
  • 对于不支持SSL的程序,使用Stunnel程序建立一个SSL隧道(虚拟私有网络)
  • 在隧道模式下使用SSH
# 生成自签名的公有证书放在/etc/ssl/certs/master.pem
# 生成自签名的四有密钥放在/etc/ssl/private/master.key
# slave同样
sudo openssl req -new -x509 -days 365 -nodes \
        -config /etc/ssl/openssl.cnf \
        -out /etc/ssl/certs/master.pem -keyout /etc/ssl/private/master.key
使用内置支持建立安全复制
# master
[mysqld]
ssl-capath=/etc/ssl/certs
ssl-cert=/etc/ssl/certs/master.pem
ssl-key=/etc/ssl/private/master.key
-- slave
CHANGE MASTER TO
    MASTER_HOST = 'master-1',
    MASTER_USER = 'repl_user',
    MASTER_PASSWORD = 'xyzzy',
    MASTER_SSL_CAPATH = '/etc/ssl/certs',
    MASTER_SSL_CERT = '/etc/ssl/certs/slave.pem',
    MASTER_SSL_KEY = '/etc/ssl/private/slave.key';
使用Stunnel建立安全复制

slave服务器上的一个Stunnel实例接受来自从服务器的标准MySQL客户机连接上的数据,对它进行加密,并将其发送到master服务器上的Stunnel实例。 master服务器上的Stunnel实例依次侦听专用的SSL端口,接收加密数据,解密它,并将其发送到master服务器上的非SSL端口的客户机连接上。

Replication-over-an-insecure-channel-using-Stunnel

# /etc/stunnel/master.conf
cert=/etc/ssl/certs/master.pem
key=/etc/ssl/private/master.key
CApath=/etc/ssl/certs
[mysqlrepl]
accept = 3508
connect = 3306
# /etc/stunnel/slave.conf
cert=/etc/ssl/certs/slave.pem
key=/etc/ssl/private/slave.key
CApath=/etc/ssl/certs
[mysqlrepl]
accept = 3408
connect = master-1:3508
CHANGE MASTER TO
    MASTER_HOST = 'localhost',
    MASTER_PORT = 3408,
    MASTER_USER = 'repl_user',
    MASTER_PASSWORD = 'xyzzy';

细粒度控制复制

关于复制状态的信息

SHOW SLAVE HOSTS命令仅显示使用report-host参数的slave信息,slave使用report-host参数告诉master服务器的链接信息。 除了主机名外还有其它参数提供了关于连接slave的信息:

  • report-host
  • report-port
  • report-user
  • report-password
  • show-slave-auth-info
SHOW SLAVE HOSTS;
-- 显示master的二进制文件
SHOW MASTER LOGS;
SHOW MASTER STATUS;
SHOW SLAVE STATUS;

SLAVE STATUS字段详解:

I/O线程和SQL线程的状态

  • Slave_IO_Running和Slave_SQL_Running分别表示I/O线程或者SQL线程是否正在运行
  • Slave_IO_State描述了当前正在运行的I/O线程的状态。
    • 等待master更新
    • 连接master
    • 检查master的版本
    • 在master上注册slave
    • 请求binlog转储
    • 等待master发送事件
    • master事件排队等待写入中继日志
    • action后等待重新连接
      • 失败重连时出现
    • action失败后重新连接
    • 等待slave互斥体退出
      • 关闭I/O线程时出现该消息
    • 等待slave的SQL线程释放中继日志空间
      • 等待处理中继日志轮换

Slave-IO-thread-states

二进制日志位置和中继日志位置

  • Master_Log_File和Read_Master_Log_Pos表示master的读位置
    • I/O线程即将从master二进制日志读取的下一个事件的位置,来自master.info 2,3
  • Relay_Master_Log_File和Exec_Master_Log_Pos表示master的执行位置
    • SQL线程即将执行的master二进制日志的下一个事件位置,来自relay-log.info 3,4
  • Relay_Log_File和Relay_Log_Pos表示中继日志执行位置
    • SQL线程即将执行slave中继日志中的下一个事件位置,来自relay-log.info 1,2

通过比较master的读位置和master的执行位置,如果相同,就可以安全的停止slave并将其重定向到新的master。
也可以通过SHOW PROCESSLIST命令检查SQL线程的状态,如果State为“已经读取所有中继日志,等待slave I/O线程更新”,那么已经读取全部中继日志。

处理断开连接的选项

如果I/O线程丢失了master的连接,则进行有限次尝试重新连接master。无响应时间、重试的时间间隔和重试次数由三个选项控制:

  • –slave-net-timeout
    • 超时时间,默认3600秒
  • –master-connect-retry
    • 重试间隔秒数,默认60秒
  • –master-retry-count
    • 重试次数,默认为86400

slave如何处理事件

slave的SQL线程顺序执行来自master的所有会话的各个事件,带来的后果:

  • slave响应是单线程的,而master是多线程的
    • 如果master上提交了很多事务,slave就难以与master保持同步
  • 有些语句时会话特定的
    • slave上单线程执行的时候可能产生不同的结果
      • 每个用户变量都是特定于会话的
      • 临时表是特定于会话的
      • 有些函数也是特定于会话的
  • 二进制日志决定了执行顺序
    • slave必须并行执行,才能保证和master一致
管理I/O线程

I/O线程只使用某些字节来判断事件的类型,然后对中继日志采取必要的行动

  • 停止事件
    • slave链中的下一个服务器被有序停止,I/O线程忽略这个时间,不把这个事件写入中继日志。
  • 轮换事件
    • 如果master上的二进制日志被轮换,中继日志也要被轮换。
    • 中继日志轮换次数可能比master多,然是每次master轮换时,中继日志都要轮换。
  • 格式化描述事件
    • 中继日志轮换时保存这种事件。应对连续的binlog文件格式不同问题。

每个服务器都要检查事件是否包含该服务器的ID,如果有,则忽略,说明本身是由这个服务器发出的,这在环形复制或双主复制中很有用。

SQL线程处理

有些事件需要SQL以外的特殊处理:

  • 将master的上下文发送给slave
    • 处理master写的一个或多个上下文事件传递额外信息
  • 处理不同线程的事件
    • master执行的事务来自多个会话。slave SQL现场称必须知道事件是由那个线程产生的。master了解每个语句,它会标记哪些线程特定的事件。
  • 过滤事件和表
    • 负责数据库过滤和表过滤
  • 跳过事件
    • 要恢复复制,重启复制时可以选择跳过事件

上下文事件

  • 用户变量事件
    • 用户自定义变量名和值
    • 还可以用来避免非确定性函数的复制问题、提高性能,以及完整性检查等。
  • 整型变量事件
    • 事件存储INSERT_ID或LAST_INSERT_ID会话变量的整型值
  • Rand事件
    • 随机种子

线程特定的事件

不同线程导致结果不同的原因:

  • 读写线程本地对象
    • 不同线程的本地对象的名字可以完全一样。临时表或用户自定义变量
    • 所有QUERY事件都有线程ID,salve收到一个线程特定的事件时,设值一个特定的复制slave线程变量,即pseudothreadID,对应事件的线程ID,然后使用这个ID创建临时表
  • 使用具有线程特定结果的变量或函数
    • 变量和函数在不同线程中运行导致值不同。服务器变量和connect_id
    • 使用基于行的复制
    • 或使用临时变量

过滤和跳过事件

-- 恢复复制之前跳过3个事件
-- 如果会导致事务中断,则事务执行结束后跳过3个事件
SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 3;
START SLAVE;

如果有slave过滤器,事件在SQL线程中过滤,过滤的事件依然会出现在中继日志中。

表过滤的使用原则:

  • 不要限定数据表所在的数据库。在语句前使用USE。
  • 不要在单个语句中更新不同数据库的表
  • 不要在单个语句中更新多个表,除非你知道所有这些表都要过滤或都不会过滤

只要有一个表被过滤,整个语句都会被过滤。

复制过滤规则:

Replication-filtering-rules

半同步复制

半同步复制的原理是复制继续运行之前,确保至少有一个slave将变更写到磁盘。对每个连接来说,如果发生master崩溃,至多只丢失一个事务。

半同步复制的事务提交:

Transaction-commit-with-semisynchronous-replication

对于每个连接来说,如果事务在提交到存储引擎之后,发送到slave之前,发生了系统崩溃,那么这个事务就会丢失。
由于slave确定事件提交后才会向客户端发送确认,所以至多只有一个事务丢失。

配置半同步复制

5.5以后的master和slave支持才可以。

启用半同步复制的步骤:

  1. 在master上安装master插件:
  2. 在每个slave上安装插件
  3. 启用插件
  4. 重启服务器
-- master
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
-- slave
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
# master
[mysqld]
rpl-semi-sync-master-enabled = 1
# slave
[mysqld]
rpl-semi-sync-slave-enabled = 1
  • 如果所有slave崩溃,就无法确认是否写入了中继日志
    • rpl-semi-sync-master-timeout=milliseconds 设置超时,如果超时则变为异步复制
  • 如果所有slave连接都断了,也无法确认
    • rpl-semi-sync-master-wait-no-slave={ON|OFF} 设置是否等待slave连入
监控半同步复制
  • rpl_semi_sync_master_clients
    • 连接到master的支持半同步slave的数目
  • rpl_semi_sync_master_status
    • master上的半同步状态,1表示活动
  • rpl_semi_sync_slave_status
    • slave上的半同步状态,1表示活动
SHOW STATUS;
-- 如果上面命令不可用
SELECT Variable_value INTO @value
	   FROM INFORMATION_SCHEMA.GLOBAL_STATUS
	  WHERE Variable_name = 'Rpl_semi_sync_master_status';

全局事务标识符

服务器上为每一个事务分配一个唯一的事务标识符,这是一个64位非0整数,根据事务提交的顺序分配。 这个值是本地的,要使事务标识符成为全局的,还要加上服务器的UUID(@@server_uuid),构成一对。

复制事务的时候如果启用了全局事务标识符,不管事务被复制了多少次,事务的GTID保持不变。

GTID组定义某个或一组范围内的事务标识符。

# GTID
2298677f-c24b-11e2-a68b-0021cc6850ca:1477
# GTID组
2298677f-c24b-11e2-a68b-0021cc6850ca:911-1066:1477-1593

注意事项:需要启动binlog才会记录这个GTID,否则不分配也不记录

使用GTID配置复制
[mysqld]
user            = mysql
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
port            = 3306
basedir         = /usr
datadir         = /var/lib/mysql
tmpdir          = /tmp
# 启用binlog
log-bin         = master-bin 
log-bin-index   = master-bin.index
server-id       = 1
# 启用GTID
gtid-mode       = ON 
# 对于备用服务器需要传递给slave来自master的变更
log-slave-updates 
# GTID强一致性,否则报错
enforce-gtid-consistency

启用后变更master步骤:

CHANGE MASTER TO
	MASTER_HOST = host_of_new_master,
	MASTER_PORT = port_of_new_master,
	MASTER_USER = replication_user_name,
	MASTER_PASSWORD = replication_user_password,
	MASTER_AUTO_POSITION = 1

master和slave会自动协商应该发送什么事务。

SHOW SLAVE STATUS:

Slave_IO_State: Waiting for master to send event
                .
                .
                .
  Slave_IO_Running: Yes
 Slave_SQL_Running: Yes
                .
                .
                .
       Master_UUID: 4e2018fc-c691-11e2-8c5a-0021cc6850ca
                .
                .
                .
Retrieved_Gtid_Set: 4e2018fc-c691-11e2-8c5a-0021cc6850ca:1-1477
 Executed_Gtid_Set: 4e2018fc-c691-11e2-8c5a-0021cc6850ca:1-1593
     Auto_Position: 1
  • Master_UUID
  • Retrieved_Gtid_Set
    • 存储在中继日志中的一组GTID
  • Executed_Gtid_Set
    • 已经执行,并已经写入slave二进制日志的GTID
使用GTID进行故障转移

切换到热备:

  • 不在需要检查master上的位置了,所以也不需要停止
  • slave没必要与master位置一致,备用服务器也没必要等待一个好的切换位置
  • 不需要获取备用服务器位置
  • 更改master时不需要提供位置

为了避免master失效时丢失事务,要养成执行故障转移钱清空中继日志的好习惯。这样避免了重复从master获取已经发送到slave的事务。

使用WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS函数阻塞直到GTID组中的所有GTID都被SQL线程处理完毕。

SHOW SLAVE STATUS;
SELECT WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS(Retrieved_Gtid_Set);
STOP SLAVE;
CHANGE MASTER TO 'standby.example.com';
START SLAVE;
使用GTID提升slave
  • GTID_EXECUTED 已写入二进制日志的GTID组
  • GTID_PURGED 已从二进制日志中清除的GTID组

GTID_EXECUTED-and-GTID_PURGED

使用GTID_EXECUTED很容易比较slave,然后决定那个slave“知道最多”。

SELECT @@GLOBAL.GTID_EXECUTED
SELECT @@GLOBAL.GTID_PURGED
GTID的复制

二进制为每个组分配了GTID,每个事务、单语句的DML语句及DDL语句。在写入组值钱写GTID时间,该事件包含事务的完整GTID。

A-binary-logfile-with-GTIDs

SQL线程按照以下方式处理GTID事件:

  1. 如果GTID已经在GTID_EXECUTED中,跳过整个事务,也不写入二进制日志
  2. 否则GTID就被分配给后面的事务,下一个事务正常执行
  3. 如果事务提交,事务的GTID就用来产生一个新的GTID事件,然后这个事件在事务之前写入二进制日志
  4. 在GTID事件之后,事务缓存的内容被写入二进制日志

在提交事务的时候,根据GTID_NEXT变量的值有不同的操作:

  • 如果GTID_NEXT的值为AUTOMATIC,那么创建一个新的GTID并将其分配给事务
  • 如果GTID_NEXT的值为GTID,那么使用这个GTID并且会随事务一起写入二进制日志

设值了GTID_NEXT并开启了事务,GTID就被这个事务拥有了

SELECT @@GLOBAL.GTID_OWNED;

mysqlbinlog with GTID events:

# at 410
#130603 20:57:54 server id 1  end_log_pos 458 CRC32 0xc6f8a5eb
#       GTID [commit=yes]
SET @@SESSION.GTID_NEXT= '01010101-0101-0101-0101-010101010101:3'/*!*/;
# at 458
#130603 20:57:54 server id 1  end_log_pos 537 CRC32 0x1e2e40d0
# Position  Timestamp   Type   Master ID        Size      Master Pos    Flags
#       Query   thread_id=4     exec_time=0     error_code=0
SET TIMESTAMP=1370285874/*!*/;
BEGIN
/*!*/;
# at 537
#130603 20:57:54 server id 1  end_log_pos 638 CRC32 0xc16f211d
#       Query   thread_id=4     exec_time=0     error_code=0
SET TIMESTAMP=1370285874/*!*/;
INSERT INTO t VALUES (1004)
/*!*/;
# at 638
#130603 20:57:54 server id 1  end_log_pos 669 CRC32 0x91980f0b
COMMIT/*!*/;

slave的安全和恢复

同步、事务以及数据库崩溃问题

为了保证master或slave崩溃以后能够安全地恢复复制,需要考虑两个问题:

  • 保证slave上存有恢复所需的所有数据
    • slave通过磁盘同步尽量满足这个条件
    • MySQL服务器定期在中继日志、master.info文件和relay-log.info文件上执行fsync调用,强制文件写入磁盘
  • 执行slave的恢复

I/O线程同步

无论什么时候处理事件都有两个fsync调用:

  • 一个将中继日志刷新到磁盘
  • 一个将master.info文件刷新到磁盘

这两个刷新保证了没有事件丢失,以下几种情况发生崩溃,可能产生重复事件:

  • 服务器刷新中继日志,正要更新master.info文件中的master读位置
  • 服务器崩溃,也就是master读位置指向事件被刷新到中继日志之前的位置
  • 服务器重启,并从master.info获得master读位置,即在最后一个事件写入中继日志之前的位置
  • 从这个位置恢复复制,导致事件重复

如果按相反顺序刷新文件,可能丢失事件,因为slave会在事件之后恢复复制。我们认为事件丢失比事件重复严重。

SQL线程同步

在处理组中所有事件时,SQL线程采用下面方式提交事务:

  1. 将事务提交到存储引擎
  2. 更新relay-log.info文件的下一个事件位置,这个位置也是处理下一个组的开始位置
  3. 发出fsync调用,将relay-log.info文件写入磁盘

如果发生崩溃,从rela-log.info文件的最后一个记录位置恢复执行。

这种方式使得SQL线程的原子更新问题与I/O线程不同,所以下面的情况可能导致slave数据库和relay-log.info文件不同步:

  1. 事件在数据库上应用,且事务已提交。下一步是更新relay-log.info文件
  2. slave崩溃。relay-log.info文件指向刚刚完成的事务的开始
  3. 恢复时,SQL线程从relay-log.info文件读取信息,并从保存的位置开始复制
  4. 重复上一次执行的事务

这些情况都是因为slave上提交事务也更新复制信息不是一个原子操作,MySQL 5.6通过事务型复制解决这个问题。

事务型复制

复制不是崩溃安全的,因为复制进度信息并不总是与数据库中的应用实际变更同步。 即使服务器崩溃时事务没有丢失,也要花点力气将slave恢复回来。

通过提交事务同时提交复制信息,增强了slave的复制安全性。 复制信息总是与数据库的变更应用一致,不论服务器是否发生崩溃。而且,master也做了调整保证能够正确恢复。

要实现事务型复制,将复制信息存储在文件或者表中。即数据和复制信息要么使用相同的事务型存储引擎,要么两个存储引擎都要支持XA。

配置事务型复制

[mysqld]
# 可选项为FILE和TABLE
master_info_repository = TABLE
relay_log_info_repository = TABLE

5.6.6之前,还要变更表的存储引擎:

ALTER TABLE mysql.slave_master_info ENGINE = InnoDB;
ALTER TABLE mysql.slave_relay_log_info ENGINE = InnoDB;

事务型复制的细节

保存事务型复制相关信息的两张表:

  • slave_master_info对应master.info
  • slave_relay_log_info对应relay_log.info

slave_master_info:

Field Line in file Slave status column
Number_of_lines 1  
Master_log_name 2 Master_Log_File
Master_log_pos 3 Read_Master_Log_Pos
Host 3 Master_Host
User_name 4 Master_User
User_password 5  
Port 6 Master_Port
Connect_retry 7 Connect_Retry
Enabled_ssl 8 Master_SSL_Allowed
Ssl_ca 9 Master_SSL_CA_File
Ssl_capath 10 Master_SSL_CA_Path
Ssl_cert 11 Master_SSL_Cert
Ssl_cipher 12 Master_SSL_Cipher
Ssl_key 13 Master_SSL_Key
Ssl_verify_servert_cert 14 Master_SSL_Verify_Server_Cert
Heartbeat 15  
Bind 16 Master_Bind
Ignored_server_ids 17 Replicate_Ignore_Server_Ids
Uuid 18 Master_UUID
Retry_count 19 Master_Retry_Count
Ssl_crl 20 Master_SSL_Crl
Ssl_crlpath 21 Master_SSL_Crlpath
Enabled_auto_position 22 Auto_Position

slave_relay_log_info:

Field Line in file Slave status column
Number_of_lines 1  
Relay_log_name 2 Relay_Log_File
Relay_log_pos 3 Relay_Log_Pos
Master_log_name 4 Relay_Master_Log_File
Master_log_pos 5 Exec_Master_Log_Pos
Sql_delay 6 SQL_Delay
Number_of_workers 7  
Id 8  

slave上每一个事务都被更新到slave_relay_log_info表。
slave_master_info表只保存从master获取的事件的位置, 如果发生崩溃,slave将从上一次执行的位置恢复,而不是上一次获取的位置恢复,所以这个信息只对master崩溃有用。 这时,中继日志中的事件将被执行,避免更多事件丢失。
slave_master_info表不包含事务型复制的重要信息,sync_master_info选项表示提交到slave_master_info表或刷新到磁盘的频率以提高性能。 0表示有操作系统控制文件刷新,当轮换或启动和停止的收,信息都会被刷新到磁盘或者表。

保护非事务型语句的规则

master上崩溃,MyISAM表上的语句由于崩溃中断,这个语句不再计入日志,因为只有执行完的语句才记入日志。 重启时表包含一部分更新,但是二进制日志没有记录该语句。
slave上如果执行过程中发生崩溃,表的变更可能还在,但是组位置不变,重启后将重复执行。

通过观察一些规则,可以观察到一些错误信息:

  • INSERT语句
    • 要复制的表中必有主键,主键重复可能导致slave停止
  • DELETE语句
    • 避免使用limit从句,这样重复执行也会没有影响
  • UPDATE语句
    • 语句必须是幂等的,或者执行两次的偶然性是可以接受的

多源复制

设计上有个问题:如何处理更新冲突

典型的实现方法:更新不同的数据库,或者更新同一张表的不同行

MySQL目前不支持多个源复制,可以近似实现:将slave在多个master之间切换,轮流从其中一个master定期复制,成为轮盘多源复制。

Round-robin-multisource-replication-using-a-client-to-switch

  1. 将slave配置为从一个master进行复制。
  2. 设值slave复制的固定工作时间,slave从当前master中读取更新,然后应用更新,这是负责切换的客户端处于休眠状态。
  3. 使用STOP SLAVE IO_THREAD停止slave的I/O线程
  4. 等待中继日志为空
  5. 使用STOP SLAVE SQL_THREAD停止SQL线程。CHANGE MASTER要求两个线程都停止
  6. 保存当前master的slave位置,存储SHOW SLAVE STATUS命令输出的Exec_Master_Log_Pos和Relay_Master_Log_File的值
  7. 将slave的复制按顺序切换到下一个master上:利用之前保存的位置,并使用CHANGE MASTER命令配置复制
  8. 使用START SLAVE重启slave线程
  9. 重复2-8步

Tips:不执行3-5不也不会有问题,因为丢失的事件会从master重新读取

基于行复制的细节

基于行的复制方法不同,每个语句需要多个事件。

引入4个事件处理基于行的复制:

  • Table_map
    • 将表ID映射为表名(包括数据名),以及关于master上的表的列的基本信息
    • 表信息只有类型,按位置复制
  • Write_rows, Delete_rows, Update_rows
    • 除了行以外,每个事件还有一个表ID,来自Table_map事件,还有一个或两个列位图,说明影响了哪些列,节省空间。

每当执行一个语句时,它都会作为Table_map事件序列写入二进制日志,然后是行事件序列。 语句的最后一行事件被标记为一个特殊标志,指示它是语句的最后一个事件。

*************************** 1. row ***************************
   Log_name: master-bin.000054
        Pos: 106
 Event_type: Query
  Server_id: 1
End_log_pos: 174
       Info: BEGIN
*************************** 2. row ***************************
   Log_name: master-bin.000054
        Pos: 174
 Event_type: Table_map
  Server_id: 1
End_log_pos: 215
       Info: table_id: 18 (test.t1)
*************************** 3. row ***************************
   Log_name: master-bin.000054
        Pos: 215
 Event_type: Write_rows
  Server_id: 1
End_log_pos: 264
       Info: table_id: 18 flags: STMT_END_F
*************************** 4. row ***************************
   Log_name: master-bin.000054
        Pos: 264
		Event_type: Table_map
  Server_id: 1
End_log_pos: 305
       Info: table_id: 18 (test.t1)
*************************** 5. row ***************************
   Log_name: master-bin.000054
        Pos: 305
 Event_type: Write_rows
  Server_id: 1
End_log_pos: 354
       Info: table_id: 18 flags: STMT_END_F
*************************** 6. row ***************************
   Log_name: master-bin.000054
        Pos: 354
 Event_type: Xid
  Server_id: 1
End_log_pos: 381
       Info: COMMIT /* xid=23 */
6 rows in set (0.00 sec)

行事件大小通过binlog-row-event-max-size控制,表示它在二进制日志中的最大字节数。

Table_map事件

Table_map事件将表名映射为标识符,然后用于行事件,但这不是它唯一的用途。 它还包含master上表中字段的基本信息。slave确认结构匹配,从而复制继续。

Table-map-event-structure

  • 列类型数组
    • 表述所有列的基础数据类型数组,不包含参数
  • 空比特数组
    • 表是每个字段是否是NULL的数组
  • 列元数据
    • 表示字段元数据的数组,充实列类型数组的细节信息。如DECIMAL的精度和小数

无法区分的两种类型:

  • 整型数据是否有符号
  • 字符串类型的字符集
行事件的结构

根据不同的事件类型,结构稍有不同。

Row-event-header

  • 表宽
    • master上表的宽度,基于长度编码的,只有两个字节,大多数情况只有一个字节
  • 列位图
    • 表示作为事件一部分发送的那些列。
      • 前映像,用于删除和更新
      • 后映像,用于插入和更新

行事件及其映像:

Before image After image Event
None Row to insert Write rows
Row to delete None Delete rows
Column values before update Column values after update Update rows
行事件的执行

因为多个事件可能表示master上执行的单个语句,所以slave需要保存状态信息,当有并发线程更新同一张表时,保证行时间的正确执行。

处理步骤:

  1. 从中继日志中读取各个事件
  2. 如果是表映射事件,SQL线程将提取表信息,并保存master对这个表的定义
  3. 出现第一个行事件时,锁定列表中的所有表
  4. 线程检查每张表是否一致
  5. 如果不一致,就报错,停止复制
  6. 继续行处理,直至最后一个行事件

拥有前映像的事件需要经过查找后正确定位到需要操作的行。按照查找优先级递减的顺序,查找操作包括:

  • 主键查询
    • 最快
  • 索引扫描
    • 没有主键,但有索引,找到则delete或update,否则报错
  • 表扫描
    • 没有主键也没有索引,全表扫描

使用slave而不是master的主键或索引定位正确的行执行删除或更新操作,所以要注意:

  • 有主键,很快,没有则很慢
  • master和slave的索引可能不同
事件和触发器

由于触发器引起变化的行也会被复制到slave上执行,所以slave上不能在执行一次触发器。
事件复制后直接执行,不考虑触发器。

基于行的复制中的过滤

基于行的复制的过滤是基于真正发生变化的表,而不是语句的当前数据库。

部分行复制

5.6.2开始,可以通过binlog-row-image参数控制哪些列写入日志。参数有full、noblob和minimal。

  • full
    • 默认值,复制全部列
  • noblob
    • 忽略blob,除非它们需要被更新
  • minimal
    • 只有主键和更改值的列

什么是MySQL集群

MySQL集群是一个无共享的、分布式节点架构的存储方案,其目的是提高容错性和性能。
数据被存储和复制在单个数据节点上,其中每个数据节点运行在单独的服务器上,并维护数据的副本。每个集群还包含管理节点。 更新使用读已提交隔离级别,以确保所有节点具有一致的数据,并使用两段提交以确保节点具有相同的数据(如果任何一个写入失败,则更新失败)。

MySQL集群的高性能是它通过存储引擎层使用MySQL服务器作为查询引擎。 因此,您可以透明地将设计为与MySQL交互的应用程序迁移到MySQL集群。

无共享节点概念允许在一台服务器上执行的更新立即在其它服务器上可见。 更新的传输使用了一种复杂的通信机制,用于在网络间实现非常高的吞吐量。 目标是通过使用多个MySQL服务器来分配负载,并通过在不同位置存储数据实现高可用性和冗余性。

术语和组件

MySQL集群的典型部署是在某个网络的不同机器上安装部署集群,因此又称为网络数据库(network database,NDB)。 MySQL集群指的是MySQL集群和NDB组件,而“NDB”指集群组件。

MySQL集群和MySQL有何不同

通常认为集群包括成员、消息、冗余和自动化故障转移功能,而复制仅仅是一个服务器向另一个服务器发送消息的形式。

典型配置

MySQL集群有如下三层:

  • 应用程序层:负责与MySQL服务器通信的各种应用程序
  • MySQL服务器层:处理SQL命令,并与NDB存储引擎通信的MySQL服务器
  • NDB集群组件:NDB集群组件,即数据节点,负责处理查询,然后将结果返回给MySQL服务器

每一层都可以独立的纵向扩展,即通过更多的服务器进程来提高性能。

MySQL-Cluster

应用程序连接到MySQL服务器,通过存储引擎(如NDB存储引擎)访问NDB集群组件。

MySQL集群的特点

数据在集群内部的对等数据节点之间相互复制。数据复制采用同步机制,数据存储在多个数据节点上,每个数据节点连接到所有的其它数据节点上。

集群之间复制采用MySQL复制,是异步的。

MySQL集群有一些创建高可用性系统的专用功能,主要包括:

  • 节点恢复
    • 数据节点故障可以通过通信丢失或心跳失败来检测,您可以配置节点以使用来自其余节点的数据副本自动重新启动。故障和恢复可以包括单个或多个存储节点。节点恢复又称为本地恢复。
  • 日志
    • 在正常的数据更新期间,数据更改事件的副本被写入存储在每个数据节点上的日志。您可以使用日志将数据还原到某个时间点。
  • 检查点
    • 集群支持两种形式的检查点,即本地检查点和全局检查点。
      • 本地检查点移除日志的尾部。
      • 当将所有数据节点的日志刷新到磁盘时,将创建全局检查点,从而创建与事务一致的所有节点数据到磁盘的快照。这样,检查点允许从已知的良好同步点对所有节点进行完整的系统恢复。
  • 系统恢复
    • 如果整个系统意外关闭,您可以使用检查点和更改日志来恢复系统。通常,数据从磁盘复制到内存中,从已知的良好同步点。
  • 热备份及恢复
    • 可以在不干扰执行事务的情况下,同时创建每个数据节点的备份。备份包括关于数据库中的对象、数据本身和当前事务日志的元数据。
  • 无节点故障
    • 任何节点失败都不导致数据库系统崩溃。
  • 故障转移
    • 为了确保节点恢复是可能的,所有事务都使用读已提交隔离级别和两阶段提交。事务是双重安全的(即,在客户端接受事务之前,它们被存储在两个不同的位置)。
  • 分区
    • 数据在数据节点之间自动分区。从MySQLVersion5.1开始,MySQL集群支持用户定义的分区.
  • 联机操作
    • 您可以在没有正常中断的情况下在线执行许多维护操作。这些操作通常需要停止服务器或在数据加锁。
    • 例如,可以在线添加新的数据节点,更改表结构,甚至可以重新组织集群中的数据。

本地和全局冗余

  • NDB集群还有一个优化的两阶段提交版本,它减少了使用同步复制发送的消息数量。两阶段协议确保数据被冗余地存储在多个数据节点上,这种状态称为本地冗余。
  • 全局冗余使用集群之间的MySQL复制。这将在复制拓扑中建立两个节点。MySQL复制是异步的,因为它不包括复制事件的到达或执行的确认或接收。

Local-and-global-redundancy

日志处理

  • 本地检查点,用于清除部分重做日志
  • 全局检查点,主要用于不同数据节点之间同步,全局检查点形成了事务组之间的边界,称为epoch,每个epoch是集群之间复制的单位。MySQL复制把两个连续的全局检查点之间的事务组看成单个事务。

冗余和分布式数据

数据冗余用副本(replica)实现,每个副本包含数据的一份拷贝。这样集群可以容错,一个节点失效,仍然可以访问数据,副本越多,容错越好。

可以指定集群中的副本数目(NoOfReplicas)。

还可以利用分区将数据分布到各个数据节点,这样查询更快。为此,每个数据都要多个节点来存储。

脑裂综合征:

需要一个网络分区算法解决各组数据节点之间的竞争每组独立进行选举。节点数据较少的组将重启,然后分别将该组中的每个节点添加到节点数据较多的组。
如果数目相当,可以顶一个一个仲裁器,规定第一个成功连接到仲裁其的组获胜。仲裁器可以是MySQL服务器或管理节点,为了高可用,最后将仲裁器放在非数据节点的系统上。
带有仲裁器的网络分区算法在MySQL集群中是完全自动化的。

MySQL集群的架构

MySQL集群由一个或多个MySQL服务器组成,通过NDB存储引擎与NDB集群通信。 NDB集群本身由几个组件组成:存储和检索数据的数据或存储节点以及协调数据节点启动、关闭和恢复的一个或多个管理节点。 大多数NDB组件都是作为守护进程实现的,而MySQL集群还提供了客户端实用程序来操作守护进程的功能。

下面是守护进程和实用程序的列表:

The-MySQL-Cluster-components

  • mysqld
    • MySQL服务器
  • ndbd
    • 数据节点
  • ndbmtd
    • 多线程数据节点
  • ndb_mgmd
    • 集群的管理服务器
  • ndb_mgm
    • 集群的管理客户端

MySQL服务器通常都支持一个或多个SQL查询应用,然后接收来自数据节点的返回结果。
数据节点是一系列NDB守护进程,负责存储和检索内存或硬盘上的数据。数据节点安装在集群中的各个服务器上。 还有一个名为ndbmtd的多线程数据节点守护进程,运行在支持多核CPU的平台上。 多核CPU专用服务器上使用多线程数据节点,可以提高数据节点的性能。
管理守护进程ndb_mgmd运行在服务器上,负责读入配置文件,然后将信息分发到集群中的所有节点上。
管理客户端ndb_mgm可以检查集群的状态,开始备份,然后执行其它管理功能。

实用程序:

  • ndb_config
    • 抽取已有节点配置信息
  • ndb_delete_all
    • 删除NDB表的所有行
  • ndb_desc
    • 描述NDB表
  • ndb_drop_index
    • 删除NDB表的索引
  • ndb_drop_table
    • 删除NDB表
  • ndb_error_reporter
    • 诊断集群中的错误和问题
  • ndb_redo_log_reader
    • 检查并输出集群的重做日志
  • ndb_restore
    • 执行集群的恢复

如何存储数据

MySQL群集将所有索引列保存在主内存中。可以将其余的非索引列存储在内存中,也可以存储在具有内存页缓存的磁盘上。

当数据被更改时(通过INSERT、UPDATE、DELETE等),MySQL集群会将更改的记录写入重做日志,定期将数据指向磁盘。

日志和检查点允许在发生故障后从磁盘恢复。但是,由于重做日志是与提交异步写入的,因此在失败期间可能会丢失有限数量的事务。 为了避免这种风险,MySQL集群实现了写延迟选项(默认为2秒,但这是可配置的)。 这允许检查点写入完成,这样如果发生故障,最后一个检查点就不会因为失败而丢失。 单个数据节点的正常故障不会由于集群内的同步数据复制而导致任何数据丢失。

在内存中维护MySQL群集表时,集群访问磁盘存储的目的只是将更改的记录写入重做日志并执行所需的检查点。 由于日志和检查点的写入是连续的,涉及的随机访问模式也很少,与传统的关系数据库系统中使用的磁盘缓存相比, MySQL集群可以在有限的磁盘硬件下获得更高的写入吞吐量。

计算一个数据节点需要多大内存:

(数据库的大小副本的数量1.1)/数据节点的数量

./ndb_size.pl --database=cluster_test --user=root

分区

MySQL集群水平地划分数据(即,行自动地在数据节点之间分配,使用一个函数来分配行)。 这是基于使用表主键的散列算法。在MySQL的早期版本中,该软件使用内部机制进行分区,但是MySQLVersion5.1及更高版本允许您为数据分区提供自己的分区算法。

分区允许MySQL集群实现更高的查询性能,因为它支持在数据节点之间分发查询。 因此,在跨几个节点收集数据时,查询返回结果的速度将比从单个节点返回的速度快得多。

如果有多个数据副本(副本),则会保护分布在数据节点上的数据不发生故障。 如果要使用分区将数据分布到多个数据节点以实现并行查询,则还应确保至少有两个对每一行的副本,以便群集具有容错性。

事务管理

MySQL群集协调跨数据节点的事务更改。这使用了两个子进程,即事务协调器和本地查询处理程序。

  • 事务协调器在全局级别上处理分布式事务和其它数据操作。
  • 本地查询处理程序管理集群数据节点本地的数据和事务,并充当数据节点上两阶段提交的协调器。

每个数据节点都可以是事务协调器(您可以调优此行为)。当应用程序执行事务时,群集连接到一个数据节点上的事务协调器。 默认行为是选择群集的网络层定义的最近的数据节点。如果在同一距离内有多个可用的连接,则循环算法将选择事务协调器。

然后,选定的事务协调器将查询发送到每个数据节点,本地查询处理程序执行查询,并与事务协调器协调两阶段提交。 一旦所有数据节点验证了查询,就会与事务协调器协调两阶段提交。一旦所有数据节点验证了事务,事务协调器就会验证(提交)事务。

联机操作

在MySQL版本5.1及更高版本中,您可以在集群联机时执行某些操作,这意味着您不必关闭服务器或锁定系统或数据库的部分。

  • 备份
    • 可以使用NDB管理控制台执行快照备份(非阻塞操作),以创建群集中数据的备份。
    • 此操作包括元数据(所有表的名称和定义)、表数据和事务日志(更改的历史记录)的副本。
    • 它不同于mysqldump备份,因为它不使用表扫描来读取记录。可以使用特殊的ndb_restore实用程序还原数据。
  • 添加和删除索引
    • 您可以使用ONLINE关键字执行CREATE INDEX或DROP INDEX命令。
    • 当请求联机操作时,该操作是不复制的-它不复制数据以对其进行索引-因此索引不必在之后重新创建。
    • 这样做的一个优点是,事务可以在ALTER TABLE操作期间继续进行,而被更改的表不会因其它SQL节点的访问而被锁定。
    • 但是,该表针对执行ALTER操作的SQL节点上的其它查询而锁定。
  • 修改表
    • 您可以使用ONLINE关键字在线执行ALTER TABLE语句。
    • 它也是不复制的,并具有与在线添加索引相同的优点。
    • 此外,在MySQL Cluster Version7.0及更高版本中,只要不使用INTO(paration_deverions)选项,就可以使用REORGORGATION分区命令在线重组跨分区的数据。
  • 添加数据节点和节点组
    • 可以在线管理数据节点的扩展,以进行扩展或在失败后进行节点替换。
    • 简单地说,它涉及更改配置文件,执行NDB管理守护进程的滚动重新启动,对现有数据节点执行滚动重新启动,启动新的数据节点,然后重新组织分区。

配置实例

集群配置的简单实例:

Sample-cluster-configuration

如果将副本数量设置为2,则这个最小配置可以容错,如果将副本数量配置为1,为了获得更好的性能,这个配置可以分区,但是不能容错。

同一节点即运行NDB管理守护进程又作为MySQL服务器是允许的,但是如果节点的数目很多,或者想保证最大荣作,可能需要将这个守护进程迁移到另一个系统中。

入门

最小配置项:

# /var/lib/mysql-cluster/config.ini
[ndbd default]
NoOfReplicas= 2
DataDir= /var/lib/mysql-cluster

[ndb_mgmd]
hostname=192.168.0.183
datadir= /var/lib/mysql-cluster

[ndbd]
hostname=192.168.0.12

[ndbd]
hostname=192.168.0.188

[mysqld]
hostname=192.168.0.183

启动MySQL集群

  1. 启动管理节点
  2. 启动数据节点
  3. 启动MySQL服务器(SQL节点)
启动管理节点
# --initial需要清除以前的配置信息
sudo ../libexec/ndb_mgmd --initial --config-file /var/lib/mysql-cluster/config.ini
启动管理控制台
# mysql安装目录的bin目录下
./ndb_mgm
ndb_mgm> SHOW
ndb_mgm> STATUS
ndb_mgm> ALL STATUS
启动数据节点

拷贝ndbd可执行文件至/var/lib/mysql-cluster

sudo ./ndbd --initial-start --ndb-connectstring=192.168.0.183
启动SQL节点
  • ndbcluster
    • 使用哪个NDB集群存储引擎
  • ndb_connectstring
    • NDB管理守护进程的位置
  • ndb_nodeid and server_id
    • 节点ID,在管理控制台中SHOW命令输出节点ID信息
sudo ../libexec/mysqld --ndbcluster --console -umysql

关闭集群

  1. 如果有复制,先使slave跟上进度,然后停止复制
  2. 关闭SQL节点
  3. 在NDB控制台上发送SHUTDOWN命令
  4. 推出NDB控制台

获得高可用性

MySQL集群通过以下方式保证高可用:

  • 数据节点之间的数据分布(减少单个节点的数据丢失风险)
  • 集群中副本之间的复制
  • 丢失的数据节点的自动恢复(故障转移)
  • 通过心跳进行数据故障检测
  • 本地和全局检查点来保证数据一致性等

配置高可用MySQL集群最佳实践:

  • 在不同硬件的数据节点上使用多个副本
  • 使用冗余的网络连接防止网络出现故障
  • 使用多个SQL节点
  • 使用多个数据节点来提高性能,将数据分布化

高可用MySQL集群

A-highly-available-MySQL-cluster

系统恢复

如果是正常关闭,会从日志检查点开始恢复。这很大程度上是自动的,是启动过程的正常阶段。 系统从每个数据节点的本地检查点加载最近的数据,从而在重新启动时将数据恢复到最新的快照。 数据节点从其本地检查点加载数据后,系统将重做日志执行到最近的全局检查点,从而将数据同步到关闭前的最后一次更改。 无论是在有意关闭之后重新启动,还是在失败后重新启动整个系统,过程都是相同的。

MySQL集群是内存中的数据库,因此,在启动时必须从磁盘重新加载数据。将数据加载到最近的检查点即可完成此任务。

灾难中从备份恢复使用ndb_restore实用程序。

首先在管理控制台进入单用户模式:

-- node-id是ndb_restore所在的数据节点的id
ENTER SINGLE USER MODE node-id
-- 然后进行数据恢复
-- 退出单用户模式
EXIT SINGLE USER MODE

节点恢复

网络、硬件、内存或操作系统问题或故障都可能导致节点故障。

  • 硬件
    • 更换硬件,重启节点
  • 网络
    • 修复网络,重启节点
  • 内存
    • 增加内存或者重新调整分配,执行数据节点的滚动启动
  • 操作系统
    • 重启数据节点

复制

MySQL集群复制又称为内部集群复制或内部复制,MySQL复制又称为外部复制

集群内部复制和MySQL复制
  • 内部复制使用同步复制,支持两段提交协议,保证数据的完整性
  • MySQL复制是异步复制,依赖于稳定交付的单向数据传输,不需要确认
集群内部复制

内部MySQL集群复制通过存储多个数据副本(称为副本)提供冗余。 该过程确保在查询被确认为完整(提交)之前将数据写入多个节点。这是使用两阶段提交完成的。 这种复制形式是同步的,因为在确认查询或提交完成时,数据保证是一致的。

数据以片段形式复制,其中片段被定义为表中行的子集。由于分区片段分布在数据节点上,每个副本的其它数据节点之上都有该片段的一个副本。 其中一个片段被指定为主片段,用于执行查询。同一数据的所有其它副本都被视为次要片段。在更新期间,首先更新主片段。

集群之间的MySQL复制

如同MySQL复制一样,只不过数据存储在NDB集群中。

  • 外部复制必须是基于行的
    • 主SQL节点必须使用–binlog-format=ROW或–binlog-format=MIXED启动
  • 外部复制不能是环形的
  • 外部复制不支持auto_increment_*选项
  • 二进制日志的大小可能比常规的MySQL复制更大
MySQL集群(外部)复制的架构

同MySQL复制,但是每个epoach(检查点之间的时间跨度)被视为一个事务。

binlog注入线程维护下面表:

  • ndb_binlog_index,存储二进制日志的索引数据(对于SQL节点是本地的)
  • ndb_apply_status,存储已复制到从服务器的操作的记录,可用于对失效的复制slave进行即时恢复。
单通道复制和多通道复制

MySQL复制连接称为一个通道,一般只有单通道,但是为了保证最大可用性,可以建立一个备用通道来容错,称为多通道复制。

多通道外部复制:

Multichannel-external-replication

master和slave都有主备,使用不同的通道通信。

启动多通道复制:

  1. 启动主master
  2. 启动备用master
  3. 将主slave连接到主master
  4. 将备用slave连接到备用master
  5. 启动主slave

故障转移为了避免同样的数据被复制两次,需要确定上一次复制的epoch:

  1. 找到slave接收到的最近全局检查点的时间
     SELECT @latest := MAX(epoch) FROM mysql.ndb_apply_status;
    
  2. 获取主master上的ndb_binlog_index表中的行
     SELECT @file := SUBSTRING_INDEX(File, '/', 1), @pos := Position
     FROM mysql.ndb_binlog_index
     WHERE epoch > @latest ORDER BY ASC LIMIT 1;
    
  3. 同步备用通道
     CHANGE MASTER TO MASTER_LOG_FILE = 'file', MASTER_LOG_POS = pos;
    
  4. 在备用通道上启动复制
     -- 备用slave
     START SLAVE;
    

获得高性能

特性对高性能的支持:

  • 集群间复制(全局冗余)
  • 集群内部复制(本地冗余)
  • 主存储器存储
    • 无需等待磁盘写,保证了数据更新的快速处理

高性能的注意事项

  • 保证应用程序尽可能高效
    • 修改服务器配置
    • 重构程序成为更高性能的程序
  • 最大化数据库的访问
    • 横向扩展
    • 分发数据
  • 提高集群性能
    • 增加节点

Tips:JOIN比较耗费性能

需要在可用性和性能之间做出权衡,因为副本会消耗性能,而读取操作不需要副本。
由于分布式特性,服务器性能和网络性能都很重要。

高性能的最佳实践

  • 调整访问模式
    • 使用索引
  • 确保应用程序是分布式敏感的
    • 查询最好集中在单个节点上
      • 修改哈希函数是数据分布在同个节点
      • 分区剪裁
  • 使用批操作
    • transaction_allow_batching参数,在单个事务中包含多个操作。
  • 优化数据库模式
    • 使用高效的数据模型
  • 优化查询
    • 从检索的角度优化
    • 对JOIN操作敏感
  • 优化服务器参数
    • 优化配置
  • 使用连接池
    • ndb-cluster-connection-pool修改SQL节点和NDB集群的连接线程数
  • 使用多线程数据节点
    • ndbmtd提升额外性能,支持8核CPU
  • 使用NDB API个性化应用程序
    • 程序直连NDB集群
  • 使用正确的硬件
    • CPU、内存、网络
  • 不要使用交换空间
    • 使用真正的内存而不是交换空间,会影响性能和稳定性
  • 为数据节点使用处理器亲和度
    • 数据节点进程锁定在与网络通信无关的CPU上

应用层优化

常见问题

  • 什么东西在消耗系统中每台主机的CPU、硬盘、网络以及内存资源?如果值不合理,对应用程序做检查。
    • 配置文件是解决问题的最简单方式。
  • 真的需要所有获取到的数据吗?
  • 应用在处理本应该由MySQL处理的事情吗?或者反过来?
  • 让应用来做手工关联有时候是个好主意
  • 应用创建了没必要的MySQL连接吗?如果可以从缓存中获取数据,就不要再连接数据库
  • 应用对一个MySQL实例连接的次数太多了么?通常来说复用连接是一个好主意。
  • 应用是否做了太多的垃圾查询?如先插件数据库是否存活或者每次选择需要的数据库而不是指定库名和表名
  • 应用是否使用了连接池?这可能是好事也可能是坏事,如事务、临时表、连接相关的配置或者自定义变量之间相互干扰。
  • 应用是否使用长连接?这可能导致太多的连接。
  • 应用是否在不使用的时候还保持连接打开?

Web服务器问题

  • 使用缓存代理服务器,不让所有请求都到达WEB服务器,如Squid或者Varnish
  • 对动态和静态资源都设置过期时间
  • 不重复使用文件名,以在客户端使用更长时间的缓存周期
  • 打开gzip压缩
  • 配置Keep-alive时需要使用代理

代理可以使Apache不被长连接拖垮,产生更少的工作进程:

代理可以使Apache不被长连接拖垮,产生更少的工作进程

寻找最优并发度

让进程处理请求尽可能快,并且不超过系统负载的最优并发连接数。进行简单的测试建模或者反复实验。

对于大流量的网站,Web服务器同一时刻处理上千个连接是很常见的。然后只有一小部分连接需要进程实时处理。其它可能的是读请求,处理文件上传,填鸭式服务内容,或者只是等待客户端的下一步操作。

随着并发的增加,服务器会逐渐到达它的最大吞吐量。在这之后,吞吐量通常开始降低,响应时间和会因为排队而开始增加。

缓存

可以把缓存分为两大类:主动缓存和被动缓存。

  • 被动缓存除了存储和返回数据外不做任何事情。当请求被动缓存时,要么得到结果或者返回结果不存在。典型例子是memcached。
  • 主动缓存会在访问未命中时做一些额外的工作。通常会将请求转发给应用程序的其他部分来生成请求结果,然后存储结果并返回数据。Squid缓存代理服务器就是一个主动缓存。

设计应用程序的时候,通常希望缓存是主动的, 因为他们对引用隐藏了检查、生成、存储这个逻辑过程。

应用层以下的缓存

MySQL自己有内部缓存,也可以自己构建缓存和汇总表。

可以对缓存表进行量身定制,使它们最有效的过滤、排序、与其他表关联、计数或者用于其他用途。这也比其他应用层缓存更持久,因为重启后还在。

应用层缓存

通常在同一台机器的内存中存储数据,或者通过网络存在另一台机器的内存中。

应用层缓存可以节省两方面的工作:获取数据以及基于这些数据的计算。

应用缓存由很多种:

  • 本地缓存
    • 通常很小,只在进程处理请求期间存在于进程内存中。
    • 通常只是在应用代码中增加一个变量或者哈希表。
  • 本地共享内存缓存
    • 中等大小,几个G,最大好处是非常快,难以在多台机器中同步。对小型的半静态位数据比较合适。
    • 如每个州的城市列表,分片数据存储的分区函数(映射表),或者使用存活时间策略(TTL)进行失效的数据等。
  • 分布式内存缓存
    • 比本地共享内存缓存要大得多,增长也容易。如memcached。
    • 适合存储共享对象。
    • 比本地共享缓存的延时要高得多,所以最高效的使用方法是批量进行多个获取操作。
  • 磁盘上的缓存
    • 磁盘是很慢的,搜易缓存在磁盘上的最好是持久化对象,很难全部装进内存的对象,或者静态内容。
    • 对于磁盘上的缓存和Web服务器,可以使用404错误处理机制来捕捉缓存未命中情况。404后发生错误处理生成图片到磁盘后发出一个重定向将图片返回给浏览器。
    • 删除文件即缓存失效,可以通过访问时间和定时任务做LRU算法。

缓存控制策略

  • TTL(time to live,存活时间)
    • 设置过期时间,然后通过清理进程或者下次访问时清理
    • 对于很少变更或者没有新数据的情况,这是最好的失效策略
  • 显式失效
    • 如果不能接受脏数据,那么更新原始数据的同时需要使缓存失效。
    • 有两个变种
      • 写–失效
      • 写–更新
        • 如果生成缓存很昂贵是有益的,也可以通过后台处理
  • 读时失效
    • 避免同时失效一些脏数据,在缓存中保留一些信息,在读取数据时根据一些缓存信息判断是否已经失效。
    • 与显示策略比优势是成本固定且可以分散在不同的时间内。避免了出现负载冲高和延迟增大的峰值。

一个最简单的读时失效的方法是采用对象版本控制。在缓存中存储一个对象时,可以存储对象所依赖的数据的当前版本号或者时间戳。如前面提到的高层数据版本号。

缓存对象分层

对缓存进行批量读取调用是非常重要的。LAN环境下的网络往返缓存服务器通常要0.3ms左右。 对缓存进行分层,采用小一些的本地缓存也可能获得很大收益。

预生成内容

好处:

  • 应用代码没有复杂的命中和未命中处理路径
  • 当未命中的处理路径不可接受时,这种方案可以保证未命中不会发生。但需要注意保证稳定性
  • 避免缓存未命中导致的雪崩效应

可能需要大量占用空间。

作为基础组件的缓存

为了避免缓存失效导致服务器压力激增而不可用,可以设计一些高可用缓存的解决方案。 或者至少评估好禁用缓存或丢失缓存时的性能影响。比如可以设计应用遇到这种问题时能够进行自动降级处理。

使用HandlerSocket和memcached

  • 使用后台进程插件HandlerSocket,可以用所谓的NoSQL方式访问MySQL
  • 通过memcached协议访问InnoDB。

除了速度之外最大的原因可能是简单。还可以摆脱缓存,以及所有的失效逻辑,还有为他们服务的额外的基础设施。

拓展MySQl

如果它不能做你需要的事,一种方式是扩展它。

MySQL的替代品

  • 对于简单的单一键值粗处,在复制严重落后的非常告诉访问环境中,建议使用Redis替换MySQL。Redis也通常用来做队列。
  • 混合MySQL和Hadoop的部署在处理大型或半结构化数据是非常常见。

锁的调试

通过SHOW PROCESSLIST查看蛛丝马迹。任何支持行级别锁的存储引擎,都实现了自己的锁。

服务器级别的锁等待

锁等待可能发生在服务器级别和存储引擎级别。

MySQL服务器使用的几种类型的锁:

  • 表锁
    • 表可以被显式的读锁和写锁进行锁定。
    • 查询过程中还有隐式锁
  • 全局锁
    • 通过FLUSH TABLES WITH READ LOCK或设置read_only=1来获取单个全局读锁,它与任何表锁都冲突。
  • 命名锁
    • 是表锁的一种,在重命名或者删除一个表时创建
  • 字符锁
    • 可以用GET_LOCK()及其相关函数在服务器级别内锁住和释放任意一个字符串

表锁

显式的锁用LOCK TABLES创建,UNLOCK TABLES解除。

-- 先执行读锁
LOCK TABLES sakila.film READ;
-- 然后再执行下面语句
LOCK TABLES sakila.film WRITE;
SHOW PROCESSLIST\G
*************************** 1. row ***************************
     Id: 7
   User: baron
   Host: localhost
     db: NULL
Command: Query
   Time: 0
  State: NULL
   Info: SHOW PROCESSLIST
*************************** 2. row ***************************
     Id: 11
   User: baron
   Host: localhost
     db: NULL
Command: Query
   Time: 4
  State: Locked
   Info: LOCK TABLES sakila.film WRITE
2 rows in set (0.01 sec)

线程11的状态是Locked,当一个进程持有该锁后,其它线程只能不断尝试获取。看到这样的信息证明线程在等待一个服务器中的锁,而不是存储引擎的。

隐式锁:

SELECT SLEEP(30) FROM sakila.film LIMIT 1;
SHOW PROCESSLIST\G
*************************** 1. row ***************************
     Id: 7
   User: baron
   Host: localhost
     db: NULL
Command: Query
   Time: 12
  State: Sending data
   Info: SELECT SLEEP(30) FROM sakila.film LIMIT 1
*************************** 2. row ***************************
     Id: 11
   User: baron
   Host: localhost
     db: NULL
Command: Query
   Time: 9
  State: Locked
   Info: LOCK TABLES sakila.film WRITE

隐式锁和显示锁从内部来说有相同的结构。

InnoDB对给定的服务器级别的锁,为其创建特定类型的InnoDB表锁。

命名锁

是一种表锁,服务器会在命名或者删除一个表时创建。命名锁与普通的表锁相冲突,无论是隐式的还是显式的。

RENAME TABLE sakila.film2 TO sakila.film;
mysql> SHOW PROCESSLIST\G
...
*************************** 2. row ***************************
     Id: 27
   User: baron
   Host: localhost
     db: NULL
Command: Query
   Time: 3
  State: Waiting for table
   Info: rename table sakila.film to sakila.film 2

查看命名锁的影响:

SHOW OPEN TABLES;
+----------+-----------+--------+-------------+
| Database | Table     | In_use | Name_locked |
+----------+-----------+--------+-------------+
| sakila   | film_text |      3 |           0 |
| sakila   | film      |      2 |           1 |
| sakila   | film2     |      1 |           1 |
+----------+-----------+--------+-------------+

新名旧名都被锁住了。file_text上有个指向它的触发器而被锁住,这也解释了另一种锁方式。

当冲突时,一般是命名锁在等待一个表锁,使用mysqladmin debug来查看。

找出谁持有锁

使用mysqladmin工具:

mysqladmin debug
Thread database.table_name Locked/Waiting   Lock_type
7      sakila.film         Locked - read    Read lock  without concurrent inserts
8      sakila.film         Waiting - write  Highest priority write lock

线程8正在等待线程7持有的锁。

全局读锁

MySQL服务器还实现了一个全局读锁。

FLUSH TABLES WITH READ LOCK;
-- 死锁
LOCK TABLES sakila.film WRITE;
SHOW PROCESSLIST\G
...
*************************** 2. row ***************************
     Id: 22
   User: baron
   Host: localhost
     db: NULL
Command: Query
   Time: 9
  State: Waiting for release of readlock
   Info: LOCK TABLES sakila.film WRITE

Waiting for release of readlock证明正在登呢个带一个全局读锁。

用户锁

基本是一个命名互斥量,需要制定锁的名称字符串,以及等待的超时时间。

SELECT GET_LOCK('my lock', 100);
+--------------------------+
| GET_LOCK('my lock', 100) |
+--------------------------+
|                        1 |
+--------------------------+
SHOW PROCESSLIST\G
*************************** 1. row ***************************
     Id: 22
   User: baron
   Host: localhost
     db: NULL
Command: Query
   Time: 9
  State: User lock
   Info: SELECT GET_LOCK('my lock', 100)

User lock是这种锁状态独有的。

InnoDB中的锁等待

InnoDB在SHOW ENGINE INNODB STATUS中显示了一些锁信息。

SET AUTOCOMMIT=0;
BEGIN;
SELECT film_id FROM sakila.film LIMIT 1 FOR UPDATE;

事务等待的锁显示在TRANSACTIONS部分中。

1  LOCK WAIT 2 lock struct(s), heap size 1216
2  MySQL thread id 8, query id 89 localhost baron Sending data
3  SELECT film_id FROM sakila.film LIMIT 1 FOR UPDATE
4  ------- TRX HAS BEEN WAITING 9 SEC FOR THIS LOCK TO BE GRANTED:
5  RECORD LOCKS space id 0 page no 194 n bits 1072 index `idx_fk_language_id` of table
   `sakila/film` trx id 0 61714 lock_mode X waiting

结果显示查询在等待idx_fk_language_id索引的194页上的一个排它锁(lock_mode X)。

使用InnoDB锁监视器,它最多可以显示每个事务中拥有的10把锁。

CREATE TABLE innodb_lock_monitor(a int) ENGINE=INNODB;

发起这个查询后InnoDB开始定时打印SHOW ENGINE INNODB STATUS的一个加强版输出到标准输出,这个输出大多被定向到错误日志中。 或者直接使用SHOW INNODB STAUTS查看。

监视器输出:

 1  ---TRANSACTION 0 61717, ACTIVE 3 sec, process no 5102, OS thread id 1141152080
 2  3 lock struct(s), heap size 1216
 3  MySQL thread id 11, query id 108 localhost baron
 4  show ENGINE innodb status
 5  TABLE LOCK table `sakila/film` trx id 0 61717 lock mode IX
 6  RECORD LOCKS space id 0 page no 194 n bits 1072 index `idx_fk_language_id` of table
    `sakila/film` trx id 0 61717 lock_mode X
 7  Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 8   ... omitted ...
 9
10  RECORD LOCKS space id 0 page no 231 n bits 168 index `PRIMARY` of table `sakila/film`
    trx id 0 61717 lock_mode X locks rec but not gap
11  Record lock, heap no 2 PHYSICAL RECORD: n_fields 15; compact format; info bits 0
12   ... omitted ...

使用innotop工具查看锁信息。

使用INFORMATION_SCHEMA表

INNODB_LOCKS及其他表。

一个显示谁阻塞和谁在等待,以及等待多久的查询:

SELECT r.trx_id AS waiting_trx_id,  r.trx_mysql_thread_id AS waiting_thread,
       TIMESTAMPDIFF(SECOND, r.trx_wait_started, CURRENT_TIMESTAMP) AS wait_time,
       r.trx_query AS waiting_query,
       l.lock_table AS waiting_table_lock,
       b.trx_id AS blocking_trx_id, b.trx_mysql_thread_id AS blocking_thread,
       SUBSTRING(p.host, 1, INSTR(p.host, ':') - 1) AS blocking_host,
       SUBSTRING(p.host, INSTR(p.host, ':') +1) AS blocking_port,
       IF(p.command = "Sleep", p.time, 0) AS idle_in_trx,
       b.trx_query AS blocking_query
FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS  AS w
INNER JOIN INFORMATION_SCHEMA.INNODB_TRX   AS b ON  b.trx_id = w.blocking_trx_id
INNER JOIN INFORMATION_SCHEMA.INNODB_TRX   AS r ON  r.trx_id = w.requesting_trx_id
INNER JOIN INFORMATION_SCHEMA.INNODB_LOCKS AS l ON  w.requested_lock_id = l.lock_id
LEFT JOIN  INFORMATION_SCHEMA.PROCESSLIST  AS p ON  p.id     = b.trx_mysql_thread_id
ORDER BY wait_time DESC\G
*************************** 1. row ***************************
    waiting_trx_id: 5D03
    waiting_thread: 3
         wait_time: 6
     waiting_query: select * from store limit 1 for update
waiting_table_lock: `sakila`.`store`
   blocking_trx_id: 5D02
   blocking_thread: 2
     blocking_host: localhost
     blocking_port: 40298
       idle_in_trx: 8
    blocking_query: NULL

结果显示线程3已经等待store表中的锁达6s。它在被线程2阻塞,而该线程已经空闲了8s。

有多少查询被哪些线程阻塞:

SELECT CONCAT('thread ', b.trx_mysql_thread_id, ' from ', p.host) AS who_blocks,
       IF(p.command = "Sleep", p.time, 0) AS idle_in_trx,
       MAX(TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW())) AS max_wait_time,
       COUNT(*) AS num_waiters
FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS  AS w
INNER JOIN INFORMATION_SCHEMA.INNODB_TRX   AS b ON  b.trx_id = w.blocking_trx_id
INNER JOIN INFORMATION_SCHEMA.INNODB_TRX   AS r ON  r.trx_id = w.requesting_trx_id
LEFT JOIN  INFORMATION_SCHEMA.PROCESSLIST  AS p ON  p.id     = b.trx_mysql_thread_id
GROUP BY who_blocks ORDER BY num_waiters DESC\G
*************************** 1. row ***************************
   who_blocks: thread 2 from localhost:40298
  idle_in_trx: 1016
max_wait_time: 37
  num_waiters: 8

结果显示线程2已经空闲了很长时间,并且至少有一个线程已经等待它释放的锁37s。有8个线程正在等待2完成它的工作并提交。

监控和管理

监控入门

有许多不同的东西可以监视、测量和计划来处理这些类型的更改。下面是一些例子:

  • 可以将索引添加到经常读取的表中。
  • 可以重写查询或更改数据库结构,以加快执行时间。
  • 如果锁被保存了很长时间,这表明几个连接在使用同一个表。更换存储引擎可能会有好处。
  • 如果一些过热的slave正在处理不成比例的查询,系统可能需要进行一些重新均衡,以确保所有的过热的slave都被均匀地击中。
  • 要处理资源使用的突然变化,必须确定每个服务器的正常负载,并理解系统何时因为负载突然增加开始缓慢响应。

如果没有监视,就无法发现问题查询、过热的slave或使用不当的表。

监控方法

不影响操作或运行唤醒的前提下,用仪器观察、记录或检测操作或环境。

系统监控主要有三种类型:系统性能、应用程序性能和安全性。

  • 通过监控确保一切不变称为主动监控
  • 通过监控确定哪里出错称为被动监控

监控系统组件

处理器

监视系统的CPU,以确保没有失控的进程,并且CPU周期在运行的程序之间被平等地共享。

  • 一种方法是调用正在运行的程序列表,并确定每个程序使用的CPU百分比。
  • 另一种方法是检查系统进程的平均负载。大多数操作系统提供CPU性能的几个视图。

CPU超载一些常见的解决办法:

  • 添加新服务器运行某些进程
  • 删除不必要的进程
  • 杀死失控的进程
    • 失控进程可能是有缺陷的应用程序导致的,间歇或偶尔出现问题是,往往也是它们的问题。
  • 优化应用程序
  • 较低的进程优先级
    • 有些线程可以后台作业
  • 重新安排进程
    • 调整执行线程执行时机

消耗太多CPU时间的进程被称为CPU受限的。

内存

为了确保应用程序不要请求过多的内存,因为内存过多会浪费很多系统时间用于内存管理。

使用磁盘存储器来存储贮存中未使用的部分或页,这种技术称为分页或交换。

在交换操作很频繁的时候,可用内存很少可能因为失控进程占用了太多内存,或者太多进程请求了内存。

消耗过多内存的进程被称为内存受限的。

  • 增加内存
  • 为不同的系统组件或支持内存优化的程序分配不同数量的内存
  • 更改分页子系统的优先级,让操作系统早点开始分页
硬盘

为了确保系统拥有足够的可用磁盘空间和I/O带宽,以使进程执行时不会出现明显的延时。

使用单进程传输率和整体传输率衡量读写磁盘的传输速率。

消耗太多磁盘传输率的进程被称为是磁盘受限的。

  • 处理磁盘争用的一个方法是添加磁盘控制器和磁盘阵列,将一个受磁盘限制的进程的数据移动到新的磁盘控制器上。
  • 将磁盘受限的进程移动到另一个使用率低的服务器上
  • 升级磁盘系统

优化选择:

  • 如果需要运行大量的进程,需要最大化磁盘传输速率,或者将不同的进程分布在不同的磁盘阵列和系统上
  • 如果少数几个进程但是访问量达,需要优化单进程的传输率,通过增加文件系统的块大小来实现
网络子系统

以确保拥有足够的带宽,而且正在发送或接收的数据具有足够高的质量。

如果消耗了过多的带宽或访问网络子系统的时间太长,这样的进程就称为网络受限的。

  • 网络带宽问题通常由网络接口最大带宽的百分比利用率决定
    • 通过给不同进程分配特定的端口,能够解决这个问题。
  • 网络质量问题通常表现为网络接口遇到了大量错误。操作系统和数据传输应用通常采用校验和或其它算法来检测这类错误,但是重发会给网络和操作系统带来沉重负担。
    • 将某些应用转移到同一网络上的其它系统
    • 安装额外的网卡
    • 重新配置网络协议
    • 将系统转移到网络上的另一个子网

监控方案

  • up.time
    • 监控和报告服务器性能的集成系统
  • Cacti
    • RRDtool图形数据的图形报表解决方案
  • KDE System Guard
    • 允许用户跟踪和控制进程,配置简单
  • Gnome System Monitor
    • 监控CPU、网络、内存和进程的图形化工具
  • Nagios
    • 监控所有服务器、网络开关、应用程序和服务器的一整套解决方案
  • MySQL Enterprise Monitor
    • MySQL数据的性能和可用性提供实时可见能力

Linux和Unix的监控

Linux和Unix的系统监控工具:

Utility Description
ps 显示系统上运行的进程列表
top 显示CPU使用率排序的进程活动
vmstat 显示内存、分页、块存储和CPU活动的相关信息
uptime 显示系统运行了多长时间,显示用户数量及在1分钟、5分钟、15分钟内系统的平均负荷量
free 显示内存使用率
iostat 显示平均磁盘活动和处理器负载情况
sar 显示系统活动报告
pmap 显示各个进程分别占用内存的情况
mpstat 显示多处理器系统的CPU使用率
netstat 显示网络活动情况
cron 计划执行
进程活动

top命令

# 三秒刷新一次
top -d 3
  • 用户占用的CPU百分比(%us)
  • 系统占用的CPU百分比(%sy)
  • nice值(%ni),优先级发生变化的用户进程占用的CPU百分比
    • nice
    • ionice
    • renice
  • I/O等待的CPU百分比(%wa)
  • 处理软件和硬件中断所占用的CPU百分比
  • 内存大小
  • 交换空间大小
  • 缓冲区大小
  • 按照CPU时间的多少降序排序进程列表

iostat命令

显示CPU时间的统计信息,设备I/O的统计信息,分区和网络子系统的统计信息。

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          22.30    0.00    3.20    0.27    0.01   74.23

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
vda               2.56         3.74       279.90    7743570  579741324
scd0              0.00         0.00         0.00         36          0

CPU利用率的百分比:

  • 用户级别执行
  • nice优先级在用户级别执行
  • 在系统级别执行
  • 等待I/O
  • 等待虚拟进程
  • 空闲时间

mpstat命令

mpstat与iostat信息相似,但是将各处理器的信息分开显示。

mpstat
mpstat -A
mpstat -P ALL

ps命令

ps -A | grep mysqld

也可用于确定是否存在一些未知进程,或者是否有单个用户运行了大量进程。这可能是由于脚本配置不合理导致的。

内存利用率

free命令

free命令显示可用的物理内存量,包括宗内存量,已用内存量、可用内存量以及交换空间,还可显示内核使用的内存缓冲区和缓存的大小。

# 轮询
free -t -s 5

pmap命令

输出包括所有内存地址的详细信息,以及报告缠上时进程使用的内存的大小,还显示了内存块模式。

pmap -d 112756

最后一行显示多少内存被映射到文件,私有内存空间的大小,以及与其它进程共享的内存大小。

硬盘利用率

iostat命令

列举了每个设备、设备的传输速率、每秒读写块的数量和读写块的总数量。

df命令

df -h

sar命令

sar -bBdS 1 1
04:10:40 PM  pgpgin/s pgpgout/s   fault/s  majflt/s  pgfree/s pgscank/s pgscand/s pgsteal/s    %vmeff
04:10:41 PM      0.00      0.00    172.73      0.00   8310.10      0.00      0.00      0.00      0.00

04:10:40 PM       tps      rtps      wtps   bread/s   bwrtn/s
04:10:41 PM      0.00      0.00      0.00      0.00      0.00

04:10:40 PM kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
04:10:41 PM         0         0      0.00         0      0.00

04:10:40 PM       DEV       tps  rd_sec/s  wr_sec/s  avgrq-sz  avgqu-sz     await     svctm     %util
04:10:41 PM  dev252-0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
04:10:41 PM dev252-16      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
04:10:41 PM dev252-32      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00

Average:     pgpgin/s pgpgout/s   fault/s  majflt/s  pgfree/s pgscank/s pgscand/s pgsteal/s    %vmeff
Average:         0.00      0.00    172.73      0.00   8310.10      0.00      0.00      0.00      0.00

Average:          tps      rtps      wtps   bread/s   bwrtn/s
Average:         0.00      0.00      0.00      0.00      0.00

Average:    kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
Average:            0         0      0.00         0      0.00

Average:          DEV       tps  rd_sec/s  wr_sec/s  avgrq-sz  avgqu-sz     await     svctm     %util
Average:     dev252-0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
Average:    dev252-16      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
Average:    dev252-32      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
  • 分页子系统性能的分页信息
    • 分页置换率
    • 每秒内不需要磁盘访问的分页错误数
    • 需要磁盘访问的重大错误数
      • 过多进程,如果错误书很高且磁盘使用率很高,则可能不是磁盘子系统的问题
    • 其它信息
  • I/O传输率的报告
    • 美妙的事务数量
    • 读写请求和读写块的总数量
  • 交换空间的报告
    • 可使用交换空间大小
    • 被使用交换空间大小和使用百分比
    • 缓存的使用量
  • 设备及其统计信息的列表
    • 传输速率
    • 每秒的读写速率和平均等待时间
    • 如果这些值都很高,说明可能达到了设备的最大带宽
  • 最后是所有样本参数的平均值

如果分页报告显示错误率异常高,表明系统给可能运行了太多的应用程序或者没有足够的内存。 如果这些纸较低或者一般,则需要检查交换空间。如果交换空间也正常,就检查设备使用报告。

vmstat命令

vmstat -d
网络活动

netstat命令

netstat -i

ifconfig命令

ifconig
常见系统统计信息

uptime命令

系统运行时间即时间间隔内的平均负载。

uptime

平均状态是针对活动状态的进程而言的,而不是阻塞或等待状态。

vmstat

提供有关进程、内存、分页系统、I/O块和CPU活动的信息。

vmstat
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 6  0      0 825928 1694996 7619648    0    0    12    10    0    0 24 12 64  0  0	
  • r表示等待运行的进程
  • b表示处于不可中断状态的进程
  • si换入
  • so换出
  • bi接收块
  • bo发送块
  • in每秒的中断数
  • cs每秒的上下文切换数
  • us用户空间上进程运行的时间
  • sy内核空间上进程运行的时间
  • id限制空间
  • wa等待I/O的时间
使用cron自动监控

每天运行性能监控工具,然后将检测结果与基准进行比较。这是主动检控官的基本前提

监控MySQL

什么是性能

按照用户希望的那样得当的运行,响应时间和而低性能被定义成系统运行不佳。

除非有深思熟虑的计划,并且相当了解变更后的期望和后果,否则永远不要更改服务器、数据库或存储引擎的参数。

MySQL服务器监控

如何显示MySQL性能

读取和更改服务器变量:

SHOW [GLOBAL | SESSION] VARIABLES;
SET [GLOBAL | SESSION] variable_name = value;
SET [@@global. | @@session. | @@]variable_name = value;
-- like
SHOW STATUS LIKE '%thread%';
SQL命令

很多命令可以查询:

USE INFORMATION_SCHEMA;
SHOW TABLES;
select * from INFORMATION_SCHEMA.TABLES
  • SHOW INDEX FROM table;
    • 显示索引
  • SHOW PLUGINS;
    • 列出所有已知插件名称和状态
  • SHOW [FULL] PROCESSLIST;
    • 显示系统上运行的所有线程数据,包括处理客户端连接的线程
    • 诊断响应差的、僵尸进程或者诊断连接问题
    • 使用KILL命令终止进程
    • 需要SUPER权限
  • SHOW [GLOBAL | SESSION] STATUS;
    • 显示所有系统变量的值
  • SHOW TABLE STATUS [FROM db];
    • 给定数据库表的详情,包括存储引擎、排序规则、创建数据、索引和行统计信息。
  • SHOW [GLOBAL | SESSION] VARIABLES;
    • 显示系统变量
  • SHOW ENGINE engine_name LOGS;
    • 显示指定引擎的日志信息
  • SHOW ENGINE engine_name STATUS;
    • 显示指定引擎的状态信息
  • SHOW ENGINES;
    • 显示已知的存储引擎
  • SHOW BINLOG EVENTS [IN log_file] [FROM pos] [LIMIT offset row_count];
    • 显示二进制日志中的事件
  • SHOW BINARY LOGS;
    • 显示服务器上的二进制日志列表
  • SHOW RELAYLOG EVENTS [IN log_file] [FROM pos] [LIMIT offset row_count];
    • 显示中继日志文件内容
  • SHOW MASTER STATUS;
    • 显示master当前配置
  • SHOW SLAVE HOSTS;
    • 显示连接到master的slave列表
  • SHOW SLAVE STATUS;
    • 限制复制中的slave状态

查询缓存是MySQL最重要的性能特征之一。

使缓存失效的事件:

  • 数据或索引的变更
  • 同一个查询的微小区别会产生不同的结果集,从而导致缓存未命中。因此使用标准的查询访问公共数据是非常重要的。
  • 从临时表获取数据
  • 是内存中的查询无效的事务事件,如commit
SHOW VARIABLES LIKE '%query_cache%';
+------------------------------+----------+
| Variable_name                | Value    |
+------------------------------+----------+
| have_query_cache             | YES      |
| query_cache_limit            | 1048576  |
| query_cache_min_res_unit     | 4096     |
| query_cache_size             | 33554432 |
| query_cache_type             | ON       |
| query_cache_wlock_invalidate | OFF      |
+------------------------------+----------+

have_query_cache仅表明查询缓存是否可用,设置query_cache_type还不够,因为这并不会释放查询缓存的缓冲区。 必须同时将query_cache_size设置为0才能完全关闭查询缓存。

通过query_cache开头的变量控制查询缓存,而状态变量以Qcache开头。

-- 查询缓存的状态变量
SHOW STATUS LIKE '%Qcache%';
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| Qcache_free_blocks      | 1         |
| Qcache_free_memory      | 3128392   |
| Qcache_hits             | 0         |
| Qcache_inserts          | 0         |
| Qcache_lowmem_prunes    | 0         |
| Qcache_not_cached       | 305395518 |
| Qcache_queries_in_cache | 0         |
| Qcache_total_blocks     | 1         |
+-------------------------+-----------+
-- 整理查询缓存,而不是清空
FLUSH QUERY CACHE;
mysqladmin实用工具

常用命令:

  • status
    • 服务器状态信息
  • extended-status
    • 完整统计信息
  • processlist
    • 进程列表
  • kill thread id
    • 杀死指定进程
  • variables
    • 显示系统服务器变量和值
# 每3秒执行一次
mysqladmin -uroot --password processlist --sleep 3
# 显式密码
mysqladmin -uroot -ppassword processlist --sleep 3
# 查询系统变量先前值和当前值
mysqladmin -uroot --password extended-status --relative --sleep 3
# 同时查看
mysqladmin -uroot --password processlist status --sleep 3
MySQL工作台
  • 服务器管理器
  • SQL开发
  • 数据建模
  • 数据库迁移向导
第三方工具

MySAR

集成了SHOW STATUS、SHOW VARIABLES和SHOW PROCESSLIST的输出结果。

mytop

列出了常规统计信息,如主机名称、服务器版本、运行的查询数量、查询的平均执行时间、线程的总数量和其它的重要统计信息。

InnoTop

监控事务、死锁、外键、查询活动、复制活动、系统变量的主要统计信息及主机的其它详情。

如果使用InnoDB作为存储引擎,并且需要一个以文本模式运行良好的监控工具,那就选择InnoTop

MONyog

可以为安全和性能的主要组件设置参数,还包含有助于服务器性能调优的工具。 另外还可以设置事件以监听特定参数,并在系统达到指定临界点时发出警告。

  • 监控服务器资源
  • 识别运行不佳的SQL语句
  • 监控服务器日志,如错误日志
  • 实施监控查询性能,并识别长时间运行的查询
  • 预警重大事件

MONyog还提供了GUI组件

MySQL基准测试套件

基准测试:

  • 过程:确定系统在某种负载下是如何运行的。
  • 目的:分别在服务器处于轻负载、中等负载和高负载的情况下,运行定义良好的测试实例,并衡量和记录系统的统计信息。

基准测试为系统性能设置了期望。

运行Perl的基准测试套件:

./run-all-tests --server=mysql --cmp=mysql --user=root
benchmark函数
SELECT BENCHMARK(10000000, "SELECT CONCAT('te','s',' t')");
EXPLAIN语句
EXPLAIN SELECT * FROM table LIMIT 1;

查询中每个表输出一行。别名表算作一个表,因此如果把一个表与自己关联,输出也会是两行。

表的意义在这里里相当广:可以是一个子查询,以UNION结果等。

EXPLAIN主要有两个变种:

  • EXPLAIN EXTENDED告诉服务器逆向编译执行计划为一个SELECT语句。通过SHOW WARNINGS来看到这个语句。
  • EXPLAIN PARTITIONS会显示查询讲访问的分区,如果查询时基于分区表的话。

如果EXPLAIN时FROM子句中包括子查询,实际上是会执行查询,将其结果放在一个临时表中,然后完成外层查询优化。5.6中有所改变。

EXPLAIN是个近似结果,以下是一些相关限制:

  • EXPLAIN不会告诉触发器、存储过程或者UDF如何响应查询
  • 不支持存储过程,可以手动抽取查询并EXPLAIN
  • 不会显示MySQL在执行过程中做的优化
  • 不会显示关于查询执行计划的所有信息
  • 很多操作的名字都相同,如对内存排序和临时文件排序都使用filesort,对于内存和磁盘临时表都适用Using temporary

重写非SELECT查询

EXPLAIN只能解释SELECT语句。其它语句需要重写某些非SELECT查询来EXPLAIN。5.6中将允许解释非SELECT语句。

将语句转化成一个等价的访问所有相同列的SELET。任何提及的列都必须在SELECT列表,关联子句或者WHERE语句。

-- 原查询
UPDATE sakila.actor
   INNER JOIN sakila.film_actor USING (actor_id)
SET actor.last_update=film_actor.last_update;
-- 不等价
EXPLAIN SELECT film_actor.actor_id
FROM sakila.actor
   INNER JOIN sakila.film_actor USING (actor_id)\G
-- 等价,必须查询用到的列
EXPLAIN SELECT film_actor.last_update, actor.last_update
FROM sakila.actor
   INNER JOIN sakila.film_actor USING (actor_id)\G

EXPLAIN中的列

+----+-------------+----------+-------+---------------+----------------------------------+---------+------+---------+----------+-------------+
| id | select_type | table    | type  | possible_keys | key                              | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+----------+-------+---------------+----------------------------------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | material | index | NULL          | idx_user_id_and_removed_and_type | 776     | NULL | 1905491 |   100.00 | Using index |
+----+-------------+----------+-------+---------------+----------------------------------+---------+------+---------+----------+-------------+

id列

标识SELECT所属的行。如果没有子查询或联合,只会有唯一的SELECT,于是每行都显示1,否则内层的SELECT语句一般会顺序编号,对应于其在原始语句中的位置。

MySQL将SELECT查询分为简单和复杂类型,复杂类型可以分为三大类:简单子查询、所谓的派生表(在FROM子句中的子查询)以及UNION查询。

-- 简单子查询
EXPLAIN SELECT (SELECT 1 FROM sakila.actor LIMIT 1) FROM sakila.film;
+----+-------------+-------+
| id | select_type | table |
+----+-------------+-------+
|  1 | PRIMARY     | film  |
|  2 | SUBQUERY    | actor |
+----+-------------+-------+
-- 在FROM子句中的子查询和联合id
EXPLAIN SELECT film_id FROM (SELECT film_id FROM sakila.film) AS der;
+----+-------------+------------+
| id | select_type | table      |
+----+-------------+------------+
|  1 | PRIMARY     | <derived2> |
|  2 | DERIVED     | film       |
+----+-------------+------------+
-- UNION查询
EXPLAIN SELECT 1 UNION ALL SELECT 1;
+------+--------------+------------+
| id   | select_type  | table      |
+------+--------------+------------+
|  1   | PRIMARY      | NULL       |
|  2   | UNION        | NULL       |
| NULL | UNION RESULT | <union1,2> |
+------+--------------+------------+

UNION会输出额外行。UNION结果总是放在一个匿名临时表中,之后MySQL将结果读取到临时表外。

select_type列

如果查询有任何复杂的子查询,则最外层部分标记位PRIMARY,其它部分标记如下:

  • SUBQUERY
    • 包含在SELECT列表中的子查询的SELECT标记位SUBQUERY。
  • DERIVED
    • 在FROM子句的子查询中的SELECT,MySQL会递归执行并将结构放到一个临时表中。服务器内部称其为派生表,因为临时表是从查询中派生来的
  • UNION
    • UNION中的第二个和随后的SELECT被标记为UNION。
    • 第一个SELECT显示为PRIMARY。如果UNION被FROM字句中的子查询包含,那么它的第一个SELECT会被标记位DERIVED。
  • UNION RESULT
    • 从UNION的匿名临时表检索结果的SELECT被标记为这个值。

除了这些值,SUBQUERY和UNION还可以被标记为DEPENDENT和UNCACHEABLE。 DEPENDENT意味着SELECT依赖于外层查询发现的数据; UNCACHEABLE意味着SELECT中的某些特性阻止结果被缓存于一个Item_cache中。

table列

访问的表或者表的别名。

通过这一列观察MySQL的关联优化器为查询选择的关联顺序。

EXPLAIN SELECT film.film_id
FROM sakila.film
   INNER JOIN sakila.film_actor USING(film_id)
   INNER JOIN sakila.actor USING(actor_id);
+----+-------------+------------+
| id | select_type | table      |
+----+-------------+------------+
|  1 | SIMPLE      | actor      |
|  1 | SIMPLE      | film_actor |
|  1 | SIMPLE      | film       |
+----+-------------+------------+

查询计划与EXPLAIN中的行相对应的方式:

How the query execution plan corresponds to the rows in EXPLAIN

派生表和联合:

当FROM子句中有子查询或有UNION时,table列会变得复杂得多。table列是<derivedN>的形式,其中N是子查询的id。N指向EXPLAIN输出中的后面一行。 UNION RESULT的table列包含一个参与UNION的id列表。

 1  EXPLAIN
 2  SELECT actor_id,
 3     (SELECT 1 FROM sakila.film_actor WHERE film_actor.actor_id =
 4        der_1.actor_id LIMIT 1)
 5  FROM (
 6     SELECT actor_id
 7     FROM sakila.actor LIMIT 5
 8  ) AS der_1
 9  UNION ALL
10  SELECT film_id,
11     (SELECT @var1 FROM sakila.rental LIMIT 1)
12  FROM (
13     SELECT film_id,
14        (SELECT 1 FROM sakila.store LIMIT 1)
15     FROM sakila.film LIMIT 5
16  ) AS der_2;
+------+----------------------+------------+
| id   | select_type          | table      |
+------+----------------------+------------+
|  1   | PRIMARY              | <derived3> |
|  3   | DERIVED              | actor      |
|  2   | DEPENDENT SUBQUERY   | film_actor |
|  4   | UNION                | <derived6> |
|  6   | DERIVED              | film       |
|  7   | SUBQUERY             | store      |
|  5   | UNCACHEABLE SUBQUERY | rental     |
| NULL | UNION RESULT         | <union1,4> |
+------+----------------------+------------+
  • The first row is a forward reference to der_1, which the query has labeled as <derived3>. It comes from line 2 in the original SQL. To see which rows in the output refer to SELECT statements that are part of <derived3>, look forward …
  • …to the second row, whose id is 3. It is 3 because it’s part of the third SELECT in the query, and it’s listed as a DERIVED type because it’s nested inside a subquery in the FROM clause. It comes from lines 6 and 7 in the original SQL.
  • The third row’s id is 2. It comes from line 3 in the original SQL. Notice that it comes after a row with a higher id number, suggesting that it is executed afterward, which makes sense. It is listed as a DEPENDENT SUBQUERY, which means its results depend on the results of an outer query (also known as a correlated subquery). The outer query in this case is the SELECT that begins in line 2 and retrieves data from der_1.
  • The fourth row is listed as a UNION, which means it is the second or later SELECT in a UNION. Its table is <derived6>, which means it’s retrieving data from a subquery in the FROM clause and appending to a temporary table for the UNION. As before, to find the EXPLAIN rows that show the query plan for this subquery, you must look forward.
  • The fifth row is the der_2 subquery defined in lines 13, 14, and 15 in the original SQL, which EXPLAIN refers to as <derived6>.
  • The sixth row is an ordinary subquery in <derived6>’s SELECT list. Its id is 7, which is important…
  • …because it is greater than 5, which is the seventh row’s id. Why is this important? Because it shows the boundaries of the <derived6> subquery. When EXPLAIN outputs a row whose SELECT type is DERIVED, it represents the beginning of a “nested scope.” If a subsequent row’s id is smaller (in this case, 5 is smaller than 6 ), it means the nested scope has closed. This lets us know that the seventh row is part of the SELECT list that is retrieving data from <derived6>—i.e., part of the fourth row’s SELECT list (line 11 in the original SQL). This example is fairly easy to un- derstand without knowing the significance and rules of nested scopes, but some- times it’s not so easy. The other notable thing about this row in the output is that it is listed as an UNCACHEABLE SUBQUERY because of the user variable.
  • Finally, the last row is the UNION RESULT. It represents the stage of reading the rows from the UNION’s temporary table. You can begin at this row and work backward if you wish; it is returning results from rows whose ids are 1 and 4, which are in turn references to <derived3> and <derived6>.

type列

关联类型,访问类型,MySQL决定如何查找表中的行。

访问防范,一次从最差到最优:

  • ALL
    • 全表扫描
  • index
    • 扫描表时按照索引顺序而不是行。
    • 主要的优点是避免了排序;最大缺点是要承担按索引次序读取整个表的开销,通常意味着按照随机次序访问,开销很大。
    • 如果是Extra列的Using index则表示使用索引覆盖扫描
  • range
    • 有限制的索引扫描
    • 开销跟索引扫描相当
    • WHERE中带有>或者BETWEEN语句
    • IN()、OR列表也会显示为range,但是性能不同
  • ref
    • 索引访问,返回所有匹配某个单个值的行。可能找到多个行,是查找和扫描的混合体。
    • ref_or_null是ref的一个变体,意味着MySQL必须在初次查找的时候进行第二次查找以找出NULL条目。
  • eq_ref
    • 索引查找并返回一条符合条件的记录。可以在主键或者唯一索引查找时见到。
    • 这类优化做的很好,无序估计匹配行的范围或者在查找后继续查找
  • const, system
    • 能对查询的某部分进行优化并将其转换成一个常量时,就会着用这些访问类型。
    • 如通过主键放入WHERE子句里的方式,就能把查询转换为一个常量,高效的将表从连接执行中移除
  • NULL
    • 意味着在优化阶段分解查询语句,在执行阶段甚至用不着访问索引和表。
    • 如返回最小值。

possible_keys列

在优化早期创建的,可能用到的索引。基于查询访问的列和使用的比较操作符来判断的。

key列

显示了MySQL决定使用哪些索引来优化对表的访问。

possible_keys揭示了哪一个索引能够有助于高效的行查找,key显示的是优化采用哪一个索引可以最小化查询成本。

key_len列

MySQL在索引里使用的字节数。显示的是表定义中计算出的使用了的索引字段的最大长度。

CREATE TABLE t (
   a char(3) NOT NULL,
   b int(11) NOT NULL,
   c char(1) NOT NULL,
   PRIMARY KEY  (a,b,c)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
INSERT INTO t(a, b, c)
   SELECT DISTINCT LEFT(TABLE_SCHEMA, 3), ORD(TABLE_NAME),
      LEFT(COLUMN_NAME, 1)
   FROM INFORMATION_SCHEMA.COLUMNS:
EXPLAIN SELECT a FROM t WHERE a='sak' AND b = 112;
+------+---------------+---------+---------+
| type | possible_keys | key     | key_len |
+------+---------------+---------+---------+
| ref  | PRIMARY       | PRIMARY | 13      |
+------+---------------+---------+---------+

13即为a列和b列的总长度。a列是3个字符,utf8下每一个最多为3字节,而b列是一个4字节整型。

ref列

显示之前的表在key列记录的索引中查找值所用的列或常量。

EXPLAIN
SELECT STRAIGHT_JOIN f.film_id
FROM sakila.film AS f
   INNER JOIN sakila.film_actor AS fa
      ON f.film_id=fa.film_id AND fa.actor_id = 1
   INNER JOIN sakila.actor AS a USING(actor_id);
+-------+...+--------------------+---------+------------------------+
| table |...| key                | key_len | ref                    |
+-------+...+--------------------+---------+------------------------+
| a     |...| PRIMARY            | 2       | const                  |
| f     |...| idx_fk_language_id | 1       | NULL                   |
| fa    |...| PRIMARY            | 4       | const,sakila.f.film_id |
+-------+...+--------------------+---------+------------------------+

rows列

估计为了找到所需的行而要读取的行数。这个数字是内嵌循环关联计划里的循环数目。

通过把多行的rows值相乘,可以得到大约的估计行数。

filtered列

表里符合某个条件的记录数的百分比所做的一个悲观估值。

CREATE TABLE t1 (
   id INT NOT NULL AUTO_INCREMENT,
   filler char(200),
   PRIMARY KEY(id)
);
-- 1000行数据
EXPLAIN EXTENDED SELECT * FROM t1 WHERE id < 500\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
         type: ALL
possible_keys: PRIMARY
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1000
     filtered: 49.40
        Extra: Using where

Extra列

常见重要值:

  • “Using index”
    • 表示使用覆盖索引,避免访问表
  • “Using where
    • 在存储引擎检索行后进行过滤。
  • “Using temporary
    • 对结果排序时会使用一个临时表
  • “Using filesort
    • 对结果进行一个外部索引排序,而不是按索引次序从表里读取行。
  • “Range checked for each record (index map: N)”
    • 没有好用的索引,新的索引将在联接的每一行上重新估算。N是显示在possible_keys列中索引的位图,并且是冗余的。

树形格式输出

使用pt-visual-explain查看属性结构的执行计划。

服务器日志

MySQL日志类型:

  • 常规查询日志
  • 慢查询日志
  • 错误日志
  • 二进制日志

一般启动了错误日志和常规查询日志。常规日志与它们从客户端返回的顺序一致。

通过--general-log启动常规查询日志,--log-output指定日志位置。

等价动态变量:

SET GLOBAL log_output = FILE;

通过--log-slow-quer⁠ies控制慢查询日志。服务器变量log_query_time用来控制什么查询被记录到慢查询日志。

slave不记录慢查询,使用--log-slow-slave-statements开启记录。

--log-error开启或关闭错误日志,general_log_file重写文件相关信息。

--console启动,会将错误输出到标准输出。

--log-bin启动,开启二进制日志并指定文件名。--logbin-index更改二进制日志的索引名。

# 轮换日志
FLUSH LOGS;

性能模式

表现为名为performance_schema的数据库。其中包含很多动态表。

用于诊断死锁、互斥和线程问题。还可以获取查询优化的阶段指标、文件I/O、连接等。

概念
USE performance_schema;
SHOW TABLES;
+----------------------------------------------------+
| Tables_in_performance_schema                       |
+----------------------------------------------------+
| accounts                                           |
| cond_instances                                     |
| events_stages_current                              |
| events_stages_history                              |
| events_stages_history_long                         |
| events_stages_summary_by_account_by_event_name     |
| events_stages_summary_by_host_by_event_name        |
| events_stages_summary_by_thread_by_event_name      |
| events_stages_summary_by_user_by_event_name        |
| events_stages_summary_global_by_event_name         |
| events_statements_current                          |
| events_statements_history                          |
| events_statements_history_long                     |
| events_statements_summary_by_account_by_event_name |
| events_statements_summary_by_digest                |
| events_statements_summary_by_host_by_event_name    |
| events_statements_summary_by_thread_by_event_name  |
| events_statements_summary_by_user_by_event_name    |
| events_statements_summary_global_by_event_name     |
| events_waits_current                               |
| events_waits_history                               |
| events_waits_history_long                          |
| events_waits_summary_by_account_by_event_name      |
| events_waits_summary_by_host_by_event_name         |
| events_waits_summary_by_instance                   |
| events_waits_summary_by_thread_by_event_name       |
| events_waits_summary_by_user_by_event_name         |
| events_waits_summary_global_by_event_name          |
| file_instances                                     |
| file_summary_by_event_name                         |
| file_summary_by_instance                           |
| host_cache                                         |
| hosts                                              |
| mutex_instances                                    |
| objects_summary_global_by_type                     |
| performance_timers                                 |
| rwlock_instances                                   |
| session_account_connect_attrs                      |
| session_connect_attrs                              |
| setup_actors                                       |
| setup_consumers                                    |
| setup_instruments                                  |
| setup_objects                                      |
| setup_timers                                       |
| socket_instances                                   |
| socket_summary_by_event_name                       |
| socket_summary_by_instance                         |
| table_io_waits_summary_by_index_usage              |
| table_io_waits_summary_by_table                    |
| table_lock_waits_summary_by_table                  |
| threads                                            |
| users                                              |
+----------------------------------------------------+

性能模式监控事件。事件是已经生效(在代码中启用,称为监控点)的任意不相关的执行,而且持续时间可测量。事件存储为当前事件(最近值)、历史值和概要(聚集值)。

监控器是由服务器(源)中的监控点构成的,这些监控点在执行时产生了事件。监控器必须启用才能产生事件。

使用setup_actors表监控特定用户(线程)。 使用setup_objects表监控特定表或某个数据库中的所有表。

定时器(timer)是以持续事件测量的一种执行。定时器分为空闲(idle)、等待(wait)、阶段(stage)和语句(statement)。
更改定时器的持续事件可以改变测量的频率。值为:CYCLE, NANOSECOND, MICROSECOND, MILLISECOND, TICK。
检查performance_timers表中的行可以查看可用的定时器。

配置表用来启动或禁用行为体(actor)、监控器、对象(表)和定时器

入门

--performance-schema启动参数也可以控制。

[mysqld]
performance_schema=ON

开启步骤:

  1. 设置定时器(适用于有定时元素的监控点)
  2. 开启监控点
  3. 开启consumer
-- 检查当前设置
select * from setup_timers;
-- 开启监控点
UPDATE setup_instruments SET enabled='YES', timed='YES' WHERE name = 'statement/sql/show_grants';
-- 开启consumer
UPDATE setup_consumers SET enabled='YES' WHERE name = 'events_statements_current';
UPDATE setup_consumers SET enabled='YES' WHERE name = 'events_statements_history';
-- 执行命令
show grants \G
-- 检查结果
select * from events_statements_current \G
select * from events_statements_history \G

events_statements_table的输出结果是最后一次记录的执行语句。
events_statements_history的输出结果是所有开启的事件上的最近查询。

还可以启用事前过滤和事后过滤。

使用性能模式诊断性能问题

一次改变一个值,如果无效就恢复这个值再尝试下一次更改。

  1. 查询setup_instruments表以识别所有相关的监控点并启用它们。
  2. 设置您需要记录的频率的定时器。大多数情况下,默认值是正确的定时器值。如果更改定时器,请记录它们的原始值。
  3. 找到与监控点相关的消费者(事件表)并启用它们。确保启用了current、history和history_long。
  4. 截断history和history_long表,以确保以“干净”状态开始。
  5. 重现问题。
  6. 查询性能模式表。如果您的服务器有多个客户端正在运行,则可以通过线程ID隔离行。
  7. 观察这些值并将其记录下来。
  8. 调优一个选项/参数/变量集。
  9. 回到第5步。重复,直到性能得到改善。
  10. 截断*history和History_long表,以确保以“干净”状态结束。
  11. 禁用之前启用的事件。
  12. 禁用之前启用的监控点。
  13. 将定时器恢复到原来的状态。
  14. 再次截断*history和History_long表,以确保以“干净”状态结束。

MySQL的监控分类

表查询需要切换schema,如USER mysql;

关注点 设备 指标 示例
性能 System Variables Query Cache SHOW VARIABLES LIKE ‘%query_cache%’
性能 Status Variables Number of Inserts SHOW STATUS LIKE ‘com_insert’
性能 Status Variables Number of Deletes SHOW STATUS LIKE ‘com_delete’
性能 Status Variables Table Lock Collisions SHOW STATUS LIKE ‘table_locks_waited’
性能 Logging Slow Queries SELECT * FROM slow_log ORDER BY query_time DESC
性能 Logging General SELECT * FROM general_log
性能 Logging Errors –log-error=file name (startup variable)
性能 Performance Schema Thread Information SELECT * FROM threads
性能 Performance Schema Mutex Information SELECT * FROM events_wait_current
性能 Performance Schema Mutex Information SELECT * FROM mutex_instances
性能 Performance Schema File Use Summary SELECT * FROM file_summary_by_instance
性能 Storage Engine Features InnoDB Status SHOW ENGINE innodb STATUS
性能 Storage Engine Features InnoDB Statistics SHOW STATUS LIKE ‘%Innodb%’
性能 External Tools Processlist mysqladmin -uroot –password processlist –sleep 3
性能 External Tools Connection Health (graph) MySQL Workbench
性能 External Tools Memory Health (graph) MySQL Workbench
性能 External Tools InnoDB Rows Read MySQL Workbench
性能 External Tools Logs MySQL Workbench
性能 External Tools All Variables MySQL Workbench
性能 External Tools Query Plan/Execution[a] MySQL Workbench
性能 External Tools Benchmarking MySQL Benchmark Suite
可用性 Status Variables Connected Threads SHOW STATUS LIKE ‘threads_connected’
可用性 Operating System Tools Accessibility ping
可用性 External Tools Accessibility mysqladmin -uroot –password extended-status –relative –sleep 3
资源 Status Variables Storage Engines Supported SHOW ENGINES
资源 Operating System Tools CPU Usage top -n 1 -pid mysqld_pid
资源 Operating System Tools RAM Usage top -n 1 -pid mysqld_pid
资源 MySQL Utilities Disk Usage mysqldiskusage
资源 MySQL Utilities Server Information mysqlserverinfo
资源 MySQL Utilities Replication Health mysqlepladmin

还可以使用EXPLAIN SQL命令

数据库性能

衡量数据库的性能

一般的数据库提供了分析工具和索引工具,这些工具生产一些统计信息以优化索引。

MySQL提供了一些简单的工具,有助于确定表和查询是否是最优的。它们都是SQL命令,包括EXPLAIN、ANALYZE TABLE和OPTIMIZE TABLE。

使用EXPLAIN

EXPLAIN命令给出如何执行SELECT语句的信息(仅对SELECT有效)。与其它数据库的DESCRIBE相似。

[EXPLAIN | DESCRIBE] [EXTENDED] SELECT select options
-- 查看表的列或分区信息
[EXPLAIN | DESCRIBE] [PARTITIONS SELECT * FROM] table_name

EXPLAIN table_name 同义命令 SHOW COLUMNS FROM table_name、DESC table_name

用法1:查看MySQL优化器如何执行SELECT语句。

其结果是优化器预计执行该语句需要的JOIN操作列表。

这个命令的最佳用处是确定表是否有索引,从而更精确的定位候选行。

  • id
    • 执行序列号
  • select_type
    • 查询语句类型
  • table
    • 在这一步操作的表
  • type
    • 使用的JOIN类型
  • possible_keys
    • 如果有包含主键的索引,可用字段的列表
  • key
    • 被优化器选中的主键
  • key_len
    • 主键或部分主键的长度
  • ref
    • 约束或需要对比的字段
  • rows
    • 估计要处理的行数
  • extra
    • 优化器的额外信息
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: film
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 892
        Extra: Using where

如果type为ALL,是做全表扫描,应该尽量避免,方法是添加索引或重写查询。
如果type为INDEX,则执行全索引扫描,这是非常低效的。

如果不使用枚举值定义一个查找表的话,就必须执行JOIN来选择指定值的结果。枚举值能代替小型查找表,可以提升性能。
因为枚举值的温暖本仅保存一次,在表头结构中,行中保存的是数字,形成一个枚举值的索引(数组索引)。枚举值表能节省空间。

-- EXPLAIN后使用这个语句查看优化器的重写语句
SHOW WARNINGS \G

会话变量last_query_cost存储最近一次查询的成本。

使用ANALYZE TABLE

重新计算一个或多个表的主键分布。这个信息确定JOIN操作中表的顺序。

ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE table_list

每当表上有重大变更(如批量增加数据)时,都应该执行此命令,这个命令会在表上加锁。

只能为MyISAM和InnoDB表更新主键分布。

-- 查看索引
SHOW INDEX FROM table_name;

使用OPTIMIZE TABLE

重建一个或多个表的数据结构。对于长度可变的字段(行)尤其有用。

OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE table_list

LOCAL或NO_WRITE_TO_BINLOG防止被写进二进制日志。

每当表上有重大变更(如批量增加数据)时,都应该执行此命令,这个命令会在表上加锁,且可能运行很长时间。

数据库优化的最佳实践

谨慎而有效的使用索引

索引可以提高性能。

索引会为表的删除和插入增加开销。还会降低复制和恢复操作的性能。

没有被使用、使用有效或分布很广的索引应该删除。

使用规范化,但不要过度使用

适时违背范式。

冗余计算结果防止重复计算而提高性能。

使用枚举而不是创建表从而避免使用JOIN。

使用正确的存储引擎完成任务

SHOW ENGINES;
CREATE TABLE t1 (a int) ENGINE=InnoDB;
ALTER TABLE t1 ENGINE=MEMORY;
  • InnoDB
    • 默认引擎
    • 与NDB同是MySQL是仅有的事务引擎
    • 适用于高性能和事务环境
  • MyISAM
    • 适用只读为主的环境
    • 高级缓存和索引机制提高数据检索和索引速度
  • Blackhole
    • 通常用于中继代理,不处理数据而需要二进制日志
  • CSV
    • 存储引擎读写csv
    • 不支持索引,日期和时间也存在一些问题
  • Memory
    • 也称作HEAP,是内存中的存储器,比MyISAM快一个数量级
    • 使用哈希机制检索频繁使用的数据,使检索更快;使用表级锁,并发写入性能低
    • 用于频繁访问而很少更改的静态数据情况,保存周期性结果,中间数据等。
    • 重启后结构还在,数据丢失
  • Federated
    • 创建参照多个数据库系统的单个表,将多个数据库服务器的表连接起来
    • 适合分布式或数据集环境
    • 不移动数据,也不要求使用相同的存储引擎
  • Archive
    • 只支持INSERT和SELECT,支持行级锁和专用的缓冲区,可以实现高并发插入,是一个针对插入和压缩优化了的简单引擎
    • 以压缩格式存储大量数据I/O比MyISAM少
    • 适合存储和检索很少访问的文档或历史数据
    • 5.1之前不支持索引,只能通过表扫描,因此常规数据库要避免使用
  • Merge
    • MRG_MYISAM,将一组MyISAM表分装成单个表
    • 这些表按单个表的位置分区,但是没有使用额外的划分机制,必须放在同一个服务器上
    • 搜素和执行的速度更快,因为单个表管理的数据更少了,修复速度也更快
    • 缺点
      • 必须都使用MyISAM表
      • 替换操作不可用
      • 索引比单表的索引效率低
    • 适合在非常大的数据库(VLDB)如数据仓库使用;还用于解决数据划分问题
    • 引入分区功能后,该引擎已经被废弃

通过查询缓存使用视图来更快获得记过

封装复杂查询,简化数据处理工作的一个很方便的方法就是视图。

使用视图可以水平的(更少的列)或垂直的(WHERE子句)限制数据。这也减少了带宽,避免了滥用SELECT *查询。

消除低性能的JOIN操作。

查询缓存存储频繁访问的查询结果。

使用约束

  • 唯一索引
  • 主键
  • 外键
  • 枚举值
  • 集合(sets)
    • 与枚举相似,限定值
  • 默认值
    • 避免数据结构差或判断并返回默认值。减少服务器发送量
  • NOT NULL选项
    • 保证数据完整性
    • NULL值会导致查询慢

使用EXPLAIN、ANALYZE、OPTIMIZE

合理使用,而不是有规律的定期使用。

提高性能的最佳实践

一切都很慢
  • 检查硬件问题
  • 改善硬件环境
  • 考虑将数据迁移到独立的磁盘上
  • 检查操作系统配置
  • 考虑将有些应用程序建议到其它服务器上
  • 考虑横向扩展的复制
  • 优化服务器性能
查询慢
  • 规范化数据库模式
  • 使用EXPLAIN识别丢失或不正确的索引
  • 使用benchmark()函数测试部分查询
  • 考虑重写查询
  • 对标准查询使用视图
  • 使用查询缓存进行测试
应用慢
  • 开启查询缓存
  • 关闭查询缓存。使用DEMAND模式和SELECT SQL_CACHE,按需使用查询缓存
  • 考虑并优化存储引擎
  • 确定是否使服务器或操作系统的问题
  • 定义应用程序基准测试,并测试比较
  • 检查内部查询语句,优化它们的性能
  • 使用分区来分散数据
  • 检查分区的索引
复制慢
  • 确保网络峰值性能最佳
  • 确保服务器配置正确
  • 优化数据库
  • 限制master的更新
  • 将读操作分散到多个slave中
  • 检查slave的复制延迟
  • 定期维护日志(二进制日志和中继日志)
  • 如果带宽有限,使用压缩
  • 使用包容性和排它性的日志选项,最小化复制内容

监控存储引擎

InnoDB

提供了,高可用和高性能的事务型操作,完全支持ACID事务。

索引、缓冲池、日志文件和表空间可以被监控和改进性能。

InnoDB表使用的索引是聚集索引,即使未指定索引,也会为每行分配一个内部值,从而使用聚集索引。
聚集索引是一种数据结构,它不仅存储索引,还存储数据本身。一旦确定到索引中的某个值,就可以直接检索数据而无需额外的磁盘寻道。
主键也唯一索引都采用聚集索引创建。

如果创建了二级索引,聚集索引的关键字(主键、唯一键或者行ID)也会存在二级索引中。 这样可以快速按关键字查找和快速获取聚集索引的原始数据。 着这意味着使用主键列扫描二级索引,则只需要二级索引就可以获取数据。

缓冲池是用于管理事务和读写磁盘数据的缓存机制,如果配置得当,可以减少磁盘访问。 缓冲池同时还是崩溃恢复的一个重要组成部分,因为缓冲池内的信息将会被定期写入磁盘。默认保存在ib_buffer_poll文件中 InnoDB使用缓冲池来存储数据变更和事务。将数据变更保存到缓冲池中的数据页中。每次引用的时候,该页都会放到缓冲池中。 如果发生改变,就标记为“脏页”。然后这个变更被写入磁盘以更新数据,并想重做日志写入一个副本,这些日志文件为ib_logfile0或ib_logfile1。

InnoDB存储引擎使用两种基于磁盘的机制存储数据,即日志文件和表空间,在关机或死机之前,InnoDB还会使用这些日志来重建或重做数据修复。 在程序启动时,InnoDB引擎读取日志并自动将脏页写入磁盘,从而在系统崩溃前恢复缓冲中的更新。

表空间时InnoDB用来组织与机器无关的文件的工具,包括数据、索引及混滚机制。 默认情况下所有表共享一个表空间(称为共享表空间)。共享表空间不会自动生成多个文件。 默认情况下,一个表空间只占据单个文件,该文件随数据增加而增长。指定autoextend可以允许表空间自动创建新文件。
还可以将表存储在自己的表空间中(独立表空间)。独立表空间包含数据和表的索引。 虽然仍有一个InnoDB文件,但独立表空间能够将数据隔离在不同的文件中,这些表空间可以自动扩展成多个文件,使得表可以存储更多数据,超出了操作系统可以处理的数据量。 还可以将表空间划分为多个文件,然后存储在不同的磁盘上。

使用SHOW ENGINE命令

SHOW ENGINE INNODB STATUS命令(又称InnoDB监视器)显示有关InnoDB存储引擎的状态的统计和配置信息。

SHOW ENGINE INNODB STATUS \G

SHOW ENGINE INNODB MUTEX显示了InnoDB的互斥体信息,对存储引擎中的线程调优很有帮助。

SHOW ENGINE INNODB MUTEX;

Status列显示了互斥体在操作系统上的等待次数。

使用InnoDB监视器

InnoDB时唯一支持直接监控的本地存储引擎。背后有一个称为监视器的特殊机制,它为父亲和客户端工具手机和报告统计信息。

监控内容:

  • 表和记录锁
  • 锁等待
  • 信号量等待
  • 文件I/O请求
  • 缓冲池
  • 清除和插入缓冲合并活动

使用上面的SHOW ENGINE INNODB STATUS命令可以直接连接监视器,还可以通过创建特殊的表,直接从监视器获取信息。
一旦创建,每个表中的数据都会转储到标准错误。通过MySQL错误日志可以查看这些信息。

创建下面表来启动监视器,停止只需删除表,监视器每隔15s自动生成新数据。重启会删除表,需要重建。

innodb_lock_monitor
innodb_monitor
innodb_table_monitor
innodb_tablespace_monitor

每个监视器提供以下数据:

  • innodb_monitor
    • 标准监视器显示的信息与SQL命令相同。
  • innodb_lock_monitor
    • 与监视器与SQL命令相同,但是不包括锁信息,可以用来检测死锁
  • innodb_table_monitor
    • 表监视器生成内部数据字典的详细报告,用于诊断表问题或者了解索引细节。
  • innodb_tablespace_monitor
    • 显示共享表空间的扩展信息,包括文件段的列表,还验证表空格键分配的数据结构
监控日志文件

InnoDB日志文件在数据和操作系统之间缓冲数据,所以保证日志正常运行可以获得良好的性能。

SHOW STATUS LIKE 'InnoDB%log%';
  • Innodb_log_waits
    • 当日志文件太小时(没有足够空间存储所有数据),操作必须等待日志刷新的等待时间计数器。
    • 如果该值开始增加并长时间大于0,可能需要增加日志文件的大小
  • Innodb_log_write_requests
    • 写日志请求的数量
  • Innodb_log_writes
    • 数据被写入日志的次数
  • Innodb_os_log_fsyncs
    • 操作系统文件同步次数,即fsync()方法调用
  • Innodb_os_log_pending_fsyncs
    • 挂起文件同步请求的数量。
    • 如果该值开始增加并长时间大于0,可能需要检查磁盘访问问题
  • Innodb_os_log_pending_writes
    • 挂起日志写请求的次数
    • 如果该值开始增加并长时间大于0,可能需要检查磁盘访问问题
  • Innodb_os_log_written
    • 吸入日志的总字节数
  • Innodb_available_undo_logs
监控缓冲池

缓冲池时InnoDB缓存频繁访问数据的地方。缓冲池任何数据变更也会被缓存。缓冲池还存储当前事务的信息。因此它时关乎性能问题的关键机制。

SHOW ENGINE INNODB STATUS可以查看缓冲池的行为信息。

报告中的注意事项:

  • 空缓冲区
    • 空的、可用于缓冲数据的缓冲段个数
  • 已修改的页
    • 已经发生表换的页(脏页)数
  • 待处理读请求
    • 等待中的读请求的个数,该值应该保持在低水平
  • 待处理的写请求
    • 等待中的写请求的个数,该值应该保持在低水平
  • 命中率
    • 缓冲区成功命中的请求个数与总请求之间的比例,最好接近1:1

缓冲池的状态变量:

SHOW STATUS LIKE 'InnoDB%buf%';
  • InnoDB_buffer_pool_pages_data
    • 含有数据的页数,包括不变的页和更改过的页
  • InnoDB_buffer_pool_pages_dirty
    • 更改过的页的数目
  • InnoDB_buffer_pool_pages_flushed
    • 缓冲池页面被刷新的次数
  • InnoDB_buffer_pool_pages_free
    • 空闲页的数目
  • InnoDB_buffer_pool_pages_misc
    • InnoDB引擎执行管理性工作用到的页数

      X = InnoDB_buffer_pool_pages_total – InnoDB_buffer_pool_pages_free – InnoDB_buffer_pool_pages_data

  • InnoDB_buffer_pool_pages_total
    • 缓冲池中的总页数
  • InnoDB_buffer_pool_read_ahead_rnd
    • InnoDB扫描大数据块时发生随机预读的数量
  • InnoDB_buffer_pool_read_ahead_seq
    • 顺序扫描全表时发生顺序预读的数量
  • InnoDB_buffer_pool_read_requests
    • 逻辑读请求的次数
  • InnoDB_buffer_pool_reads
    • 直接从磁盘中逻辑读取(而不是从缓冲池)的次数
  • InnoDB_buffer_pool_wait_free
    • 如果缓冲池忙或者没有空闲页,等待页面刷新的次数
  • InnoDB_buffer_pool_write_requests
    • 写入InnoDB缓冲池的次数
监控表空间

InnoDB可以在运行缓慢时扩展表空间,那么基本自给自足。autoextend选项配置innodb_data_file_path变量,可以自动扩展。

--innodb_data_file_path=ibdata1:10M:autoextend
使INFORMATION_SCHEMA中的表

INFORMATION_SCHEMA数据库中包含大量的InnoDB表。从技术上说,这些表是视图,因为它们的数据是查询时生成的。

这些表用于监控压缩、事务、锁等

  • INNODB_CMP
    • 显示压缩表的详细信息和统计信息
  • INNODB_CMP_RESET
    • 与INNODB_CMP相同,但是从查询时会重置统计信息,从而可以定期跟踪统计信息
  • INNODB_CMPMEM
    • 显示在缓冲池中压缩使用情况的详细信息和统计信息
  • INNODB_CMPMEM_RESET
    • 与INNODB_CMPMEM相同,但是从查询时会重置统计信息,从而可以定期跟踪统计信息
  • INNODB_TRX
    • 显示所有事务的详细信息和统计信息
  • INNODB_LOCKS
    • 显示事务请求的所有锁的详细信息和统计信息,描述每个锁的状态、模式、类型信息
  • INNODB_LOCK_WAITS
    • 显示事务请求的所有被阻塞的锁的详细信息和统计信息,描述每个锁的状态、模式、类型和阻塞事务。

使用压缩表可以监控表的压缩信息,包括页大小、使用哪些页、压缩时间和解压时间等详细信息。
如果使用了压缩并希望压缩带来的开销不会影响数据库服务器的性能,那么这些信息是重要的监控对象。

使用事务和锁顶表来监控事务。可以保证事务型数据顺利运行。它可以精确的确定各个事务所在的状态,以及哪些事务被阻塞,哪些被锁定。

使PERFORMANCE_SCHEMA表
-- 查看活动线程列表
SELECT thread_id, name, type FROM threads WHERE NAME LIKE '%innodb%';

一些InnoDB特有的项存在于wlock_instances、mutex_instances、file_instances、file_summary_by_event_name、和file_summary_by_instances表中。

其它要考虑的参数
  • 某些情况下调节innodb_thread_concurrency选项可以提高性能
    • 默认0或者8(早期)
    • 比值设置为处理器个数加上独立磁盘的和
  • innodb_fast_shutdown选项能快速关闭InnoDB
    • 跳过了一些步骤
  • innodb_lock_wait_timeout可以控制InnoDB如何处理死锁
    • 默认50s
  • AUTOCOMMIT设置为0,保证整个装载只提交一次
  • 还可以关闭外键和唯一约束来改善批量装载
InnoDB故障排除的技巧

错误

去错误日志中寻找错误信息使用--log-error选项开启

死锁

--innodb_print_all_deadlocks选项将所有死锁信息写入日志。这样SHOW ENGINE INNODB STATUS看到的就不止一个死锁。

数据字典问题

存储崩溃或文件损坏:

  • 孤立临时表
    • ALTER TABLE失败,可能由于没有正确清理临时表。
    • 使用表监视器确定表名,然后用DROP TABLE命令删除这个孤表
  • 无法打开表
    • Can’t open file: ‘somename.innodb’
    • Cannot find table somedb/somename…
    • 数据库文件夹有个一somename.frm的孤立文件,删除它
  • 表空间不存在
    • 使用了–innodb_file_per_table选项,InnoDB data dictionary has tablespace id N, but tablespace with the id or name does not exist…
    • 删除表然后重建
      • 在另一个库中重建这个表
      • 找到frm文件并复制到原始库
      • 删除表
      • 这样报找不到ibd文件,这样重建表或从备份中恢复
  • 无法创建表
    • 可能由于没有frm文件,根据错误日志信息进行处理

观察控制台信息

有些错误信息只在标准输出中才有。

I/O问题

通常在启动或创建/删除新对象的时候出现。

检查错误日志或控制台的错误信息,检查操作系统相关的错误,它们会提示引发错误的原因。 还要检查数据目录下丢失或崩溃的文件夹和InnoDB文件。

操作系统对磁盘进行诊断防止硬件问题被认为是性能问题。

检查innodb_data_*配置

数据库崩溃

配置文件中innodb_force_recovery恢复选项启动服务器,其值设置为1到6的整型,可是InnoDB在启动时跳过某些操作。

只应该在某些极端情况使用。

  1. 发出SELECT语句时,跳过损坏的页。仅允许部分数据恢复
  2. 不要启动master或清除线程
  3. 不要在崩溃之后执行回滚
  4. 不要执行插入缓冲区操作。不要计算表的统计信息
  5. 启动时忽略掉撤销(undo)日志
  6. 运行恢复时不要执行重做(redo)日志

MyISAM

只需要调整key cache。

提高性能的方法分为三大类:优化粗盘存储;通过监控和优化key cache来有效地使用内存;优化数据库表。

  • 优化磁盘存储
  • 优化数据库表的性能
  • 使用MyISAM实用工具
  • 按照索引顺序存储表
  • 压缩表
  • 对数据表进行碎片整理
  • 监控key cache
  • 预加载key cache
  • 使用多个key cache
  • 其它需要考虑的参数
优化磁盘存储

将数据保存为myd文件和一个或多个myi文件,这些文件与frm文件一起存储在与数据库同名的目录下,由–datadir启动选项决定。

因此,MyISAM的磁盘空间优化与服务器上的磁盘空间优化方法相同。 可以将数据目录移动到自己的磁盘上可以提高性能,还可以使用RAID或其它高可用性存储选项来进一步提升性能。

修复表

优化表:ANALYZE TABLE、OPTIMIZE TABLE、REPAIR TABLE。

  • ANALYZE TABLE:检测表的关键字分布。参考InnoDB部分的介绍
  • REPAIR TABLE:为MyISAM、Archive和CVS存储引擎修复崩溃的表,用于恢复哪些崩溃的或运行很慢的表
  • OPTIMIZE TABLE:用于恢复被删除的块和重组表,从而提高性能。参考InnoDB部分的介绍
使用MyISAM实用工具
  • myisam_ftdump
    • 显示全文索引信息
  • myisamchk
    • 在MyISAM表上执行分析
  • myisamlog
    • 查看MyISAM表的更改日志
  • myisampack
    • 压缩表以减少存储量

注意事项:使用优化工具之前做好备份。

myisamchk时检控官的主力工具。

性能提升、恢复和状态报告的选项:

  • analyze
    • 分析索引的关键字分布以提升性能
  • backup
    • 更改表之前备份
  • check
    • 检查表的错误信息
  • extended-check
    • 彻底检查表包括索引的错误信息
  • force
    • 如果发现错误,执行修复
  • information
    • 显示表的统计信息
  • medium-check
    • 更加深入的检查和修复表,少于extended-check
  • recover
    • 全面修复表,执行除了唯一索引重复的所有修复操作
  • safe-recover
    • 传统形式的修复,有序的读取所有行,并更新所有索引
  • sort index
    • 从高到低排列索引树,这样能够减少索引结构的查找时间,加快索引的访问速度
  • sort records
    • 按指定的索引顺序对记录进行排序,这样可以提高某些基于索引的查询性能
按索引顺序存储表

可以提高大量的数据范围查询的检索效率。

有序的访问数据,而无需查找磁盘页。

# 按照第二个索引顺序排序
myisamchk -R 2 /usr/local/mysql/data/test/table1

使用ALTER TABLE和ORDER BY达到同样的结果。

由于新增操作使表不再有序,导致数据库性能下降,在经常变更的表上采用这个技术,可以确保表的存储顺序最佳。

压缩表

MyISAM只能压缩只读表。因为MyISAM不能压缩、重新排序,也不能对压缩数据执行添加或删除操作。

# 备份
myisampack -b /usr/local/mysql/data/test/table1
对数据表进行碎片整理

OPTIMIZE TABLE命令或者myisampack工具

监控key cache

key cache是一个高效的存储结构,用于存储频繁使用的索引数据。只有MyISAM才能使用key cache。 通过快速查找机制(通常使B-tree)存储关键字。索引内部的存储形式使连接列表,可以被快速检索到。

MyISAM数据表数据读取时自动创建key cache。每次查询前,都检查key cache。

-- 查询相关的变量
SHOW STATUS LIKE 'Key%';
SHOW VARIABLES LIKE 'key%';

调整key cache需要通过监控调优。

提高缓存命中率:

  • 预加载缓存
  • 使用多个key cache
  • 为默认key cache分配更多的内存
预加载key cache

预加载是提高查询速度的有效办法。

-- IGNORE LEAVES只加载非叶子节点
LOAD INDEX INTO CACHE table_name IGNORE LEAVES;
使用多个key cache

MyISAM允许创建多个key cache或自定义key cache,以减少对默认key cache的争用。
该特性允许将一个或多个表的索引加载到某个特殊的缓存中。这意味着按任务分配内存。 如果一段时间内对一组表执行大量的查询操作,而且频繁引用这些表上的索引,那么这个方法则能够大大提高性能。

首先使用SET命令分配内存,然后执行一个或多个CACHE INDEX命令加载一个或多个表的索引。 与默认的不同,可以将缓存大小设置为0将其刷新或删除。

SET GLOBAL emp_cache.key_buffer_size=128*1024;
CACHE INDEX salaries IN emp_cache;
SET GLOBAL emp_cache.key_buffer_size=0;
-- 其实是创建了一个新的全局用户变量
select @@global.emp_cache.key_buffer_size;

可以在配置文件中保存配置,init-file=path_to_file命令引入到主配置中。

其它要考虑的参数
  • myisam_data_pointer_size
    • 如果没有为MAX_ROWS指定值,CREATE TABLE使用默认指针大小一般取2-7。默认为6.
  • myisam_max_sort_file_size
    • 数据排序时使用的临时文件大小的最大值。增大值可以加速索引的修复和重组。
  • myisam_recover_options
    • MyISAM的恢复模式。也可用于OPTIMIZE TABLE。
    • 模式包括:默认(default)、备份(backup)、强制(force)、快速(quick),选项可以任意组合
    • 默认模式指不检查备份、强制或快速的情况下执行恢复
    • 备份模式是指在恢复前后弦创建备份
    • 强制模式指即使数据丢失,仍然进行数据恢复
    • 快速模式是指如果没有标记为删除的模块,就不检查表中的数据行
  • myisam_repair_threads
    • 如果大于1,则并行执行修复和排序操作,从而加快操作速度否者顺序执行
  • myisam_sort_buffer_size
    • 排序操作的缓存区大小。增加该值有助于排序索引,但是如果该值操作4GB,只是用64位机器
  • myisam_stats_method
    • 在统计操作中用于控制服务器如何统计所引致分布的NULL值,这会影响到优化器。
  • myisam_use_mmap
    • 为读写MyISAM表开启存储器映选项。如果同时存在很多小写入和返回大数据集的读查询,这个功能非常有用。

其它注意事项:

  • MyISAM数据损坏的概率比InnoDB高,因此需要较长的恢复时间。
  • 由于不支持事务会导致语句部分执行。
  • slave会由于查询导致滞后,索引包含事务的高可用方案中使用MyISAM可能会出现问题。

监控复制

入门

有两个方面影响复制拓扑性能。必须同时优化它们。

  • 确保有足够的带宽去处理复制数据
  • 确保被复制的数据库是被优化过的
    • master的任何低效率的操作都会导致salve的低效
    • 特别是索引和规范化
    • 查询语句页需要优化,防止拖垮slave性能

服务器设置

确保服务器性能是最优的。

确保服务器操作系统又有足够的内存,而且存储设备和存储引擎对数据库来说都是最优的。

master执行的操作,slave也要复制。 master使用多线程运行,slav使用单线程复制,所以负载基本相同时,slave处理和执行事件也可能花更多的时间。

故障转移时提升的slave应该与master拥有相同的性能。

包容性和排它性复制

  • 可以将复制配置成复制所有数据;
  • 也可hi只记录master上的某些数据或者忽略某些数据,从而限定哪些写入二进制日志,哪些被复制;
  • 或者还可以配置slave,使其对某些数据进行操作。

参考复制过滤部分

复制线程

  • master上有个Binlog Dump线程
  • slave上有个Slave IO线程和Slave SQL线程
SHOW PROCESSLIST;
  • Id
    • 连接Id
  • User
    • 运行语句的用户
  • Host
    • 语句来源主机
  • db
    • 指定的库,NULL则没有指定默认库
  • Command
    • 运行的命令类型
  • Time
    • 处于报告状态的时间,秒为单位
  • State
    • 描述当前动作或状态(如等待)
  • Info
    • 正在执行的语句信息。NULL表明没有语句正在执行,等待状态的线程也为NULL

监控master

SHOW MASTER STATUS \G
SHOW BINARY LOGS \G
SHOW BINLOG EVENTS \G
master的监控命令

SHOW MASTER STATUS \G:

  • File
    • binlog文件名称
  • Position
    • 二进制当前位置
  • Binlog_Do_DB
    • –binlog-do-db指定的所有库
  • Binlog_Ignore_DB
    • –binlog-ignore-db指定的所有库
  • Executed_Gtid_Set
    • master上的所有GTID,与gtid_executed的值一样

SHOW BINLOG EVENTS \G:

SHOW BINLOG EVENTS [IN <log>] [FROM <pos>] [LIMIT [<offset>,] <rows>]

这个命令会产生大量数据,用于将master上都是事件与slave中继日志中的事件进行对比。

master的状态变量
  • Com_change_master
    • CHANGE MASTER命令执行的次数
    • 如果该值变化的频繁或者高于服务器数乘以slave的计划启动次数的乘积高的多,说明连接不稳定
  • Com_show_master_status
    • 显示SHOW MASTER STATUS命令执行的次数
    • 如果该值很高,表明重连请求次数不正常

监控slave

SHOW SLAVE STATUS \G
SHOW BINARY LOGS \G
SHOW BINLOG EVENTS \G
SHOW RELAYLOG EVENTS \G
slave的监控命令

SHOW SLAVE STATUS \G:

包括slave的二进制日志、slave到master的连接和复制活动,当前binlog文件的文件名和偏移位置。

结果信息分组:master连接信息、slave性能、日志信息、过滤、日志性能和错误条件。

  • 第一行信息最重要,显示了当前I/O线程的状态
    • 正在连接到master
    • 等待master事件
    • 重新连接master等
  • master连接的信息包括当前master的主机名、连接的用户账号以及用于连接master的slave端口,最后时SSL信息
  • master二进制日志和中继日志信息,包括文件名和位置信息
    • Relay_Master_Log_File表明了中继日志最近事件所在的master日志文件名
  • 复制过滤器配置猎取了所有slave端的复制过滤器
  • slave和I/O、SQL线程的最近的错误号和文本。
  • slave配置信息,跳过计数器的设置和until条件
  • 底部时当前的错误信息,如果slave正常运行,这些值应该总是0

重要的性能字段:

  • Connect_Retry
    • 重连事件间隔,秒为单位
  • Exec_Master_Log_Pos
    • 显示master二进制日志中最后执行的事件位置
  • Relay_Log_Space
    • 所有中继日志文件的总大小,确定是否需要清除中继日志
  • Seconds_Behind_Master
    • 事件执行和事件写入master二进制日志之间的间隔事件
    • 过高表示复制滞后
  • Retrieved_Gtid_Set
    • slave收到的GTID事务列表
    • 如果列表不一致,slave读取事件可能比master滞后
  • Executed_Gtid_Set
    • slave执行的GTID列表
    • 如果与Retrieved_Gtid_Set不同步,说明slave没有执行全部事务,或者有些事务时由slave发出的
slave的状态变量

前四个变量应该与slave的维护频率相对应,如果不一致:拓扑结构上slave数量是否比预期的多或者某个slave重启次数过于频繁

  • Com_show_slave_hosts
    • SHOW SLAVE HOSTS命令执行次数
  • Com_show_slave_status
    • 命令执行次数
  • Com_slave_start
    • 命令执行次数
  • Com_slave_stop
    • 命令执行次数
  • Slave_heartbeat_period
    • master的心跳检测的间隔时间的当前配置信息
  • Slave_last_heartbeat
    • 最近收到的心跳事件。显示为一个时间戳。
  • Slave_open_temp_tables
    • slave的SQL线程使用的临时表数量
  • Slave_received_heartbeats
    • 从master得到恢复的心跳书
  • Slave_retried_transactions
    • slave启动后SQL线程重试事务次数
  • Slave_running
    • 已经连接到master上正常运行为ON,否则为OFF

其它要考虑的因素

网络

使用slave_compressed_protocol变量可以设置压缩信息。

SSL信息参考《通过Internet进行复制》部分

设置心跳间隔:

CHANGE MASTER命令中的master_heartbeat_period=<value>配置master的心跳

SHOW STATUS like 'slave_heartbeat period';
SHOW STATUS like 'slave_received_heartbeats';
监控和管理slave滞后

大规模数据更新、slave负担过重或其它严重的网络性能事件都会导致slave滞后于master。

  • SHOW SLAVE STATUS查看Seconds_Behind_Master表明slave滞后于master的秒数。
  • SHOW PROCESSLIST也可以表明slave的延迟时间。

这种情况一帮增加slave平衡负载

slave滞后的原因和预防错是

使用多线程slave可以缓解滞后问题。

滞后原因:

  • I/O线程读取日志中的事件被延迟,通常由于slave单线程而master多线程执行导致
  • 低效率的JOIN的长查询
  • 磁盘读取I/Odds限制
  • 锁竞争
  • InnoDB线程并发问题

缓解滞后:

  • 组织数据
    • 规范化和使用数据分片实现分布式,提高性能
  • 分而治之
    • 横向扩展
    • 过滤
  • 识别并重构长时间运行的查询
    • 重构查询、操作或应用发出较短的或更紧凑的事务
    • 注意与过滤共同使用引发的事务完整性问题
  • 负载均衡
    • 平衡各个slave之间的负载
  • 使用最新的硬件
    • 至少和master一样强大
  • 减少锁竞争
    • 重构查询避免使用锁
使用GTID
  • enforce_gtid_consistency
    • 服务器禁止执行任何不安全的事务
    • 包括事务内部使用CREATE TALBE … SELECT和CREATE TEMPORARY TABLE。
    • 默认是禁用的、只读的全局变量
  • gtid_executed
    • 会话范围则表示会话中写入缓存的一组事务
    • 全局范围则显示二进制日志中记录的全部事务
  • gtid_mode
    • 是否正在使用GTID
  • gtid_next
    • 确定GTID的创建方式
    • AUTOMATIC表示通过标准全局唯一机制创建GTID。
    • ANONYMOUS表示使用文件和位置生成GTID,因此不是唯一的
  • gtid_owned
    • 会话范围表示当前服务器拥有的所有GTID列表
    • 全局范围表示所有GTID列表及每个GTID的拥有者
  • gtid_purged
    • 显示已经从二进制日志中清除的事务

因为无法排除GTID,所以需要在master上做全备份,设置gtid_purged中的GTID列表,然后在slave上恢复。

备份和恢复

备份应对一下情况:

  • 数据保护
    • 当错误语句slave已经生效后的错误处理
  • 创建新服务器
# 备份数据库内容(结构加数据,没有数据库本身)
mysqldump -uusername -ppassword databasename>/bak.sql
# 恢复
mysql -uusername -ppassword databasename</bak.sql
# 登陆后恢复
mysql -uusername -ppassword
source /bak.sql

三范式

  • 列不可分
  • 要有主键,不能存在部分依赖,确保表中的每列都和主键相关(多对多关系拆分成三张表,防止非主键列部分依赖主键)
  • 非主键列必须直接依赖于主键,不能存在传递依赖,(避免查询路径过长而导致询问时间过长或者更新异常)

行转列

静态拼接行转列

SELECT DISTINCT
  PRODID             AS ID,
  sum(CASE WHEN color = 'r'
    THEN COUNTS END) AS red,
  sum(CASE WHEN color = 'b'
    THEN COUNTS END) AS blue,
  sum(CASE WHEN color = 'y'
    THEN COUNTS END) AS yellow
FROM
  PROD
GROUP BY PRODID;

优化

  • 表建立索引
  • 查询时最左索引原则(索引列条件靠近where)

以上概念总结于传智播客JavaWeb课程、《高性能MySQL》、《高可用MySQL》

Search

    Post Directory