那些不得不说的性能优化套路(二)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 你有没有想过,为什么跨行转账要告诉你2小时内到账,而不是立即到账?为什么抖音那么多用户同时在使用,却很少出现崩溃的情况?电商网站是如何支撑住双十一全国人民买买买的?

后端架构优化思路

前面介绍了一些高频的性能优化思路,这部分主要从架构的视角去谈谈如何优化你的应用,不只是性能,有包括稳定性和吞吐量。

一开始我们的应用可能是一个很简单的单体应用,慢慢地用户变得越来越多,这个时候最开始那种简单的架构可能已经不足以支撑这个用户量和数据量,这个时候就需要对它进行升级,那如何升级呢?核心就是一个字:拆。

微服务是从业务视角把一个大的应用拆成许多小的应用。使应用减小依赖,更容易开发、部署、管理。而拆成一个个微服务后,一些高频使用的微服务需要搭建集群对外提供能力,这个时候可能就要依赖微服务框架的负载均衡和弹性扩容/缩容能力了。

负载均衡

单台服务器的处理能力是有限的。所以我们可以在多台服务器上部署一模一样的应用。这样进来的请求就可以分摊到多台服务器上。

负载均衡

负载均衡的手段和算法有很多种。软件的话有nginx,可以支撑好几万的并发。一些微服务框架比如Spring Cloud也提供了负载均衡的能力(Ribbon)。也有一些专门的负载均衡硬件,比如F5等。各大云厂商也提高了负载均衡产品(比如AWS的ELB),可以很方便地与其它云组件结合起来使用。

使用负载均衡需要注意,应用最好是“无状态”的,比如HTTP session最好不要放在应用内存里。

弹性扩容/缩容

使用负载均衡能够让应用支撑住更多的请求。但服务器是要花钱滴,很多应用的访问量在一天中并不是均衡的,可能在某个时间段会显著比其它时间段要高一些,比如下班后到睡觉这段时间。 也可能会有一些访问量激增的情况。不管是意料之中(比如电商网站的秒杀活动),还是意料之外(比如某明星绯闻上了微博热搜)。

弹性扩容和弹性缩容,可以在检测到流量上升的时候增加服务器的数量,在流量下降的时候减小服务器的数量。这样就可以在保证业务正常运行的情况下,尽可能地节约成本。

容器时代已经来临,k8s提供了容器的弹性扩容和缩容功能,用户只需要进行很简单的配置,即可实现弹性扩容和缩容,非常的nice。

读写分离

解决了应用层面的问题,接下来我们就需要解决数据库层面的问题了。因为Web应用都是木桶原理,如果在数据库层面性能不行,哪怕你的应用层面做得再好,那也无济于事。

在大多数业务场景中,读的请求一般是远远大于写的请求数量的。而写的时候,往往比较耗时,还有可能锁住某些行,导致读数据库的请求阻塞。

所以我们可以把单个数据库拆成主库和从库,然后把所有到数据库的请求分成读请求和写请求。一般为一主一从或者一主多从。其中,主数据库负责处理写请求,然后同步到从数据库。从数据库主要负责读请求。如果是多个从节点,一般会在前面负载均衡。由于数据库请求一般是走的TCP协议,所以比较推荐的负载均衡开源工具是LVS。

读写分离和负载均衡

使用读写分离也是有一定的代价的。比如主从同步的时间间隙可能会造成用户读到的数据不是实时数据,只能保证主从节点上的数据是最终一致的。所以这点要从业务上考虑清楚,是否可以接受这种数据不一致。

分库分表

读写分离后可以在一定程度上缓解数据库压力。但如果数据量持续增多,使用读写分离也不能解决问题。分库分表或者分布式数据库可以解决这个问题。比如MySQL,如果一个表数据上了千万级别,就可以考虑拆分了。

如何拆分呢?对于分表来说,我们一般有垂直分表和水平分表这两种思路。

垂直分表

垂直分表是把一个表按照不同的字段分成多个表。举个常见的例子,商品列表页和商品详情页。商品列表页往往是分页查询一个列表,所以需要高性能的一个索引。而商品列表页往往不会展示太多的信息,所以商品列表页的字段和商品详情页的字段可以拆开成两个表,它们之间用商品id关联起来就行了。

这样商品列表页所在的表,一行的字段就比较少,也可以维护各种查询索引。而商品详情页,只需要维护一个商品id的索引就行了。

当然了,垂直分表需要从业务视角去考虑,需要匹配业务的需求,不能盲目拆分。上面举的商品列表页和商品详情页应该在开发的时候就考虑到这一层。不过有些老旧的设计,可能还存在这种把很多字段放在一个表里面的情况。

垂直拆分也不能完全解决数据量过大的问题,比如商品数量上亿、甚至是几十亿、几百亿的时候,数据库一样承受不住。这个时候就要考虑水平分表了。

水平分表

水平分表指的是把数据放到不同的表里。水平分表一般适用于两种情况,这里分别介绍一下。

单机水平分表第一种是数据“冷热不均”,比如经常用到的就是最近一段时间新写入的数据。比较典型的业务场景是微信朋友圈。最近几天新发的朋友圈的读写请求明显比几个月前要大得多。

那这种情况我们可以很简单的按照写入的顺序把数据分成多个表。比如每张表放一千万数据,写完后就新建下一个表。或者按照时间来,比如2020年5月份的数据放在202005,6月份的数据放在202006。

单机水平分表非常适合那种数据“冷热不均”的场景,比如日志记录等等。可以保证热点数据的读写性能,也能支持数据量的无限增加(只要磁盘够大)。最重要的是,它的成本不大,不需要额外的机器。所以如果业务支持的话,可以考虑单机水平分表哟。

分库分表另一种是数据热度相对分布均匀的,比如前面提到的商品表,或者是并发量确实太大的。这种情况一般会使用分库分表,把数据按一定的分布算法(比如根据某个字段哈希),水平分布到多个库里,一般来说都是一个库对应一个服务器节点,这样就可以把流量拆分到每个节点,大大减小数据库的压力。

水平分表的问题水平分表会带来问题,很多复杂一点的查询都需要去查找所有的表,然后把结果聚合起来,经过加工整理再返回。比如想count查询一个数量,需要去每个节点都count一次,然后把结果加起来。至于涉及order、join之类的查询就更复杂了。

还有自增ID的问题,如何才能保证全局的自增ID,也是一个需要注意的问题。

解决这些问题有两个办法,第一种办法是使用数据库中间件,这方面的开源产品有MyCat、shardingsphere(推荐)等。另一种办法是实现一个分布式数据库,用户就像使用单机数据库一样去使用它。比较典型的产品有TiDB等。分布式数据库由于需要自己实现查询引擎,所以不一定能够100%兼容所有最新语法,不过也能够满足绝大多数的需求了。


代码中的优化思路

前面介绍了一些架构上的优化思路。最终我们还是回归到代码本身,聊一聊在代码中的一些性能优化思路。因为改动架构的成本是非常大的,其实很多时候时候可能瓶颈并不在架构上,而是因为不合理的代码,造成了性能瓶颈。

我们的T字型发展,架构的优化思路是横向,那代码优化思路就是纵向了,是作为一个程序员的基本功,是我们吃饭的根本。

应用内缓存

俗话说,要想快,加缓存。一层不够加两层,两层不够加三层。

缓存的原理是把计算结果暂时存储在内存中。这样就可以在下次需要这个计算结果的时候,直接从内存中去取,可以省去重复计算和重复的网络开销。

许多框架都利用了缓存的技术,尤其是许多持久化框架,比如Hibernate和Mybatis,都支持两级缓存。如果你的应用是分布式的,也可以在一些地方加上缓存。比如你的程序在好几个地方都会去调同一个接口。那可以在第一次调用的时候把它存起来,后面需要用到的时候直接去内存中取,不需要再次调用。

加缓存是一种思想,实现缓存有很多种工具和方式。如果你的缓存需要在多个服务节点之间共享,那推荐使用redis这种内存数据库。如果你的缓存仅仅是在单个节点里面临时使用,那推荐Ehcache等工具,Google提供的Guava Cache也很不错。

使用缓存有几个需要注意的点。一个是注意缓存的生命周期,该清理的时候要清理,该过期的时候要过期,不要让无效的缓存占据大量的内存,因为内存是很贵的。另外需要注意数据的一致性,如果业务上要求数据的一致性和及时性,就要好好考虑使用缓存会不会让应用受到影响。

使用缓存也会带来一些列的问题,常见的有缓存穿透、缓存击穿、缓存雪崩、双写不一致等问题。我的个人网站有一篇文章《缓存常见问题及解决方案》介绍了这些问题及常见的解决方案,有兴趣的读者朋友可以参考。

串行改并行

另外一种常见的代码优化思路是串行改并行。比如,有时候你执行多个任务,他们可能彼此之间并没有数据的前后关系。那是不是可以由改到并行提升计算效率呢?Java 8的函数式编程提供了串行和并行两种Stream,如果数据量比较大,可以使用并行的Stream来利用计算机的多核优势。

collections.stream(); // 串行stream
collections.parallelStream(); // 并行stream

并行stream底层是使用的ForkJoin框架来实现的

这个方法同样适用于网络请求。比如下面这段代码中,需要去调用三个接口,然后把他们的结果收集起来进行下一步处理。这个时候我们就可以利用多线程,让他们同时去请求这几个接口,而不是串行的去做这个事情。

// 串行方式:
OneDTO one = oneService.get();
TwoDTO two = twoService.get();
ThreeDTO three = threeService.get();
nextHandle(new Result(one, two, three));
// 并行方式:
Result result = new Result();
CompletableFuture oneFuture = CompletableFuture.runAsync(
    () -> result.setOne(oneService.get()));
CompletableFuture twoFuture = CompletableFuture.runAsync(
    () -> result.setTwo(twoService.get()));
CompletableFuture threeFuture = CompletableFuture.runAsync(
    () -> result.setThree(threeService.get()));
CompletableFuture.allOf(oneFuture, twoFuture, threeFuture)
    .thenRun( () -> nextHandle(result))

异步

我在这篇文章的开头提到过一个问题,就是我们使用支付宝转账的时候,为什么支付宝不会告诉你立即到账,而是说两个小时之内到账呢?因为转账是一件非常复杂的操作,比较耗时。如果每次转账都要等转账完全结束之后再返回给用户,那需要等很久,而且会产生大量的长连接,没有必要。

这个优化的思路就是从业务上把它变成异步的。用户点击转账之后就异步进行转账操作,立即返回结果。等转账完成后,再发通知告诉用户已完成。

比如我的个人网站,每次我新写了文章,都会给之前留过邮箱的读者朋友发送一封邮件。发送邮件这个操作其实是可以异步进行的,这样的话我的响应就会比较快。

异步是一种思想。我们程序员最常用的使用异步的方式就是用多线程、NIO、消息中间件。但是多线程和NIO又比较复杂,自己去使用多线程很容易出问题。所以诞生了一些异步框架,比如ReactiveX,也很多语言的实现版本,Java的版本是RXJava。

Spring现在也在提倡“响应式编程”,提供了WebFlux来支持用户使用响应式编程。

Spring大图

Nginx、Nodejs、Netty、使用消息中间件等等,归根结底都是利用了“异步”的思想来实现更高的性能。

能不能使用异步,还是取决于业务。如果业务上可以使用异步,那才可以使用它。另外使用异步也会造成一些数据不一致的问题,往往异步只能保证最终一致性,并且很难保证事务。

避免线程同步

使用多线程可以利用服务器多个CPU的优势,但很多时候可能我们需要多个线程之间的协作。比如多个线程去获取同一个资源,可能需要上锁排队。如果这个过程比较长,就有可能越来越多的线程卡在那,造成线程池爆满,严重的时候甚至拒绝服务。慢SQL导致服务器宕机就是一个典型的例子。

其实有时候我们是可以使用一些手段,去避免线程同步,或者缩小线程同步和范围的。比如我在前段时间写的一篇文章《ThreadLocal是如何避免线程同步的?》有提到过一种思路。

这里有一些避免同步的经验,也欢迎读者朋友在评论区交流更多相关的经验。

  1. 弄清楚JMM模型和HappensBefore原则,如果可以使用volatile等轻量级的实现,就尽量不要上锁。
  2. JDK提供了非常多优秀的多线程工具类,尽量不要自己去实现多线程方面的工具类,因为很容易出错,推荐看我们《深入浅出Java多线程》的第17章。
  3. 锁有很多种,有些场景可以使用“读写锁”来增加读的性能。推荐看我们《深入浅出Java多线程》的第14章。

使用合理的数据结构和算法

合理使用数据结构和算法是作为一个程序员的基本素养

Java的工具类提供了很多十分方便的数据结构,比如List、Map、Set等。至少常用的几种常用的工具类底层的数据结构我们要明白,这里也列一些最最基础的:

  • 数组和链表实现的区别
  • 红黑树有什么用,是什么原理
  • ArrayList和HashMap的扩容过程
  • Map是如何做到查找时间复杂度是O(1)的

很多时候,我们用List的时候,先问自己一句,这个地方是不是可以利用Set和Map来提升性能?在初始化ArrayList和HashMap的时候,是不是可以考虑到初始容量,避免它在后面频繁扩容?

前面提到的MySQL索引,也是需要理解索引底层的数据结构,才能更好地理解和掌握索引。

Mysql的InnoDB索引的数据结构是带顺序索引的B+Tree

使用Redis的时候同样需要注意数据结构。五大基础的数据结构底层是如何实现的?布隆过滤器、bitmaps、HyperLogLog是什么原理,他们分别有什么应用场景?这些都可以去了解一下。在我的个人网站上搜“redis”,也有几篇这方面的文章:

搜索Redis


资料

性能优化是一个很大的概念,它需要我们考虑到方方面面的细节。涉及的知识点也比较多,需要慢慢积累。

那么问题来了,如何才能快速学习到这些性能优化知识呢?

Yasin这边给大家准备好了学习资料,在我的公众号回复“性能优化”,就可以获得一整套性能优化方面的学习视频,快来领取吧~

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
C++ UED
C/C++ 性能优化思路
C/C++ 性能优化思路
58 0
|
2月前
|
消息中间件 缓存 NoSQL
如何做性能优化?
如何做性能优化?
|
3月前
|
Web App开发 缓存 前端开发
当面试官问我前端可以做的性能优化有哪些
当面试官问我前端可以做的性能优化有哪些
|
2天前
|
缓存 监控 前端开发
前端如何做性能优化?
【4月更文挑战第21天】前端性能优化涉及代码、图片、资源加载、渲染、网络等多个层面,包括压缩合并代码、利用缓存、压缩图片、使用CDN、减少DOM操作、启用HTTP/2等策略。其他方法还包括代码拆分、使用Web Workers和性能监控。优化过程应根据项目实际需求灵活调整,并注意平衡性能与代码可读性。
14 2
|
3月前
|
缓存 前端开发 JavaScript
为什么面试官这么爱问性能优化?
为什么面试官这么爱问性能优化?
|
4月前
|
缓存 JavaScript 前端开发
性能优化面试题
性能优化面试题
27 0
|
7月前
|
缓存 前端开发 JavaScript
前端面试的性能优化部分(12)每天10个小知识点
前端面试的性能优化部分(12)每天10个小知识点
37 0
|
7月前
|
缓存 前端开发 JavaScript
前端面试的性能优化部分(13)每天10个小知识点
前端面试的性能优化部分(13)每天10个小知识点
39 0
|
7月前
|
缓存 前端开发 安全
前端面试的性能优化部分(14)每天10个小知识点
前端面试的性能优化部分(14)每天10个小知识点
53 0
|
7月前
|
编解码 缓存 前端开发
前端面试的性能优化部分(9)每天10个小知识点
前端面试的性能优化部分(9)每天10个小知识点
44 0