什么是限流
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。
为什么要限流
Java程序员在大厂面试时 或者 想拿到一份不错的薪资时,肯定会涉及到分布式程序的开发。在分布式程序中有各种各样的问题需要我们去解决,今天我们就和大家聊聊分布式限流。
首先我们先聊聊为什么要限流 我们先来看看下面的这两张图片
这是十一黄金周的景区的奇观,景区如果不对旅游的人员做限制就会出现图片上的样子。因为不做限制会超出景区的服务极限,一旦请求多到超出它的处理极限就会崩溃。为了不出现最坏的崩溃情况,只能耽误一下大家进入景区的时间。
由于互联网公司的流量巨大,系统上线会做一个流量峰值的评估,尤其是像各种秒杀促销活动,为了保证系统不被巨大的流量压垮,会在系统流量到达一定阈值时,拒绝掉一部分流量。
限流会导致用户在短时间内(这个时间段是毫秒级的)系统不可用,一般我们衡量系统处理能力的指标是每秒的QPS或者TPS,假设系统每秒的流量阈值是1000,理论上一秒内有第1001个请求进来时,那么这个请求就会被限流。
限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。
限流的算法
限流的算法主要有三大类 分别是 计数器限流, 漏桶 和 令牌桶。其中计数器进行粗暴的限流又分为固定时间窗口限流和滑动时间窗口限流。
计数器也可以进行粗暴限流
限流算法-计数器(固定窗口)
将某一个时间段当做一个窗口,在这个窗口内存在一个计数器记录这个窗口接收请求的次数,每接收一次请求便让这个计数器的值加一,如果计数器的值大于请求阈值的时候,即开始限流。当这个时间段结束后,会初始化窗口的计数器数据,相当于重新开了一个窗口重新监控请求次数。客户端在第一分钟的59秒请求100次,在第二分钟的第1秒又请求了100次, 2秒内后端会受到200次请求的压力,形成了流量突刺
限流算法-计数器(滑动窗口)
滑动窗口其实是细分后的计数器,它将每个时间窗口又细分成若干个时间片段,每过一个时间片段,整个时间窗口就会往右移动一格
时间窗口向右滑动一格,这时这个时间窗口其实已经打满了100次,客户端将被拒绝访问,时间窗口划分的越细,滑动窗口的滚动就越平滑,限流的效果就会越精确
漏桶
漏桶算法类似一个限制出水速度的水桶,通过一个固定大小FIFO队列+定时取队列元素的方式实现,请求进入队列后会被匀速的取出处理(桶底部开口匀速出水),当队列被占满后后来的请求会直接拒绝(水倒的太快从桶中溢出来)
优点是可以削峰填谷,不论请求多大多快,都只会匀速发给后端,不会出现突刺现象,保证下游服务正常运行 , 缺点就是在桶队列中的请求会排队,响应时间拉长
令牌桶
令牌桶算法是以一个恒定的速度往桶里放置令牌(如果桶里的令牌满了就废弃),每进来一个请求去桶里找令牌,有的话就拿走令牌继续处理,没有就拒绝请求
令牌桶的优点是可以应对突发流量,当桶里有令牌时请求可以快速的响应,也不会产生漏桶队列中的等待时间, 缺点就是相对漏桶一定程度上减小了对下游服务的保护
限流解决方案
限流方案分析
限流的解决方案主要分为单机限流和分布式限流,这两种限流方案又有很多具体的实现如下:
单机限流
Java单机限流可以使用AtomicInteger,RateLimiter或Semaphore来实现
分布式限流
常用的方案有Nginx限流和Spring Cloud Gateway Sentinel
限流方案实现举例
单机限流AtomicInteger
```java
private int maxCount = 10;
private long interval = 60;
// 原子类计数器
private AtomicInteger atomicInteger = new AtomicInteger(0);
private long startTime = System.currentTimeMillis();
public boolean limit(int maxCount, int interval) {
atomicInteger.addAndGet(1);
if (atomicInteger.get() == 1) {
startTime = System.currentTimeMillis();
atomicInteger.addAndGet(1);
return true;
}
// 超过了间隔时间,直接重新开始计数
if (System.currentTimeMillis() - startTime > interval * 1000) {
startTime = System.currentTimeMillis();
atomicInteger.set(1);
return true;
}
// 还在间隔时间内,check有没有超过限流的个数
if (atomicInteger.get() > maxCount) { return false; }
return true;
}
```
分布式限流Gateway
分布式限流本质上是一个集群并发问题,Redis单进程单线程的特性,天然可以解决分布式集群的并发问题。所以很多分布式限流都基于Redis,比如说Spring Cloud的网关组件Gateway。Gateway的底层实现是Redis + Lua。
Redis执行Lua脚本会以原子性方式进行,单线程的方式执行脚本,在执行脚本时不会再执行其他脚本或命令。并且,Redis只要开始执行Lua脚本,就会一直执行完该脚本再进行其他操作,所以Lua脚本中不能进行耗时操作。使用Lua脚本,还可以减少与Redis的交互,减少网络请求的次数。
Gateway在SpringCloud中的具体配置如下
```yaml
spring:
redis:
host: 127.0.0.1
port: 6379
application:
name: lagou-cloud-gateway
cloud:
gateway:
routes: # 路由可以有多个
- id: service-resume-router # 我们自定义的路由 ID,保持唯一
uri: http://127.0.0.1:8081 # 目标服务地址
predicates: # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
- Path=/resume/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
key-resolver: "#{@ipKeyResolver}"
```
大家对限流案例感兴趣可以加微信 Geanmingti 领取相关的资料。
分布式限流 sentinel
Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。sentinal的功能非常强大,这些强大的功能在拉勾的课程里都会讲到,当然拉勾的课程肯定不只有限流,大家可以参考拉勾教育官网咨询Java相关的课程大纲。大家对上面课程感兴趣可以加微信 Geanmingti 领取相关的资料。
免费获取限流课程资料!
仅前 50 人有效,先到先得!