毫不夸张地说,无论我们的后端工程师在哪家公司,呆在哪个团队,做哪个系统,第一个头疼的问题绝对是数据库的性能。如果我们有一套成熟的方法,我们可以快速准确地选择合适的优化方案,我相信我们可以快速准备解决我们每天遇到的80%-90%的性能问题。从解决问题的角度来看,我们必须首先了解问题的原因;其次,我们必须有一套思考和判断问题的过程方法,这样我们才能合理地从哪个层面选择解决方案;最后,从众多解决方案中选择合适的解决方案,找到合适解决方案的前提是我们对各种解决方案的优缺点和场景有足够的了解。没有一个解决方案可以通用,软件工程没有银弹。以下是我多年来使用的八个方案,结合我通常学习和收集的一些材料,以系统全面的方式整理成这篇博客文章。我也希望一些有需要的同龄人能在工作和成长中提供一些帮助。以下是我多年来使用的八个方案,结合我通常学习和收集的一些材料,以系统全面的方式整理成这篇博客文章。我也希望一些有需要的同龄人能在工作和成长中提供一些帮助。
前言毫不夸张地说,无论我们的后端工程师在哪家公司,呆在哪个团队,做哪个系统,我们遇到的第一个头疼的问题绝对是数据库的性能。如果我们有一套成熟的方法论,我相信我们可以快速准确地解决我们每天遇到的80%-90%的性能问题。
从解决问题的角度来看,我们必须首先了解问题的原因;其次,我们必须有一套思考和判断问题的过程,让我们合理地选择解决方案;最后,从许多解决方案中选择合适的解决方案,找到合适的解决方案的前提是我们对各种解决方案之间的优缺点和场景有足够的了解,没有一个解决方案是完全通用的,软件工程没有银弹。
以下是我工作多年以来使用的八个计划,结合我通常学习收集的一些信息,以系统、全面的方式整理成这篇博客文章,也希望让一些有需要的同行在工作和成长中提供一定的帮助。
为什么数据库会慢?
无论是关系数据库还是关系数据库 NoSQL,决定其查询性能的存储系统主要有三种:
- 时间复杂
- 数据总量
- 高负载
有两个主要因素决定了搜索时间的复杂性:
- 查找算法
- 存储数据结构
无论哪种存储方式,数据量越少,自然查询性能越高,随着数据量的增加,资源的消耗也会增加(CPU、磁盘读写繁忙)、时间也会越来越长。
从关系数据库的角度来看,索引结构基本固定 B+Tree,时间复杂度是 O(log n),存储结构是行式存储。因此,我们通常只能优化关系数据库的数据量。
高负荷的原因包括高并发请求、复杂查询等。 CPU、磁盘繁忙,服务器资源不足会导致查询缓慢等问题。这类问题通常通过集群和数据冗余来分担压力。
二、思考优化应该站在哪个层面?
从上图可以看出,自上而下有硬件、存储系统、存储结构和具体实现四层。
层与层紧密相连,每层的上层都是层的载体;因此,顶层越高,性能上限越高,优化成本相对较高,性价比越低。
以底层的具体实现为例,索引优化的成本应该是最小的。可以说,添加索引后,无论是否 CPU 消耗或响应时间立竿见影。
然而,一个简单的句子,无论如何优化和索引也是有限的,当没有任何优化空间的具体实现层[存储结构]思考,思考是否从物理表设计水平优化(如库表、压缩数据量等),如果是文档数据库必须考虑文档聚合的结果。
如果存储结构的优化没有效果,我们必须继续考虑上次,关系数据库是否不适合当前的业务场景?如果你想改变存储,那么如何改变呢? NoSQL?
因此,出于性价比的优先考虑,我们的优化理念是具体实现的。没有优化的空间。当然,如果公司有钱,直接使用钞票的能力,绕过前三层,这也是一种方便的应急处理方法。
本文不讨论顶部和底部两个层次的优化,主要从存储结构、存储系统中间两个层次的角度进行探讨。
三、八大方案总结
数据库优化方案有三个核心本质:减少数据量、改变空间性能以及选择合适的存储系统,这也与开头解释缓慢的三个原因相对应:数据总量、高负荷和搜索时间复杂性。
以下是收入类型的一般解释:短期收入、低处理成本、紧急响应,长期存在技术债务;长期收入与短期收入相反,短期处理成本高,但效果可长期使用,可扩展性更好。
静态数据意味着相对变化频率相对较低,不需要过多的联表,where 相反,动态数据更新频率高,通过动态条件筛选过滤。
减少数据量
有四种方法可以减少数据量类型:数据序列化存储、数据归档、中间表生成、分库分表。
正如上面所说,无论存储什么样的存储,数据量越少,自然查询性能就越高。随着数据量的增加,资源的消耗(CPU、磁盘读写繁忙)、时间也会越来越高。
目前市场上的 NoSQL 它基本上支持分片存储,因此其自然分布式写作能力可以从数据量中得到非常好的解决方案。
对于关系数据库,搜索算法和存储结构几乎没有优化的空间。因此,我们通常只考虑如何从减少数据量的角度进行选择和优化。因此,这种类型的优化方案主要用于处理关系数据库。
1)数据归档
注意事项:不要一次迁移太多,建议低频多次限量迁移。像 MySQL 因为删除数据后不会释放空间,所以可以执行命令 OPTIMIZE TABLE 释放存储空间,但锁表。如果存储空间仍然满足,则不能执行。
建议优先考虑该方案,主要通过数据库操作将非热点数据转移到历史表。如果需要查看历史数据,可以将新的业务入口路由添加到相应的历史表(库)。
2)中间表(结果表)
中间表(结果表)实际上是使用调度任务将复杂查询的结果运行并存储在额外的物理表中,因为该物理表存储在运行批次汇总的数据中,因此可以理解为根据原始业务进行高度数据压缩。
以报告为例,如果一个月有数十万的源数据,我们通过调度任务按月维度生成,那么原始数据就被压缩了几十万分之一。
下一个季度和年度报告可以根据月度报告进行*N 统计数据,即使三年、五年甚至十年的数据量也可以在接受范围内,并且可以准确地计算。那么,数据的压缩率是否越低越好呢?
以下是一段公式:
- 字段越多,粒度越细,灵活性越高,不同的业务联表可以用中间表处理。
- 字段越少,粒度越粗,灵活性越低,一般作为结果表查询。
3)数据序列化存储
对于一些不需要结构化存储的业务来说,在数据库中进行序列化存储是减少数据量的好方法,特别是对于一些M*N数据量的业务场景,如果以M为主表进行优化,数据量最多可以保持为M。此外,对于订单的地址信息,此类业务通常不需要根据内部字段进行检索,这也更合适。
我认为这个方案属于临时优化方案,无论是序列化后部分字段的查询能力丧失,还是该方案的优化能力都是有限的。
4)分库分表
作为一种非常经典的数据库优化优化方案,分库分表尤其是以前 NoSQL 还不是一个很成熟的时代,这个方案就像救命草一样存在。
如今,许多同行也会选择这种优化方法,但从我的角度来看,分库分表是一种高成本的优化方案。
这里我有几个建议:
- 真的没有办法分库分表,应该放在最后的选择上。
- 优先选择 NoSQL 代替,因为 NoSQL 基本上是为了扩展性和高性能。
- 分库还是分表?量大分表,高分库
- 不考虑扩容,一个到位。因为技术更新太快,经常 3-5 年一大变。
① 拆分方式
只要涉及到这个拆分,无论是微服务还是分库分表,拆分主要分为垂直拆分和水平拆分两种方式。
垂直拆分更多的是从业务角度进行拆分,主要是为了降低业务耦合度;此外 SQL Server 例如,一页是 8KB 存储,如果一个表中的字段越多,一行数据自然占用的空间就越大,那么一页数据存储的行数就越少,所以每次查询都需要 IO 性能越高,性能自然越慢。因此,减少字段也可以提高性能。之前听说有同行的一些表有 80 一个字段,数百万的数据开始慢下来。
水平拆分更多的是从技术角度进行的。拆分后,每个表的结构完全相同。简而言之,原始表的数据通过技术手段分成多个表存储,从根本上解决了数据量的问题。
② 路由方式
水平拆分后,根据分区键进行分区(sharding key)原来同一张表的数据拆解应该写在不同的物理表中,所以查询也应该根据分区键定位到相应的物理表中,以便查询数据。
路由通常有三个区间范围Hash、每种路由方式都有自己的优缺点,可根据相应的业务场景进行选择。
以时间为例,如果有一个业务,我们希望以月为单位拆分,那么表格就会被拆分 table_2022-04,这种文档类型,ElasticSearch 这类型的 NoSQL 也适用,无论是定位查询,还是以后的清洁维护都非常方便。
因此,缺点也很明显,数据会因业务独特性而不均匀,甚至不同范围之间的数据量也会有很大的差异。
Hash也是一种常用的路由方式,根据 Hash 算法模具以数据量均匀地存储在物理表中。缺点是它特别依赖于带分区键的查询。如果没有分区键,则无法定位到特定的物理表,导致所有相关表都被查询一次,并且在分库的情况下 Join、聚合计算、分页等 RDBMS 不能使用特性功能。
一般来说,只有一个分区键。如果有时业务场景必须使用不是分区键的字段进行查询,是否必须再次扫描?事实上,分片映射表可以用来记录额外字段与分区键之间的映射关系。
例如,有张订单表,原本是以 UserID 现在希望用作分区键的拆分 OrderID 查询时,必须有额外的物理表记录 OrderID 与 UserID 映射关系。因此,首先查询映射表,获得分区键,然后根据分区键的值路由查询相应的物理表。
有些朋友可能会问,这个映射表是多一个映射关系,还是多个映射关系在同一个表中。我优先考虑单独处理它。如果映射表的字段太多,它实际上与不进行水平分割时的状态一致,这是一个老问题。
用空间换性能
这两种方案都是用来处理高负荷场景的,有两种方案:分布式缓存,一主多从。
我认为用空间换资源比用空间换性能更合适。因此,这两种方案的本质主要是通过数据冗余和集群来分担负载压力。
对于关系数据库,因为他 ACID 特性使其自然不支持写作的分布式存储,但它仍然自然支持分布式阅读。
1)分布式缓存
缓存级别可分为几种类型:客户端缓存,API 为本地缓存和分布式缓存提供服务,这次我们只讨论分布式缓存。
一般来说,当我们选择分布式缓存系统时,我们会优先考虑 NoSQL 键值型数据库,例如, Memcached、Redis,如今 Redis 在分布式缓存中,数据结构多样性、高性能、易扩展性也逐渐占据主导地位。
缓存策略主要有很多种:Cache-Aside、Read/Wirte-Through、Write-Back,我们使用更多的方法,主要是Cachee-Aside。
具体流程见下图:
相信大家对分布式缓存都比较熟悉,但我在这里还是有几个注意事项要提醒大家:
① 避免滥用缓存
缓存应按需使用,从 28 从法律的角度来看,80% 主要原因是性能问题 20% 功能。滥用缓存的后果会增加维护成本,一些数据一致性问题也不容易定位。
特别是一些动态条件的查询或分页,key 组装多样化,量大,不易使用 keys 当然,我们可以使用额外的指令来处理它 key 记录数据 key 以集合的方式存储,删除时进行两次查询。首先检查 Key 集合,然后再次历历 Key 删除相应的内容。
这顿饭的操作无疑是很浪费时间的,谁弄谁知道。
② 避免缓存击穿
当缓存没有数据时,你必须跑到数据库查询,这就是缓存穿透。
如果临界点的数据在某个时间是空的,比如周排行榜,无论搜索多少次,穿透过去的数据库仍然是空的,查询的消耗 CPU 相对较高,由于缺乏缓存层对高并发性的响应,此时数据库资源消耗过高,即缓存突破。数据库资源消耗过高会导致其他查询加班等问题。
这个问题的解决方案也很简单,缓存了查询数据库的空结果,但给出了一个相对较快的过期时间。有些同行可能会问,这不会导致数据不一致吗?一般来说,有数据同步的方案,如分布式缓存和后续会说的一主多从CQRS,只要有数据同步这个词,就意味着数据一致性会有问题。因此,如果使用上述方案,相应的业务场景应允许容忍某些数据不一致。
③ 并非所有的慢查询都适用于所有的慢查询
一般来说,慢查询意味着吃资源(CPU、磁盘 I/O)。
例如,如果需要查询功能, 3 在几秒钟内,串行查询没有问题。我们继续假设这个功能每秒大约是每秒 QPS 为 100,然后在第一次查询结果返回之前,所有下一次查询都应穿透数据库。这意味着这几秒钟有时间 300 如果此时数据库需要数据库,请求 CPU 达到了 100%,然后所有下一个查询都会加班,即第一个查询结果无法缓存,从而形成缓存击穿。
2)一主多从
共享数据库压力的另一种常见做法是阅读和写作分离,一个主要主要主要从事。我们都知道关系数据库自然没有分布式分片存储,也就是说,它不支持分布式写作,但它自然支持分布式阅读。
大多数主要部署多个从库中只读取的例子,通过冗余主库的数据来分担读取请求的压力。路由算法可以通过代码或中间件来实现,可以根据团队的操作和维护能力以及代码组件的支持来选择。
在找到根治方案之前,大多数大师都是一个非常好的应急解决方案,特别是在当前的云服务时代,扩展从图书馆是一件非常方便的事情,一般只需要操作和维护或 DBA 没有开发人员的接入就可以解决问题。
当然,这个方案也有缺点,因为数据不能分割,所以主从的数据量完全冗余,这也会导致高硬件成本。从图书馆也有其上限,从图书馆到主图书馆的多线程同步数据的压力。
选择合适的存储系统
NoSQL 主要有以下五种类型:键值类型、文档类型、列类型、图形类型和搜索引擎。不同的存储系统直接决定了搜索算法和存储数据结构,并处理需要解决的不同业务场景。NoSQL 它的出现也解决了关系数据库之前面临的问题(性能、高并发性、扩展性等)。
例如,ElasticSearch 搜索算法是倒排索引,可用于取代低性能、高消耗的关系数据库 Like 搜索(全表扫描)。而且 Redis 的 Hash 结构决定了时间的复杂性 O(1)还有它的内存存储,结合分片集群存储模式,可以支持数十万 QPS。
因此,主要有两种类型的方案:CQRS、替换(选择)存储,这两种方案的最终本质基本相同,主要使用适当的存储来弥补关系数据库的缺点,但切换过渡的方式会有点不同。
1)CQRS
CQS(命令查询分离)是指在同一对象中作为查询或命令的方法,每种方法或返回状态,要么改变状态,但不能两者兼有。
讲解 CQRS 前得了解 CQS,一些朋友可能不太清楚。我在这里用一个流行的词来解释:在对象的数据访问方法中,要么只是查询,要么只是写入(更新)。
而 CQRS(命令查询职责分离)基于 CQS 在此基础上,用物理数据库写入(更新),用其他存储系统查询数据。
因此,当我们在某些业务场景中设计存储架构时,我们可以通过关系数据库 ACID 数据更新和写入的特征,使用 NoSQL 数据查询处理的高性能和可扩展性。
这样做的好处是关系数据库和 NoSQL 所有的优点都可以兼得,对于一些不适合一刀切替换存储的企业来说,也可以有一个平稳的过渡。
从代码实现的角度来看,不同的存储系统只调用相应的接口 API,因此 CQRS 难点主要在于如何同步数据。
2)数据同步模式
一般来说,讨论数据同步的方法主要是分推和拉:
- 推送是指数据变更端通过直接或间接的方式将数据变更记录发送到接收端,从而对数据进行一致性处理。这种积极的方法具有实时性高的优点。
- 拉动是指接收端的定期轮询数据库,以检查是否有需要同步的数据。从实现的角度来看,这种被动方法比推动简单,因为推动需要数据变更端支持推动变更日志。
推的方式有两种:CDC(变更数据捕获)和领域事件。对于一些旧项目,有些业务有很多数据入口,无法完全清晰地梳理。这个时候 CDC 只要从底层数据库层面获取变更记录,就是一个很好的方法。
对于已经服务化的项目来说,领域事件是一种舒适的方式,因为 CDC 数据库需要打开额外的功能或部署额外的中间件,而不需要领域事件。从代码可读性的角度来看,它将更高,并比较开发人员的维护思维模式。
3)替换(选择)存储系统
因为从本质上看,这个模型和 CQRS 核心本质是一样的,主要是对的 NoSQL 全面了解其优缺点,从而在相应的业务场景中选择和判断合适的存储系统。
在这里,我想给大家介绍一本马丁.福勒的《NoSQL精华》,这本书我已经重复了好几遍,也很好地全面介绍了各种各样的书。 NoSQL 优点和缺点以及使用场景。
当然,在更换存储时,我也有一个建议:添加一个中间版本。该版本应与业务开关进行数据同步。数据同步应确保完整和增加的处理,并且可以随时重新开始。业务开关主要是一个临时功能,用于后续版本的更新,主要避免后续版本更新不顺利或由于版本更新引起的数据不一致。
运行一段时间后,验证两个不同存储系统的数据是一致的,然后可以调用数据访问层的底部进行替换。这样,切换就可以顺利更新。
总结
本文在这里介绍了八个解决方案,这里再次提醒,每个解决方案都有自己的响应场景,我们只能根据业务场景选择相应的解决方案,没有全部食物,没有银弹。
在这八种方案中,大多数都有数据同步,只要有数据同步,无论是一个主从、分布式缓存,CQRS 所有这些方案都是由于数据一致性问题造成的,所以这些方案更适合一些只读的商业场景。
当然,有些场景可以通过用户点击过渡页面或广告页面关闭和切换页面来缓解数据不一致性。
通过这篇文章,相信大家对数据库设计优化有了全面的了解。
作者丨陈珙
作者:古道轻风,