【巨人的肩膀】后端思维之数据库性能优化方案

简介

毫不夸张的说咱们后端工程师,无论在哪家公司,呆在哪个团队,做哪个系统,遇到的第一个让人头疼的问题绝对是数据库性能问题。如果我们有一套成熟的方法论,能让大家快速、准确的去选择出合适的优化方案,我相信能够快速准备解决咱么日常遇到的 80% 甚至 90% 的性能问题
从解决问题的角度出发,我们得先了解到问题的原因;其次我们得有一套思考、判断问题的流程方式,让我们合理的站在哪个层面选择方案;最后从众多的方案里面选择一个适合的方案进行解决问题,找到一个合适的方案的前提是我们自己对各种方案之间的优缺点、场景有足够的了解,没有一个方案是完全可以通吃通用的,软件工程没有银弹
下文的我工作多年以来,曾经使用过的八大方案,结合了平常自己学习收集的一些资料,以系统、全面的方式整理成了这篇博文,也希望能让一些有需要的同行在工作上、成长上提供一定的帮助

为什么数据库会慢?

在这里插入图片描述
无论是关系型数据库还是NoSQL,任何存储系统决定于其查询性能的主要有三种:

  • 查找的时间复杂度
  • 数据总量
  • 高负载

而决定于查找时间复杂度主要有两个因素:

  • 查找算法
  • 存储数据结构

无论是哪种存储,数据量越少,自然查询性能就越高,随着数据量增多,资源的消耗(CPU、磁盘读写繁忙)、耗时也会越来越高

从关系型数据库角度出发,索引结构基本固定是 B+Tree,时间复杂度是 O(log n),存储结构是行式存储。因此咱们对于关系数据库能优化的一般只有数据量

而高负载造成原因有高并发请求、复杂查询等,导致 CPU、磁盘繁忙等,而服务器资源不足则会导致慢查询等问题。该类型问题一般会选择集群、数据冗余的方式分担压力
在这里插入图片描述

应该站在哪个层面思考优化?

在这里插入图片描述
从上图可见,自顶向下的一共有四层,分别是硬件、存储系统、存储结构、具体实现。层与层之间是紧密联系的,每一层的上层是该层的载体;因此越往顶层越能决定性能的上限,同时优化的成本也相对会比较高,性价比也随之越低。以最底层的具体实现为例,那么索引的优化的成本应该是最小的,可以说加了索引后无论是 CPU 消耗还是响应时间都是立竿见影降低;然而一个简单的语句,无论如何优化加索引也是有局限的,当在具体实现这层没有任何优化空间的时候就得往上一层【存储结构】思考,思考是否从物理表设计的层面出发优化(如分库分表、压缩数据量等),如果是文档型数据库得思考下文档聚合的结果;如果在存储结构这层优化得没效果,得继续往再上一次进行考虑,是否关系型数据库应该不适合用在现在得业务场景?如果要换存储,那么得换怎样得 NoSQL?

所以咱们优化的思路,出于性价比的优先考虑具体实现,实在没有优化空间了再往上一层考虑。当然如果公司有钱,直接使用钞能力,绕过了前面三层,这也是一种便捷的应急处理方式

该篇文章不讨论顶与底的两个层面的优化,主要从存储结构、存储系统中间两层的角度出发进行探讨

八大方案总结

在这里插入图片描述
数据库的优化方案核心本质有三种:减少数据量、用空间换性能、选择合适的存储系统,这也对应了开篇讲解的慢的三个原因:数据总量、高负载、查找的时间复杂度

这里大概解释下收益类型:短期收益,处理成本低,能紧急应对,久了则会有技术债务;长期收益则跟短期收益相反,短期内处理成本高,但是效果能长久使用,扩展性会更好。

静态数据意思是,相对改动频率比较低的,也无需过多联表的,where过滤比较少。动态数据与之相反,更新频率高,通过动态条件筛选过滤

减少数据量

减少数据量类型共有四种方案:数据序列化存储、数据归档、中间表生成、分库分表
就如上面所说的,无论是哪种存储,数据量越少,自然查询性能就越高,随着数据量增多,资源的消耗(CPU、磁盘读写繁忙)、耗时也会越来越高。目前市面上的NoSQL基本上都支持分片存储,所以其天然分布式写的能力从数据量上能得到非常的解决方案。而关系型数据库,查找算法与存储结构是可以优化的空间比较少,因此咱们一般思考出发点只有从如何减少数据量的这个角度进行选择优化,因此本类型的优化方案主要针对关系型数据库进行处理
在这里插入图片描述

数据归档

做法:利用数据库作业,定时把历史数据移到历史表或者库
场景:局部的热点数据
优点:结构无需改动,少侵入性
缺点:热点数据过多仍会导致性能问题

注意点:别一次性迁移数量过多,建议低频率多次限量迁移。像 MySQL 由于删除数据后是不会释放空间的,可以执行命令 OPTIMIZE TABLE 释放存储空间,但是会锁表,如果存储空间还满足,可以不执行

建议优先考虑该方案,主要通过数据库作业把非热点数据迁移到历史表,如果需要查历史数据,可新增业务入口路由到对应的历史表(库)
在这里插入图片描述

中间表(结果表)

做法:通过调度任务定时,把某个业务以多个维度进行聚合分组
场景:报表型、排行榜等静态数据
优点:压缩比率大
缺点:需要开发人员针对场景业务进行开发

中间表(结果表)其实就是利用调度任务把复杂查询的结果跑出来存储到一张额外的物理表,因为这张物理表存放的是通过跑批汇总后的数据,因此可以理解成根据原有的业务进行了高度的数据压缩。以报表为例,如果一个月的源数据有数十万,我们通过调度任务以月的维度生成,那么等于把原有的数据压缩了几十万分之一;接下来的季报和年报可以根据月报*N来进行统计,以这种方式处理的数据,就算三年、五年甚至十年数据量都可以在接受范围之内,而且可以精确计算得到

那么数据的压缩比率是否越低越好?下面有一段口诀:

  • 字段越多,粒度越细,灵活性越高,可以以中间表进行不同业务联表处理
  • 字段越少,粒度越粗,灵活性越低,一般作为结果表查询出来

数据序列化存储

做法:把一对多的数据,通过序列化字符串存储
场景:不需要要求所有字段作为结构化存储
优点:压缩比率高
缺点:序列化的字段无法联表
在这里插入图片描述
在数据库以序列化存储的方式,对于一些不需要结构化存储的业务来说是一种很好减少数据量的方式,特别是对于一些 M*N 的数据量的业务场景,如果以 M 作为主表优化,那么就可以把数据量维持最多是 M 的量级。另外像订单的地址信息,这种业务一般是不需要根据里面的字段检索出来,也比较适合

这种方案我认为属于一种临时性的优化方案,无论是从序列化后丢失了部份字段的查询能力,还是这方案的可优化性都是有限的

分库分表

分库分表作为数据库优化的一种非常经典的优化方案,特别是在以前 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 特性让它天生不支持写的分布式存储,但是它依然天然的支持分布式读
在这里插入图片描述

分布式缓存

做法:Cache Aside
场景:应对高并发读;伪静态数据(业务配置、低时效的数据)
缺点:动态条件比较多的业务场景,缓存命中低;实时性要求高的数据场景,处理起来比较花功夫

缓存层级可以分好几种:客户端缓存、API服务本地缓存和分布式缓存,咱们这次只聊分布式缓存。一般我们选择分布式缓存系统都会优先选择 NoSQL 的键值型数据库,例如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 秒时间,串行查询的时候并没什么问题,我们继续假设这功能每秒大概 QPS 为 100,那么在第一次查询结果返回之前,接下来的所有查询都应该穿透到数据库,也就意味着这几秒时间有 300 个请求到数据库,如果这个时候数据库 CPU 达到了 100%,那么接下来的所有查询都会超时,也就是无法有第一个查询结果缓存起来,从而还是形成了缓存击穿

一主多从

做法:分担数据库读压力
优点:应急调整方便,单以运维直接解决;还没找到更好的降低数据库负载的临时方案
缺点:高硬件成本;扩展性有限

常用的分担数据库压力还有一种常用做法,就是读写分离、一主多从。咱们都是知道关系型数据库天生是不具备分布式分片存储的,也就是不支持分布式写,但是它天然的支持分布式读。一主多从是部署多台从库只读实例,通过冗余主库的数据来分担读请求的压力,路由算法可有代码实现或者中间件解决,具体可以根据团队的运维能力与代码组件支持视情况选择

一主多从在还没找到根治方案前是一个非常好的应急解决方案,特别是在现在云服务的年代,扩展从库是一件非常方便的事情,而且一般情况只需要运维或者DBA解决就行,无需开发人员接入。当然这方案也有缺点,因为数据无法分片,所以主从的数据量完全冗余过去,也会导致高的硬件成本。从库也有其上限,从库过多了会主库的多线程同步数据的压力

在这里插入图片描述

选择合适的存储系统

NoSQL 主要以下五种类型:键值型、文档型、列型、图型、搜素引擎,不同的存储系统直接决定了查找算法、存储数据结构,也应对了需要解决的不同的业务场景。NoSQL的出现也解决了关系型数据库之前面临的难题(性能、高并发、扩展性等)

例如,ElasticSearch 的查找算法是倒排索引,可以用来代替关系型数据库的低性能、高消耗的 Like 搜索(全表扫描)。而 Redis 的 Hash 结构决定了时间复杂度为 O(1),还有它的内存存储,结合分片集群存储方式以至于可以支撑数十万QPS

因此本类型的方案主要有两种:CQRS、替换(选择)存储,这两种方案的最终本质基本是一样的主要使用合适存储来弥补关系型数据库的缺点,只不过切换过渡的方式会有点不一样
在这里插入图片描述

CQRS

CQS(命令查询分离)指同一个对象中作为查询或者命令的方法,每个方法或者返回的状态,要么改变状态,但不能两者兼备

场景:需要保留关系型数据库的使用,又要使用 NoSQL 的高性能与可扩展性;允许非实时的数据场景
优点:原应用改动范围比较小,兼容旧业务,只需要替换读的底层;即保留了关系型数据库的 ACID 特性,又使用 NoSQL 的可扩展性与高性能
缺点:高硬件成本;数据同步

讲解 CQRS 前得了解 CQS,有些小伙伴看了估计还没不是很清晰,我这里用通俗的话解释:某个对象的数据访问的方法里,要么只是查询,要么只是写入(更新)。而 CQRS(命令查询职责分离)基于 CQS 的基础上,用物理数据库来写入(更新),而用另外的存储系统来查询数据。因此我们在某些业务场景进行存储架构设计时,可以通过关系型数据库的 ACID 特性进行数据的更新与写入,用 NoSQL 的高性能与扩展性进行数据的查询处理,这样的好处就是关系型数据库和 NoSQL 的优点都可以兼得,同时对于某些业务不适于一刀切的替换存储的也可以有一个平滑的过渡

从代码实现角度来看,不同的存储系统只是调用对应的接口 API,因此 CQRS 的难点主要在于如何进行数据同步

数据同步方式

方式:推;拉
实时性:高;低
方案类型:CDC(变更数据捕获);领域事件;调度任务定时同步
优点:无业务侵入,解决多业务入口;可读性高;同CDC
缺点:额外中间件;需要在框架代码层面处理;物理删除无法识别,只能全量

一般讨论到数据同步的方式主要是分推和拉:

  • 推指的是由数据变更端通过直接或者间接的方式把数据变更的记录发送到接收端,从而进行数据的一致性处理,这种主动的方式优点是实时性高
  • 拉指的是接收端定时的轮询数据库检查是否有数据需要进行同步,这种被动的方式从实现角度来看比推简单,因为推是需要数据变更端支持变更日志的推送的

而推的方式又分两种:CDC(变更数据捕获)和领域事件。对于一些旧的项目来说,某些业务的数据入口非常多,无法完整清晰的梳理清楚,这个时候 CDC 就是一种非常好的方式,只要从最底层数据库层面把变更记录取到就可
对于已经服务化的项目来说领域事件是一种比较舒服的方式,因为 CDC 是需要数据库额外开启功能或者部署额外的中间件,而领域事件则不需要,从代码可读性来看会更高,也比较开发人员的维护思维模式
在这里插入图片描述

替换(选择)存储系统

因为从本质来看该模式与 CQRS 的核心本质是一样的,主要是要对 NoSQL 的优缺点有一个全面认识,这样才能在对应业务场景选择与判断出一个合适的存储系统。这里我像大家介绍一本书马丁.福勒《NoSQL精粹》,这本书我重复看了好几遍,也很好全面介绍各种 NoSQL 优缺点和使用场景

当然替换存储的时候,我这里也有个建议:加入一个中间版本,该版本做好数据同步与业务开关,数据同步要保证全量与增加的处理,随时可以重来,业务开关主要是为了后续版本的更新做的一个临时型的功能,主要避免后续版本更新不顺利或者因为版本更新时导致的数据不一致的情况出现。在跑了一段时间后,验证了两个不同的存储系统数据是一致的后,接下来就可以把数据访问层的底层调用替换了。如此一来就可以平滑的更新切换

结束

本文到这里就把八大方案介绍完了,在这里再次提醒一句,每个方案都有属于它的应对场景,咱们只能根据业务场景选择对应的解决方案,没有通吃,没有银弹

这八个方案里,大部分都存在数据同步的情况,只要存在数据同步,无论是一主多从、分布式缓存、CQRS都好,都会有数据一致性的问题导致,因此这些方案更多适合一些只读的业务场景。当然有些写后既查的场景,可以通过过渡页或者广告页通过用户点击关闭切换页面的方式来缓解数据不一致性的情况

通过这篇文章我相信大家对数据库设计优化有了一个全面的认识,如果有更加的建议可以在下方评论反馈给给我

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《奔跑吧巨人》是一款基于3D冒险闯关游戏的源码。 这款游戏主要基于Unity引擎实现,采用C#编程语言进行编写。 作为一款冒险闯关游戏,玩家将扮演一个小巨人,并且需要在一个充满挑战的世界中奔跑,穿越各种障碍物和陷阱。在游戏中,玩家需要使用方向键控制小巨人进行移动,并且可以通过空格键进行跳跃。玩家需要适应不同的地形和环境,躲避障碍物、收集道具等,以便尽可能地奔跑更远和解锁更多关卡。 游戏源码中,包含了基本的角色控制逻辑、碰撞检测、动画效果以及关卡设计等功能。通过修改源码,开发者可以进行一些可定制化的操作,例如调整关卡的难度、增加新的道具和敌人等。此外,源码还包含了一些基本的图形和音效资源,供开发者使用。 《奔跑吧巨人》的源码结构清晰,注释详细,便于开发者理解和修改。同时,Unity引擎提供了强大的可视化编辑工具,开发者可以在编辑器中进行场景的编辑、角色的动画状态机设计等操作,加快游戏开发的进程。 总之,《奔跑吧巨人》的源码为开发者提供了一个基于3D冒险闯关游戏的框架,具备基本功能并可根据需求进行扩展和定制。开发者可以通过学习源码的实现方式和结构,加快开发进度,同时也可以根据自己的创意和需求进行定制,打造属于自己的冒险闯关游戏。 ### 回答2: 《奔跑吧巨人》是一款3D冒险闯关游戏,以下将用300字中文详细回答该游戏的源码相关内容。 《奔跑吧巨人》的源码可以包括游戏的各个模块和功能的实现代码。这款游戏主要是以冒险与闯关为核心玩法,玩家需要扮演一个巨人角色,在游戏世界中展开一系列的冒险与挑战。 首先,源码中会涉及到巨人角色的移动代码。这部分代码主要包括角色的行走、奔跑和跳跃等动作,以及与玩家输入的互动代码。通过这些代码,玩家可以通过操控键盘或手柄来控制巨人角色在游戏世界中自由移动。 其次,源码还包括游戏关卡的设计和实现。每个关卡都有不同的地形、陷阱和敌人等元素,所以源码中需要实现这些地形和物体的生成与碰撞检测等功能。同时,还需要编写关卡逻辑代码,如触发事件、计分系统和通关条件等。 此外,源码中还会含有一些特殊道具和技能的实现代码。这些道具和技能可以给予巨人角色特殊能力,如变大、飞行和破坏障碍物等。源码需要实现这些能力的触发和效果,以增加游戏的乐趣和挑战性。 最后,源码中还会包括用户界面的实现,如开始菜单、游戏界面和结算界面等。这些界面需要有对应的交互和显示逻辑,供玩家进行游戏的开始、继续和结束等操作。 综上所述,3D冒险闯关游戏《奔跑吧巨人》的源码涉及到角色移动、关卡设计、道具技能和用户界面等方面的代码实现。这些源码的编写和功能实现,可以通过游戏引擎或编程语言来完成,以提供给玩家一个丰富有趣的游戏体验。 ### 回答3: 《奔跑吧巨人》是一款基于3D冒险闯关的游戏。这款游戏的源码可以实现玩家扮演巨人,在一个奇幻的世界中奔跑、跳跃、攀爬等各种冒险动作。以下是有关该游戏源码的几个主要方面: 首先,游戏源码包含了游戏的主要场景和角色设计。场景设计是基于3D技术的,在游戏中呈现出一个多样化的世界,包括山脉、河流、森林等不同的地形。巨人角色设计包括了奔跑、跳跃、攀爬等动作,通过3D建模和动画技术实现了逼真的角色表现。 其次,游戏源码还包含了游戏的基本功能和玩法规则。玩家可以通过触屏控制或键盘操作来操纵巨人角色在游戏中进行各种动作。游戏的目标是通过跳跃、攀爬等动作来克服各种障碍物,到达终点。游戏中会有各种道具和特殊元素,增加了游戏的趣味性和挑战性。 最后,游戏源码还包含了游戏的音效和特效设计。通过添加适当的音效和特效,可以给玩家带来更加沉浸式的游戏体验。比如,巨人奔跑时的脚步声、跳跃时的气势磅礴的音效,以及碰撞、爆炸等特效,都会为游戏增添真实感和刺激感。 总之,3D冒险闯关游戏源码《奔跑吧巨人》提供了一种精彩的游戏体验。通过源码,开发者可以进一步扩展和优化游戏内容,丰富游戏玩法,增加关卡和道具等元素,为玩家带来更加刺激和有趣的冒险体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值