应对高并发其实是个复杂的系统性的任务,需要考虑的因素很多。我们将以一种自上而下的思考方式来梳理一下什么是高并发、高并发的分类、如何应对等,并在梳理的过程中,进行一些相关知识点的串联。
通产意义上讲,我们每天上网(比如刷抖音、购物、看新闻等)其实是一次次的网络访问。简单来讲,网络访问由网络请求(Request)和网络响应(Response)。
经典面试题:浏览器中输入www.baidu.com,展示了网页,都经历了什么?
在浏览器地址栏中输入网址
浏览器获取这个网址之后,会先去缓存中看看有没有要访问的资源,从浏览器缓存-系统缓存-路由缓存中查看,如果有就不再进行http请求,直接从缓存中加载资源。
浏览器拿到域名自动去向DNS(域名系统)服务器发起请求,查询用户输入的域名对应的ip地址。
这一步很多幺蛾子:
DNS优化 1.DNS缓存 DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。 2.DNS负载均衡 DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,
浏览器拿到ip地址后,通过Ip地址和端口号和服务器建立tcp连接(三次握手)
建立连接成功之后,浏览器开始向服务器发起http请求,并通过http协议将请求信息包装成请求报文(包含请求行、请求头、空行、请求体),然后通过socket发送到服务器。
注:url与uri的区别:
uri:统一资源标志符(Uniform Resource Identifier, URI),指示每一个资源,由三部分组成:资源的命名机制、存放资源的主机名、资源自身的名称
url:URL是URI的一个子集。它是Uniform Resource Locator的缩写,译为“统一资源定位符”。格式:protocol :// hostname[:port] / path / ;parameters#fragment
url格式由三部分组成:
①第一部分是协议(或称为服务方式)。
②第二部分是存有该资源的主机IP地址(有时也包括端口号)。
③第三部分是主机资源的具体地址,如目录和文件名等。
两者区别:URI和URL都定义了资源是什么,但URL还定义了该如何访问资源。URL是一种具体的URI,它是URI的一个子集,它不仅唯一标识资源,而且还提供了定位该资源的信息。URI 是一种语义上的抽象概念,可以是绝对的,也可以是相对的,而URL则必须提供足够的信息来定位,是绝对的。
后端处理完后发送报文给浏览器
浏览器按照HTTP协议将报文解析出来
浏览器拿到响应报文中响应体的数据开始渲染html、css,执行JS
如果在解析过程中(从上到下)中,发现有外链的标签(link、css、img),浏览器会自动对该标签的路径地址发起新的请求,同上。
定性:单位时间内,非常密集的网络请求。
定量:并发到底有多密集,一般用QPS来表示。
结果:如果没有好的应对措施或者架构建设,常见的结果是:用户发出网络请求,但网络响应延迟严重,甚至得不到网络响应。
应对高并发的难点在于,高并发来到的时候,高可用无法保证。
极端的例子:恶意的高并发:DDOS(Distributed Denial of Service)攻击。找很多ip抢占流量、耗费服务器资源。
网络请求分两种:读请求、写请求
对应的高并发场景也可分两种:高并发读场景、高并发写场景(往往伴随有高并发读)。
应对和讨论高并发场景的时候,我们首先要分清我们是应对高并发读场景还是应对高并发写场景。
现象:单位时间内,读请求的流量突然变大了;可能会导致服务响应变慢,甚至无法提供服务。
应对高并发读,通常的思路有分布式和缓存。
其他思路,还有类似限流、降级、熔断这种偏防御型策略。
分布式
关键词:More
思路:靠量取胜,一台不行就一百台,一百台不行就一千台。
分布式是一种思想或者架构。支持高并发的同时,带来高可用性。
常见名词:分布式系统、分布式集群、分布式计算、分布式存储、分布式锁、分布式事务。
分布式的存在,解决问题的同时,也极大地带来了服务端技术的复杂性。
分布式相关问题:
1.负载均衡:
a.常见的负载均衡算法:轮询(Round Robin)、随机、加权随机、最小连接数等。
b.动态的负载均衡算法:一致性哈希
2.分布式协议算法:Paxos、Raft、ZAB、Gossip
3.CAP理论、BASE理论
注:分布式和集群的区别
集群:很多人干一个活,所有人干一样的事
分布式:很多人干一个大活,每个人负责一部分小活
缓存
关键词:Better
思路:如果用铁锹不行,那用挖土机呢?如果DB撑不住,用缓存呢?
缓存的使用,利用了计算机存储介质访问速度的金字塔原理。
缓存也可以看作是一种思想,让更快的缓存来提供访问的结果。
缓存的使用在各个维度都有,到处都是。例如Nginx、浏览器、CDN、MySQL、操作系统等。
常见的缓存,内存(Local Cache)、分布式缓存(Redis Memcached)
缓存的使用,也会带来相关的复杂性。比如:
缓存的容量相对不大,需要提高缓存的命中率,让高频的热的数据存在于缓存中。
缓存的预加载和过期策略;
缓存的替换算法:LRU、LFU等;
缓存或者多级缓存的使用,当有数据写操作时,会涉及到数据如何同步。
缓存一致性问题
缓存失效情况的考虑和处理
缓存穿透:访问一个缓存和DB都不存在的key
接口鉴权:调之前先看你有没有权利调
缓存空值:缓存你攻击的key,value为空值
布隆过滤器:判断不存在的,则一定不存在;判断存在的,大概率存在;比HashMap节省空间==》
用三个哈希函数算三个值,输入的key如果没有重合则一定不存在,如果重合则可能存在(因为可能组合不同)
缓存击穿:热key过期,导致大量请求打到DB上=》热key不过期,关注更新策略
缓存雪崩:大量key或者热key同时或者密集过期,导致系统压力骤增,引起雪崩==》过期时间打散
缓存数据结构的选择、关注大KEY(会影响redis的响应速度)
以下是防御型措施:
关键词:Limit
思路:既然处理不了那么多量,那就排个队~
限流通常是用消息队列来实现的,几种常见的限流算法:漏桶、令牌桶
漏桶算法
通过漏桶算法来进行限流,比如每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
(连桶都放不下的请求就直接拒绝)
特点:没有流量红峰,而且对于突发流量一点变法都无,无法一定程度上应对突然增加的流量。
令牌桶算法
在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待可用的令牌、或者直接拒绝。
令牌桶算法,除了能限制数据的平均传输速率外,还能允许某种程度的突发流量。==》之前有一段空闲累积了很多令牌的话,下一次请求可以拿所有空闲令牌。
降级:在有限的资源情况下,为了能抗住大量的请求,就需要对系统的分支功能做出一些牺牲,有点“弃卒保帅”的意思。放弃一些功能,保证整个系统能平稳运行。
熔断:系统中,由于某些原因使得服务出现了过载现象,为了防止造成整个系统故障,从而采用一种保护措施,暂时“熔断”对下游的访问。所以很多地方把熔断亦称为过载保护,熔断一般还可以自动检测修复。
降级和熔断在严格意义上说不是应对高并发的,而是尽量保证在高并发下的高可用程度和万一出现问题的恢复速度。
大部分高并发写场景,也会同时伴随高并发读,并且可能读的并发量比写请求的并发量还大。
幂等问题(其任意多次执行所产生的影响均与一次执行的影响相同)
数据一致性问题
写覆盖或写乱序
写与读之间的延迟
原子性(或者全部执行、或者全部不执行)
对于幂等问题,一般的思路是使用分布式锁(有一个共有的地方能够控制,只有一份有效);
对于数据一致性问题
写覆盖或写乱序:使用锁
写与读之间的延迟:保证最终一致性
原子性:使用分布式事务
分布式锁
先介绍一下锁的分类:悲观锁、乐观锁
悲观锁:悲观主义者,做事之前先加锁,先取锁再访问。具有强烈的独占和排他特性(读写都排斥的锁&读写锁)。
乐观锁:CAS(MVCC用版本号解决ABA问题),乐观主义者,先做事,再检查,不行就重试。乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户
乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
分布式锁:分布式环境中,不同进程间的协同并发资源要使用的锁。一般的实现方式是基于Redis、Zookeeper或者数据库。
缓存一致性
缓存一致性指缓存中的数据与数据库中的数据是一致的。
一、缓存主要用于读取数据、数据库用于更新数据
数据读取的时候:先查缓存,缓存查不到数据库,然后把查到的结果放到缓存中。(要及时更新缓存)
数据更新的时候:
不管什么顺序都有一定的风险:
先删除缓存,再更新数据库:删除缓存后有人访问数据库,则缓存里最终是未更新前的记录
先更新数据库再更新缓存:若多数人更改,应该先做修改的先刷新进缓存,后做修改的后刷新进缓存,但是有可能前者延迟才做,导致缓存值错误。
先更新数据库,再删除缓存(这种情况风险最小);
问题一:在缓存中把热点数据删除了。
问题二:如果缓存删除失败,则错误数据永久地存在于缓存当中。
缓存设置过期时间,保证最终一致性;
二、缓存主要用于读取和存储,数据库用于最终存储(数据库用作备份)
数据读取的时候:先查缓存,一般所有需要读取的数据会预加载在缓存中。
数据更新的时候:先更新缓存,再异步(可以批量)更新数据库。(因为直接更新数据库性能不好)(例如用AOF做持久化)
基于分布式缓存redis的实践
1.分布式锁
SET lock_key unique_value NX PX 10000
第一次SET成功的,拿到锁;拿到的锁,有过期时间;拿到锁后,不需要锁时,删除锁。
为什么加锁操作要设置过期时间?==》避免死锁
加了过期时间就没问题了吗?==》Redis集群中会出现两个进程都拿到锁的情况:master拿锁之后还没同步到slave上就挂了,结果slave升级为master,另一个进程又从新的master上又拿锁,结果又拿到了。==》解决方案:红锁:一个锁打散,一个进程要拿好几次,拿到超过半数才算拿到了。
2.扣减库存
针对减库存的高并发场景,更好的方法是Redis的INCR(或者DECR)(不用管原来是什么值,不用先读后写,改完返回目前值,并且这个操作是原子的,Redis自己已经处理好并发同步问题了。)避免了先读后写的尴尬。
CAP理论指的是一个分布式系统最多只能同时满足一致性(Consistency,正确性,能够读到最近的写,比如修改数据后可以立刻读到;不能一半用户读到了新数据,另一半读到了老数据,要所有用户读到相同的数据)、可用性(Avaliablity,要保证所有的请求都有响应,不管响应中的值是否正确,只要能读到值就是可用的)、分区容错性(Partition tolerance,在分布式环境中,不同机器时间要相互通信,在对外提供服务时,如果互相之间的通信(网络)出现问题了,那首先还是要保证对外的服务是正常的)
事实上,我们只能同时满足CAP中的两项:
CA:相当于不是一个分布式数据库了,相当于满足ACID的单机数据库,而不再是分布式的。
PC:主从复制时不能提供服务,死抓同步,没法及时响应
PA:复制之间也可以提供服务,只是可能不同的从状态不同
BASE理论:
基本可用(Basically Available)软状态(Soft State)最终一致性(Eventually Consistent),主要是牺牲了强一致性。 只要几个从主机最终都和主机一样是对的就行,不需要在过程中时时刻刻都是正确的,抛弃了强一致性。
分布式事务
分布式事务指事务的操作(很多台数据库和很多台服务器)位于不同的节点上,需要保证事务的ACID特性,要么都成功,要么都失败。
例如,在下单场景下,订单生成、扣减库存、扣减金额可能不在同一个节点上,就会涉及分布式事务。
基于DB:两阶段协议、三阶段协议
两阶段协议:对锁机制,保证事务可串行性的最常用协议是两阶段封锁协议。该协议要求每个事务分两个阶段提出加锁和解锁申请:
(1)增长阶段。事务可以获得锁,但不能释放锁。
(2)缩减阶段。事务可以释放锁,但不能获得锁。
一开始,事务处于增长阶段,事务根据需要获得锁。一旦该事务释放了锁,它就进入缩减阶段,不能再发出加锁请求。
两阶段封锁协议实现了事务集的串行化调度,但同时,一个事务的失败可能会引起一连串事务的回滚。为避免这种情况的发生,我们需要进一步加强对两阶段封锁协议的控制,这就是:严格两阶段封锁协议和强两阶段封锁协议。
严格两阶段封锁协议除了要求封锁是两阶段之外,还要求事务持有的所有排它锁必须在事务提交之后方可释放。这个要求保证未提交事务所写的任何数据,在该事务提交之前均以排它锁封锁,防止其他事务读取这些数据。
强两阶段封锁协议,要求事务提交之前不得释放任何锁。使用锁机制的数据库系统,要么使用严格两阶段封锁协议,要么使用强两阶段封锁协议。
两阶段封锁协议并不保证不会发生死锁,数据库系统必须采取其他的措施,预防和解决死锁问题。
基于系统:TCC(try,confirm,cancel)TCC
基于本地消息表、消息队列