大家好啊,我是阿朗,最近工作中须要用到限流,这篇文章介绍常见的限流形式。

文章继续更新,能够关注公众号程序猿阿朗或拜访未读代码博客。
本文 Github.com/niumoo/JavaNotes 曾经收录,欢送Star。

前言

最近几年,随着微服务的风行,服务和服务之间的依赖越来越强,调用关系越来越简单,服务和服务之间的稳定性越来越重要。在遇到突发的申请量激增,歹意的用户拜访,亦或申请频率过高给上游服务带来较大压力时,咱们经常须要通过缓存、限流、熔断降级、负载平衡等多种形式保障服务的稳定性。其中限流是不可或缺的一环,这篇文章介绍限流相干常识。

1. 限流

限流顾名思义,就是对申请或并发数进行限度;通过对一个工夫窗口内的申请量进行限度来保障系统的失常运行。如果咱们的服务资源无限、解决能力无限,就须要对调用咱们服务的上游申请进行限度,以避免本身服务因为资源耗尽而进行服务。

在限流中有两个概念须要理解。

  • 阈值:在一个单位工夫内容许的申请量。如 QPS 限度为10,阐明 1 秒内最多承受 10 次申请。
  • 回绝策略:超过阈值的申请的回绝策略,常见的回绝策略有间接回绝、排队期待等。

2. 固定窗口算法

固定窗口算法又叫计数器算法,是一种简略不便的限流算法。次要通过一个反对原子操作的计数器来累计 1 秒内的申请次数,当 1 秒内计数达到限流阈值时触发回绝策略。每过 1 秒,计数器重置为 0 开始从新计数。

2.1. 代码实现

上面是简略的代码实现,QPS 限度为 2,这里的代码做了一些优化,并没有独自开一个线程去每隔 1 秒重置计数器,而是在每次调用时进行工夫距离计算来确定是否先重置计数器。

/** * @author https://www.wdbyte.com */public class RateLimiterSimpleWindow {    // 阈值    private static Integer QPS = 2;    // 工夫窗口(毫秒)    private static long TIME_WINDOWS = 1000;    // 计数器    private static AtomicInteger REQ_COUNT = new AtomicInteger();        private static long START_TIME = System.currentTimeMillis();    public synchronized static boolean tryAcquire() {        if ((System.currentTimeMillis() - START_TIME) > TIME_WINDOWS) {            REQ_COUNT.set(0);            START_TIME = System.currentTimeMillis();        }        return REQ_COUNT.incrementAndGet() <= QPS;    }    public static void main(String[] args) throws InterruptedException {        for (int i = 0; i < 10; i++) {            Thread.sleep(250);            LocalTime now = LocalTime.now();            if (!tryAcquire()) {                System.out.println(now + " 被限流");            } else {                System.out.println(now + " 做点什么");            }        }    }}

运行后果:

20:53:43.038922 做点什么20:53:43.291435 做点什么20:53:43.543087 被限流20:53:43.796666 做点什么20:53:44.050855 做点什么20:53:44.303547 被限流20:53:44.555008 被限流20:53:44.809083 做点什么20:53:45.063828 做点什么20:53:45.314433 被限流

从输入后果中能够看到大略每秒操作 3 次,因为限度 QPS 为 2,所以均匀会有一次被限流。看起来能够了,不过咱们思考一下就会发现这种简略的限流形式是有问题的,尽管咱们限度了 QPS 为 2,然而当遇到工夫窗口的临界渐变时,如 1s 中的后 500 ms 和第 2s 的前 500ms 时,尽管是加起来是 1s 工夫,却能够被申请 4 次。

简略批改测试代码,能够进行验证:

// 先休眠 400ms,能够更快的达到工夫窗口。Thread.sleep(400);for (int i = 0; i < 10; i++) {    Thread.sleep(250);    if (!tryAcquire()) {        System.out.println("被限流");    } else {        System.out.println("做点什么");    }}

失去输入中能够看到间断 4 次申请,距离 250 ms 没有却被限度。:

20:51:17.395087 做点什么20:51:17.653114 做点什么20:51:17.903543 做点什么20:51:18.154104 被限流20:51:18.405497 做点什么20:51:18.655885 做点什么20:51:18.906177 做点什么20:51:19.158113 被限流20:51:19.410512 做点什么20:51:19.661629 做点什么

3. 滑动窗口算法

咱们曾经晓得固定窗口算法的实现形式以及它所存在的问题,而滑动窗口算法是对固定窗口算法的改良。既然固定窗口算法在遇到工夫窗口的临界渐变时会有问题,那么咱们在遇到下一个工夫窗口前也调整工夫窗口不就能够了吗?

上面是滑动窗口的示意图。

上图的示例中,每 500ms 滑动一次窗口,能够发现窗口滑动的距离越短,工夫窗口的临界渐变问题产生的概率也就越小,不过只有有工夫窗口的存在,还是有可能产生工夫窗口的临界渐变问题

3.1. 代码实现

上面是基于以上滑动窗口思路实现的简略的滑动窗口限流工具类。

package com.wdbyte.rate.limiter;import java.time.LocalTime;import java.util.concurrent.atomic.AtomicInteger;/** * 滑动窗口限流工具类 * * @author https://www.wdbyte.com */public class RateLimiterSlidingWindow {    /**     * 阈值     */    private int qps = 2;    /**     * 工夫窗口总大小(毫秒)     */    private long windowSize = 1000;    /**     * 多少个子窗口     */    private Integer windowCount = 10;    /**     * 窗口列表     */    private WindowInfo[] windowArray = new WindowInfo[windowCount];    public RateLimiterSlidingWindow(int qps) {        this.qps = qps;        long currentTimeMillis = System.currentTimeMillis();        for (int i = 0; i < windowArray.length; i++) {            windowArray[i] = new WindowInfo(currentTimeMillis, new AtomicInteger(0));        }    }    /**     * 1. 计算以后工夫窗口     * 2. 更新以后窗口计数 & 重置过期窗口计数     * 3. 以后 QPS 是否超过限度     *     * @return     */    public synchronized boolean tryAcquire() {        long currentTimeMillis = System.currentTimeMillis();        // 1. 计算以后工夫窗口        int currentIndex = (int)(currentTimeMillis % windowSize / (windowSize / windowCount));        // 2.  更新以后窗口计数 & 重置过期窗口计数        int sum = 0;        for (int i = 0; i < windowArray.length; i++) {            WindowInfo windowInfo = windowArray[i];            if ((currentTimeMillis - windowInfo.getTime()) > windowSize) {                windowInfo.getNumber().set(0);                windowInfo.setTime(currentTimeMillis);            }            if (currentIndex == i && windowInfo.getNumber().get() < qps) {                windowInfo.getNumber().incrementAndGet();            }            sum = sum + windowInfo.getNumber().get();        }        // 3. 以后 QPS 是否超过限度        return sum <= qps;    }    private class WindowInfo {        // 窗口开始工夫        private Long time;        // 计数器        private AtomicInteger number;        public WindowInfo(long time, AtomicInteger number) {            this.time = time;            this.number = number;        }        // get...set...    }}

上面是测试用例,设置 QPS 为 2,测试次数 20 次,每次距离 300 毫秒,预计胜利次数在 12 次左右。

public static void main(String[] args) throws InterruptedException {    int qps = 2, count = 20, sleep = 300, success = count * sleep / 1000 * qps;    System.out.println(String.format("以后QPS限度为:%d,以后测试次数:%d,距离:%dms,预计胜利次数:%d", qps, count, sleep, success));    success = 0;    RateLimiterSlidingWindow myRateLimiter = new RateLimiterSlidingWindow(qps);    for (int i = 0; i < count; i++) {        Thread.sleep(sleep);        if (myRateLimiter.tryAcquire()) {            success++;            if (success % qps == 0) {                System.out.println(LocalTime.now() + ": success, ");            } else {                System.out.print(LocalTime.now() + ": success, ");            }        } else {            System.out.println(LocalTime.now() + ": fail");        }    }    System.out.println();    System.out.println("理论测试胜利次数:" + success);}

上面是测试的后果。

以后QPS限度为:2,以后测试次数:20,距离:300ms,预计胜利次数:1216:04:27.077782: success, 16:04:27.380715: success, 16:04:27.684244: fail16:04:27.989579: success, 16:04:28.293347: success, 16:04:28.597658: fail16:04:28.901688: fail16:04:29.205262: success, 16:04:29.507117: success, 16:04:29.812188: fail16:04:30.115316: fail16:04:30.420596: success, 16:04:30.725897: success, 16:04:31.028599: fail16:04:31.331047: fail16:04:31.634127: success, 16:04:31.939411: success, 16:04:32.242380: fail16:04:32.547626: fail16:04:32.847965: success, 理论测试胜利次数:11

4. 滑动日志算法

滑动日志算法是实现限流的另一种办法,这种办法比较简单。根本逻辑就是记录下所有的申请工夫点,新申请到来时先判断最近指定工夫范畴内的申请数量是否超过指定阈值,由此来确定是否达到限流,这种形式没有了工夫窗口渐变的问题,限流比拟精确,然而因为要记录下每次申请的工夫点,所以占用的内存较多

4.1. 代码实现

上面是简略实现的 一个滑动日志算法,因为滑动日志要每次申请独自存储一条记录,可能占用内存过多。所以上面这个实现其实不算谨严的滑动日志,更像一个把 1 秒工夫切分成 1000 个工夫窗口的滑动窗口算法。

package com.wdbyte.rate.limiter;import java.time.LocalTime;import java.util.HashSet;import java.util.Set;import java.util.TreeMap;/** * 滑动日志形式限流 * 设置 QPS 为 2. * * @author https://www.wdbyte.com */public class RateLimiterSildingLog {    /**     * 阈值     */    private Integer qps = 2;    /**     * 记录申请的工夫戳,和数量     */    private TreeMap<Long, Long> treeMap = new TreeMap<>();    /**     * 清理申请记录距离, 60 秒     */    private long claerTime = 60 * 1000;    public RateLimiterSildingLog(Integer qps) {        this.qps = qps;    }    public synchronized boolean tryAcquire() {        long now = System.currentTimeMillis();        // 清理过期的数据老数据,最长 60 秒清理一次        if (!treeMap.isEmpty() && (treeMap.firstKey() - now) > claerTime) {            Set<Long> keySet = new HashSet<>(treeMap.subMap(0L, now - 1000).keySet());            for (Long key : keySet) {                treeMap.remove(key);            }        }        // 计算以后申请次数        int sum = 0;        for (Long value : treeMap.subMap(now - 1000, now).values()) {            sum += value;        }        // 超过QPS限度,间接返回 false        if (sum + 1 > qps) {            return false;        }        // 记录本次申请        if (treeMap.containsKey(now)) {            treeMap.compute(now, (k, v) -> v + 1);        } else {            treeMap.put(now, 1L);        }        return sum <= qps;    }    public static void main(String[] args) throws InterruptedException {        RateLimiterSildingLog rateLimiterSildingLog = new RateLimiterSildingLog(3);        for (int i = 0; i < 10; i++) {            Thread.sleep(250);            LocalTime now = LocalTime.now();            if (rateLimiterSildingLog.tryAcquire()) {                System.out.println(now + " 做点什么");            } else {                System.out.println(now + " 被限流");            }        }    }}

代码中把阈值 QPS 设定为 3,运行能够失去如下日志:

20:51:17.395087 做点什么20:51:17.653114 做点什么20:51:17.903543 做点什么20:51:18.154104 被限流20:51:18.405497 做点什么20:51:18.655885 做点什么20:51:18.906177 做点什么20:51:19.158113 被限流20:51:19.410512 做点什么20:51:19.661629 做点什么

5. 漏桶算法

漏桶算法中的漏桶是一个形象的比喻,这里能够用生产者消费者模式进行阐明,申请是一个生产者,每一个申请都如一滴水,申请到来后放到一个队列(漏桶)中,而桶底有一个孔,一直的漏出水滴,就如消费者一直的在生产队列中的内容,生产的速率(漏出的速度)等于限流阈值。即如果 QPS 为 2,则每 1s / 2= 500ms 生产一次。漏桶的桶有大小,就如队列的容量,当申请沉积超过指定容量时,会触发回绝策略。

上面是漏桶算法的示意图。

由介绍能够晓得,漏桶模式中的生产解决总是能以恒定的速度进行,能够很好的爱护本身零碎不被从天而降的流量冲垮;然而这也是漏桶模式的毛病,假如 QPS 为 2,同时 2 个申请进来,2 个申请并不能同时进行解决响应,因为每 1s / 2= 500ms 只能解决一个申请。

6. 令牌桶算法

令牌桶算法同样是实现限流是一种常见的思路,最为罕用的 Google 的 Java 开发工具包 Guava 中的限流工具类 RateLimiter 就是令牌桶的一个实现。令牌桶的实现思路相似于生产者和生产之间的关系。

零碎服务作为生产者,依照指定频率向桶(容器)中增加令牌,如 QPS 为 2,每 500ms 向桶中增加一个令牌,如果桶中令牌数量达到阈值,则不再增加。

申请执行作为消费者,每个申请都须要去桶中拿取一个令牌,取到令牌则继续执行;如果桶中无令牌可取,就触发回绝策略,能够是超时期待,也能够是间接回绝本次申请,由此达到限流目标。

上面是令牌桶限流算法示意图。

思考令牌桶的实现能够以下特点。

  1. 1s / 阈值(QPS) = 令牌增加工夫距离。
  2. 桶的容量等于限流的阈值,令牌数量达到阈值时,不再增加。
  3. 能够适应流量突发,N 个申请到来只须要从桶中获取 N 个令牌就能够持续解决。
  4. 有启动过程,令牌桶启动时桶中无令牌,而后依照令牌增加工夫距离增加令牌,若启动时就有阈值数量的申请过去,会因为桶中没有足够的令牌而触发回绝策略,不过如 RateLimiter 限流工具曾经优化了这类问题。

6.1. 代码实现

Google 的 Java 开发工具包 Guava 中的限流工具类 RateLimiter 就是令牌桶的一个实现,日常开发中咱们也不会手动实现了,这里间接应用 RateLimiter 进行测试。

引入依赖:

<exclusion>      <groupId>com.google.guava</groupId>    <artifactId>guava</artifactId>      <version>31.0.1-jre</version></exclusion>

RateLimiter 限流体验:

// qps 2RateLimiter rateLimiter = RateLimiter.create(2);for (int i = 0; i < 10; i++) {    String time = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME);    System.out.println(time + ":" + rateLimiter.tryAcquire());    Thread.sleep(250);}

代码中限度 QPS 为 2,也就是每隔 500ms 生成一个令牌,然而程序每隔 250ms 获取一次令牌,所以两次获取中只有一次会胜利。

17:19:06.797557:true17:19:07.061419:false17:19:07.316283:true17:19:07.566746:false17:19:07.817035:true17:19:08.072483:false17:19:08.326347:true17:19:08.577661:false17:19:08.830252:true17:19:09.085327:false

6.2. 思考

尽管演示了 Google Guava 工具包中的 RateLimiter 的实现,然而咱们须要思考一个问题,就是令牌的增加形式,如果依照指定距离增加令牌,那么须要开一个线程去定时增加,如果有很多个接口很多个 RateLimiter 实例,线程数会随之减少,这显然不是一个好的方法。显然 Google 也思考到了这个问题,在 RateLimiter 中,是在每次令牌获取时才进行计算令牌是否足够的。它通过存储的下一个令牌生成的工夫,和以后获取令牌的时间差,再联合阈值,去计算令牌是否足够,同时再记录下一个令牌的生成工夫以便下一次调用。

上面是 Guava 中 RateLimiter 类的子类 SmoothRateLimiter 的 resync() 办法的代码剖析,能够看到其中的令牌计算逻辑。

void resync(long nowMicros) { // 以后微秒工夫    // 以后工夫是否大于下一个令牌生成工夫    if (nowMicros > this.nextFreeTicketMicros) {           // 可生成的令牌数 newPermits = (以后工夫 - 下一个令牌生成工夫)/ 令牌生成工夫距离。          // 如果 QPS 为2,这里的 coolDownIntervalMicros 就是 500000.0 微秒(500ms)        double newPermits = (double)(nowMicros - this.nextFreeTicketMicros) / this.coolDownIntervalMicros();                // 更新令牌库存 storedPermits。          this.storedPermits = Math.min(this.maxPermits, this.storedPermits + newPermits);                // 更新下一个令牌生成工夫 nextFreeTicketMicros          this.nextFreeTicketMicros = nowMicros;    }}

7. Redis 分布式限流

Redis 是一个开源的内存数据库,能够用来作为数据库、缓存、消息中间件等。Redis 是单线程的,又在内存中操作,所以速度极快,得益于 Redis 的各种个性,所以应用 Redis 实现一个限流工具是非常不便的。

上面的演示都基于Spring Boot 我的项目,并须要以下依赖。

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-redis</artifactId></dependency>

配置 Redis 信息。

spring:  redis:    database: 0    password:     port: 6379    host: 127.0.0.1    lettuce:      shutdown-timeout: 100ms      pool:        min-idle: 5        max-idle: 10        max-active: 8        max-wait: 1ms

7.1. 固定窗口限流

Redis 中的固定窗口限流是应用 incr 命令实现的,incr 命令通常用来自增计数;如果咱们应用工夫戳信息作为 key,天然就能够统计每秒的申请量了,以此达到限流目标。

这里有两点要留神。

  1. 对于不存在的 key,第一次新增时,value 始终为 1。
  2. INCR 和 EXPIRE 命令操作应该在一个原子操作中提交,以保障每个 key 都正确设置了过期工夫,不然会有 key 值无奈主动删除而导致的内存溢出。

因为 Redis 中实现事务的复杂性,所以这里间接只用 lua 脚本来实现原子操作。上面是 lua 脚本内容。

local count = redis.call("incr",KEYS[1])if count == 1 then  redis.call('expire',KEYS[1],ARGV[2])endif count > tonumber(ARGV[1]) then  return 0endreturn 1

上面是应用 Spring Boot 中 RedisTemplate 来实现的 lua 脚本调用测试代码。

/** * @author https://www.wdbyte.com */@SpringBootTestclass RedisLuaLimiterByIncr {    private static String KEY_PREFIX = "limiter_";    private static String QPS = "4";    private static String EXPIRE_TIME = "1";    @Autowired    private StringRedisTemplate stringRedisTemplate;    @Test    public void redisLuaLimiterTests() throws InterruptedException, IOException {        for (int i = 0; i < 15; i++) {            Thread.sleep(200);            System.out.println(LocalTime.now() + " " + acquire("user1"));        }    }    /**     * 计数器限流     *     * @param key     * @return     */    public boolean acquire(String key) {        // 以后秒数作为 key        key = KEY_PREFIX + key + System.currentTimeMillis() / 1000;        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();        redisScript.setResultType(Long.class);        //lua文件寄存在resources目录下        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limiter.lua")));        return stringRedisTemplate.execute(redisScript, Arrays.asList(key), QPS, EXPIRE_TIME) == 1;    }}

代码中尽管限度了 QPS 为 4,然而因为这种限流实现是把毫秒工夫戳作为 key 的,所以会有临界窗口渐变的问题,上面是运行后果,能够看到因为工夫窗口的变动,导致了 QPS 超过了限度值 4。

17:38:23.122044 true17:38:23.695124 true17:38:23.903220 true# 此处有工夫窗口变动,所以上面持续 true17:38:24.106206 true17:38:24.313458 true17:38:24.519431 true17:38:24.724446 true17:38:24.932387 false17:38:25.137912 true17:38:25.355595 true17:38:25.558219 true17:38:25.765801 true17:38:25.969426 false17:38:26.176220 true17:38:26.381918 true

7.3. 滑动窗口限流

通过对下面的基于 incr 命令实现的 Redis 限流形式的测试,咱们曾经发现了固定窗口限流所带来的问题,在这篇文章的第三局部曾经介绍了滑动窗口限流的劣势,它能够大幅度降低因为窗口临界渐变带来的问题,那么如何应用 Redis 来实现滑动窗口限流呢?

这里次要应用 ZSET 有序汇合来实现滑动窗口限流,ZSET 汇合有上面几个特点:

  1. ZSET 汇合中的 key 值能够主动排序。
  2. ZSET 汇合中的 value 不能有反复值。
  3. ZSET 汇合能够不便的应用 ZCARD 命令获取元素个数。
  4. ZSET 汇合能够不便的应用 ZREMRANGEBYLEX 命令移除指定范畴的 key 值。

基于下面的四点个性,能够编写出基于 ZSET 的滑动窗口限流 lua 脚本。

--KEYS[1]: 限流 key--ARGV[1]: 工夫戳 - 工夫窗口--ARGV[2]: 以后工夫戳(作为score)--ARGV[3]: 阈值--ARGV[4]: score 对应的惟一value-- 1. 移除工夫窗口之前的数据redis.call('zremrangeByScore', KEYS[1], 0, ARGV[1])-- 2. 统计以后元素数量local res = redis.call('zcard', KEYS[1])-- 3. 是否超过阈值if (res == nil) or (res < tonumber(ARGV[3])) then    redis.call('zadd', KEYS[1], ARGV[2], ARGV[4])    return 1else    return 0end

上面是应用 Spring Boot 中 RedisTemplate 来实现的 lua 脚本调用测试代码。

@SpringBootTestclass RedisLuaLimiterByZset {    private String KEY_PREFIX = "limiter_";    private String QPS = "4";    @Autowired    private StringRedisTemplate stringRedisTemplate;    @Test    public void redisLuaLimiterTests() throws InterruptedException, IOException {        for (int i = 0; i < 15; i++) {            Thread.sleep(200);            System.out.println(LocalTime.now() + " " + acquire("user1"));        }    }    /**     * 计数器限流     *     * @param key     * @return     */    public boolean acquire(String key) {        long now = System.currentTimeMillis();        key = KEY_PREFIX + key;        String oldest = String.valueOf(now - 1_000);        String score = String.valueOf(now);        String scoreValue = score;        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();        redisScript.setResultType(Long.class);        //lua文件寄存在resources目录下        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limiter2.lua")));        return stringRedisTemplate.execute(redisScript, Arrays.asList(key), oldest, score, QPS, scoreValue) == 1;    }}

代码中限度 QPS 为 4,运行后果信息与之统一。

17:36:37.150370 true17:36:37.716341 true17:36:37.922577 true17:36:38.127497 true17:36:38.335879 true17:36:38.539225 false17:36:38.745903 true17:36:38.952491 true17:36:39.159497 true17:36:39.365239 true17:36:39.570572 false17:36:39.776635 true17:36:39.982022 true17:36:40.185614 true17:36:40.389469 true

这里介绍了 Redis 实现限流的两种形式,当然应用 Redis 也能够实现漏桶和令牌桶两种限流算法,这里就不做演示了,感兴趣的能够本人钻研下。

8. 总结

这篇文章介绍实现限流的几种形式,次要是窗口算法和桶算法,两者各有劣势。

  • 窗口算法实现简略,逻辑清晰,能够很直观的失去以后的 QPS 状况,然而会有工夫窗口的临界渐变问题,而且不像桶一样有队列能够缓冲。
  • 桶算法尽管略微简单,不好统计 QPS 状况,然而桶算法也有劣势所在。

    • 漏桶模式生产速率恒定,能够很好的爱护本身零碎,能够对流量进行整形,然而面对突发流量不能疾速响应。
    • 令牌桶模式能够面对突发流量,然而启动时会有迟缓减速的过程,不过常见的开源工具中曾经对此优化。

单机限流与分布式限流

下面演示的基于代码模式的窗口算法和桶算法限流都实用于单机限流,如果须要分布式限流能够联合注册核心、负载平衡计算每个服务的限流阈值,但这样会升高肯定精度,如果对精度要求不是太高,能够应用。

而 Redis 的限流,因为 Redis 的单机性,自身就能够用于分布式限流。应用 Redis 能够实现各种能够用于限流算法,如果感觉麻烦也能够应用开源工具如 redisson,曾经封装了基于 Redis 的限流。

其余限流工具

文中曾经提到了 Guava 的限流工具包,不过它毕竟是单机的,开源社区中也有很多分布式限流工具,如阿里开源的 Sentinel 就是不错的工具,Sentinel 以流量为切入点,从流量管制、熔断降级、零碎负载爱护等多个维度爱护服务的稳定性。

判若两人,文章中的代码寄存在:github.com/niumoo/JavaNotes

参考

Redis INCR:https://redis.io/commands/incr

Rate Limiting Wikipedia:https://en.wikipedia.org/wiki/Rate_limiting

SpringBoot Redis:https://www.cnblogs.com/lenve/p/10965667.html

订阅

能够微信搜一搜程序猿阿朗或拜访未读代码博客浏览。
本文 Github.com/niumoo/JavaNotes 曾经收录,欢送Star。