面试官:说说你了解几种限流算法,手写个demo?

news/2024/5/20 6:30:59 标签: 限流, 计数器, 滑动窗口, 漏桶, 令牌桶

前言

在流量突增的场景下,为了保证后端服务在整体上一个稳定性,我们需要对请求进行限流,来避免系统崩溃。

不过限流会对少部分用户的请求直接进行拒绝或者延迟处理,影响这些用户的体验。

本文会介绍一些常见的限流算法,并在最后附上对分布式限流的一些思考。


计数器

计数器算法,也成固定窗口法。可以控制在固定的时间窗口内,允许通过的最大的请求数。

例如,我们设定时间间隔窗口intervalWindow为1分钟,该窗口内的最大请求数max为100。

当第1个请求到来时,我们记录下当前窗口内的第一个请求的时间firstReqTime,那么之后的请求到来时,先判断是否进入下一个窗口。

如果进入,则直接重置firstReqTime为当前时间,该请求通过。如果没进入,再判断是否超过max。

demo如下(并没有考虑线程安全):

/**
 * @author qcy
 * @create 2021/12/04 18:20:30
 */
public class CountLimiter {
    /**
     * 时间间隔窗口(单位:毫秒)
     */
    private long intervalWindow;
    /**
     * 该窗口内的最大请求数
     */
    private int max;
    /**
     * 当前窗口内的请求计数
     */
    private int count;
    /**
     * 当前窗口内的第一个请求的时间
     */
    private long firstReqTime = System.currentTimeMillis();

    public CountLimiter(int intervalWindow, int max) {
        this.intervalWindow = intervalWindow;
        this.max = max;
    }

    //省略get与set方法

    public boolean limit() {
        long now = System.currentTimeMillis();

        if (now > firstReqTime + intervalWindow) {
            //代表已经进入下一个窗口
            firstReqTime = now;
            count = 1;
            return true;
        }

        //还在当前的时间窗口内
        if (count + 1 <= max) {
            count++;
            return true;
        }

        return false;
    }
    
}

计数器法,非常简单粗暴,以上demo只是单机式限流

如果需要进行分布式限流,可以使用Redis。以接口名称作为key,max作为value,intervalWindow作为key过期时间。

当请求过来时,如果key不存在,则代表已经进入下一个窗口,value赋值为max-1,并允许请求通过。

如果key存在,则再判断value是否大于0。大于0则允许请求通过,否则进行限流

使用Redis进行分布式限流,需要注意保证代码的原子性,可以直接使用lua脚本。

计数器法的缺点

该算法无法应对突发的流量,因为计数器法是固定窗口的。

例如第一个请求10:00:00到来,那么第一个时间窗口为10:00:00-10:01:00。之后在10:00:59时,突然来了99个请求,又在下一个时间窗口的10:01:01来了100个请求。

也就是说,在10:00:59-10:01:01的短短几秒内,共有199个请求到来,可能会瞬间压垮我们的应用。


滑动窗口

滑动窗口法可以解决计数器在固定窗口法下无法应对突发流量的问题

固定窗口法是以第一个请求为窗口开始期,并向后截取intervalWindow长度,只有当窗口时间流逝完,才开辟新的窗口。

滑动窗口法以每一个请求为窗口结束期,向前截取intervalWindow长度,检查该范围内的请求总和,相当于会为每个请求开辟一个新窗口。

既然要知道前intervalWindow长度内到底有多少个请求,那么就要为每个放行的请求记录发生时间。

demo如下:

public class SlidingWindowLimiter {

    /**
     * 时间间隔窗口(单位:毫秒)
     */
    private long intervalWindow;
    /**
     * 窗口内的最大请求数
     */
    private int max;
    /**
     * 限流容器
     * 队列尾部保存最新通过的请求时间
     */
    private LinkedList<Long> list = new LinkedList<>();

    public SlidingWindowLimiter(int intervalWindow, int max) {
        this.intervalWindow = intervalWindow;
        this.max = max;
    }

    //省略get与set方法


    public boolean limit() {
        long now = System.currentTimeMillis();

        //队列未满,说明当前窗口还可以接收请求
        if (list.size() < max) {
            list.addLast(now);
            return true;
        }

        //队列已满
        Long first = list.getFirst();
        if (now - first <= intervalWindow) {
            //说明新请求和队列中的请求还处于一个窗口内,触发了限流
            return false;
        }

        //说明新请求和队列中的请求不在通过窗口内
        list.removeFirst();
        list.addLast(now);
        return true;
    }
}

当然,也可以使用Redis的List或Zset实现吗,大致步骤和以上demo类似。

这里多说一句,限流中的滑动窗口法和TCP的滑动窗口其实很像。滑动窗口法是去主动限流,而TCP的滑动窗口则是接收方为了告诉发送方自己还能接受多少数据,是对发送方的“限流”。

滑动窗口法的缺点

滑动窗口法中,因为要倒推窗口的开始期,所以需要记录每个请求的执行时间,会额外占用一些内存。

此外,在算法中会频繁地removeFirst与addLast,在选择错误的数据结构下(例如数组),可能会造成很大的移动开销。


漏桶

水龙头可以通过松紧来控制出水的速率,下方有一个储蓄桶来保存当前的水。储蓄通底部有一个出口,内部的水会以恒定的速率从出口漏掉。

如果储蓄桶满了之后,再进来的水会全部溢出。只有当出水速率和漏水速率相同时,储蓄桶才会在不漏水的前提下达到最大的吞吐量。

我们把水比作请求,水龙头就是客户端。请求产生的速率肯定不是恒定的,但处理请求的速率是恒定的。当储蓄桶满了之后,请求产生的速率必须要和处理请求的速率一致。

demo如下:

public class LeakyBucketLimiter {
    /**
     * 上次请求到来的时间
     */
    private long preTime = System.currentTimeMillis();
    /**
     * 漏水速率,n/s
     */
    private int leakRate;
    /**
     * 储蓄桶容量
     */
    private int capacity;
    /**
     * 当前水量
     */
    private int water;

    public LeakyBucketLimiter(int leakRate, int capacity) {
        this.leakRate = leakRate;
        this.capacity = capacity;
    }

    //省略get与set方法

    public boolean limit() {
        long now = System.currentTimeMillis();

        //先漏水,计算剩余水量
        water = Math.max(0, water - (int) ((now - preTime) / 1000) * leakRate);
        preTime = now;

        //桶未满
        if (water + 1 <= capacity) {
            water++;
            return true;
        }

        return false;
    }
}

仔细一想,储蓄桶能够把不定速率的请求转化为恒定速率的请求,和消息队列一样,具有削峰填谷的作用。

其实整套装置和ScheduledThreadPoolExecutor线程池更像,将储蓄桶想象为具有延时特性的阻塞队列,超出队列容量的请求,将直接执行拒绝策略。

如果储蓄桶的容量为Integer.MAX_VALUE,流速为10/s,则可通过以下的代码来模拟漏桶

        //最大任务数为Integer.MAX_VALUE,即储蓄桶容量
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(30);
        //每隔0.1秒处理1个请求,即流速为10/s
        pool.scheduleAtFixedRate(() -> System.out.println("处理请求"), 0, 100, TimeUnit.MILLISECONDS);

漏桶法的缺点

使用漏桶法去做限流,在业务平稳期其实已经够用了。但是在业务高峰期,我们又希望动态地去调整处理请求的速率,而不是一成不变的速率。

我们大可以动态地去改变参数leakRate的值,不过在计算剩余水量的时候,将会十分复杂。

因此,如果要考虑到对突发流量的控制,就不太推荐漏桶法了。


令牌桶

首先有一个令牌桶,然后系统以一个恒定的速率向桶中放入令牌。当桶满时,会丢弃生成的令牌。

每有一个请求过来时,拿到令牌就可以执行,否则阻塞获取或者被直接丢弃。

一个简要的demo如下:

public class TokenBucketLimiter {
    /**
     * 上次请求到来的时间
     */
    private long preTime = System.currentTimeMillis();
    /**
     * 放入令牌速率,n/s
     */
    private int putRate;
    /**
     * 令牌桶容量
     */
    private int capacity;
    /**
     * 当前令牌数
     */
    private int bucket;

    public TokenBucketLimiter(int putRate, int capacity) {
        this.putRate = putRate;
        this.capacity = capacity;
    }

    //省略get与set方法

    public boolean limit() {
        long now = System.currentTimeMillis();

        //先放入令牌,再获取令牌
        bucket = Math.min(capacity, bucket + (int) ((now - preTime) / 1000) * putRate);
        preTime = now;

        if (bucket == 0) {
            return false;
        }

        bucket--;
        return true;
    }
}

看的出来,令牌桶漏桶的原理有些相似。

漏桶是以一个恒定速率的出水,即处理请求的速率是恒定的。而令牌桶则是以一个恒定的速率往桶中放入令牌,在桶中令牌用完之前,并不限制处理请求的速率。

令牌桶的一个优势在于,可以允许短时间内的一次突发流量。但不会允许在短时间内的多次突发流量,因为令牌的填充也是需要时间的。

Guava中的RateLimiter

google的工具包Guava中的RateLimiter就是对令牌桶的实现,其包含了两种限流模式,位置处于SmoothRateLimiter的两个静态内部类中:

  • SmoothBursty,稳定模式,令牌生成的速率是恒定的,为默认模式。
  • SmoothWarmingUp,预热模式,逐渐提升令牌的生成速率到一固定值。

其中acquire方法支持阻塞式获取,tryAcquire支持获取不到就返回或者在指定时间内阻塞获取。

关于RateLimiter源码分析,后面应该会另起篇幅介绍。


分布式限流

以上的RateLimiter属于单机式限流,如果要进行分布式限流该怎么处理呢?

无非是将控制请求的阈值从单机中挪到统一的中间件上,例如Redis。

对于计数器

如果要限制一天中对某个接口的调用次数,则可以使用接口的名称作为key,value作为预设的阈值,过期时间为24小时。请求到来时利用原子指令判断key是否存在,不存在则设置该key;存在则减1,再判断是否大于0。

对于滑动窗口

单机中我们使用list,在分布式系统中,则可以使用Redis的有序集合zset,key为某个接口名称,value为处理请求的时间戳。请求到来时,先使用removeRangeByScore移除上一个时间窗口内的记录,接着使用size获取集合长度,若大于阈值,则进行限流

对于漏桶

阿里巴巴的开源分布式限流系统Sentienel,支持漏桶令牌桶算法。

个人觉得非常有必要去了解一下Sentienel的整体架构,可以看这篇文章入门阿里巴巴开源限流系统 Sentinel 全解析

对于令牌桶

可以在每个应用中起一个延时的线程池,定时生产令牌到Redis中,这种方案在水平扩展时可以同比例的扩大限流阈值,但性能不高。

当然也可以利用lua脚本,在lua脚本中直接将生产令牌与获取令牌的操作合在一起,即和上文的demo一样,先放入令牌再获取令牌。之后将脚本放在代码中,每个应用先判断Redis中是否存在该脚本,若不存在再加载该脚本,后续获取令牌时直接执行该脚本即可。


http://www.niftyadmin.cn/n/1704461.html

相关文章

代码审计--43--NodeJS代码审计中常见的漏洞(二)

经过漫长时间的迭代更新,内容从一开始的碎片化网罗搜集,到排版与整合划分,再到学习与实践验证,最后摒弃初始,用自己的语言与实践结论通俗的将它展现出来。强化知识技能的同时也为读者带来更好的阅读体验。 本篇介绍了以下代码安全问题: 编号漏洞1template标签使用v-show…

Redisson可重入与锁续期源码分析

一、前言 在跨进程的前提下访问某个共享资源时&#xff0c;需要使用到分布式锁来保证同一时间只有一个进程能够操作共享资源。 这个时候&#xff0c;锁对象需要从单个JVM内存中迁移到某个多进程共用的中间件上&#xff0c;例如MySQL、Redis或ZK上。 我们常常选择Redis来实现…

光脚丫学LINQ(015):使用LINQ to SQL可以执行的操作

视频演示&#xff1a;http://u.115.com/file/f2f877c8d1 LINQ to SQL 支持您作为 SQL 开发人员所期望的所有关键功能。 您可以查询表中的信息、在表中插入信息以及更新和删除表中的信息。 选择通过在您自己的编程语言中编写 LINQ 查询&#xff0c;然后执行此查询以检索结果&am…

代码审计--45--Java代码审计自动化实现

准备:语法树和词法树 首先,需要了解一下语法树和词法树的概念,这是代码审计工具实现审计功能的根本。 简单举例介绍: 源码: const a = 1; const b = a + 1;词法树与语法树解析: 它的词法树: [{"type": "Keyword",

简单谈谈MySQL的两阶段提交

一、简单回顾三种日志 在讲解两阶段提交之前&#xff0c;需要对MySQL中的三种日志即binlog、redo log与undo log有一定的了解。 在这三种日志中&#xff0c;很多同学会把binlog与redo log混淆&#xff0c;下面使用一张表格来简单对比下两者的区别。 当然&#xff0c;如果对bi…

移动安全--45--MobSF-v3.0源代码分析(一)

一、项目说明 源码地址:https://github.com/MobSF/Mobile-Security-Framework-MobSF 关于如何搭建源码分析环境,请阅读我的另一篇博客:移动安全–44–MobSF框架安装与开发环境搭建(基于3.0) 移动安全框架(MobSF)是一种自动化的移动应用程序(Android/iOS/Windows)测…

光脚丫学LINQ(016):[演练]创建简单对象模型和LINQ查询(C#)

视频演示&#xff1a;http://u.115.com/file/f2e3bc874c 本演练提供了复杂性最低的基本端对端 LINQ to SQL 方案。您将创建一个可为示例 Northwind 数据库中的 Customers 表建模的实体类。 然后您将创建一个简单查询&#xff0c;用于列出位于伦敦的客户。 本演练在设计上是面向…

面试官:能给我画个Zookeeper选举的图吗?

一、前言 Zookeeper是一个分布式协调框架&#xff0c;提供分布式锁、配置项管理、服务注册与集群管理等功能。 为了保证Zookeeper的高可用&#xff0c;一般都会以集群的模式部署。 这个时候需要考虑各个节点的数据一致性&#xff0c;那么集群在启动时&#xff0c;需要先选举…