一、服务器的优化
CPU:处理器通常被认为是衡量系统速度的指标,注意 MySQL 进程及其所需的处理器使用百分比
内存:内存表示您的 MySQL数据库存储服务器中的 RAM 总量。您可以调整内存缓存(稍后会详细介绍)以提高性能。
像其他瓶颈一样,如果您的服务器经常耗尽内存,您可以通过添加更多来升级。
硬盘:如果您使用的是传统硬盘驱动器 (HDD),则可以升级到固态驱动器 (SSD) 以提高性能,SSD硬盘读取速度相对更快
网络:监控网络流量以确保您有足够通道进行数据的传输
二、存储引擎与表结构
2.1 选择存储引擎
一般情况下,我们会选择MySQL默认的存储引擎存储引擎InnoDB,但是当对数据库性能要求精益求精的时候,存储引擎的选择也成为一个关键的影响因素。
建议根据不同的业务选择不同的存储引擎,例如:
查询操作、插入操作多的业务表,推荐使用MyISAM;
临时表使用Memory;
并发数量大、更新多的业务选择使用InnoDB;
不知道选啥直接默认。
2.2 优化字段
字段优化的最终原则是:使用可以正确存储数据的最小的数据类型。
2.2.1 整数类型
MySQL提供了6种整数类型,分别是
- tinyint
- smallint
- mediumint
- int
- integer
- bigint
不同的存储类型的最大存储范围不同,占用的存储的空间自然也不同。
例如,是否被删除的标识,建议选用tinyint,而不是bigint。
2.2.2 字符类型
你是不是直接把所有字符串的字段都设置为varchar格式了?甚至怕不够,还会直接设置成varchar(1024)的长度?
如果不确定字段的长度,肯定是要选择varchar,但是varchar需要额外的空间来记录该字段目前占用的长度;因此如果字段的长度是固定的,尽量选用char,这会给你节约不少的内存空间。
2.2.3 非空
非空字段尽量设置成NOT NULL,并提供默认值,或者使用特殊值代替NULL。
因为NULL类型的存储和优化都会存在性能不佳的问题,具体原因在这里就不展开了。
2.2.4 不要用外键、触发器和视图功能
这也是「阿里巴巴开发手册」中提到的原则。原因有三个:
降低了可读性,检查代码的同时还得查看数据库的代码;
把计算的工作交给程序,数据库只做好存储的工作,并把这件事情做好;
数据的完整性校验的工作应该由开发者完成,而不是依赖于外键,一旦用了外键,你会发现测试的时候随便删点垃圾数据都变得异常艰难。
2.2.5 图片、音频、视频存储
不要直接存储大文件,而是要存储大文件的访问地址。
2.2.6 大字段拆分和数据冗余
大字段拆分其实就是前面说过的垂直分表,把不常用的字段或者数据量较大的字段拆分出去,避免列数过多和数据量过大,尤其是习惯编写SELECT *的情况下,列数多和数据量大导致的问题会被严重放大!
字段冗余原则上不符合数据库设计范式,但是却非常有利于快速检索。比如,合同表中存储客户id的同时可以冗余存储客户姓名,这样查询时就不需要再根据客户id获取用户姓名了。因此针对业务逻辑适当做一定程度的冗余也是一种比较好的优化技巧
三、连接配置优化
1.1 服务端配置
mysql 连接数太多(SequelizeConnectionError: Too many connections)
这个就是最常见的问题了。因为我做的这个是前端监控系统,日志上报量比较大,所以经常会遇到连接数不够用的情况。 除了你要使用其他技术来缓解并发量,还需要适当的设置mysql数据库的最大连接数
我们可以从两个方面解决连接数不够的问题:
1. 增加可用连接数,修改环境变量max_connections
,默认情况下服务端的最大连接数为151
个
mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 151 |
+-----------------+-------+
1 row in set (0.01 sec)
2. 及时释放不活动的连接,系统默认的客户端超时时间是28800秒(8小时),我们可以把这个值调小一点
mysql> show variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout | 28800 |
+---------------+-------+
1 row in set (0.01 sec)
3. 如何设置mysql的最大连接数呢?
第一种方法:通过mysql命令设置(这种方法是临时性的,重启mysql以后就会失效)
mysql> set GLOBAL max_connections=1000;
Query OK, 0 rows affected (0.00 sec)
第二种方法:进入找到mysql的my.cnf文件(不同版本可能不一样),添加如下配置(注意:最大连接数不要设置过大,有可能会在启动的时候爆掉哦),然后执行命令service mysql restart
[mysqld]
max_connections=1000
1.2 客户端优化
客户端能做的就是尽量减少和服务端建立连接的次数,已经建立的连接能凑合用就凑合用,别每次执行个SQL语句都创建个新连接,服务端和客户端的资源都吃不消啊。
解决的方案就是使用连接池来复用连接。
常见的数据库连接池阿里的Druid、Hikari。
但是需要注意的是连接池并不是越大越好,比如Druid的默认最大连接池大小是8,Hikari默认最大连接池大小是10,盲目地加大连接池的大小,系统执行效率反而有可能降低。为什么?
对于每一个连接,服务端会创建一个单独的线程去处理,连接数越多,服务端创建的线程自然也就越多。而线程数超过CPU个数的情况下,CPU势必要通过分配时间片的方式进行线程的上下文切换,频繁的上下文切换会造成很大的性能开销。
Hikari官方给出了一个PostgreSQL数据库连接池大小的建议值公式,CPU核心数*2+1。假设服务器的CPU核心数是4,把连接池设置成9就可以了。这种公式在一定程度上对其他数据库也是适用的,大家面试的时候可以吹一吹。
四、架构优化
4.1 使用缓存
平时有些经常用,或者比较慢的查询,不经常变化的数据可以存储在缓存当中,直接从缓存系统中获取数据,这样就可以减轻数据库的压力并提升查询效率
4.2 读写分离(集群、主从复制)
在对数据库有少量写请求,但有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压力,您可以创建一个或多个只读实例,利用只读实例满足大量的数据库读取需求。
创建只读实例后,您可以开通只读地址,然后在应用程序中配置主实例地址和只读地址,可以实现写请求转发到主实例,读请求转发到只读地址,只读地址会根据权重将读请求自动转发给各个只读实例。
4.3 分库分表
当一张表的数据达到几千万时,你查询一次所花的时间会变多,如果有联合查询的话,我想有可能会死在那儿了。分表的目的就在于此,减小数据库的负担,缩短查询时间。这时候可以考虑分库分表。
分库分表中的节点的含义比较宽泛,要是把数据库作为节点,那就是分库;如果把单张表作为节点,那就是分表。这里包括:垂直分库、垂直分表、水平分库和水平分表
垂直分库:分库一般按照业务划分,比如订单库,用户库
垂直分表:垂直分表就是在单表的基础上垂直切一刀(或几刀),将一个表的多个字短拆成若干个小表,这种操作需要根据具体业务来进行判断,通常会把经常使用的字段(热字段)分成一个表,不经常使用或者不立即使用的字段(冷字段)分成一个表,提升查询速度
水平分库:一般是按照一定的规则把数据库进行水平拆分,比如:A公司的的数据放在一个库,B公司的放在一个库
水平分表:把单张表的数据按照一定的规则(行话叫分片规则)保存到多个数据表上,横着给数据表来一刀(或几刀),就是水平分表了。比如订单数据,2021年的放在一张A表,2020年的放在B表
4.4 、消息队列削峰
通常情况下,用户的请求会直接访问数据库,如果同一时刻在线用户数量非常庞大,极有可能压垮数据库(参考明星出轨或公布恋情时微博的状态)。
这种情况下可以通过使用消息队列降低数据库的压力,不管同时有多少个用户请求,先存入消息队列,然后系统有条不紊地从消息队列中消费请求。
五、SQL分析与优化
5.1 慢查询
慢查询就是执行地很慢的查询(这句话说得跟废话似的。。。),只有知道MySQL中有哪些慢查询我们才能针对性地进行优化。
因为开启慢查询日志是有性能代价的,因此MySQL默认是关闭慢查询日志功能,使用以下命令查看当前慢查询状态
mysql> show variables like 'slow_query%';
+---------------------+--------------------------------------+
| Variable_name | Value |
+---------------------+--------------------------------------+
| slow_query_log | OFF |
| slow_query_log_file | /var/lib/mysql/9e74f9251f6c-slow.log |
+---------------------+--------------------------------------+
2 rows in set (0.00 sec)
slow_query_log
表示当前慢查询日志是否开启,slow_query_log_file
表示慢查询日志的保存位置。
除了上面两个变量,我们还需要确定“慢”的指标是什么,即执行超过多长时间才算是慢查询,默认是10S
,如果改成0
的话就是记录所有的SQL。
mysql> show variables like '%long_query%';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.00 sec)
5.1.1 打开慢日志
有两种打开慢日志的方式
- 修改配置文件
my.cnf
此种修改方式系统重启后依然有效
# 是否开启慢查询日志
slow_query_log=ON
#
long_query_time=2
slow_query_log_file=/var/lib/mysql/slow.log
2. 动态修改参数(重启后失效)
mysql> set @@global.slow_query_log=1;
Query OK, 0 rows affected (0.06 sec)
mysql> set @@global.long_query_time=2;
Query OK, 0 rows affected (0.00 sec)
5.1.2 慢日志分析
MySQL不仅为我们保存了慢日志文件,还为我们提供了慢日志查询的工具mysqldumpslow
,为了演示这个工具,我们先构造一条慢查询:
mysql> SELECT sleep(5);
然后我们查询用时最多的1条慢查询:
[root@iZ2zejfuakcnnq2pgqyzowZ ~]# mysqldumpslow -s t -t 1 -g 'select' /var/lib/mysql/9e74f9251f6c-slow.log
Reading mysql slow query log from /var/lib/mysql/9e74f9251f6c-slow.log
Count: 1 Time=10.00s (10s) Lock=0.00s (0s) Rows=1.0 (1), root[root]@localhost
SELECT sleep(N)
其中,
- Count:表示这个SQL执行的次数
- Time:表示执行的时间,括号中的是累积时间
- Locks:表示锁定的时间,括号中的是累积时间
- Rows:表示返回的记录数,括号中的是累积数
更多关于mysqldumpslow
的使用方式,可以查阅官方文档,或者执行mysqldumpslow --help
寻求帮助。
5.2 查看运行中的线程
我们可以运行show full processlist
查看MySQL中运行的所有线程,查看其状态和运行时间,找到不顺眼的,直接kill。
其中,
- Id:线程的唯一标志,可以使用Id杀死指定线程
- User:启动这个线程的用户,普通账户只能查看自己的线程
- Host:哪个ip和端口发起的连接
- db:线程操作的数据库
- Command:线程的命令
- Time:操作持续时间,单位秒
- State:线程的状态
- Info:SQL语句的前100个字符
5.3 查看服务器运行状态
使用SHOW STATUS
查看MySQL服务器的运行状态,有session
和global
两种作用域,一般使用like+通配符
进行过滤。
-- 查看select的次数
mysql> SHOW GLOBAL STATUS LIKE 'com_select';
+---------------+--------+
| Variable_name | Value |
+---------------+--------+
| Com_select | 168241 |
+---------------+--------+
1 row in set (0.05 sec)
5.4 查看存储引擎运行信息
SHOW ENGINE
用来展示存储引擎的当前运行信息,包括事务持有的表锁、行锁信息;事务的锁等待情况;线程信号量等待;文件IO请求;Buffer pool统计信息等等数据。
例如:
SHOW ENGINE INNODB STATUS;
上面这条语句可以展示innodb存储引擎的当前运行的各种信息,大家可以据此找到MySQL当前的问题,限于篇幅不在此意义说明其中信息的含义,大家只要知道MySQL提供了这样一个监控工具就行了,等到需要的时候再来用就好。
5.5 EXPLAIN执行计划
通过慢查询日志我们可以知道哪些SQL语句执行慢了,可是为什么慢?慢在哪里呢?
MySQL提供了一个执行计划的查询命令EXPLAIN
,通过此命令我们可以查看SQL执行的计划,所谓执行计划就是:优化器会不会优化我们自己书写的SQL语句(比如外连接改内连接查询,子查询优化为连接查询...)、优化器针对此条SQL的执行对哪些索引进行了成本估算,并最终决定采用哪个索引(或者最终选择不用索引,而是全表扫描)、优化器对单表执行的策略是什么,等等等等。
EXPLAIN在MySQL5.6.3之后也可以针对UPDATE、DELETE和INSERT语句进行分析,但是通常情况下我们还是用在SELECT查询上。
这篇文章主要是从宏观上多个角度介绍MySQL的优化策略,因此这里不详细说明EXPLAIN
的细节,之后单独成篇。
5.6 SQL与索引优化
3.6.1 SQL优化
SQL优化指的是SQL本身语法没有问题,但是有实现相同目的的更好的写法。比如:
- 使用小表驱动大表;用join改写子查询;or改成union
- 连接查询中,尽量减少驱动表的扇出(记录数),访问被驱动表的成本要尽量低,尽量在被驱动表的连接列上建立索引,降低访问成本;被驱动表的连接列最好是该表的主键或者是唯一二级索引列,这样被驱动表的成本会降到更低
- 大偏移量的limit,先过滤再排序
针对最后一条举个简单的例子,下面两条语句能实现同样的目的,但是第二条的执行效率比第一条执行效率要高得多(存储引擎使用的是InnoDB),大家感受一下:
-- 1. 大偏移量的查询
mysql> SELECT * FROM user_innodb LIMIT 9000000,10;
Empty set (8.18 sec)
-- 2.先过滤ID(因为ID使用的是索引),再limit
mysql> SELECT * FROM user_innodb WHERE id > 9000000 LIMIT 10;
Empty set (0.02 sec)
5.6.2 索引优化
为慢查询创建适当的索引是个非常常见并且非常有效的方法,可以参考文章《MySQL 索引与使用详解》
5.6.3 SELECT语句其他优化
1. 避免出现select *
首先,select * 操作在任何类型数据库中都不是一个好的SQL编写习惯。
使用select * 取出全部列,会让优化器无法完成索引覆盖扫描这类优化,会影响优化器对执行计划的选择,也会增加网络带宽消耗,更会带来额外的I/O,内存和CPU消耗。
建议提出业务实际需要的列数,将指定列名以取代select *.具体详情见《为什么大家都说SELECT * 效率低》
2. 避免出现不确定结果的函数
特定针对主从复制这类业务场景。由于原理上从库复制的是主库执行的语句,使用如now()、rand()、sysdate()、current_user()等不确定结果的函数很容易导致主库与从库相应的数据不一致。另外不确定值的函数,产生的SQL语句无法利用query cache。
3.多表关联查询时,小表在前,大表在后。
在MySQL中,执行 from 后的表关联查询是从左往右执行的(Oracle相反),第一张表会涉及到全表扫描,所以将小表放在前面,先扫小表,扫描快效率较高,在扫描后面的大表,或许只扫描大表的前100行就符合返回条件并return了。
例如:表1有50条数据,表2有30亿条数据;如果全表扫描表2,可以相像一下结果。
4. 使用表的别名
当在SQL语句中连接多个表时,请使用表的别名并把别名前缀于每个列名上。这样就可以减少解析的时间并减少哪些友列名歧义引起的语法错误。
5. 用where字句替换HAVING字句
避免使用HAVING字句,因为HAVING只会在检索出所有记录之后才对结果集进行过滤,而where则是在聚合前刷选记录,如果能通过where字句限制记录的数目,那就能减少这方面的开销。HAVING中的条件一般用于聚合函数的过滤,除此之外,应该将条件写在where字句中。
where和having的区别:where后面不能使用组函数
6.调整Where字句中的连接顺序
MySQL采用从左往右,自上而下的顺序解析where子句。根据这个原理,应将过滤数据多的条件往前放,最快速度缩小结果集。
六、业务优化
业务优化主要是对业务的结构和流程进行优化,优化好了能非常有效地减轻数据库访问压力,这里要大家仁者见仁,智者见智,根据实际情况调整。