当前位置: 首页 > 后端技术 > Java

压倒90%性能问题,数据库优化常用八招!

时间:2023-04-01 19:10:36 Java

大家好,我是陈步才~毫不夸张的说,我们的后端工程师,无论在哪个公司,在哪个团队,在哪个系统上工作,遇到的第一个头疼问题肯定是数据库性能问题。如果我们有一套成熟的方法论,可以让大家快速准确地选择合适的优化方案,相信我们可以快速准备解决我们每天遇到的80%甚至90%的性能问题。从解决问题的角度出发,首先要了解问题产生的原因;其次,我们要有一套思考和判断的过程方法,这样我们才能选择一个合理的层次来选择解决方案;最后,从众多解决方案中选择一个合适的解决方案找到合适的解决方案的前提是我们对各种解决方案的优缺点和场景有足够的了解。没有一种解决方案是完全通用的,软件工程中也没有灵丹妙药。以下是我工作多年以来使用过的八大解决方案,结合自己学习收集的一些资料,系统全面的整理了这篇博文。也希望一些有需要的同仁能够在工作中,提供一些成长上的帮助。关注公众号:码猿技术专栏,回复关键词:1111获取阿里巴巴内部Java性能调优手册为什么数据库慢?慢的本质:搜索慢的本质的时间复杂度搜索算法存储数据结构存储数据结构总数据数据拆分高负载CPU,繁忙的磁盘无论是关系数据库还是NoSQL,任何存储系统都由其主要决定查询性能分三种:搜索的时间复杂度数据总量高,负载由搜索的时间复杂度决定。主要有两个因素:搜索算法存储数据结构不管是哪种存储,数据越少,自然查询性能就越高。随着数据量的增加,资源消耗(CPU、磁盘读写忙)、耗时也会越来越高。从关系数据库的角度来看,索引结构基本固定为B+Tree,时间复杂度为O(logn),存储结构为行式存储。所以,对于关系型数据库,我们能优化的一般只是数据量。高负载是由于高并发请求、复杂查询等导致CPU和磁盘繁忙,服务器资源不足会导致查询慢等问题。对于这类问题,一般采用集群和数据冗余来分担压力。我们应该在什么层面上考虑优化?从上图可以看出,从上到下有四层,分别是硬件、存储系统、存储结构、具体实现。层之间关系密切,每一层的上层是本层的载体;因此,性能的上限可以随着上层的深入而确定,优化的成本相对会更高,性价比会更低。以底层具体实现为例,索引优化的代价应该是最小的。可以说,加入索引之后,无论是CPU消耗还是响应时间,都会立马降低;然而,一个简单的语句,无论如何优化和建立索引都是有局限性的。当这一层没有优化空间的时候,就得考虑上层【存储结构】,考虑是否从物理表设计层面优化(比如分库分表,压缩数据量),etc.),如果是文档型数据库,就得考虑文档聚合的结果;如果存储结构层的优化没有效果,还得继续上次的思考。关系型数据库不适合现在的业务场景吗?如果你想改变存储,你如何获得NoSQL?所以,我们的优化思路是,出于性价比,优先考虑具体实现,实在没有优化余地再考虑下一层。当然,如果公司有钱,可以直接使用纸币容量,绕过前三层。这也是一种方便的急救方法。本文不讨论顶层和底层的优化,主要从存储结构和存储系统中间两层的角度进行讨论。关注公众号:码猿科技专栏,回复关键字:1111获取阿里巴巴内部Java性能调优手册八大方案总结 数据库优化方案的三个核心精髓:降低数据量,用空间换性能,选择对了一个Storagesystem,也对应了开头解释的慢的三个原因:数据总量,高负载,*search的时间复杂度。*  这里对收益的种类做一个大概的解释:短期收益,处理成本低,可以紧急处理,时间长了会有技术债;长期收益与短期收益相反,短期内处理成本高,但效果可以长期使用,Scalability会更好。 静态数据是指相对变化频率比较低,不需要太多的表,过滤的地方也比较少。相反,动态数据具有较高的更新频率,并且被动态条件过滤。数据减量数据减量方案有四种:数据序列化存储、数据归档、生成中间表、分库分表。上面说了,不管是哪种存储,数据量越小,自然查询性能就越高。随着数据量的增加,资源消耗(CPU、磁盘读写忙)、耗时也会增加。高的。目前市面上的NoSQL基本都支持分片存储,所以其天然的分布式写入能力在数据量方面可以提供很好的解决方案。对于关系型数据库,搜索算法和存储结构的优化空间较小,所以我们一般都是从如何减少数据量的角度出发来选择和优化。因此,这类优化方案主要针对关系型数据库。数据归档注意事项:一次迁移不要太多,建议低频多次迁移。比如MySQL删除数据后不会释放空间。可以执行命令OPTIMIZETABLE释放存储空间,但会锁表。如果存储空间还足够,则可以不执行。推荐优先考虑该方案,主要是通过数据库操作将非热点数据迁移到历史表。如果需要查看历史数据,可以在对应的历史表(库)中添加新的业务入口路由。中间表(resulttable)中间表(resulttable)其实就是利用调度任务,把复杂查询的结果跑出来,存储在一个额外的物理表中,因为这个物理表存储的是运行批处理汇总的数据,所以可以据了解,数据已经按照原有业务进行了高度压缩。以报表为例,如果一个月有几十万条源数据,我们通过调度任务生成月维度,相当于将原始数据压缩十万分之一;以后的季报和年报可以根据月报*N进行统计。这样处理出来的数据,即使是三年、五年甚至十年,都可以在可接受的范围内,并且可以准确计算。那么数据的压缩比是不是越低越好呢?下面有个口头禅:字段越多,粒度越细,灵活性越高,可以使用中间表来处理不同的业务联表。字段越少,粒度越粗,灵活性越低,通常作为结果表查询。对于一些不需要结构化存储的业务,尤其是一些数据量为M*N的业务场景,数据库中数据的序列化存储是减少数据量的好方法。如果用M作为主表优化的话,那么数据量最多可以维持在M个数量级。另外,比如订单的地址信息,这种业务一般不需要检索基于里面的字段,比较合适。我认为这种方案是一种临时的优化方案,无论是序列化后丢失了一些字段的查询能力,还是这种方案的可优化性有限。关注公众号:码猿科技专栏,回复关键字:1111获取阿里内部Java性能调优手册年龄,这个计划就像救命稻草一样存在。现在很多同行也会选择这种优化方式,但是在我看来,分库分表是一种代价高昂的优化方案。这里我有几点建议:分库分表实在没办法,应该是最后的选择。相反,NoSQL是首选,因为NoSQL的诞生基本上是为了可扩展性和高性能。是分库还是分表?量大就分表,并发高就不扩容数据库,一部分到位。因为技术更新太快了,3-5年就换一次。只要涉及到这种拆分的拆分方式,不管是微服务、分库还是表,主要有两种拆分方式:垂直拆分和水平拆分。垂直拆分更多的是从业务的角度出发,主要是为了降低业务耦合度;另外,以SQLServer为例,一页是8KB的存储,如果一个表的字段越多,一行数据占用的空间自然越大,一页存储的行就越少数据量大,每次查询需要的IO越高,性能越慢;因此,反过来,减少字段也可以提高性能。之前听说有的同行有80个字段的表,几百万的数据就开始变慢了。水平拆分更多的是从技术角度进行拆分。拆分后,每个表的结构完全一样。简而言之,就是通过技术手段将原表的数据拆分成多个表进行存储。从根本上解决了数据量的问题。路由方式水平拆分后,按照partitionkey(shardingkey)应该在同一张表的数据要拆开写入不同的物理表,那么查询也必须根据partitionkey定位到对应的物理表,让数据给查询出来。路由方式一般有区间范围、Hash、分片映射表三种。每种路由方式都有自己的优缺点,大家可以根据对应的业务场景进行选择。区间范围是根据某个元素的区间来划分的。以时间为例,如果有一个业务,我们要以月为单位进行拆分,那么表会拆分成table_2022-04,分别是document类型和ElasticSearch类型。NoSQL也适用,不管是定位查询,还是以后的清理维护都很方便。那么缺点也很明显,因为业务的独特性,数据会不均匀,甚至不同区间之间的数据量也会相差很大。哈希也是一种常用的路由方法。根据Hash算法,将数据量均匀的存放在物理表中。缺点是带分区键的查询特别依赖。如果没有partitionkey,则无法定位到具体的物理表。这样一来,所有关联的表都被查询一次,在分库的情况下,一些RDBMS的Join、聚合计算、分页等特性无法使用。一般只有一个分区键。如果有时候业务场景需要使用不是partitionkey的字段来查询,是不是都要全部扫描?其实可以使用分片映射表。简单的说就是多了一张表,用来记录附加字段和分区键的映射关系。比如有一张订单表,原来是用UserID作为分区键进行拆分的。既然要用OrderID来查询,就必须多一张物理表,记录OrderID和UserID的映射关系。所以必须先查询映射表得到partitionkey,然后根据partitionkey的值路由到对应的物理表进行查询。可能有朋友会问,这个映射表是多了一个映射关系,多了一张表,还是同一张表有多个映射关系。我首先建议单独处理。如果映射表中的字段太多,其实和没有进行水平拆分时的状态是一样的。这是一个可以追溯到的老问题。该类型的两种方案用于处理高负载场景。有两种方案:分布式缓存和一主多从。与其说这个方案叫用空间换性能,不如说用空间换资源更合适。因此,两种方案的本质主要是通过数据冗余和集群来分担负载压力。对于关系型数据库,由于其ACID的特性,对于写来说,自然不支持分布式存储,但还是天然支持分布式读。分布式缓存缓存级别可以分为几种:客户端缓存、API服务本地缓存、分布式缓存。这次我们只讲分布式缓存。一般我们在选择分布式缓存系统时,会优先考虑NoSQLkey-value数据库,比如Memcached、Redis。如今,Redis以其数据结构多样性、高性能、易扩展性等特点逐渐在分布式缓存中占据主导地位。缓存策略也有很多种:Cache-Aside、Read/Wirte-Through、Write-Back。我们用的比较多的方法主要是Cache-Aside。具体过程可以看下图:分布式缓存相信大家比较熟悉,我也比较熟悉,但是在这里还是有几点提醒大家:避免滥用缓存。缓存应该按需使用。从28法则来看,80%的性能问题是由主要的20%的功能引起的。滥用缓存的后果会导致维护成本增加,一些数据一致性问题也不容易定位。尤其是像一些动态条件查询或者分页,key的组合是多样化的,大量使用keys命令很难处理。当然,我们可以使用一个额外的key,将记录数据的key存储在一个集合中,在删除的时候做两次查询,先查Key集合,然后遍历Key集合,删除对应的内容。这操作无疑是白费功夫,谁知道谁知道。避免缓存穿透当缓存中没有数据时,你必须去数据库中查询。这就是缓存穿透。如果某个时间临界点的数据是空的,比如周榜单,不管查多少遍数据库还是空的,查询CPU消耗比较高。并发进来的时候,是因为没有高并发的缓存层。响应,此时会因为并发导致数据库资源消耗过高,这就是缓存击穿。过多的数据库资源消耗会导致其他查询超时等问题。这个问题的解决方法也很简单,查询到数据库的空结果也缓存起来,但是给出了一个比较快的过期时间。有的同事可能又会问了,这样不会造成数据不一致吗?一般有分布式缓存、一主多从、后面会提到的CQRS等数据同步方案。只要有数据同步的话,就意味着会有数据一致性问题。因此,如果采用上述方案,对应的业务场景应该允许容忍一定的数据不一致。不是所有的慢查询都适合一般来说,慢查询意味着它们消耗更多的资源(CPU、磁盘I/O)。比如某个查询函数耗时3秒,串行查询没有问题,我们继续假设这个函数每秒100QPS左右,那么在第一个查询结果返回之前,后面所有的查询都应该渗透到数据库,这意味着在几秒钟内有300个请求到数据库。如果此时数据库CPU达到100%,那么之后的所有查询都会超时,即第一个查询结果得不到缓存,从而形成缓存崩溃。常用一主多从来分担数据库的压力。另一种常见的做法是读写分离,一主多从。我们都知道,关系型数据库本身并没有分布式分片存储,即不支持分布式写,但天然支持分布式读。一主多从就是部署多个从库的只读实例,通过冗余主库的数据来分担读请求的压力。路由算法可以通过代码实现,也可以通过中间件解决,可以根据团队的运维能力和代码组件。支持根据情况选择。一主多从是一个很好的应急方案,在没有找到根本解决方案之前,尤其是在云服务时代,扩展从库非常方便,一般只需要运维或者DBA来解决即可它,不需要开发人员访问权限。当然这种方案也有缺点,因为数据不能分片,所以master和slave的数据量是完全冗余的,也会导致硬件成本高。从库也有上限。从库过多会对主库的多线程同步数据造成压力。NoSQL主要有五种类型来选择合适的存储系统:键值类型、文档类型、列类型、图类型和搜索引擎。不同的存储系统直接决定了搜索算法和存储数据结构,也处理了需要解决的差异。业务场景。NoSQL的出现也解决了关系型数据库面临的问题(性能、高并发、可扩展性等)。比如ElasticSearch的搜索算法是倒排索引,可以用来替代关系型数据库的低性能高消耗的Like搜索(全表扫描)。Redis的Hash结构决定了时间复杂度为O(1),其内存存储,结合shard集群存储方式,可以支持几十万的QPS。因此,这类方案主要有两类:CQRS和替换(选择)存储。这两种方案的最终本质是基本相同的。它主要是利用合适的存储来弥补关系型数据库的不足,只是切换转换的方式有点不同。相同的。CQRSCQS(CommandQuerySeparation)是指在同一对象中用作查询或命令的方法。每个方法要么返回状态,要么改变状态,但不能同时返回。 在讲解CQRS之前,你必须了解CQS。估计不是很清楚。我这里通俗的解释一下:在一个对象的数据访问方式中,要么只是查询,要么就是写入(更新)。CQRS(CommandQueryResponsibilitySeparation)是在CQS的基础上,使用一个物理数据库来写入(更新),使用另一个存储系统来查询数据。因此,我们在设计一些业务场景的存储架构时,可以通过关系数据库的ACID特性来更新和写入数据,利用NoSQL的高性能和可扩展性来查询和处理数据。这样做的好处是关系型数据库可以共享NoSQL和NoSQL两者的优点,同时对于一些不适合一刀切替换存储的业务可以有一个平滑的过渡。从代码实现的角度来看,不同的存储系统只是调用相应的接口API,所以CQRS的难点主要在于如何同步数据。数据同步方式一般讨论数据同步方式主要分为push和pull:push是指数据变更端直接或间接向接收端发送数据变更记录,进行数据一致性处理。这种主动方法的优势在于其高实时性能。拉是指接收端定期轮询数据库,检查是否有需要同步的数据。这种被动方式从实现上来说比push简单,因为push需要数据变更端支持变更日志的推送。有两种推送方式:CDC(变更数据捕获)和领域事件。对于一些老项目,某些业务的数据条目过多,无法完整清晰地梳理。这时候CDC就是一个很好的方式,只要从最底层的数据库层面检索变更记录即可。对于已经服务过的项目,领域事件是一种比较舒服的方式,因为CDC需要在数据库上启用额外的功能或者部署额外的中间件,而领域事件则不需要,而且在代码可读性方面会更高,也比较开发人员的维护心态。存储系统的更换(选型)因为这个模型的核心本质和CQRS在本质上是一样的,所以主要是需要对NoSQL的优缺点有一个全面的了解,这样才能有一个合适的存储系统在相应的业务场景中进行选择和判断。在这里我想介绍一本书MartinFowler《NoSQL精粹》。这本书我看了好几遍,对各种NoSQL的优缺点和使用场景的介绍也很好。当然,在更换存储的时候,我这里也有一个建议:增加一个中间版本,这个版本会很好的做好数据同步和业务切换。数据同步要保证全量和增量处理,随时可以重新开始。业务切换主要针对后续版本更新是一个临时功能,主要是为了避免后续版本更新不顺畅或者因为版本更新导致数据不一致的情况。运行一段时间后,验证两个不同存储系统的数据一致后,就可以更换数据访问层的底层调用了。这样就可以实现平滑的更新切换。在本文的最后,我在这里介绍了八个主要的解决方案。这里再次提醒您,每个解决方案都有自己的响应场景。我们只能根据业务场景选择相应的解决方案。没有万能的解决方案,也没有灵丹妙药。在这八种解决方案中,大多数都具有数据同步功能。只要有数据同步,不管是一主多从,分布式缓存,还是CQRS,都会有数据一致性的问题,所以这些方案比较合适。一些只读的业务场景。当然,在一些先写后查的场景下,可以使用过渡页或广告页,通过点击用户关闭和切换页面来缓解数据不一致的情况。