在对 SQL 进行分析之前,需要明确可能导致 SQL 执行性能下降的原因进行分析,执行性能下降可以体现在很多方面:
数据库的查询性能变慢,一般我们第一时间想到的就是增加索引或者优化索引。
以下情况我们可以考虑在表的字段上增加索引优化性能:
如果已经建立了索引查询性能还是慢,这个时候就需要考虑优化索引:
明明建了索引了,结果 explain 却发现索引并未命中。
需要注意以下几种情况:
核心重点就是,尽量利用一两个复杂的多字段联合索引,抗下你80%以上的查询,然后用一两个辅助索引抗下剩余20%的非典型查询,保证你99%以上的查询都能充分利用索引,就能保证你的查询速度和性能!
既然数据库的查询如此缓慢,我们可以考虑减轻它的压力,不必要的时候就不去查询数据库,而是去查询缓存。
读取频繁,数据能接受一定延迟或者更新不那么频繁,就可以把数据放到分布式或者本地缓存中提升读取性能,减少数据库的查询压力。
数据一致性
缓存数据与数据库数据的一致性是非常重要的。在更新数据库数据时,需要及时更新缓存,或者设置合适的过期时间,以确保下次访问时可以回源获取最新的数据,回源数据的过程也需要考虑并发安全等。
缓存击穿
当某个热点数据突然失效,且有大量并发请求同时访问该数据时,可能导致大量请求直接打到数据库上,引起数据库压力剧增。可以采用互斥锁、预先加载等方式来避免缓存击穿。
缓存雪崩
当多个缓存数据同时失效,导致大量请求直接打到数据库上,从而引起数据库压力剧增。可以通过设置不同的失效时间、使用分布式缓存、限流等方式来避免缓存雪崩。
缓存穿透
查询一个不存在的数据,导致每次请求都会直接访问后端系统,从而引起后端系统负载过高。为了避免缓存穿透,可以采用布隆过滤器、缓存空对象等方式。
缓存大小和淘汰策略
需要考虑缓存的大小限制以及缓存淘汰策略,以确保缓存系统不会占用过多内存或存储资源,同时能保证高命中率。
缓存访问压力
缓存系统本身也可能成为性能瓶颈,需要考虑合理配置缓存服务器的数量、性能以及负载均衡策略,以满足高并发访问需求。
热点数据处理
对于热点数据,需要考虑缓存预热、预加载等策略,以及合适的缓存失效策略,确保热点数据的及时更新。
当表的数据出现冷热现象的时候,可以对数据进行冷热隔离,把更新和查询都不频繁的数据放到历史表或者是离线数仓里,减少表中的数据量来提升效率,这个方案也叫数据归档。
当表的数据量巨大且都是无用数据的时候,可以考虑直接把数据删除。
在执行删除操作之前,务必仔细评估删除数据对业务的影响,确保不会误删重要数据。另外,删除操作可能导致数据库事务日志膨胀,需要在合适的时间段进行操作,以减少对其他业务的干扰。如果删除数据的表中存在约束关系,需要确保删除操作不会破坏约束关系。
DELETE FROM your_table WHERE condition ORDER BY id LIMIT 1000;
上述 SQL 语句将按照id升序删除满足条件的前 1000 条数据。如果需要按照其他字段排序,可以将 ORDER BY 后面的字段改成目标字段。
当数据量很大,搜索条件比较复杂比如模糊配置 like ‘%×××’ 等情况出现或者搜索条件灵活多变,SQL 查询比较慢的时候,此时就可以考虑使用 ES 来代替 MySQL 执行检索功能。
MySQL 架构天生不适合海量数据查询,它只适合海量数据存储,但无法应对海量数据下各种复杂条件的查询,有人说加索引不是可以避免全表扫描,提升查询速度吗,为啥说它不适合海量数据查询呢,有两个原因:
加索引确实可以提升查询速度,但在 MySQL 中加多个索引最终在执行 SQL 的时候它只会选择成本最低的那个索引,如果没有索引满足搜索条件,就会触发全表扫描,而且即便你使用了组合索引,也要符合最左前缀原则才能命中索引,但在海量数据多种查询条件下很有可能不符合最左前缀原则而导致索引失效,而且我们知道存储都是需要成本的,如果你针对每一种情况都加索引,以 innoDB 为例,每加一个索引,就会创建一颗 B+ 树,如果是海量数据,将会增加很大的存储成本,之前就有人反馈说他们公司的某个表实际内容的大小才 10G, 而索引大小却有 30G!这是多么巨大的成本!所以千万不要觉得索引建得越多越好。并且索引越多数据库的写入性能也会收到影响。
有些查询条件是 MySQL 加索引都解决不了的,比如我要查询商品中所有 title 带有「格力空调」的关键词,如果你用 MySQL 写,会写出如下代码
SELECT * FROM product WHERE title like '%格力空调%'
这样的话无法命中任何索引,会触发全表扫描,而且你不能指望所有人都能输对他想要的商品,是人就会犯错误,我们经常会犯类似把「格力空调」记成「格空调」的错误,那么 SQL 语句就会变成:
SELECT * FROM product WHERE title like '%格空调%'
这种情况下就算你触发了全表扫描也无法查询到任何商品,综上所述,MySQL 的查询确实能力有限。
与其说上面列的这些点是 MySQL 的不足,倒不如说 MySQL 本身就不是为海量数据查询而设计的,MySQL是关系型数据库,它在表现数据间的关系还是很可以的,而检索数据的能力就相对贫瘠,尤其是非精准的数据检索。术业有专攻,海量数据查询还得用专门的搜索引擎,这其中 ES 是其中当之无愧的王者,它是分布式的搜索分析引擎,可以提供针对 PB 数据的近实时查询,广泛用在全文检索、日志分析、监控分析等场景。
它主要有以下三个特点:
那么 ES 中的索引为何如此高效,能在海量数据下达到秒级的效果呢?它采用了多种优化手段,最主要的原因是它采用了一种叫做倒排索引的方式来生成索引,避免了全文档扫描,那么什么是倒排索引呢,通过文档来查找关键词等数据的我们称为正排索引,返之,通过关键词来查找文档的形式我们称之为倒排索引。

要在其中找到含有 comming 的文档,如果要正排索引,那么要把每个文档的内容拿出来查找是否有此单词,毫无疑问这样的话会导致全表扫描,那么用倒排索引会怎么查找呢,它首先会将每个文档内容进行分词,小写化等,然后建立每个分词与包含有此分词的文档之前的映射关系,如果有多个文档包含此分词,那么就会按重要程度即文档的权重将文档进行排序,于是我们可以得到如下关系:

这样的话我们我要查找所有带有 comming 的文档,就只需查一次,而且这种情况下查询多个单词性能也是很好的,只要查询多个条件对应的文档列表,再取交集即可,极大地提升了查询效率。
除了倒排索引外,ES 的分布式架构也天然适合海量数据查询,来看下 ES 的架构:

一个 ES 集群由多个 node 节点组成,每个 index 是以分片(Shard,index 子集)的数据存在于多个 node 节点上的,这样的话当一个查询请求进来,分别在各个 node 查询相应的结果并整合后即可,将查询压力分散到多个节点,避免了单个节点 CPU,磁盘,内存等处理能力的不足。
另外当新节点加入后,会自动迁移部分分片至新节点,实现负载均衡,这个功能是 ES 自动完成的,对比一个下 MySQL 的分库分表需要开发人员引入 Mycat 等中间件并指定分库分表规则等繁琐的流程是不是一个巨大的进步?这也就意味着 ES 有非常强大的水平扩展的能力,集群可轻松扩展致几百上千个节点,轻松支持 PB 级的数据查询。
当然 ES 的强大不止于此,它还采用了比如主备分片提升搜索吞率,使用节点故障探测,Raft 选主机制等提升了容灾能力等等,这些不是本文重点,读者可自行查阅,总之经过上面的简单总结大家只需要明白一点:ES 的分布式架构设计天生支持海量数据查询。
读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。
如果程序使用数据库较多时,而更新少,查询多的情况下会考虑使用,利用数据库主从同步 。可以减少数据库压力,提高性能。
因为数据库的“写”(写10000条数据到 MySQL 可能要3分钟)操作是比较耗时的。
但是数据库的“读”(从 MySQL 读10000条数据可能只要5秒钟)。
所以读写分离,解决的是,数据库的写入,影响了查询的效率。
一台主、多台从,主提供写操作,从提供读操作,读取压力大的时候可以考虑增加从库提升读取性能。

分表指的是在数据库数量不变的情况下对表进行拆分。
比如我们将 SPU 表从一张拆成四张。

分库指的是在表数量不变的情况下对数据库进行拆分。
比如我们一个库里面放了两张表,一张 SPU,一张 SKU,我们将两张表拆到不同的库里面去。

分库分表指的是数据库的数量还有表的数据都发生变更。
比如我们一个数据库里面有一张 SPU 表,我们把它拆成四张表,并且放到两个数据库里面。

表层面:
水平拆分指的是表结构不发生变更的情况下将一张表的数据拆分成多张表,因为当一张表的数据量越来越大的时候这张表的查询跟写入性能会越来越差,通过拆成多张表使每张表数据量变小,从而提供更好的读写性能。
垂直拆分指的是将一张表的字段拆分到多张表中,一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。
例如我们将 pic 字段单独拆分出来,剩下的三个字段还保留在原表里面,但是随着业务发展我们发现 pic 字段可能会越来越大,从而影响我们商品信息的查询,这个时候我们可以将 pic 字段单独拆分出去,一般都需要 id 关联到原来那张表上。
库层面:
库层面水平垂直拆分也是一样的,意义在于将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。
单台 MySQL 的硬件资源是有限的,随着我们业务发展,我们的请求量和数据量都会不断增加,数据库的压力会越来越大,到了某一时刻数据库的读写性能开始出现下降,数据库就会成为我们请求链路中的一个瓶颈,此时可能需要我们对数据库进行优化,在业务初期我们可能使用一些诸如增加优化索引、读写分离、增加从库的手段优化,随着数据量的增加,这些手段的效果可能变得越来越小,此时可能就需要分库分表来进行优化,对数据进行切分,将单库单表的数据控制在一个合理的范围内,以保证数据库提供一个高效的读写能力。
分库分表的优点
数据库出现瓶颈通常有两个维度,第一个是单表出现瓶颈,一般是单表的数据量大导致表的读写性能都慢;另外一种数据库整体都出现瓶颈,一般表现为 QPS 过高,导致磁盘、IO、网络、CPU、内存负载过高或者表很多数据量很大,存储空间都几近于耗尽。
只出现表维度瓶颈就只分表,只出现库维度瓶颈就只分库,表和库维度的瓶颈都出现就既分库又分表。
分库分表的方案设计需要考虑分表字段、分表算法、全局ID的选择,还会带来跨表查询、跨表事务的问题,而且分库分表之后基本上也就告别了分页查询和排序,当然这些问题也有办法解决,不过还是强烈建议不到万不得已不要上分库分表,可以先尝试基本优化,建好索引、减少多表join、适量的字段冗余其实大多数情况是能扛得住的,其次是可以想办法减少数据库的压力,比如上一层缓存,把能接受延迟或者更新不那么频繁的放到分布式或者本地缓存中,第三是冷热数据的隔离,把更新和查询都不频繁的数据放到历史表或者是离线数仓里,减少表中的数据量来提升效率,这个方案也叫数据归档;第四个是考虑上分布式数据库。
总之就是优化、缓存、归档先尝试一遍都搞不定且没有预算上分布式数据库的话再考虑分库分表。
总的来说就是数据库读写性能出现瓶颈,通过其他手段已经没办法很好解决了,这是最终的解决手段,优先使用其他手段。数据达到千万级别就分库分表是不准确的,需要结合情况分析,千万这个数字只是一个参考。分库分表应当以未来3-5年的业务情况评估,不能只以当前数据业务量评估,否则可能会出现频繁的分库分表,因为分库分表的代价是比较大的,所以最好是充分评估,能支撑未来3-5年的增长。