MySQL 优化大合集

Posted 飞鱼的梦呓

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL 优化大合集相关的知识,希望对你有一定的参考价值。

阅读本文大约需要 18 分钟

大家好,一年一度的端午节它来了,为庆祝端午节的到来,特地分享一篇 mysql 笔记。话不多说,直接看大纲(看这大纲就知道本期内容不简单,逃...)。

字段设计规范

俗话说「细节决定成败」,字段的选型设计可见一斑,常见的字段设计规范大致有下面这些


优先选择符合业务的最小的字段类型

由于 MySQL 查找数据是以数据页为基本单位搜索的,因此选择比较小的字段类型,一页可以存放的索引节点就越多,这样相同数据量的情况下,遍历时的数据页就越少,涉及到的 I/O 次数也越少。

字符串类型字段

char 和 varchar

char 是定长,varchar 是变长,两者有各种的使用场景。

char 适合存储定长的字段,比如:MD5密文数据。

varchar 适合存储字段会发生变化的字段。

IP 类型字段

转成整型存储

金额字段

设计到金额相关的字段,设计为 decimal,这样涉及到计算时,不会有精度丢失。

Null

Null 在 MySQL 中会占用额外的存储空间,而且查询优化时也需要单独处理,因此需要避免将字段设为 Null。

图片/文件

索引设计规范

限制单表索引数量

索引虽然可以提供查询效率,但也是一把双刃剑,过多的索引会导致插入和更新的性能下降。因此应该只对经常查询的字段建立索引。

Innodb 尽量选择小字段作为主键

Innodb 是索引组织表,非主键索引上保存着主键 ID,因此使用小字段作为主键可以节省空间。

哪些字段需要建立索引

在 where, order by, group by, distinct 之后的字段以及 join on 后的字段。同时要注意,对于那些区分度不高的数据建索引并不是一个明智之举。

联合索引如何选择索引列的顺序
  • 区分度(列中不同值的行数/列的总行数)高的列放在前面;

  • 字段长度小的列放在前面;

  • 重复利用前缀索引,避免建立冗余索引(eg: index(a,b,c),index(a),index(b));

合理的使用覆盖索引

覆盖索引即索引里包含了所有要查询的字段。对于 Innodb 来说,可以避免二次回表查询。

对于字符串索引可以合理建立前缀索引来减少存储空间

对于索引前缀长度的选取,可以通过 select count(*)/count(distinct left(password,prefixLen)); 通过调整   prefixLen 的值,当结果趋近于 1 时,就说明可以唯一确定某一个记录了。

查询优化

  • 对查询中经常过滤并且区分度高的字段要建索引;

  • 避免 where 子句中出现 is null,!=,or,not in 。因为这会导致 MySQL 无法使用索引;

  • 避免使用 select * ,只返回客户端需要的字段;

  • 避免对 where 子句后的字段使用函数,这会破坏索引的有序性,导致无法使用索引;

  • 避免字段类型隐式转换和隐式字符编码转换,这两者本质上还是上面的对字段使用了函数;

  • 避免使用子查询,可以使用 join 替代子查询;

  • 在不会出现重复值的情况下使用 union all 替代 union,避免去重的过程;

  • group by 查询在末尾加上 order by null 可以避免排序过程;

  • 对于某些非常复杂的 SQL 语句,通过化整为零,拆分成多条 SQL,在应用层合并结果,这样就能利用上 MySQL 多线程特性;

  • 对于某些特殊的场景,需要考虑从业务功能上进行优化,利用深度分页问题;

从上面可以看出,如何查询优化大部分情况下都可以通过「优化索引使用」来解决。

分库分表原则

需要说明的是:分库分表类似武功秘籍里面的大招,不到万不得已不要轻易使用,避免"过度设计"和"过早优化"。因为它在解决一个问题的同时会带来其他的一些问题,文章后面会说到。那么什么时候才需要使用分库分表呢?

考虑分库分表一般是因为业务发展较快,导致数据量剧增,从而产生了大表。我们知道大表会影响查询性能,增加 DDL 变更时间,影响业务的可用性,同时导致从库延迟变大。

在进行了「SQL 语句优化、索引优化、业务优化、读写分离、升级硬件、升级网络」这些尝试后,还存在上面这些问题时,那就要考虑分库分表了。


垂直分表

使用场景:大表中的字段较多(超过 20 个),且存在某些字段访问频繁,某些字段不怎么访问的情况。因为 MySQL 是按行存储的,每次都会把一行中的所有字段都加载进内存,这时就可以通过垂直分表来减少 I/O。

来看一个具体栗子:用户表 users 中有两种类型的数据,一种是关键信息,用于登录系统,一种是详情信息,用于详情页展示。通过将详情信息拆分出去,可以减低表每行的平均大小,这样就能存储更多的数据了。具体的拆分策略如下图所示:

MySQL 优化大合集

通过垂直分表,将原先的 users 表分成了两种子表,原先的查询也需要进行调整。例如:

MySQL 优化大合集

从上面可以看出「垂直拆表」这种代码改造量太大,而且容易出错,因此这种垂直拆分在实际业务中用的不多。

由于查询的字段分散在不同的子表中,因此需要先根据「表名 + 字段名」去获取要查询的子表(通过增加一个路由表实现),再通过应用层改写 SQL,执行结果,最后拼接好结果返回给客户端。


水平分表

使用场景:系统绝对并发量没有上来,只是单表的数据量太多,影响了 SQL 效率,加重了 CPU 负担,以至于成为瓶颈,可以考虑水平分表。

同样来看个具体案例:案例的业务需求是,在小程序用户表 wx_users 中通过 open_id 来查询用户的基本信息用于数据回显。但是 wx_users 表的数据量已经达到了 500 w,日后大约会稳定在 2000 w 左右。这种场景就需要进行水平分表了,拆分策略如下图所示:

MySQL 优化大合集

上图可以看出:通过计算微信用户的 open_id 对应的 crc32值再和子表数取模,获取用户所在的表编号,拼接上子表前缀就可以确定子表名称了。需要注意的是,子表的个数需要根据业务量提前规划好,因为子表数一旦确定,后续修改的话就会非常的麻烦。

此外,这种拆分方式是和业务的查询方式紧密结合的,目前是根据用户的 open_id 来获取用户信息。如果后面需求改成了按手机号查询用户信息的话,这种拆分方式就不能很好的支持了,因为按照目前的架构的实现方式是:应用层遍历所有的子表,然后将结果汇总返回。可见这样的查询性能势必会很低下。


日志记录分表

对于日志类的表,可以采用按年月的方式进行拆分,这类日志型的数据特点是:数据量大,只增不改。按月拆分的好处是:便于管理历史数据,方便做冷热部署。


小结

垂直分表的拆分原则是将热点数据(可能经常会查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表,这样更多的热点数据就能被缓存下来,进而减少了随机读 IO。拆了之后,要想获取全部数据就需要关联两个表来取数据。

但记住千万别用 join,因为 join 不仅会增加 CPU 负担并且会将两个表耦合在一起(必须在一个数据库实例上)。关联数据应该在应用层进行,分别获取主表和扩展表的数据,然后用关联字段(通常是主键ID)关联得到全部数据。


水平分表在按分区字段查询时,有较大的优势,可是一旦使用非分区字段查询,就会适得其反。


按业务分库

举个栗子,交易系统 trade 数据库单独部署在一台 RDS 实例,现在交易需求及功能越来越多,订单,价格及库存相关的表增长很快,部分接口的耗时增加,同时有大量的慢查询告警,升级 RDS 配置效果不大,这时候就需要考虑拆分业务,将库存,价格相关的接口独立出来。



这样按照业务模块拆分之后,相应的 trade 数据库被拆分到了三个 RDS 实例中,数据库的写入能力提升,服务的接口响应时间也变短了,提高了系统的稳定性。


按表分库

举个栗子,交易数据库的订单表 orders 有 2 亿多数据,并且已经做了水平分表,分成了 20 个子表,但 RDS 实例遇到了写入瓶颈,普通的 insert 都需要50ms,时常也会收到 CPU 使用率告警,这时就要考虑分库了。根据业务量增长趋势,计划扩容一台同配置的 RDS 实例,将订单表的 20 个子表,平均分配給每个 RDS 实例。

这样解决了单个 RDS 性能瓶颈问题,查询的时候要先通过分区键 user_id 定位是哪个 RDS 实例,再定位到具体的子表,然后做 DML 操作,问题是代码改造的工作量大,而且服务调用链路变长了,对系统的稳定性有一定的影响。


拆分后的问题及解决方案

前面说到,分库分表会解决了 MySQL 性能问题的同时,同样引入了其他的一些问题,大致包括:事务,join 查询,聚合查询,全局主键,扩容。


事务一致性问题

分布式事务

当更新内容同时存在于不同库时,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用“XA协议”和“两阶段提交”处理。分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间,导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。

最终一致性

对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误立刻回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等。


关联查询 join 问题

切分之前,系统中很多列表和详情表的数据可以通过 join 来完成,但是切分之后,数据可能分布在不同的节点上,此时 join 带来的问题就比较麻烦了,考虑到性能,尽量避免使用 join 查询。解决的一些方法:

全局表

全局表,也可看做“数据字典表”,就是系统中所有模块都可能依赖的一些表,为了避免库join查询,可以将这类表在每个数据库中都保存一份。这些数据通常很少修改,所以不必担心一致性的问题。

字段冗余

一种典型的反范式设计,利用空间换时间,为了性能而避免 join 查询。例如,订单表在保存 userId 的时候,也将 userName 也冗余的保存一份,这样查询订单详情顺表就可以查到用户名 userName,就不用查询买家 user 表了。但这种方法适用场景也有限,比较适用依赖字段比较少的情况,而冗余字段的一致性也较难保证。


数据组装

在系统 service 业务层面,分两次查询,第一次查询的结果集找出关联的数据id,然后根据id发起器二次请求得到关联数据,最后将获得的结果进行字段组装。这是比较常用的方法。


ER 分片

关系型数据库中,如果已经确定了表之间的关联关系(如订单表和订单详情表),并且将那些存在关联关系的表记录存放在同一个分片上,那么就能较好地避免跨分片 join 的问题,可以在一个分片内进行 join。在1:1或1:n的情况下,通常按照主表的 ID 进行主键切分。


跨节点分页、排序、函数问题

跨节点多库进行查询时,会出现 limit 分页、order by 排序等问题。

分页需要按照指定字段进行排序,当排序字段就是分页字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂.需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。

对于获取前 N 页这类的场景,情况就变得复杂的多,因为各分片节点中的数据可能是随机的,为了排序的准确性,需要将所有节点的前 N 页数据都排序好做合并,最后再进行整体排序,这样的操作很耗费 CPU 和内存资源,所以页数越大,系统性能就会越差。在使用 Max、Min、Sum、Count 之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总再次计算。


全局主键避重问题

在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成ID无法保证全局唯一。因此需要单独设计全局主键,避免跨库主键重复问题。常用的策略有:

UUID

UUID 标准形式是 32 个 16 进制数字,分为 5 段,形式是8-4-4-4-12的36个字符。UUID是最简单的方案,本地生成,性能高,没有网络耗时,但是缺点明显,占用存储空间多,另外作为主键建立索引和基于索引进行查询都存在性能问题,尤其是 InnoDb 引擎下,UUID 的无序性会导致索引位置频繁变动,导致分页。

结合数据库维护主键 ID 表
Snowflake 分布式自增 ID 算法

Twitter 的 snowfalke 算法解决了分布式系统生成全局 ID 的需求,生成 64 位 Long 型数字,组成部分:

  • 第一位未使用

  • 接下来的41位是毫秒级时间,41 位的长度可以表示69年的时间

  • 5 位datacenterId,5 位 workerId。10位长度最多支持部署 1024 个节点

  • 最后 12 位是毫秒内计数,12 位的计数顺序号支持每个节点每毫秒产生 4096 个ID序列。


数据扩容

举个例子,目前交易数据库 trade 中的订单表 orders 已经做了水平分库(位于两个不同 RDS 实例上),这时发现两个 RDS 写入性能还是不够,需要再扩容一个RDS,同时将 orders 从原来的 20 个子表扩容到 40个(user_id % 40),这就需要迁移数据来实现数据重平衡,既要停机迁移数据,又要修改代码,整体会非常麻烦。


小结

根据局部性原理,在实际开发中,会遇到核心业务表增长很快,数据量很大,MySQL 写入性能瓶颈的问题,这时需要根据业务的特性考虑分库分表,主要有两种方案:代码改造(数据库中间件 mycat,sharding-sphere)和分布式数据库(实际业务中使用比较多的有 PingCAP TiDB,阿里云 DRDS),可以优先使用分布式数据库方案,虽然成本会有所增加,但对应用程序没有侵入性,同时也可以比较好的支撑业务增长和系统快速迭代。

常见配置项

IO 相关配置

innodb_buffer_pool_size

衡量总的IO处理能力上限,一般设置为机器物理内存的60%-70%。

innodb_io_capacity

每秒后台进程处理IO数据的上限,一般为IO QPS总能力的75%

innodb_log_files_in_group

innodb redo log日志组个数,生产环境中可适当增加。

innodb_log_file_size

redo log 日志循化写,生产必须大于 1G,如果太小,那么 innodb_buffer_pool_size 的数据有可能不能及时写入 redo log 造成 halt 等待。

innodb_max_dirty_pages_pct

当脏块达到innodb_buffer_pool_size的50%时,触发检查点,写磁盘。

innodb_file_per_table

一表一文件,可以避免共享表空间的IO竞争,MySQL 5.6 版本的默认值为 ON。

innodb_page_size

数据页的大小,默认为 16 k。

innodb_flush_neighbors

刷脏页时是否连带刷附近的脏页,SSD 设置为0,HDD 打开刷新相邻块,随机访问转换为顺序访问。

innodb_flush_log_at_trx_commit

innodb redo log 日志持续化配置项,有三种可选值:

  • 0,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中;

  • 1,表示每次事务提交时都将 redo log 直接持久化到磁盘,安全性最高,性能最差;

  • 2,设置为2时,表示每次事务提交时都只是把事务写到操作系统的 page cache中;

sync_binlog

  • sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;

  • sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;

  • sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。

在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成 0,比较常见的是将其设置为 100~1000 中的某个数值。


连接相关配置

back_log

默认是250,TCP/IP的连接数量,一个连接占用 256KB 内存,最大是 64MB,256 * 300 = 75MB内存。

max_connections

最大连接的客户端数,默认为 500,生产环境需要调大。

wait_timeout

指的是客户端连接 mysql 进行操作完毕后,空闲多少秒后会断开。

max_allowed_packet

限制接收数据包的大小,单条数据超过该值时插入或更新失败。


其他常用配置

sort_buffer_size

每个连接独享,用于优化不能通过 sql 或者索引优化的 group 和 order 等。

join_buffer_size

用于表间关联缓存的大小,每个连接独享。

log_bin

数据库操作二进制记录,数据库备份,复制所需。

binlog_format

数据复制模式。

binlog_cache_size

二进制日志缓存,可以提高log-bin记录效率。

max_binlog_size

二进制日志文件大小默认 500M。

expire_logs_days

保留二进制日志的天数。

总结

今天的内容比较多,也算对 MySQL 做了个整体的复习和归纳吧。好了,今天的分享就到这里了。积沙成塔,日拱一卒。我是飞鱼,咱们下次再会。




以上是关于MySQL 优化大合集的主要内容,如果未能解决你的问题,请参考以下文章

ELK性能优化实战总结:Java大厂74道高级面试合集

mysql死锁的例子,附高频面试题合集

音视频大合集,先从零开始万事开头难

最新JAVA面试合集:java教程pdf下载百度云

从零开始搭建Java开发环境第一篇:Java工程师必备软件大合集

图像融合论文及代码整理最全大合集