MySQL(八)MySQL性能优化

MySQL的优化最终目的时为了更快的查询数据,一个SQL的执行又由多个环节组成,每个环节都会消耗时间,为了提高整个SQL的执行效率,就需要从每一个环节入手。

SQL的执行流程:

配置优化--连接

执行SQL的第一步是客户端连接到服务端。

在连接的时候可能出现服务端连接数不够导致应用程序获取不到连接。比如报了一个 Mysql: error 1040: Too many connections 的错误。
可以从两个方面来解决连接数不够的问题:
1.从服务端来说,我们可以增加服务端的可用连接数。
(1)修改配置参数增加可用连接数,修改max_connections的大小
(2)及时释放不活动的连接。交互式和非交互式的客户端的默认超时时间都是28800秒,8小时,我们可以把这个值调小

show variables like  'max_connections '; -- 修改最大连接数
show global variables like 'wait_timeout'; --及时释放不活动的连接

2、从客户端来说,可以减少从服务端获取的连接数,可以引入连接池,实现连接的重用。
我们可以在ORM层面使用连接池(MyBatis自带了一个连接池)或者使用专用的连接池工具(DBCP、C3P0、阿里的Druid等)。
注意:连接池并不是越大越好,只要维护一定数量大小的连接池,其他的客户端排队等待获取连接就可以了。

每一个连接,服务端都需要创建一个线程去处理它。连接数越多,服务端创建的线程数就会越多。
如果线程数远远超过它的核数大小,那么就需要通过时间片进行频繁的上下文切换来执行这些线程,这会造成比较大的性能开销。

在Hikari的github文档中,给出了一个PostgreSQL数据库建议的设置连接池大小的公式:

https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
它的建议是机器核数乘以2加1。也就是说,4 核的机器,连接池维护9个连接就够了。

架构优化

缓存

我们可以引入缓存来缓解数据库的压力,从而达到提升数据库性能的效果,比方说Redis或者自己在应用中对热点数据做缓存都是可行的

主从复制

如果单台数据库服务满足不了访问需求,那我们可以做数据库的集群方案。

这就涉及到不同节点的数据一致性,需要使用MySQL的主从复制来实现
其原理就是依赖MySQL数据库的binlog,每次操作数据库的SQL都会记录到binlog,这是一种逻辑日志。
从服务器会获取主服务器的 binlog文件,然后解析里面的 SQL语句,在从服务器上面执行一遍,从而保持主从数据库的一致性

I/O Thread : 连接到master获取binlog,并且解析binlog写入中继日志
Log Dump Thread : 发送binlog给slave
SQL Thread : 读取relay log,把数据写入到数据库

有了主从复制,我们就可以只把数据写入master节点,而查询的请求可以分担到slave节点。这种方案就叫做读写分离

读写分离可以一定程度低减轻对数据库服务器的访问压力,但是需要特别注意主从复制的数据同步延迟问题

为什么会有主从复制数据同步延迟呢

在早期的MySQL中,slave的SQL线程是单线程,而Master是可以支持SQL语句的并行执行的。那么从数据库的SQL却只能单线程排队执行,在master并发量很大的情况下,同步数据肯定会出现延迟。

之所以SQL Thread不设计成多线程的,是因为binlog只有一个,而不是所有的SQL都可以随机执行的。比方说用户首先新增了书数据,然后又进行了删除,这两个SQL在从库的执行顺序肯定是不能颠倒的

怎么减少主从复制的延迟?

半同步复制

在主从复制的过程中,MySQL默认是异步复制的。也就是说,对于Master节点来说,只要写入binlog,事务结束,就直接返回数据结果给客户端了。master不关心slave的数据有没有写入成功。

如果要减少延迟,那是不是就可以等待全部从库的数据执行完毕,再返回给客户端,这样的方式叫做全同步复制。从库写完数据,主库才返会给客户端。
这种方式虽然可以保证在读之前,数据已经同步成功了,但是会导致SQL执行的时间变长,使得master节点性能下降。

既然这样,那么不妨考虑折中一下--- 一种介于异步复制和全同步复制之间的半同步复制

所谓的半同步复制就是在主库执行完客户端提交的SQL后不是立刻返回给客户端,而是等待至少一个从库接收到bin log并写到relay log中才返回给客户端。master不需要等待很长的时间,但是返回给客户端的时候,数据就即将写入成功了,因为它只剩最后一步了----读取relaylog,写入从库。

对于多个slave,必然有多个I/O Thread同时从master获取binlog。因为多个slave的复制是并行的,所以至少有一个slave写入到relay log时,代表其他slave可能也已经接收了bin log。这个slave返回给master后,客户端发起查询,其他slave很大概率也已经应用到数据库了。当然在极端情形下可能就只有那么一个slave节点即将写入,如果查询的时候负载到其他从库,那么可能还是会出现数据不一致的问题,但是半同步复制本身就是一种折中的方案,目的是为了减少主从复制的延迟,并不能保证消除主从复制的延迟

如果我们要在数据库里面用半同步复制,需要安装一个插件,这个是谷歌的一位工程师贡献的。目录如下:

需要在从库和主库分别执行如下命令:

-- 主库执行
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
set global rpl_semi_sync_master_enabled=1; 
show variables like '%semi_sync%';

-- 从库执行 
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
set global rpl_semi_sync_slave_enabled=1; 
show global variables like '%semi%';

相对于异步复制,半同步复制提高了数据的安全性,但它同时也不可避免的挥有一定程度的延迟,因为需要等待一个slave写入中继日志。所以,半同步复制最好在低延时的网络中使用

多库并行复制

如果3条语句是在三个数据库执行,操作各自的数据库,就不需要会产生并发的问题

所以如果是操作三个数据库,这三个数据库的从库的SQL线程可以并发执行。这是MySQL5.6版本里面支持的多库并行复制。

但是在大部分的情况下,我们都是单库多表的情况,这就需要在一个数据库里面实现并行复制。我们的主库本身就是支持多个事务同时操作的,那么在主库上可以并行执行的事务,在从库上肯定也是可以并行执行。

异步复制之 GTID 复制 

https://dev.mysql.com/doc/refman/5.7/en/replication-gtids.html

我们可以把那些在主库上并行执行的事务,分为一个组,并且给他们编号,这一个组的事务在从库上也可以并行执行。这个编号,我们把它叫做 GTID(Global Transaction Identifiers),这种主从复制的方式,叫做基于GTID的复制

如果我们要使用GTID复制,我们可以通过修改配置参数来打开它,默认是关闭的:

show global variables like 'gtid_mode';

分库分表

垂直分库,减少并发压力。水平分表,解决存储瓶颈。
垂直分库的做法,把一个数据库按照业务拆分成不同的数据库:比方说用户表和订单表分别存储在用户数据库和订单数据库里

水平分库分表的做法,就是把单张表的数据按照一定的规则分布到多个数据库。

通过主从或者分库分表都可以减少单个数据库节点的访问压力和存储压力,达到提升数据库性能的目的。

但是如果master节点挂了,所有的设计就都没用了,那就需要高可用方案

高可用

基于主从复制 :传统的HAProxy + keepalived的方案, 
MGR
https://dev.mysql.com/doc/refman/5.7/en/group-replication.html
https://dev.mysql.com/doc/refman/5.7/en/mysql-cluster.html
MySQL 5.7.17 版本推出的 InnoDB Cluster,也叫 MySQL Group Replicatioin(MGR)。

高可用HA方案需要解决的问题就是当一个master节点宕机的时候,如何选举一个数据最新的slave成为master。如果同时运行多个master,又必须要解决master之间的数据复制问题,以及对于客户端来说连接路由的问题。各个服务的高可用方案的本质都是差不多的(比放说MySQL,Redis等)

SQL 语句分析与优化 —— 优化器

慢查询日志 slow query log 

https://dev.mysql.com/doc/refman/5.7/en/slow-query-log.html

1.打开慢查询开关

因为开启慢查询日志是会消耗资源,所以它默认是关闭的:

#查看慢查询开关
show variables like 'slow_query%';

#控制执行超过多长时间的SQL才记录到慢日志,默认是10秒。
show variables like '%long_query_time%';

我们可以直接动态修改参数(重启后会失效)。
set@@global.slow_query_log=1; --1 开启,0 关闭
set@@global.long_query_time=3; -- mysql 默认的慢查询时间是 10 秒

或者修改配置文件my.cnf,定义了慢查询日志的开关、慢查询的时间、日志文件的存放路径。
slow_query_log=ON 
long_query_time=2 
slow_query_log_file=/var/lib/mysql/localhost-slow.log

2.慢查询日志分析

通过show variables like 'slow_query%';可以查看慢查询日志的位置

可以通过mysqldumpslow来分析慢日志,mysqldumpslow 由MySQL提供,在MySQL的bin目录下。

例如:查询用时最多的20条慢SQL:
mysqldumpslow -s t -t 20 -g 'select' /var/lib/mysql/2064471e0de5-slow.log

还可以通过SHOW PROFILE来查看SQL语句执行的时候使用的资源,比如CPU、IO的消耗情况。

https://dev.mysql.com/doc/refman/5.7/en/show-profile.html 

#查看是否开启  1--开启 0--未开启
select@@profiling; 
set@@profiling=1;

查看统计信息 show profiles;

查看最后一个SQL的执行详细信息 : show profile

根据ID查看执行详细信息,在后面带上for query + ID eg. show profile for query 1 ;

#其他系统命令
#查看用户运行线程
show processlist;

#查看服务器运行状态
show status ;

#查看存储引擎运行信息
#show engine用来显示存储引擎的当前运行信息,包括事务持有的表锁、行锁信息;事务的锁等待情况;
#线程信号量等待;文件IO请求;buffer pool统计信息等
show engine ;

eg.查看innodb的运行信息
show engine innodb status;

#我们还可以将监控信息输出到错误信息errorlog中(15秒钟一次)
show variables like 'innodb_status_output%'; 
#开启输出
SET GLOBAL innodb_status_output=ON; 
SET GLOBAL innodb_status_output_locks=ON;

知道了慢SQL之后,就需要具体分析SQL为什么会慢了。通过EXPLAIN 我们可以模拟优化器执行SQL 语句的过程,来知道 MySQL 是怎么处理一条SQL语句的。MySQL5.6.3以前只能分析SELECT; MySQL5.6.3以后就可以分析update、 delete、
insert 了

EXPLAIN 执行计划

创建SQL
CREATE TABLE `course` (
 `cid` int(3) DEFAULT NULL,
 `cname` varchar(20) DEFAULT NULL,
 `tid` int(3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `teacher` (
 `tid` int(3) DEFAULT NULL,
 `tname` varchar(20) DEFAULT NULL,
 `tcid` int(3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `teacher_contact` (
 `tcid` int(3) DEFAULT NULL,
 `phone` varchar(200) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `course` VALUES ('1', 'mysql', '1');
INSERT INTO `course` VALUES ('2', 'jvm', '1');
INSERT INTO `course` VALUES ('3', 'mybatis', '2');
INSERT INTO `course` VALUES ('4', 'spring', '3');

INSERT INTO `teacher` VALUES ('1', 'Bob', '1');
INSERT INTO `teacher` VALUES ('2', 'mon', '2');
INSERT INTO `teacher` VALUES ('3', 'chenpp', '3');

INSERT INTO `teacher_contact` VALUES ('1', '12221323232');
INSERT INTO `teacher_contact` VALUES ('2', '13392729343');
INSERT INTO `teacher_contact` VALUES ('3', '18923723923');

1.id 查询序列编号

id不同时,先执行id大的;id相同则从上往下执行

执行如下SQL:

-- 查询mysql课程的老师手机号
EXPLAIN SELECT tc.phone
    FROM teacher_contact tc
    WHERE tcid = (
    SELECT tcid
    FROM teacher t
    WHERE t.tid = (
    SELECT c.tid
    FROM course c
    WHERE c.cname = 'mysql'
    )
);


id不同时,先执行id大的

-- 查询课程 ID 为 2,或者联系表 ID 为 3 的老师 
EXPLAIN SELECT t.tname,c.cname,tc.phone FROM teacher t,course c,teacher_contact tc
WHERE t.tid=c.tid AND t.tcid=tc.tcid AND(c.cid=2 OR tc.tcid=3);

id相同则从上往下执行

数据量不同的时候顺序会发生变化 -- 这个是由笛卡尔积决定的

因为MySQL要把查询的结果,包括中间结果和最终结果都保存到内存,所以MySQL会优先选择中间结果数据量比较小的顺序进行查询(简单理解小表驱动大表)

select type 查询类型 

SIMPLE: 简单查询,不包含子查询,不包含关联查询union
EXPLAIN SELECT * FROM teacher

前面执行的子查询SQL:

PRIMARY

子查询SQL语句中的主查询,也就是最外面的那层查询。

SUBQUERY

子查询中所有的内层查询都是SUBQUERY类型的。

DERIVED

衍生查询,表示在得到最终查询结果之前会用到临时表

-- 查询ID为1或2的老师教授的课程
EXPLAIN SELECT cr.cname FROM ( SELECT * FROM course WHERE tid = 1 UNION SELECT * FROM course WHERE tid = 2 ) cr;

UNION

使用了UNION的SQL,如上

对于关联查询,先执行右边的 table(UNION),再执行左边的 table,类型是DERIVED

UNION RESULT:主要是显示哪些表之间存在UNION 查询。<union2,3>代表 id=2 和 id=3 的查询存在UNION

type 连接类型 

https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-join-types

在常用的链接类型中:system > const > eq_ref > ref > range > index > all
以上访问类型除了all,都能用到索引。

Const: 主键索引或者唯一索引,只能查到一条数据的SQL。

CREATE TABLE single_data( 
id int(3) PRIMARY KEY, 
content varchar(20) 
); 
insert into single_data values(1,'a');
insert into single_data values(2,'b');

EXPLAIN SELECT * FROM single_data  where id=1;

System : system是const的特例:只有一行满足条件。eg.只有一条数据的系统表。

EXPLAIN SELECT * FROM mysql.proxies_priv;

eq_ref : 通常出现在多表的 join 查询,表示对于前表的每一个结果,,都只能匹配到后表的一行结果。一般是唯一性索引的查询(UNIQUE或PRIMARY KEY)。

#为teacher_contact表的tcid(第一个字段)创建主键索引。
ALTER TABLE teacher_contact ADD PRIMARY KEY(tcid);
 
#为teacher表的tcid(第三个字段)创建普通索引。
ALTER TABLE teacher ADD INDEX idx_tcid (tcid);

explain select t.tcid from teacher t,teacher_contact tc where t.tcid = tc.tcid;

以上三种systemconsteq_ref,都是可遇而不可求的,基本上很难优化到这个状态。

Ref: 查询用到了非唯一性索引,或者关联操作只使用了索引的最左前缀。

#使用前面创建的teacher上的tcid字段的普通索引查询:
explain SELECT * FROM teacher where tcid=3;

Range: 索引范围扫描。

               如果where后面是 betweenand 或 <或 > 或 >= 或 <=或in这些, type类型就为range

#添加索引,否则会变成全表扫描
ALTER TABLE teacher ADD INDEX idx_tid(tid);

EXPLAIN SELECT * FROM teacher t WHERE t.tid <3;

IN查询想要走range要求字段上有主键索引

EXPLAIN SELECT * FROM teacher_contact t WHERE tcid in (1,2,3);

Index : Full Index Scan,查询全部索引中的数据(比不走索引要快)

EXPLAIN SELECT tid FROM teacher;

All : FullTableScan,如果没有索引或者没有用到索引,type就是ALL。代表全表扫描

NULL: 不用访问表或者索引就能得到结果

 EXPLAIN select 1 from dual where 1=1;

小结:
一般来说,需要保证查询至少达到range级别,最好能达到ref。ALL(全表扫描)和index(查询全部索引)都是需要优化的。

possible_key、key 

可能用到的索引和实际用到的索引。如果是NULL就代表没有用到索引。
possible_key可以有一个或者多个,表示可能用到的索引,不代表一定用到索引

反过来,possible_key为空,key可能有值吗? 可能

CREATE TABLE `user_innodb` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `phone` varchar(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `comidx_name_phone` (`name`,`phone`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;


insert into user_innodb values (1,'chenpp','18818212112');
insert into user_innodb values (2,'bob','152818217212');
insert into user_innodb values (3,'mon','13823723922');
explain select phone from user_innodb where phone='126';


这里是覆盖索引的情况。注意这里虽然使用到了索引,但是index的类型(查询全部索引中的数据),因为查询的字段就是索引字段,所以不需要进行全表扫描,但是这种情形也是需要优化的

key_len

索引的长度(使用的字节数)。跟索引字段的类型、长度有关。 

rows

MySQL认为扫描多少行才能返回请求的数据,是一个预估值。一般来说行数越少越好。 

filtered 

这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,它是一个百分比。这个比例越高越高,说明返回给server层的数据有效数据比例越大,SQL性能越好

ref

使用哪个列或者常数和索引一起从表中筛选数据。 

Extra

执行计划给出的额外的信息说明。
using index
表示用到了覆盖索引,不需要回表。
EXPLAIN SELECT tid FROM teacher;
using where
使用了where过滤,表示存储引擎返回的记录并不是所有的都满足查询条件,需要在server层进行过滤(跟是否使用索引没有关系),此时filtered也可能为100。

EXPLAIN select * from user_innodb where phone='13866667777';
EXPLAIN SELECT * FROM teacher_contact t WHERE tcid in (1,2,3);


Using index condition(索引条件下推)
这个在索引里就讲过了

MySQL(四)索引的使用--索引下推

using filesort

表示不能使用索引来排序,用到了额外的排序(跟磁盘或文件没有关系)。这是需要优化的。

EXPLAIN select * from user_innodb where name ='chen' order by id;

using temporary

使用到了临时表。以下不是全部的情况:
1、distinct 非索引列

EXPLAIN select DISTINCT(tname) from teacher;


2、group by 非索引列

EXPLAIN select tname from teacher group by tname;

3、使用join的时候,group任意列

EXPLAIN select t.tid from teacher t join course c on t.tid = c.tid group by t.tid;


这种情形下也是需要优化的,比如创建复合索引。

当我们的SQL语句比较复杂,有多个关联和子查询的时候,就要分析SQL语句有没有改写的方法。
举个例子,一模一样的数据:
-- 大偏移量的 limit
select * from user_innodb limit 900000,10;
​-- 改成先过滤 ID,再 limit
select * from user_innodb where id>=900000 limit 10;

存储引擎

选择存储引擎

为不同的业务表选择不同的存储引擎,例如:查询插入操作多的业务表用MyISAM。临时数据用Memeroy。常规的并发大更新多的表用InnoDB

分区或分表

分区不推荐,比较建议分表

字段定义
原则:使用可以正确存储数据的最小数据类型。为每一列选择合适的字段类型 

  • 整数类型 : INT有8种类型,不同的类型的最大存储范围是不一样的。像性别 就推荐使用TINYINT。
  • 字符类型:变长情况下,varchar更节省空间,但是对于varchar字段,需要一个字节来记录长度。所以固定长度的用char,不要                       用varchar
  • 非空: 非空字段尽量定义成NOT NULL,提供默认值,或者使用特殊值、空串代替null。因为NULL类型的存储、优化、使用都                会存在问题。
  • 不建议使用外键、触发器、视图 : 会降低了可读性;影响数据库性能,可以把计算的事情交给程序,数据库专注于存储;
  • 大文件存储: 不要用数据库存储图片(比如base64编码)或者大文件; 把文件放在文件服务器上,数据库只需要存储URI就行
  • 表拆分:  将不常用的字段拆分出去,避免列数过多和数据量过大。

总结

除了上面的对于代码、SQL语句、表定义、架构、配置优化之外,业务层面的优化也不能忽视。比方说双十一之前开启预售就助于分流。

在应用层面同样有很多其他的方案来优化,达到减轻数据库的压力的目的,比如限流,或者引入MQ削峰等等

发布了53 篇原创文章 · 获赞 16 · 访问量 6582

猜你喜欢

转载自blog.csdn.net/qq_35448165/article/details/104302587