从后端思维来讲述数据库性能优化
作为后端开发工程师,无论是在哪家公司,哪个团队,或者是做哪个系统,数据库性能问题绝对是第一个让人头疼的问题,因为数据库性能优化是项目实施过程中必须要做的事情,加之每个项目不同,对其数据的要求、性能压力也不同,如果有一套成熟的方法论,能让大家快速、准确的去选择合适的优化方案,我相信大家就能够快速解决日常遇到的80%-90%的性能问题。
解决问题,首先就需要了解问题的原因,其次再有一套思考、判断问题的流程方式,让我们合理的站在某个层面上去选择方案,最后从众多的方案里选择一个合适的方案解决问题,但在找到一个合适方案的前提是我们自己对各种方案之间的优缺点以及场景有足够的了解,没有一个完整的方案是完全可以通用。
下面就结合我们使用过的八大方案以及收集的一些资料来详细聊聊,希望能让有需要的同行在工作上、成长上提供一定的帮助。
一、数据库为什么会慢?
无论是关系型数据库还是NoSQL,影响其存储系统查询性能的主要有三种:
➢查找的时间复杂度
➢数据总量
➢高负载
而决定于与查找时间复杂度的主要有两个因素:
➢查找算法
➢存储数据结构
不管哪种存储,数据量越少,查询性能自然越高,随着数据量的增多,资源的消耗就越大(如CPU、磁盘读写繁忙),耗时也会越来越高。关系型数据库基本固定是使用B+Tree来作为索引的数据结构,而索引就是用于提高数据库表的数据访问速度。对于数据检索,首先想到的是二叉树,二叉树的查找时间复杂度是O(log2(n)),二叉树搜索相当于一个二分查找,二分查找能大大提升查询效率,但是存在一个问题,有可能出现线性结构的二叉树,相当于全表扫描,查询效率极低。因此我们对于关系型数据库性能优化的一般只有数据量。
高负载造成的原因有复杂查询、高并发请求导致CPU、磁盘繁忙等,而服务器资源不足则会导致慢查询等问题。该类型问题一般会选择集群、数据冗余的方式来分担压力。
二、应该站在哪个层面思考优化?
从上图可见,自上而下总共为四层,分别是硬件、存储系统、存储结构、具体实现。层与层之间都是紧密联系,每一层的上层是该层的载体,因此越往上越能决定性能的上限,同时优化的成本也相对会比较高,性价比也随之越低。
首先以最底层的“具体实现”为例,索引的优化成本是最小的,加了索引后,不管是CPU消耗还是响应时间肯定都是立竿见影的降低。然而一个简单的SQL语句,不管如何优化索引,都是有局限的,当在这层没有任何优化空间时就得往上一层去思考,再上一层是“存储结构”,这层需思考是否从物理表设计的层面进行优化,如分库分表、压缩数据量等。如果存储结构这层的优化没有达到效果时,就再继续往上一层进行考虑,考虑关系型数据库是否不适合用在现在的业务场景中?如果是要存储,那么需要换怎样的NoSQL?所以说我们的优化思路,需要出于性价比的优先考虑具体实现,实在没有优化空间了就再往上一层考虑。当然,如果公司有钱,可以直接使用钞能力,绕过了前面三层,这也是一种便捷的应急处理方式。
该篇文章我们主要就从存储结构、存储系统中间两层的角度来进行探讨。
三、八大方案
数据库的优化方案核心本质有三种:减少数据量、用空间换性能、选择合适的存储系统。这对应开始说的数据库为什么慢的三个原因:数据总量、高负载、查找的时间复杂度。
上图表格里的收益类型和数据类型的意思如下:
➢短期收益是指处理成本低,能紧急应对,久了则会有技术债务;而长期收益跟短期收益相反,短期内处理成本高,但是效果能长久使用,扩展性会更好。
➢静态数据是指相对改动频率比较低,也无需过多联表,where过滤比较少;而动态数据与之相反,更新频率高,通过动态条件筛选过滤。
下面我们再来详细地介绍下各大方案。
方案类型一:减少数据量
该类型的四种方案主要是来应对数据总量的场景,方案有以下四种:数据序列化存储、数据归档、中间表生成、分库分表。
目前市面上的NoSQL基本上都支持分片存储,具有非常高的读写性能,所以在大数据量下,同样表现优秀。而关系型数据库,查找算法与存储结构是可优化的空间比较少,因此我们就会从减少数据量的角度选择优化,所以该类型的优化方案主要针对关系型数据库进行处理。
(1) 数据序列化存储
做法:把一对多的数据通过序列化字符串存储。
场景:不用要求所有字段作为结构化存储。
优点:压缩比率高。
缺点:序列化的字段无法联表。
在数据库以序列化存储的方式,对于一些不需要结构化存储的业务来说是一种很好减少数据量的方式,特别是对一些M*N数据量的场景,如果M作为主表优化,那么就可以把数据量维持最多是M的量级,如订单的地址信息,这种业务一般是不需要根据里面的字段检索出来。该方案我认为是属于一种临时性的方案,无论是从序列化后丢失了部分字段的查询能力还是可优化性,都是有限的。
(2) 数据归档
做法:将生产库的数据定时转移到拥有相同结构的数据表或者库中。
场景:局部的热点数据。
优点:结构无需改动,减少生产库记录数据量。
缺点:热点数据过多仍会导致性能问题。
注意:数据归档可以解决生产数据库因为数据量过多从而引发磁盘空间预警、表查询、变更效率变低等问题,但选择该方案时别一次性迁移数据量过多,建议低频率多次限量迁移。像MYSQL由于删除数据后是不会释放空间的,可以执行命令OPTIMIZE TABLE释放存储空间,但是会锁表,如果存储空间还满足,就可以不执行。
建议优先考虑该方案,主要把数据库中非热点数据迁移到历史表(库),如果需要查历史数据,可新增业务入口路由到对应的历史表(库)。
(3) 中间表(结果表)
做法:通过调度任务定时把某个业务以多个维度进行聚合分组。
场景:报表型、排行榜等静态数据。
优点:压缩比率大。
缺点:需要开发人员针对业务场景进行开发。
中间表(结果表)是利用调度任务把复杂查询的结果跑出来存储到一张额外的物理表,因为这张物理表存放的是通过跑批汇总后的数据,因此可以理解成根据原有的业务进行了高度的数据压缩。
以报表为例,如果一个月的源数据有数十万,我们通过调度任务以月的维度生成,那么等于把原有的数据压缩了几十万分之一;接下来的季报和年报可以根据月报*N来进行统计,以这种方式处理的数据,就算三年、五年甚至十年数据量都可以在接受范围之内,而且可以精确计算得到。
(4) 分库分表
分库分表是数据库优化的一种非常经典的优化方案,把较大的数据库和数据表按照某种策略进行拆分,目的在于降低每个库、每张表的数据量,减少数据库的负担,提高数据库的查询效率,缩短查询时间。特别是在以前NoSQL还不是很成熟的年代,这个方案就如救命草一般的存在。
如今也有很多人会选择这种优化方式,但是从个人角度来看,分库分表是一种优化成本很大的方案,这里有几个建议:
➢分库分表是实在没有办法的办法,应放到最后选择。
➢优先选择NoSQL代替,因为NoSQL诞生基本上为了扩展性与高性能。
➢究竟分库还是分表?量大则分表,并发高则分库。
➢不考虑扩容,一步做到位。因为技术更新太快了,每3-5年一大变化。
拆分方式主要分两种:垂直拆分、水平拆分。
➢垂直拆分:更多是从业务角度进行拆分,主要是为了降低耦合度,如下图:
➢水平拆分:更多是从技术角度进行拆分,拆分后每张表的结构是一模一样的,简而言之就是把原有一张表的数据,通过技术手段进行分片到多张表存储,从根本上解决了数据量的问题,如下图:
方案类型二:用空间换性能
该类型可以认为是用空间换资源,这个类型的两个方案都是用来应对高负载的场景,方案有以下两种:分布式缓存、一主多从。
该两个方案的本质主要通过数据冗余、集群等方式分担负载压力。对于关系型数据库而言,因为它的ACID特性让它天生不支持写的分布式存储,但是它可以支持分布式读。
(1) 分布式缓存
做法:Cache Aside模式。
场景:应对高并发读;伪静态数(业务配置、低时效的数据)。
缺点:动态条件较多的业务场景,缓存命中低;实时性要求高的数据场景,处理起来比较花功夫。
缓存层级可以分好几种:客户端缓存、API服务本地缓存和分布式缓存,我们这次就聊分布式缓存。一般我们选择分布式缓存系统都会优先选择NoSQL的键值型数据库,例如Memcached、Redis,如今Redis的数据结构多样性,高性能,易扩展性也逐渐占据了分布式缓存的主导地位。
缓存策略主要也有很多种:Cache-Aside、Read/Wirte-Through、Write-Back,我们用得比较多的方式主要Cache-Aside,具体流程如下图:
分布式缓存的常见问题注意点,如下:
➢避免缓存穿透:数据库中不存在数据,每次都穿过缓存查数据库,造成数据库的压力。
解决方案:放入一个特殊对象(比如特定的无效对象,当然比较好的方式是使用包装对象)。
代码示例:
➢避免缓存击穿:一般来说在缓存失效的瞬间大量请求,造成数据库的压力瞬间增大。
解决方案:更新缓存时使用分布式锁 锁住服务,防止请求穿透直达数据库。
代码示例:
➢避免缓存雪崩:大量缓存设置了相同的失效时间,同一时间失效,造成服务瞬间性能急剧下降。
解决方案:缓存时间使用基本时间加上随机时间。
代码示例:
(2) 一主多从
做法:分担数据库读压力。
优点:应急调整方便,单以运维直接解决。
缺点:高硬件成本;扩展性有限。
常用的分担数据库压力还有一种常用做法,就是读写分离、一主多从。关系型数据库是不具备分布式分片存储,也就是不支持分布式写,但是它支持分布式读。一主多从是部署多台从库只读实例,通过冗余主库的数据来分担读请求的压力,路由算法可由代码实现或者中间件解决,具体根据团队的运维能力与代码组件支持视情况选择。
一主多从在还没找到根治方案前是一个非常好的应急解决方案,特别是现在云服务的年代,扩展从库是一件非常方便的事情,而且一般情况只需要运维或者DBA解决就行,无需开发人员接入。当然这方案也有缺点,因为数据无法分片,所以主从的数据量完全冗余过去,也会导致高的硬件成本。从库也有其上限,从库过多了会造成主库的多线程同步数据的压力。
方案类型三:选择合适的存储系统
NoSQL主要有五种类型:键值型、文档型、列型、图型、搜索引擎。不同的存储系统直接决定了查找算法、存储数据结构,也应对了需要解决不同的业务场景。NoSQL的出现也解决了关系型数据库之前面临的难题(性能、高并发、扩展性等)。例如,ElasticSearch的查找算法是倒排索引,可以用来代替关系型数据库的低性能、高消耗的Like搜索(全表扫描)。而Redis的Hash结构决定了时间复杂度为O(1),还有它的内存存储,结合分片集群存储方式以至于可以支撑数十万QPS。
因此本类型的方案主要有两种:CQRS、替换(选择)存储,这两种方案的最终本质基本是一样的,主要使用合适存储来弥补关系型数据库的缺点,只不过切换过渡的方式会有点不一样。
(1) CQRS
场景:需要保留关系型数据库的使用,又要使用NoSQL的高性能与可扩展性;允许非实时的数据场景。
优点:原应用改动范围比较小,兼容旧业务,只需要替换读的底层;即保留了关系型数据库的ACID特性,又使用NoSQL的可扩展性与高性能。
缺点:高硬件成本;数据同步。
说到CQRS前得了解CQS(CQS:指同一个对象中作为查询或者命令的方法,每个方法或者返回的状态,要么改变状态,但不能两者兼备),通俗的话解释:某个对象的数据访问方法里,要么只是查询,要么只是写入(更新)。
而CQRS(命令查询职责分离)基于CQS(命令查询分离)的基础上,用物理数据库来写入(更新),而用另外的存储系统来查询数据。因此我们在某些业务场景进行存储架构设计时,可以通过关系型数据库的ACID特性进行数据的更新与写入,用NoSQL的高性能与扩展性进行数据的查询处理,这样的好处就是关系型数据库和NoSQL的优点都可以兼得,同时对于某些业务不适于一刀切的替换存储的也可以有一个平滑的过渡。
从代码实现角度来看,不同的存储系统只是调用对应的接口API,因此CQRS的难点主要在于如何进行数据同步。
一般讨论到数据同步的方式主要是分推、拉:
➢推:由数据变更端通过直接或者间接的方式把数据变更的记录发送到接收端,从而进行数据的一致性处理,这种主动的方式优点是实时性高。
而推的方式又分两种:CDC(变更数据捕获)和领域事件。
a. 对于一些旧的项目来说,某些业务的数据入口非常多,无法完整清晰的梳理清楚,这个时候CDC就是一种非常好的方式,只要从最底层数据库层面把变更记录取到即可。
b. 对于已经服务化的项目来说领域事件是一种比较好的方式,因为CDC是需要数据库额外开启功能或者部署额外的中间件,而领域事件不需要,从代码可读性来看会更高,也比较适合开发人员的思维模式。
➢拉:是接收端定时的轮询数据库检查是否有数据需要进行同步,这种被动的方式从实现角度来看比推简单,因为推是需要数据变更端支持变更日志的推送的。
(2) 更换存储系统
从本质来看该模式与CQRS的核心本质是一样的,主要是要对NoSQL的优缺点有一个全面认识,这样才能在对应业务场景选择与判断出一个合适的存储系统。
在替换存储的时候,这里有个小建议,那就是加入一个中间版本,该版本做好数据同步与业务开关,数据同步要保证全量与增加的处理,随时可以重来,业务开关主要是为了后续版本的更新做的一个临时型的功能,主要避免后续版本更新不顺利或者因为版本更新时导致的数据不一致的情况出现。在跑了一段时间后,验证了两个不同的存储系统数据是一致后,接下来就可以把数据访问层的底层调用替换了。如此一来就可以平滑地更新切换。
四、结束
现在已把8大方案讲述完了,最后再次提醒一句,每个方案都有属于它的应对场景,我们只能根据业务场景选择对应的解决方案,没有通吃,没有银弹。
这8个方案中,大部分都存在数据同步的情况,只要存在数据同步,无论是一主多从、分布式缓存、CQRS都好,都会有数据一致性的问题导致,因此这些方案更多适合一些只读的业务场景。当然有些写后既查的场景,可以通过过渡页或者广告页,通过用户点击关闭切换页面的方式来缓解数据不一致性的情况。
免责声明:本账号部分分享的资料来自网络收集和整理,所有文字和图片版权归属于原作者所有,文章仅供读者学习交流使用,并请自行核实相关内容,如文章内容涉及侵权,请联系后台管理员删除。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步