前言
我相信,每一位开发同学,多多少少都想参与或负责一个高并发的系统。
一来,可以增加自己实际的项目经验,拥有应对高并发场景的解决方案。
二来,有一个高并发的项目经验,无疑是自己简历的一个大大的加分项。
奈何,很多人都没有机会可以参与这样的项目。为此,本文将详细说明高并发系统的设计思路和方法,希望可以让你对高并发系统有全面的了解。
系统设计原则
好的系统是迭代出来的
在设计一个系统之前,我们先要有一个统一且清晰的认知:好的系统是迭代出来的。
在设计一个系统时,不要试图一次就能设计出完美的系统,避免过度复杂化。但还是要有先行的规划和对未来的预案。两者并不冲突。
避免过度复杂化
在设计高并发系统时,要明确一点:这个系统只是用户量多还是业务功能也很庞大?
根据这一点,我们可以确定系统是否要根据业务去拆分不同的子系统。
如果只是用户体量大,在设计系统时保证系统是无状态即可,这样的话可以在应对高并发时方便水平扩展。
如果业务功能也比较多导致系统体积过于庞大,就要考虑拆分系统,各子系统也可以分担流量压力。
在拆分系统时可以按照以下几个维度进行拆分:
- 系统维度:比如电商系统,我们可以拆分成商品、订单、支付等多个子系统。
- 功能维度:如果各子系统规模也比较庞大,那么将系统按功能模块再次拆分。
- 读写维度:在读多写少的场景下,可以将数据存储和访问按照读写比例进行拆分。
总之,系统的复杂度的提升是一个循序渐进的过程,不要打肿脸充胖子。
客户端优化
高并发系统的客户端优化是必不可少的,如果没有做好客户端的优化影响用户体验是一方面,有时候甚至是致命的。
分享一例惨痛的教训:我之前负责过一个秒杀业务,当时忽视了前端优化的重要性,大量用户在刷新页面时,服务器带宽承受了巨大的压力,导致页面无法正常加载,让我们失去了很多潜在用户。
所以客户端的优化是非常重要的。
减少与服务端的交互
客户端与服务端的尽量减少不必要的交互,避免服务端的压力过大。比如:
- 减少不必要传输:减少cookie的使用,因为cookie会随着请求发送到服务端,从而会增加服务端的数据量。
- 减少数据量输出:例如删除JS无效注释,一来可以减少体积,二来可以提高代码安全。或者可以将文件压缩后传输。
- 减少请求 :将资源数目多、体积小、频繁创建http请求的文件合并,比如JS合并、矢量图 SVG。
- 请求转移第三方:将请求转移至第三方(例如oss),这样可以减少与服务端的交互。
缓存资源
客户端可以缓存资源从而减少向服务端的请求。
常见的资源缓存就是图片、样式和脚本。还有些场景可以利用客户端的缓存帮助服务端分担压力,比如网约车中的预估价格,客户端可以缓存计算规则,这样就可以减少对服务端的请求。
资源解析优化
我们知道页面中资源解析的顺序是从上到下,如果上面有改变下面也需要变动。所以我们要缩小回流、重绘的范围,比如虚拟dom。
除此之外我们还可以利用懒加载和预加载进行优化:
- 懒加载:将原来一次性要加载的拆分成多次加载,减少下载数量和耗时。比如:树节点、折叠面板、二级菜单等。
- 预加载:在当前页面提前对下个页面进行解析、拉取资源。如下面的参考代码
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel='dns-prefetch" href="www.baidu.com" >
<link rel='preload" href="..js" >
<link rel='prefetch" href="..js" >
使用CDN
CDN的应用在客户端——>服务端之前。
什么是CDN?
CDN能够实时的根据网络流量和各节点的连接、负载情况以及到用户的距离和响应时间等综合信息将用户的请求导向离用户最近的服务节点上,使用户可以就近取得所需内容,解决网络拥挤的状况,提高用户访问网站的响应速度和成功率。
用一张图片直观的理解下CDN
在系统上线前,我们可以通过CDN服务商购买CDN服务,绑定我们的域名,其他的事就不用我们管了。
服务端使用缓存
服务端缓存的介入其实就是空间换时间,在高并发、高性能的项目中都会引入缓存组件来提高系统的性能和并发能力,例如 redis、memcache、guava。
在使用缓存组件时要规避一系列问题,比如《》、《》
构建服务端集群
为了抵抗大量的请求,也为了系统的高可用,通常高并发系统都存在集群节点。
(前提条件为:系统是无状态的)
构建构建服务端集群后,可以利用负载策略将一个请求分发到集群中的某个计算节点。这样,我们的系统就可以抵抗大量的请求。
通过Nginx、LVS、Keepalived等集群组件可以轻松的实现这一功能。
数据库的应用
所有的业务数据最终都会落到数据库,随着数据量的增加系统的负载也会不断上升,数据库单点压力会越来越大,这个时候对数据库的优化就不单仅是冗余、反范式、索引,可以根据业务场景参考以下方案。
表分区
很基础的知识点:在mysql中,一张表的数据对应一个ibd文件,当文件过大时查找数据就会变的很慢。
表分区是将一张表按照hash、list、key等规则进行分流,在物理上将这一张表ibd文件分成多个文件,但是逻辑上还是一张表。这样就不会因为表数据量过大而影响性能。
表分区适合访问量不大但是数据很多的表,采用这种方式的好处就是:查询的数据在一个分区时会很快的查到。
分库分表
分库分表是真正的把一张表分布在不同的库或多张表,在访问量大的时候可以把一张表分多张表,并且分布在不同的库减少单个数据库的压力,同时还可以提高并发。
但是这种方式往往会带来分布式id、事务、聚合查询等一系列问题,我们只能通过代码层面来解决,无疑增加了复杂度。
读写分离
在读多写少的场景下我们可以利用shardingjdbc、mycat等开源框架实现读写分离。
同样,读写分离也会带来一定的问题,比如主从复制问题、时间差问题等。这些问题都需要结合业务场景去规避。
服务治理
在高并发的项目中,往往我们的后端服务是很庞大的,一旦某个服务出现问题都会引起服务雪崩,导致系统瘫痪。
如何解决这些问题,让服务更稳定地运行?
我们管它叫作服务治理。
服务治理通常有以下几种方案:
- 降级:在高并发的请求下,如果服务资源不足,我们可以丢车保帅。比如将某个非核心业务进行下线。
- 熔断:通常服务间的调用可能会发生服务雪崩的情况,为了防止这种情况发生,我们为单个服务设置保险丝,当调用的服务不能访问后禁止调用,避免资源的占用。
- 限流:根据服务的QPS、线程数情况限制请求量或资源量进行自我保护。
- 隔离:隔离通常是为了达到不因某个资源异常而影响其他资源的正常使用。比如数据隔离(分库)、机器隔离(重要的用户单独配置服务器)等。
总结
做好一个高流量高并发的系统,不论前端还是后端,过程中每一个步骤都是至关重要的。除了满足功能性,还要考虑兼容性、易用性、可靠性、安全性、可维护性、可移植性等软件质量。同时要对系统的吞吐量、并发数、平均响应时间等指标要完全掌握,在指标异常时可以快速做出决策避免一系列问题发生。