🎉限流是保护高并发系统的三把利器之一,另外两个是缓存和降级。限流在很多场景中用来限制并发和请求量,比如说秒杀抢购,保护自身系统和下游系统不被巨型流量冲垮等。
限流的目的是对并发访问的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务或进行流量控制,主要是避免在流量高峰导致系统崩溃,造成系统不可用问题。
一、大多数限流目的
- 为了保护自身系统不被举行流量冲垮。
- 调用第三方平台,为了适配第三方平台接口QPS限制瓶颈。
二、限流基本算法
常见的限流算法有:
- 计数器算法(固定时间窗口)
- 滑动时间窗口
- 漏桶算法(leakyBucket)
- 令牌桶算法(tokenBucket)
2.1 计数器算法
计数器法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。一般的做法是:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个 请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter,具体算法的示意图如下:
具体的伪代码如下:
计数器算法一般用在单一维度上的访问频率限制,比如说:短信验证码,每个60秒只发送一次或者接口的调用次数等等。
2.2 滑动时间窗口
这个名称要跟TCP的窗口滑动区分开来,但是理解之后会发现其实也是有点相似。计数器算法对流量的限制比较粗放,而滑动时间窗口的算法则是对流量进行更加平稳的控制。上面的计数器的单位时间是1分钟,而在使用滑动时间窗口,可以把1分钟分成6格,每格时间长度是10s,每一格又各自管理一个计数器,单位时间用一个长度为60s的窗口描述。一个请求进入系统,对应的时间格子的计数器便会+1,而每过10s,这个窗口便会向右滑动一格。只要窗口包括的所有格子的计数器总和超过限流上限,便会拒绝后面的请求。
Spring Cloud中的Hystrix和Spring Cloud Alibaba中的Sentinel都默认采用的滑动窗口的限流算法。
2.3 漏桶算法
从图中我们可以看到,整个算法其实十分简单。算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。
不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
算法实现:准备一个队列,用于保存请求,另外通过一个线程池定期从队列中获取请求并执行,可以一次性获取多个并发执行。
算法弊端:无法应对短时间的突发流量。
2.4 令牌桶算法
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限 制请求调用的速率,而令牌桶算法能够在限 制调用的平均速率的同时还允许一定程度的突发调用。
在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。
放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
算法实现:准备一个队列,用于保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
🤔令牌桶和漏桶对比:
令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时,则拒绝新的请求;漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;- 在流量低峰的时候,令牌桶就会出现堆积,因此当出现限流高峰期的时候,需要有足够多的令牌可以获取,因此令牌桶能够处理突发的流量。
令牌桶允许一定程度的突发,而漏桶主要目的是平滑流出速率。
👣Guava RateLimiter
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用。RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率,它支持两种获取Permits接口,一种是如果拿不到立刻返回false(tryAcquire()),一种会阻塞等待一段时间看能不能拿到(tryAcquire(long timeout, TimeUnit unit))。
使用tryAcquire方法获取令牌的示例代码:
若想保证所有的请求都被执行,而不会被抛弃的话,可以选择使用acquire方法:
Guava RateLimite的限流算法具体详细介绍可以阅读:超详细的Guava RateLimiter限流原理解析
三、集群分布式限流
- Redis+Lua实现
- Nginx+Lua实现
前面讨论的几种算法都属于单机限流的范畴,但是业务需求五花八门,简单的单机限流,根本无法满足他们。
比如为了限制某个资源被每个用户或者商户的访问次数,5s只能访问2次,或者一天只能调用1000次,这种需求,单机限流是无法实现的,这时就需要通过集群限流进行实现。
如何实现?为了控制访问次数,肯定需要一个计数器,而且这个计数器只能保存在第三方服务,比如redis。
大概思路:每次有相关操作的时候,就向redis服务器发送一个incr命令,比如需要限制某个用户访问/index接口的次数,只需要拼接用户id和接口名生成redis的key,每次该用户访问此接口时,只需要对这个key执行incr命令,在这个key带上过期时间,就可以实现指定时间的访问频率。
推荐参考博主Gitee码云 :基于Redis+Spring Aop实现多种策略模式限流
😘欢迎star&follow!!!