当前位置: 首页 > 科技观察

海量请求下的接口并发解决方案

时间:2023-03-17 17:57:31 科技观察

设置一个场景,如果某个商品接口在某段时间突然涨价,会发生什么情况?举个现实生活中的例子,假设当晚冰盾盾上热搜后,立即有数十万人去淘宝下单购买。这个时候对产品的缓存预热和准备工作还没有做好。如何操作?针对这个问题,在电商高并发系统中,接口的保护一般采用:缓存、限流、降级来操作。假设接口经过风控处理,过滤掉一半机器人脚本请求,其余为人工指令请求。服务限速主要目的是限制并发访问/请求的速率,或者限制一个时间窗口内的请求速率。一旦达到限制速率,就可以拒绝、排队或等待以及降级服务。限流算法漏斗算法漏桶算法是请求到达时直接放入桶中,如果当前容量已经达到上限(限流值),则丢弃或者其他策略(触发限流战略)。漏桶以固定的速率(根据服务吞吐量)释放访问请求(即请求通过),直到漏桶为空。漏斗算法的思想是无论你发起多少请求,我的接口消费速度必须小于等于流出速率的阈值。可以基于消息队列来实现1.令牌桶算法令牌桶算法是程序以v(v=时间段/限流值)的速度向令牌桶中添加令牌,直到令牌桶满为止,当请求到达时,从令牌桶中请求一个令牌。如果获取成功,则请求通过。如果收购失败,将触发限流政策。令牌桶算法和漏斗算法的区别在于前者可以允许突发请求。2.滑动窗口算法滑动窗口算法是将一个时间段分成N个小时段,记录每个小时段的访问次数,按照时间滑动删除过期的小时段。如下图,假设时间段为1分钟,将1分钟分成2个小时段,统计每个小时段的访问次数,可以看到第一个时间段的访问次数为75,而在第二个时间段,访问次数为100,如果一个时间段内所有小周期的总和超过100,就会触发限流策略。Sentinel和TCP滑动窗口的实现。接入层限流Nginx限流Nginx限流采用漏桶算法。它可以根据客户端的特点限制其访问频率。客户端的特性主要是指IP、UserAgent等,使用IP比UserAgent更可靠,因为IP不可伪造,UserAgent可以随意伪造。limit_req模块基于IP:Modulengx_http_limit_req_module(nginx.org)tgngine:ngx_http_limit_req_module-TengineWebServer(taobao.org)本地接口限流SemaphoreJava并发库的Semaphore可以轻松完成信号量控制,Semaphore可以控制某个资源被访问的同时访问的数量,通过acquire()获取一个license,没有则等待,release()释放一个license。如果我们对外提供一个服务接口,最大允许并发数为40,我们可以这样做:privatefinalSemaphorepermit=newSemaphore(40,true);publicvoidprocess(){try{permit.acquire();//TODO处理业务逻辑}catch(InterruptedExceptione){e.printStackTrace();}finally{permit.release();}}具体Semaphore实现参见源码。分布式接口使用消息队列限流,无论是用MQ中间件实现还是用RedisList实现,都可以作为缓冲队列使用。这个想法是基于漏斗算法。当某个接口的请求达到一定阈值时,可以启用消息队列对接口数据进行缓冲,根据服务的吞吐量来消费数据。服务降级在做好接口风控的前提下,我们发现接口请求的并发量快速增长。我们可以启用自下而上的解决方案来降级服务。一般服务降级应该用于延迟或暂停某些不重要或非紧急的服务或任务的服务使用。降级解决方案停止边缘业务。比如淘宝双11之前,不能查询三个月前的订单,不能降级边缘业务来保证核心业务的高可用。拒绝请求当接口请求的并发数大于阈值,或者接口上有大量失败的请求时,可以拒绝一些访问请求。DenyPolicyRandomDeny:随机拒绝超过阈值的请求。拒绝旧请求:根据请求的时间,先接收到的请求先被拒绝。拒绝非核心请求:根据系统业务设置核心请求列表,拒绝非核心列表中的请求。恢复计划实施服务降级后,我们可以继续注册多个消费服务来应对突然的流量暴增,以应对并发,然后我们会缓载一些服务器。降级的具体实现可以参考其他文章。在数据缓存做好接口风控的前提下,我们发现接口请求的并发量快速增长。我们可以执行以下操作:使用分布式锁来阻止访问请求。在这短短的时间内,我们可以在缓存中间件中缓存操作行对应的热点数据。请求释放后,让所有请求优先操作缓存数据。然后将操作的结果通过消息队列发送到消费接口慢慢消费。缓存问题假设我们在操作一个库存接口,此时数据库中只有100个库存。那么如果此时我们把一条数据放到缓存中,如果所有的请求都来访问这个缓存,它还是会挂掉,怎么办?读写分离第一个思路是读写分离。利用Redis的哨兵集群模式进行主从复制的读写分离操作。读操作必须大于写操作。当库存消耗为0时,读取操作直接快速失败。负载均衡第二个想法是负载均衡。数据缓存后,如果所有的请求都来缓存操作库存,无论加悲观锁还是乐观锁,并发率都很低。这时候,我们可以拆分库存。我们可以参考ConcurrentHashMap中counterCells变量的设计思路,将100个库存拆分成10个缓存服务,每个缓存服务有10个缓存,然后我们对每个缓存服务的请求进行负载均衡。但是,这种方法存在问题。如果大部分用户hash到同一个缓存,导致其他缓存没有被消费,却没有返回库存,这是不合理的。pagecache的第三个思路,pagecache。这种方式其实在大多数软件架构中都会用到,比如linux内核的硬盘写入,mysql的刷盘等,也就是把短时间内写入操作的聚合结果写入,并且所有写操作都在缓存中完成。