面试官问 如何设计一个高并发系统
靠谱开发工具破解教程目录 破解合集
所谓设计高并发系统,就是设计一个系统,保证它整体可用的同时,能够处理很高的并发用户请求,能够承受很大的流量冲击。
高并发设计原则
无状态
分而治之,横向扩展
如果设计的应用是无状态的,那么应用比较容易进行水平扩展。
如果只部署一个应用,只部署一台服务器,那抗住的流量请求是非常有限的。并且,单体的应用,有单点的风险,如果它挂了,那服务就不可用了。
因此,设计一个高并发系统,我们可以分而治之,横向扩展。也就是说,采用分布式部署的方式,部署多台服务器,把流量分流开,让每个服务器都承担一部分的并发和流量,提升整体系统的并发能力。
系统拆分
将一个系统拆分为多个子系统。要提高系统的吞吐,提高系统的处理并发请求的能力。除了采用分布式部署的方式外,还可以做微服务拆分,这样就可以达到分摊请求流量的目的,提高了并发能力。
- 系统维度:按照系统功能/业务拆分,比如商品系统、购物车、结算、订单系统等。
- 功能维度:对一个系统进行功能再拆分,比如,优惠券系统可以拆分为后台券创建系统、领券系统、用券系统等;
- 读写维度:根据读写比例特征进行拆分。比如,商品系统,交易的各个系统都会读取数据,读的量大于写,因此可以拆分成商品写服务、商品读服务;读服务可以考虑使用缓存提升性能;
服务化
随着调用方越来越多,应该考虑使用服务自动注册和发现,比如使用nacos。
负载均衡
如果我们的系统部署到了多台服务器节点。那么哪些用户的请求,访问节点a,哪些用户的请求,访问节点b,哪些用户的请求,访问节点c?
我们需要某种机制,将用户的请求,转发到具体的服务器节点上。
这就需要使用负载均衡
机制了。
在linux下有Nginx
、LVS
、Haproxy
等服务可以提供负载均衡服务。
在SpringCloud微服务架构中,大部分使用的负载均衡组件就是Ribbon
或SpringCloud Loadbalancer
。
硬件方面,可以使用F5
实现负载均衡。它可以基于交换机实现负载均衡,性能更好,但是价格更贵一些。
常用的负载均衡策略有:
轮询
:每个请求按时间顺序逐一分配到不同的服务器节点,如果服务器节点down掉,能自动剔除。weight权重
:weight代表权重默认为1,权重越高,服务器节点被分配到的概率越大。weight和访问比率成正比,用于服务器节点性能不均的情况。ip hash
:每个请求按访问ip的hash结果分配, 这样每个访客固定访问同一个服务器节点,它是解诀Session共享的问题的解决方案之一。最少连接数
:把请求转发给连接数较少的服务器节点。轮询算法是把请求平均的转发给各个服务器节点,使它们的负载大致相同;但有些请求占用的时间很长,会导致其所在的服务器节点负载较高。这时least_conn方式就可以达到更好的负载均衡效果。最短响应时间
:按服务器节点的响应时间来分配请求,响应时间短的服务器节点优先被分配。
异步
比如有个用户请求接口中,需要做业务操作,发站内通知,和记录操作日志。为了实现起来比较方便,通常我们会将这些逻辑放在接口中同步执行,势必会对接口性能造成一定的影响。
接口内部流程图如下:
这个接口表面上看起来没有问题,但如果你仔细梳理一下业务逻辑,会发现只有业务操作才是核心逻辑,其他的功能都是非核心逻辑。
在这里有个原则就是:核心逻辑可以同步执行,同步写库。非核心逻辑,可以异步执行,异步写库。
上面这个例子中,发站内通知和用户操作日志功能,对实时性要求不高,即使晚点写库,用户无非是晚点收到站内通知,或者运营晚点看到用户操作日志,对业务影响不大,所以完全可以异步处理。
通常异步主要有两种:多线程 和 消息队列。
多线程
使用线程池改造之后,接口逻辑如下:
发站内通知和用户操作日志功能,被提交到了两个单独的线程池中。
这样接口中重点关注的是业务操作,把其他的逻辑交给线程异步执行,这样改造之后,让接口性能瞬间提升了。
消息队列
消息队列是用来解耦一些不需要同步调用的服务或者订阅一些自己系统关心的变化。使用消息队列可以实现服务解耦、异步处理、流量削峰。
使用mq改造之后,接口逻辑如下:
对于发站内通知和用户操作日志功能,在接口中并没真正实现,它只发送了mq消息到mq服务器。然后由mq消费者消费消息时,才真正的执行这两个功能。
这样改造之后,接口性能同样提升了,因为发送mq消息速度是很快的,我们只需关注业务操作的代码即可。
缓存
在高并发的系统中,缓存
可以说是必不可少的技术之一。缓存对于读服务来说可谓抗流量的银弹。大部分的高并发场景,都是读多写少,完全可以在数据库和缓存里都写一份,然后读的时候走缓存。
缓存分为几种:
- 浏览器端缓存
- APP客户端缓存
- CDN缓存
- Nginx缓存
- 应用层缓存
堆缓存:使用Java堆内存来存储缓存对象。可以使用Guava Cache、Ehcache 3.x、MapDB实现。
堆外缓存:即缓存数据存储在堆外内存,可以减少GC暂停时间,可以使用Ehcache 3.x、MapDB实现。
磁盘缓存:即缓存数据存储在磁盘上,可以使用Ehcache 3.x、MapDB实现。
-
分布式缓存
可以使用 Redis,Redis 轻轻松松单机几万的并发。。
-
多级缓存
所谓多级缓存,是指在整个系统架构的不同系统层级进行数据缓存,以提升访问效率。
缓存的用法一般是这样的:
使用缓存之后,可以减轻访问数据库的压力,显著的提升系统的性能。
有些业务场景,甚至会分布式缓存和二级缓存一起使用。
比如获取商品分类数据,流程如下:
不过引入缓存,虽说给我们的系统性能带来了提升,但同时也给我们带来了一些新的问题,比如:数据库和缓存双向数据库一致性问题、缓存穿透、击穿和雪崩问题等。
我们在使用缓存时,一定要结合实际业务场景,切记不要为了缓存而缓存。
分库分表
将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高查询的性能。
当系统发展到一定的阶段,用户并发量大,会有大量的数据库请求,需要占用大量的数据库连接,同时会带来磁盘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资源等问题。
如果在有些业务场景中,用户并发量很大,但是需要保存的数据量很少,这时可以只分库,不分表。
如果在有些业务场景中,用户并发量不大,但是需要保存的数量很多,这时可以只分表,不分库。
如果在有些业务场景中,用户并发量大,并且需要保存的数量也很多时,可以分库分表。
读写分离
分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高查询性能。
一般的系统读数据请求会远远大于写数据请求。
如果读数据请求和写数据请求,都访问同一个数据库,可能会相互抢占数据库连接,相互影响。
我们都知道,一个数据库的数据库连接数量是有限,是非常宝贵的资源,不能因为读数据请求,影响到写数据请求吧?
这就需要对数据库做读写分离
了。
于是,就出现了主从读写分离架构:
考虑刚开始用户量还没那么大,选择的是一主一从的架构,也就是常说的一个master
,一个slave
。
所有的写数据请求,都指向主库。一旦主库写完数据之后,立马异步同步给从库。这样所有的读数据请求,就能及时从从库中获取到数据了(除非网络有延迟)。
但这里有个问题就是:如果用户量确实有些大,如果master挂了,升级slave为master,将所有读写请求都指向新master。
但此时,如果这个新master根本扛不住所有的读写请求,该怎么办?
这就需要一主多从的架构了:
上图中我列的是一主两从,如果master挂了,可以选择从库1或从库2中的一个,升级为新master。假如我们在这里升级从库1为新master,则原来的从库2就变成了新master的的slave了。
调整之后的架构图如下:
池化技术
在高并发的场景下,数据库连接数可能成为瓶颈,因为连接数是有限的。
我们都知道创建
和销毁
数据库连接是非常耗时耗资源的操作。
如果每次用户请求,都需要创建一个新的数据库连接,势必会影响程序的性能。
为了提升性能,我们可以创建一批数据库连接,保存到内存中的某个集合中,缓存起来。
这样的话,如果下次有需要用数据库连接的时候,就能直接从集合中获取,不用再额外创建数据库连接,这样处理将会给我们提升系统性能。
因此,需要使用池化技术,即数据库连接池、 HttpClient连接池、Redis 连接池等等。使用数据库连接池,可以避免每次查询都新建连接,减少不必要的资源开销,通过复用连接池,提高系统处理高并发请求的能力。
目前常用的数据库连接池有:Druid、C3P0、hikari和DBCP等。
同理,我们使用线程池,也能让任务并行处理,更高效地完成任务。
- 异步
应用中一个服务可能会调用多个依赖服务来处理业务,而这些依赖服务是可以同时调用的。如果顺序调用的话需要耗时100ms,而并发调用只需要50ms,那么可以使用异步来并发调用依赖服务,从而降低该服务的响应时间。
索引
在高并发的系统当中,用户经常需要查询数据,对数据库增加索引
,是必不可少的一个环节。
尤其是表中数据非常多时,加了索引,跟没加索引,执行同一条sql语句,查询相同的数据,耗时可能会相差N个数量级。
虽说索引能够提升SQL语句的查询速度,但索引也不是越多越好。
在insert数据时,需要给索引分配额外的资源,对insert的性能有一定的损耗。
我们要根据实际业务场景来决定创建哪些索引,索引少了,影响查询速度,索引多了,影响写入速度。
很多时候,我们需要经常对索引做优化。
- 可以将多个单个索引,改成一个联合索引。
- 删除不要索引。
- 使用explain关键字,查询SQL语句的执行计划,看看哪些走了索引,哪些没有走索引。
- 要注意索引失效的一些场景。
- 必要时可以使用force index来强制查询sql走某个索引。
批处理
有时候,我们需要从指定的用户集合中,查询出有哪些是在数据库中已经存在的。
实现代码可以这样写:
public List<User> queryUser(List<User> searchList) {
if (CollectionUtils.isEmpty(searchList)) {
return Collections.emptyList();
}
List<User> result = Lists.newArrayList();
searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));
return result;
}
这里如果有50个用户,则需要循环50次,去查询数据库。我们都知道,每查询一次数据库,就是一次远程调用。
如果查询50次数据库,就有50次远程调用,这是非常耗时的操作。
这种场景可以采用批量处理。
具体代码如下:
public List<User> queryUser(List<User> searchList) {
if (CollectionUtils.isEmpty(searchList)) {
return Collections.emptyList();
}
List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());
return userMapper.getUserByIds(ids);
}
提供一个根据用户id集合批量查询
用户的接口,只远程调用一次,就能查询出所有的数据。
这里有个需要注意的地方是:id集合的大小要做限制,最好一次不要请求太多的数据。要根据实际情况而定,建议控制每次请求的记录条数在500以内。
另外还有批量保存。
CDN加速静态资源访问
把 js、css、img等静态资源,放到CDN中,把一些页面例如商品详情页面做静态化处理,减少服务端请求。
CDN,它的全称是Content Delivery Network,即内容分发网络。
使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
CDN加速的基本原理是:将网站的静态内容(如图片、CSS、JavaScript文件等)复制并存储到分布在全球各地的服务器节点上。
当用户请求访问网站时,CDN系统会根据用户的地理位置,自动将内容分发给离用户最近的服务器,从而实现快速访问。
国内常见的CDN提供商有阿里云CDN、腾讯云CDN、百度云加速等,它们提供了全球分布的节点服务器,为全球范围内的网站加速服务。
页面静态化
对于高并发系统的页面功能,我们要做静态化
设计。
如果并发访问系统的用户非常多,每次用户访问页面的时候,都通过服务器动态渲染,会导致服务端承受过大的压力,而导致页面无法正常加载的情况发生。
我们可以使用Freemarker
或Velocity
模板引擎,实现页面静态化功能。
以商城官网首页为例,我们可以在Job
中,每隔一段时间,查询出所有需要在首页展示的数据,汇总到一起,使用模板引擎生成到html文件当中。
然后将该html
文件,通过shell
脚本,自动同步到前端页面相关的服务器上。
高可用原则
降级
熔断降级是保护系统的一种手段。在分布式系统中偶尔会出现某个基础服务不可用,最终导致整个系统不可用的情况, 这种现象被称为服务雪崩效应。
保证设计的系统能应对高并发场景,那肯定要考虑熔断降级。
可降级的多级读服务:比如服务调用降级为只读本地缓存、只读分布式缓存、只读默认降级数据
当高并发流量来袭,在电商系统秒杀设计时保障用户能下单、能支付是核心要求,并保障数据最终一致性即可。这样就可以把一些同步调用改成异步调用,优先处理高优先级数据或特殊特征的数据,合理分配进入系统的流量,以保障系统可用。
我们在设计高并发系统时,可以预留一些服务降级的开关。
比如在秒杀系统中,核心的功能是商品的秒杀,对于商品的评论功能,可以暂时屏蔽掉。
在服务端的分布式配置中心,比如:apollo中,可以增加一个开关,配置是否展示评论功能,默认是true。
前端页面通过服务器的接口,获取到该配置参数。
如果需要暂时屏蔽商品评论功能,可以将apollo中的参数设置成false。
此外,我们在设计高并发系统时,还可以预留一些兜底方案。
比如某个分类查询接口,要从redis中获取分类数据,返回给用户。但如果那一条redis挂了,则查询数据失败。
这时候,我们可以增加一个兜底方案。
如果从redis中获取不到数据,则从apollo中获取一份默认的分类数据。
目前使用较多的熔断降级中间件是:Hystrix
和 Sentinel
。
- Hystrix是Netflix开源的熔断降级组件。
- Sentinel是阿里中间件团队开源的一款不光具有熔断降级功能,同时还支持系统负载保护的组件。
二者的区别如下图所示:
限流
限流的目的是防止恶意请求流量、恶意攻击,或者防止流量超出系统峰值。
对于高并发系统,为了保证系统的稳定性,需要对用户的请求量做限流
。
特别是秒杀系统中,如果不做任何限制,绝大部分商品可能是被机器抢到,而非正常的用户,有点不太公平。
所以,我们有必要识别这些非法请求,做一些限制。那么,我们该如何现在这些非法请求呢?
目前常用的限流方式:
- 基于nginx限流
- 基于redis限流
- 基于sentinel限流
对同一用户限流
为了防止某个用户,请求接口次数过于频繁,可以只针对该用户做限制。
对同一ip限流
有时候只对某个用户限流是不够的,有些高手可以模拟多个用户请求,这种nginx就没法识别了。
这时需要加同一ip限流功能。
对接口限流
别以为限制了用户和ip就万事大吉,有些高手甚至可以使用代理,每次都请求都换一个ip。
这时可以限制请求的接口总次数。
验证码
相对于上面三种方式,加验证码的方式可能更精准一些,同样能限制用户的访问频次,但好处是不会存在误杀的情况。
通常情况下,用户在请求之前,需要先输入验证码。用户发起请求之后,服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作,否则直接返回,并且提示验证码错误。
此外,验证码一般是一次性的,同一个验证码只允许使用一次,不允许重复使用。
普通验证码,由于生成的数字或者图案比较简单,可能会被破解。优点是生成速度比较快,缺点是有安全隐患。
还有一个验证码叫做:移动滑块
,它生成速度比较慢,但比较安全,是目前各大互联网公司的首选。
故障转移
在高并发的系统当中,同一时间有大量的用户访问系统。
如果某一个应用服务器节点处于假死状态,比如CPU使用率100%了,用户的请求没办法及时处理,导致大量用户出现请求超时的情况。
如果这种情况下,不做任何处理,可能会影响系统中部分用户的正常使用。
这时我们需要建立故障转移
机制。
当检测到经常接口超时,或者CPU打满,或者内存溢出的情况,能够自动重启那台服务器节点上的应用。
在SpringCloud微服务当中,可以使用Ribbon
做负载均衡器。
Ribbon是Spring Cloud中的一个负载均衡器组件,它可以检测服务的可用性,并根据一定规则将请求分发至不同的服务节点。在使用Ribbon时,需要注意以下几个方面:
- 设置请求超时时间,当请求超时时,Ribbon会自动将请求转发到其他可用的服务上。
- 设置服务的健康检查,Ribbon会自动检测服务的可用性,并将请求转发至可用的服务上。
此外,还需要使用Hystrix
做熔断处理。
Hystrix是SpringCloud中的一个熔断器组件,它可以自动地监测所有通过它调用的服务,并在服务出现故障时自动切换到备用服务。在使用Hystrix时,需要注意以下几个方面:
- 设置断路器的阈值,当故障率超过一定阈值后,断路器会自动切换到备用服务上。
- 设置服务的超时时间,如果服务在指定的时间内无法返回结果,断路器会自动切换到备用服务上。到其他的能够正常使用的服务器节点上。
异地多活
有些高并发系统,为了保证系统的稳定性,不只部署在一个机房当中。
为了防止机房断电,或者某些不可逆的因素,比如:发生地震,导致机房挂了。
需要把系统部署到多个机房。
我们之前的游戏登录系统,就部署到了深圳、天津和成都,这三个机房。
这三个机房都有用户的流量,其中深圳机房占了40%,天津机房占了30%,成都机房占了30%。
如果其中的某个机房突然挂了,流量会被自动分配到另外两个机房当中,不会影响用户的正常使用。
这就需要使用异地多活
架构了。
压测
高并发系统,在上线之前,必须要做的一件事是做压力测试
。
我们先要预估一下生产环境的请求量,然后对系统做压力测试,之后评估系统需要部署多少个服务器节点。
比如预估有10000的qps,一个服务器节点最大支持1000pqs,这样我们需要部署10个服务器节点。
但假如只部署10个服务器节点,万一突增了一些新的用户请求,服务器可能会扛不住压力。
因此,部署的服务器节点,需要把预估用户请求量的多一些,比如:按3倍的用户请求量来计算。
这样我们需要部署30个服务器节点。
压力测试的结果跟环境有关,在dev环境或者test环境,只能压测一个大概的趋势。
想要更真实的数据,我们需要在pre环境,或者跟生产环境相同配置的专门的压测环境中,进行压力测试。
目前市面上做压力测试的工具有很多,比如开源的有:Jemter、LoaderRunnder、Locust等等。
收费的有:阿里自研的云压测工具PTS。
监控
为了出现系统或者SQL问题时,能够让我们及时发现,我们需要对系统做监控。
目前业界使用比较多的开源监控系统是:Prometheus
。
它提供了 监控
和 预警
的功能。
我们可以用它监控如下信息:
- 接口响应时间
- 调用第三方服务耗时
- 慢查询sql耗时
- cpu使用情况
- 内存使用情况
- 磁盘使用情况
- 数据库使用情况
它的界面长这样子:
总结
其实,高并发的系统中,还需要考虑安全问题,比如:
- 遇到用户不断变化ip刷接口怎办?
- 遇到用户大量访问缓存中不存在的数据,导致缓存雪崩怎么办?
- 如果用户发起ddos攻击怎么办?
- 用户并发量突增,导致服务器扛不住了,如何动态扩容?