如何设计一个高并发系统?

pivoteic
发布于 2023-11-16 14:51
浏览
0收藏

大家好,我是苏三,又跟大家见面了。

前言

最近有位粉丝问了我一个问题:如何设计一个高并发系统?

这是一个非常高频的面试题,面试官可以从多个角度,考查技术的广度和深度。

今天这篇文章跟大家一起聊聊高并发系统设计一些关键点,希望对你会有所帮助。

如何设计一个高并发系统?-鸿蒙开发者社区

1 页面静态化

对于高并发系统的页面功能,我们必须要做​​静态化​​设计。

如果并发访问系统的用户非常多,每次用户访问页面的时候,都通过服务器动态渲染,会导致服务端承受过大的压力,而导致页面无法正常加载的情况发生。

我们可以使用​​Freemarker​​​或​​Velocity​​模板引擎,实现页面静态化功能。

以商城官网首页为例,我们可以在​​Job​​中,每隔一段时间,查询出所有需要在首页展示的数据,汇总到一起,使用模板引擎生成到html文件当中。

然后将该​​html​​​文件,通过​​shell​​脚本,自动同步到前端页面相关的服务器上。

2 CDN加速

虽说页面静态化可以提升网站网页的访问速度,但还不够,因为用户分布在全国各地,有些人在北京,有些人在成都,有些人在深圳,地域相差很远,他们访问网站的网速各不相同。

如何才能让用户最快访问到活动页面呢?

这就需要使用CDN,它的全称是Content Delivery Network,即内容分发网络。

如何设计一个高并发系统?-鸿蒙开发者社区

使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

CDN加速的基本原理是:将网站的静态内容(如图片、CSS、JavaScript文件等)复制并存储到分布在全球各地的服务器节点上。

当用户请求访问网站时,CDN系统会根据用户的地理位置,自动将内容分发给离用户最近的服务器,从而实现快速访问。

国内常见的CDN提供商有阿里云CDN、腾讯云CDN、百度云加速等,它们提供了全球分布的节点服务器,为全球范围内的网站加速服务。

3 缓存

在高并发的系统中,​​缓存​​可以说是必不可少的技术之一。

目前缓存有两种:

  1. 基于应用服务器的内存缓存,也就是我们说的二级缓存。
  2. 使用缓存中间件,比如:Redis、Memcached等,这种是分布式缓存。

这两种缓存各有优缺点。

二级缓存的性能更好,但因为是基于应用服务器内存的缓存,如果系统部署到了多个服务器节点,可能会存在数据不一致的情况。

而Redis或Memcached虽说性能上比不上二级缓存,但它们是分布式缓存,避免多个服务器节点数据不一致的问题。

缓存的用法一般是这样的:

如何设计一个高并发系统?-鸿蒙开发者社区

使用缓存之后,可以减轻访问数据库的压力,显著的提升系统的性能。

有些业务场景,甚至会分布式缓存和二级缓存一起使用。

比如获取商品分类数据,流程如下:

如何设计一个高并发系统?-鸿蒙开发者社区

不过引入缓存,虽说给我们的系统性能带来了提升,但同时也给我们带来了一些新的问题,比如:《​​数据库和缓存双向数据库一致性问题​​​》、《​​缓存穿透、击穿和雪崩问题​​》等。

我们在使用缓存时,一定要结合实际业务场景,切记不要为了缓存而缓存。

4 异步

有时候,我们在高并发系统当中,某些接口的业务逻辑,没必要都同步执行。

比如有个用户请求接口中,需要做业务操作,发站内通知,和记录操作日志。为了实现起来比较方便,通常我们会将这些逻辑放在接口中同步执行,势必会对接口性能造成一定的影响。

接口内部流程图如下:

如何设计一个高并发系统?-鸿蒙开发者社区

这个接口表面上看起来没有问题,但如果你仔细梳理一下业务逻辑,会发现只有业务操作才是核心逻辑,其他的功能都是非核心逻辑。

在这里有个原则就是:核心逻辑可以同步执行,同步写库。非核心逻辑,可以异步执行,异步写库。

上面这个例子中,发站内通知和用户操作日志功能,对实时性要求不高,即使晚点写库,用户无非是晚点收到站内通知,或者运营晚点看到用户操作日志,对业务影响不大,所以完全可以异步处理。

通常异步主要有两种:多线程 和 mq。

4.1 线程池

使用线程池改造之后,接口逻辑如下:

如何设计一个高并发系统?-鸿蒙开发者社区

发站内通知和用户操作日志功能,被提交到了两个单独的线程池中。

这样接口中重点关注的是业务操作,把其他的逻辑交给线程异步执行,这样改造之后,让接口性能瞬间提升了。

但使用线程池有个小问题就是:如果服务器重启了,或者是需要被执行的功能出现异常了,无法重试,会丢数据。

那么这个问题该怎么办呢?

4.2 mq

使用mq改造之后,接口逻辑如下:

如何设计一个高并发系统?-鸿蒙开发者社区

对于发站内通知和用户操作日志功能,在接口中并没真正实现,它只发送了mq消息到mq服务器。然后由mq消费者消费消息时,才真正的执行这两个功能。

这样改造之后,接口性能同样提升了,因为发送mq消息速度是很快的,我们只需关注业务操作的代码即可。

5 多线程处理

在高并发系统当中,用户的请求量很大。

假如我们现在用mq处理业务逻辑。

一下子有大量的用户请求,产生了大量的mq消息,保存到了mq服务器。

而mq的消费者,消费速度很慢。

可能会导致大量的消息积压问题。

从而严重影响数据的实时性。

我们需要对消息的消费者做优化。

最快的方式是使用​​多线程​​消费消息,比如:改成线程池消费消息。

当然核心线程数、最大线程数、队列大小 和 线程回收时间,一定要做成配置的,后面可以根据实际情况动态调整。

这样改造之后,我们可以快速解决消息积压问题。

除此之外,在很多数据导入场景,用多线程导入数据,可以提升效率。

温馨提醒一下:使用多线程消费消息,可能会出现消息的顺序问题。如果你的业务场景中,需要保证消息的顺序,则要用其他的方式解决问题。感兴趣的小伙伴,可以找我私聊。

6 分库分表

有时候,高并发系统的吞吐量受限的不是别的,而是数据库。

当系统发展到一定的阶段,用户并发量大,会有大量的数据库请求,需要占用大量的数据库连接,同时会带来磁盘IO的性能瓶颈问题。

此外,随着用户数量越来越多,产生的数据也越来越多,一张表有可能存不下。由于数据量太大,sql语句查询数据时,即使走了索引也会非常耗时。

这时该怎么办呢?

答:需要做​​分库分表​​。

如下图所示:

如何设计一个高并发系统?-鸿蒙开发者社区

图中将用户库拆分成了三个库,每个库都包含了四张用户表。

如果有用户请求过来的时候,先根据用户id路由到其中一个用户库,然后再定位到某张表。

路由的算法挺多的:

  • 根据id取模,比如:id=7,有4张表,则7%4=3,模为3,路由到用户表3。
  • 给id指定一个区间范围,比如:id的值是0-10万,则数据存在用户表0,id的值是10-20万,则数据存在用户表1。
  • 一致性hash算法

分库分表主要有两个方向:​​垂直​​​和​​水平​​。

说实话垂直方向(即业务方向)更简单。

在水平方向(即数据方向)上,分库和分表的作用,其实是有区别的,不能混为一谈。

  • 分库:是为了解决数据库连接资源不足问题,和磁盘IO的性能瓶颈问题。
  • 分表:是为了解决单表数据量太大,sql语句查询数据时,即使走了索引也非常耗时问题。此外还可以解决消耗cpu资源问题。
  • 分库分表:可以解决 数据库连接资源不足、磁盘IO的性能瓶颈、检索数据耗时 和 消耗cpu资源等问题。

如果在有些业务场景中,用户并发量很大,但是需要保存的数据量很少,这时可以只分库,不分表。

如果在有些业务场景中,用户并发量不大,但是需要保存的数量很多,这时可以只分表,不分库。

如果在有些业务场景中,用户并发量大,并且需要保存的数量也很多时,可以分库分表。

关于分库分表更详细的内容,可以看看我另一篇文章,里面讲的更深入《​​阿里二面:为什么分库分表?​​》

7 池化技术

其实不光是高并发系统,为了性能考虑,有些低并发的系统,也在使用​​池化技术​​,比如:数据库连接池、线程池等。

池化技术是​​多例设计模式​​的一个体现。

我们都知道​​创建​​​和​​销毁​​数据库连接是非常耗时耗资源的操作。

如果每次用户请求,都需要创建一个新的数据库连接,势必会影响程序的性能。

为了提升性能,我们可以创建一批数据库连接,保存到内存中的某个集合中,缓存起来。

这样的话,如果下次有需要用数据库连接的时候,就能直接从集合中获取,不用再额外创建数据库连接,这样处理将会给我们提升系统性能。

如何设计一个高并发系统?-鸿蒙开发者社区

当然用完之后,需要及时归还。

目前常用的数据库连接池有:Druid、C3P0、hikari和DBCP等。

8 读写分离

不知道你有没有听说过​​二八原则​​,在一个系统当中可能有80%是读数据请求,另外20%是写数据请求。

不过这个比例也不是绝对的。

我想告诉大家的是,一般的系统读数据请求会远远大于写数据请求。

如果读数据请求和写数据请求,都访问同一个数据库,可能会相互抢占数据库连接,相互影响。

我们都知道,一个数据库的数据库连接数量是有限,是非常宝贵的资源,不能因为读数据请求,影响到写数据请求吧?

这就需要对数据库做​​读写分离​​了。

于是,就出现了主从读写分离架构:

如何设计一个高并发系统?-鸿蒙开发者社区

考虑刚开始用户量还没那么大,选择的是一主一从的架构,也就是常说的一个​​master​​​,一个​​slave​​。

所有的写数据请求,都指向主库。一旦主库写完数据之后,立马异步同步给从库。这样所有的读数据请求,就能及时从从库中获取到数据了(除非网络有延迟)。

但这里有个问题就是:如果用户量确实有些大,如果master挂了,升级slave为master,将所有读写请求都指向新master。

但此时,如果这个新master根本扛不住所有的读写请求,该怎么办?

这就需要一主多从的架构了:

如何设计一个高并发系统?-鸿蒙开发者社区

上图中我列的是一主两从,如果master挂了,可以选择从库1或从库2中的一个,升级为新master。假如我们在这里升级从库1为新master,则原来的从库2就变成了新master的的slave了。

调整之后的架构图如下:

如何设计一个高并发系统?-鸿蒙开发者社区

这样就能解决上面的问题了。

除此之外,如果查询请求量再增大,我们还可以将架构升级为一主三从、一主四从...一主N从等。


文章转载自公众号: 苏三说技术

分类
标签
已于2023-11-16 14:51:21修改
收藏
回复
举报
回复
    相关推荐