字节后端训练营笔记(三)

高并发专题

透过现象看本质

应对高并发其实是个复杂的系统性的任务,需要考虑的因素很多。我们将以一种自上而下的思考方式来梳理一下什么是高并发、高并发的分类、如何应对等,并在梳理的过程中,进行一些相关知识点的串联。

通产意义上讲,我们每天上网(比如刷抖音、购物、看新闻等)其实是一次次的网络访问。简单来讲,网络访问由网络请求(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抢占流量、耗费服务器资源。

高并发的分类

网络请求分两种:读请求、写请求

对应的高并发场景也可分两种:高并发读场景、高并发写场景(往往伴随有高并发读)。

应对和讨论高并发场景的时候,我们首先要分清我们是应对高并发读场景还是应对高并发写场景。

高并发读

现象:单位时间内,读请求的流量突然变大了;可能会导致服务响应变慢,甚至无法提供服务。

字节后端训练营笔记(三)_第1张图片 

通常的应对思路

应对高并发读,通常的思路有分布式和缓存。

其他思路,还有类似限流、降级、熔断这种偏防御型策略。

分布式

关键词:More

思路:靠量取胜,一台不行就一百台,一百台不行就一千台。

字节后端训练营笔记(三)_第2张图片

分布式是一种思想或者架构。支持高并发的同时,带来高可用性。

常见名词:分布式系统、分布式集群、分布式计算、分布式存储、分布式锁、分布式事务。

分布式的存在,解决问题的同时,也极大地带来了服务端技术的复杂性。

分布式相关问题:

1.负载均衡:

a.常见的负载均衡算法:轮询(Round Robin)、随机、加权随机、最小连接数等。

b.动态的负载均衡算法:一致性哈希

2.分布式协议算法:Paxos、Raft、ZAB、Gossip

3.CAP理论、BASE理论

注:分布式和集群的区别

  • 集群:很多人干一个活,所有人干一样的事

  • 分布式:很多人干一个大活,每个人负责一部分小活

缓存

关键词:Better

思路:如果用铁锹不行,那用挖土机呢?如果DB撑不住,用缓存呢?

字节后端训练营笔记(三)_第3张图片

缓存的使用,利用了计算机存储介质访问速度的金字塔原理。

缓存也可以看作是一种思想,让更快的缓存来提供访问的结果。

缓存的使用在各个维度都有,到处都是。例如Nginx、浏览器、CDN、MySQL、操作系统等。

常见的缓存,内存(Local Cache)、分布式缓存(Redis Memcached)

缓存的使用,也会带来相关的复杂性。比如:

  • 缓存的容量相对不大,需要提高缓存的命中率,让高频的热的数据存在于缓存中。

    • 缓存的预加载和过期策略;

    • 缓存的替换算法:LRU、LFU等;

  • 缓存或者多级缓存的使用,当有数据写操作时,会涉及到数据如何同步。

    • 缓存一致性问题

  • 缓存失效情况的考虑和处理

    • 缓存穿透:访问一个缓存和DB都不存在的key

      • 接口鉴权:调之前先看你有没有权利调

      • 缓存空值:缓存你攻击的key,value为空值

      • 布隆过滤器:判断不存在的,则一定不存在;判断存在的,大概率存在;比HashMap节省空间==》

        • 用三个哈希函数算三个值,输入的key如果没有重合则一定不存在,如果重合则可能存在(因为可能组合不同)

    • 缓存击穿:热key过期,导致大量请求打到DB上=》热key不过期,关注更新策略

    • 缓存雪崩:大量key或者热key同时或者密集过期,导致系统压力骤增,引起雪崩==》过期时间打散

  • 缓存数据结构的选择、关注大KEY(会影响redis的响应速度)

以下是防御型措施:

限流

关键词:Limit

思路:既然处理不了那么多量,那就排个队~

字节后端训练营笔记(三)_第4张图片

限流通常是用消息队列来实现的,几种常见的限流算法:漏桶、令牌桶

漏桶算法

通过漏桶算法来进行限流,比如每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。字节后端训练营笔记(三)_第5张图片

(连桶都放不下的请求就直接拒绝)

特点:没有流量红峰,而且对于突发流量一点变法都无,无法一定程度上应对突然增加的流量。

令牌桶算法

在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待可用的令牌、或者直接拒绝。字节后端训练营笔记(三)_第6张图片

令牌桶算法,除了能限制数据的平均传输速率外,还能允许某种程度的突发流量。==》之前有一段空闲累积了很多令牌的话,下一次请求可以拿所有空闲令牌。

降级、熔断

降级:在有限的资源情况下,为了能抗住大量的请求,就需要对系统的分支功能做出一些牺牲,有点“弃卒保帅”的意思。放弃一些功能,保证整个系统能平稳运行。

熔断:系统中,由于某些原因使得服务出现了过载现象,为了防止造成整个系统故障,从而采用一种保护措施,暂时“熔断”对下游的访问。所以很多地方把熔断亦称为过载保护,熔断一般还可以自动检测修复。

降级和熔断在严格意义上说不是应对高并发的,而是尽量保证在高并发下的高可用程度和万一出现问题的恢复速度。

高并发写

大部分高并发写场景,也会同时伴随高并发读,并且可能读的并发量比写请求的并发量还大。

高并发写需要注意的问题

  • 幂等问题(其任意多次执行所产生的影响均与一次执行的影响相同)

  • 数据一致性问题

    • 写覆盖或写乱序

    • 写与读之间的延迟

    • 原子性(或者全部执行、或者全部不执行)

通常的应对思路

对于幂等问题,一般的思路是使用分布式锁(有一个共有的地方能够控制,只有一份有效);

对于数据一致性问题

  • 写覆盖或写乱序:使用锁

  • 写与读之间的延迟:保证最终一致性

  • 原子性:使用分布式事务

分布式锁

先介绍一下锁的分类:悲观锁、乐观锁

悲观锁:悲观主义者,做事之前先加锁,先取锁再访问。具有强烈的独占和排他特性(读写都排斥的锁&读写锁)。

乐观锁: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理论

CAP理论指的是一个分布式系统最多只能同时满足一致性(Consistency,正确性,能够读到最近的写,比如修改数据后可以立刻读到;不能一半用户读到了新数据,另一半读到了老数据,要所有用户读到相同的数据)、可用性(Avaliablity,要保证所有的请求都有响应,不管响应中的值是否正确,只要能读到值就是可用的)、分区容错性(Partition tolerance,在分布式环境中,不同机器时间要相互通信,在对外提供服务时,如果互相之间的通信(网络)出现问题了,那首先还是要保证对外的服务是正常的)字节后端训练营笔记(三)_第7张图片

 

事实上,我们只能同时满足CAP中的两项:

  • CA:相当于不是一个分布式数据库了,相当于满足ACID的单机数据库,而不再是分布式的。

  • PC:主从复制时不能提供服务,死抓同步,没法及时响应

  • PA:复制之间也可以提供服务,只是可能不同的从状态不同

BASE理论:

基本可用(Basically Available)软状态(Soft State)最终一致性(Eventually Consistent),主要是牺牲了强一致性。 只要几个从主机最终都和主机一样是对的就行,不需要在过程中时时刻刻都是正确的,抛弃了强一致性。

分布式事务

分布式事务指事务的操作(很多台数据库和很多台服务器)位于不同的节点上,需要保证事务的ACID特性,要么都成功,要么都失败。

例如,在下单场景下,订单生成、扣减库存、扣减金额可能不在同一个节点上,就会涉及分布式事务。

基于DB:两阶段协议、三阶段协议

两阶段协议:对锁机制,保证事务可串行性的最常用协议是两阶段封锁协议。该协议要求每个事务分两个阶段提出加锁和解锁申请:

(1)增长阶段。事务可以获得锁,但不能释放锁。

(2)缩减阶段。事务可以释放锁,但不能获得锁。

一开始,事务处于增长阶段,事务根据需要获得锁。一旦该事务释放了锁,它就进入缩减阶段,不能再发出加锁请求。

两阶段封锁协议实现了事务集的串行化调度,但同时,一个事务的失败可能会引起一连串事务的回滚。为避免这种情况的发生,我们需要进一步加强对两阶段封锁协议的控制,这就是:严格两阶段封锁协议和强两阶段封锁协议。

严格两阶段封锁协议除了要求封锁是两阶段之外,还要求事务持有的所有排它锁必须在事务提交之后方可释放。这个要求保证未提交事务所写的任何数据,在该事务提交之前均以排它锁封锁,防止其他事务读取这些数据。

强两阶段封锁协议,要求事务提交之前不得释放任何锁。使用锁机制的数据库系统,要么使用严格两阶段封锁协议,要么使用强两阶段封锁协议。

两阶段封锁协议并不保证不会发生死锁,数据库系统必须采取其他的措施,预防和解决死锁问题。

基于系统:TCC(try,confirm,cancel)TCC

基于本地消息表、消息队列

你可能感兴趣的:(字节跳动后端训练营第八期笔记,http,缓存,网络)