常见的限流算法和具体落地实现

对常见高并发场景的限流算法,具体限流实现进行分析,单机版的限流方法和分布式的限流方法

https://blog.csdn.net/abc592328292/article/details/80295837

https://www.cnblogs.com/java1024/p/7725632.html

https://blog.csdn.net/sinat_27535209/article/details/102904693

https://www.jiagou1216.com/blog/plan/842.html

https://blog.csdn.net/weixin_44742132/article/details/106233463?utm_medium=distribute.pc_relevant.none-task-blog-title-3&spm=1001.2101.3001.4242

https://blog.csdn.net/u010889990/article/details/81169328

在大数据量高并发访问时,经常会出现服务或接口面对暴涨的请求而不可用的情况,甚至引发连锁反映导致整个系统崩溃。此时你需要使用的技术手段之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流 。

 

缓存

 

缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。大型网站一般主要是“读”,缓存的使用很容易被想到。在大型“写”系统中,缓存也常常扮演者非常重要的角色。比如累积一些数据批量写入,内存里面的缓存队列(生产消费),以及HBase写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也可以认为是一种分布式的数据缓存。

降级

服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。

限流

限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。

一、限流算法

高并发系统设计的3个利器:缓存、限流、降级,本文就限流相关算法,分析其设计与实现。

从分布式角度来看,限流可分为分布式限流(比如基于Sentinel或者 Redis的集群限流)和单机限流。

从算法实现角度来看,限流算法可分为漏桶算法(leaky bucket)、令牌桶算法(Token Bucket)、滑动时间窗口算法、计数器算法。下面主要分析这4种限流算法和分布式限流实现方案。

1、计数器算法

计数器限流算法也是比较常用的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。也是最简单粗暴的算法。

(1)用原子类进行计数(AtomicInteger 本身是线程安全的)

public class CountRateLimiterDemo {  
   
    private static AtomicInteger count = new AtomicInteger(0);  
   
    public static void exec() {  
        if (count.get() >= 5) {  
            System.out.println("请求用户过多,请稍后在试!"+System.currentTimeMillis()/1000);  
        } else {  
            count.incrementAndGet();  
            try {  
                //处理核心逻辑  
                TimeUnit.SECONDS.sleep(1);  
                System.out.println("--"+System.currentTimeMillis()/1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } finally {  
                count.decrementAndGet();  
            }  
        }  
    }  
}  

使用AomicInteger来进行统计当前正在并发执行的次数,如果超过域值就简单粗暴的直接响应给用户,说明系统繁忙,请稍后再试或其它跟业务相关的信息。

弊端:使用 AomicInteger 简单粗暴超过域值就拒绝请求,可能只是瞬时的请求量高,也会拒绝请求。

(2)用信号量进行计数

public class CountRateLimiterDemo {  
   
    private static Semaphore semphore = new Semaphore(50);  
   
    public static void exec() {  
        if(semphore.getQueueLength()>100){  
            System.out.println("当前等待排队的任务数大于100,请稍候再试...");  
        }  
        try {  
            semphore.acquire();  
            // 处理核心逻辑  
            TimeUnit.SECONDS.sleep(1);  
            System.out.println("--" + System.currentTimeMillis() / 1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            semphore.release();  
        }  
    }  
}  

使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。
相对Atomic优点:如果是瞬时的高并发,可以使请求在阻塞队列中排队,而不是马上拒绝请求,从而达到一个流量削峰的目的。

2、令牌桶算法(使用guava的RateLimiter)

(1)什么是令牌桶算法?(匀速往桶里放令牌,一个请求则要去桶里取一个令牌。没令牌拒绝服务,满令牌拒绝新入令牌)

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 当桶满时,新添加的令牌被丢弃或拒绝。

令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:

  • 令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。
  • 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
  • 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

令牌算法是根据放令牌的速率去控制输出的速率,也就是上图的to network的速率。to network我们可以理解为消息的处理程序,执行某段业务或者调用某个RPC。

使用Guava的RateLimiter实现令牌桶算法(要自己先导入Guava依赖)

public class RateLimiterDemo {
    private static RateLimiter limiter = RateLimiter.create(5);
 
    public static void exec() {
        limiter.acquire(1);
        try {
            // 处理核心逻辑
            TimeUnit.SECONDS.sleep(1);
            System.out.println("--" + System.currentTimeMillis() / 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Guava RateLimiter 提供了令牌桶算法可用于平滑突发限流策略。
该示例为每秒中产生5个令牌,每200毫秒会产生一个令牌。
limiter.acquire() 表示消费一个令牌。当桶中有足够的令牌时,则直接返回0,否则阻塞,直到有可用的令牌数才返回,返回的值为阻塞的时间。

例2:
 

RateLimiter rateLimiter = RateLimiter.create(2); 

System.out.println(rateLimiter.acquire(5)); 

System.out.println(rateLimiter.acquire(2)); 

System.out.println(rateLimiter.acquire(1));

打印结果:

当第一次取5个令牌时,此时令牌桶中虽然没有5个,但有令牌就可以取成功了(后面会边生产边获取令牌),此时返回这次获取第一个令牌的等待时间即0.0s

第二次取2个令牌,此时因为第一次要取5个,而没秒又只生产2个,所以第二次要取到第一个令牌要阻塞2.5s(大约时间)(阻塞时间等于第一次要获取完的时间),当2.5s后获取到第一个令牌则返回阻塞时间2.480....

第三次类似,要等第二次的令牌拿完所以要阻塞大约1s。

为什么第二次的阻塞时间等于前面一次获取完令牌的时间?

——因为当第一次获取到第一个临牌时,会立刻返回阻塞时间,然后就会去执行第二次的取临牌的代码了,然而此时令牌桶不够,而第一次比第二次早,所以要等前面的5个都取完才能轮到第二次去取令牌。第二次和第三次也依次类推。

当然也可以使用tryAcquire来非阻塞的获取,可以实时返回结果。另外tryAcquire也可以传入参数,也就是等待的时间,超时直接返回false。这点等同于常见的lock,tryLock。

3、露桶算法(所有请求进来,但我匀速放行)

漏桶算法即leaky bucket是一种非常常用的限流算法,可以用来实现流量整形(Traffic Shaping)和流量控制(Traffic Policing)。贴了一张维基百科上示意图帮助大家理解:


漏桶算法的主要概念如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴;

  • 如果桶是空的,则不需流出水滴;

  • 可以以任意速率流入水滴到漏桶;

  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量,数据可以以任意速度流入到漏桶中。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。 漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶为空,则不需要流出水滴,如果漏桶(包缓存)溢出,那么水滴会被溢出丢弃。

漏桶算法比较好实现,在单机系统中可以使用队列来实现(.Net中TPL DataFlow可以较好的处理类似的问题,你可以在这里找到相关的介绍),在分布式环境中消息中间件或者Redis都是可选的方案。

漏桶和令牌桶的比较

令牌桶可以在运行时控制和调整数据处理的速率,处理某时的突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。

整体而言,令牌桶算法更优,但是实现更为复杂一些。

4、滑动时间窗口算法(sentinel其中有实现滑动窗口算法)

滑动时间窗口算法就是根据当前时间获取对应的时间窗口,时间窗口保存有流量相关的统计值,根据该统计值判断是否触发流控。

一般来说,时间窗口可以循环复用,在复用时重新初始化即可,具体实现可参考sentinel的滑动窗口实现。滑动时间窗口能够支持的瞬时流量最大可为该窗口上限,而令牌桶算法能够支持的瞬时流量最大为桶大小;注意,滑动时间窗口算法中获取token数量一次最大不能超过窗口上限,而RateLimiter实现的令牌桶可以支持一次获取超过桶大小的token。

二、其他常用的分布式限流方案

nginx、tomcat、gateway、sentnel、redis等等

Sentinel

Sentinel分布式限流是启动一个token server服务器,其他sentinel client端就是token client端,当做限流操作时,从token server获取token,获取成功表示未触发限流;否则表示触发了限流;通信出现异常,可配置降级走本地Sentinel限流机制。分布式限流文档:Sentinel集群流控

sentinel的分布式限流是token client调用以下方法到服务端获取token,相当于是每次都会获取acquireCount个token:

//获取令牌Token, 参数规则Id,获取令牌数,优先级 
TokenResult requestToken(Long ruleId, int acquireCount, boolean prioritized); 

sentinel的其他限流配置可以在客户端/代码中设置。这里就不多讲了。

基于Redis限流

基于Redis做限流操作,使用lua脚本保证命令原子性,比如qps设置为10,如果key不存在,就设置key过期时间1s,value=1;如果value小于10,则自增value;value达到10触发流控。示例lua代码如下:

local key = "rate.limit:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]
 
local is_exists = redis.call("EXISTS", key)
if is_exists == 1 then
    if redis.call("INCR", key) > limit then
        return 0
    else
        return 1
    end
else
    redis.call("SET", key, 1)
    redis.call("EXPIRE", key, expire_time)
    return 1
end

常用的限流算法有漏桶、令牌桶和滑动窗口,根据具体场景可选择不同限流算法;如果需要集群限流,可选用Sentinel或者基于Redis做分布式限流。

其他限流方案看:https://www.jiagou1216.com/blog/plan/842.html

https://blog.csdn.net/weixin_44742132/article/details/106233463?utm_medium=distribute.pc_relevant.none-task-blog-title-3&spm=1001.2101.3001.4242

微服务组件中网关gateway限流方案:看https://blog.csdn.net/u010889990/article/details/81169328

https://www.cnblogs.com/forezp/p/10140316.html

https://www.cnblogs.com/pu20065226/p/11426279.html

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值