人人都能看懂的 6 种限流实现方案!(纯干货)下

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 人人都能看懂的 6 种限流实现方案!(纯干货)

服务端限流


服务端限流需要配合限流的算法来执行,而算法相当于执行限流的“大脑”,用于指导限制方案的实现。


有人看到「算法」两个字可能就晕了,觉得很深奥,其实并不是。算法就相当于操作某个事务的具体实现步骤汇总,其实并不难懂,不要被它的表象给吓到哦~


限流的常见算法有以下三种:


  1. 时间窗口算法
  2. 漏桶算法
  3. 令牌算法


接下来我们分别看来。


1.时间窗口算法


所谓的滑动时间算法指的是以当前时间为截止时间,往前取一定的时间,比如往前取 60s 的时间,在这 60s 之内运行最大的访问数为 100,此时算法的执行逻辑为,先清除 60s 之前的所有请求记录,再计算当前集合内请求数量是否大于设定的最大请求数 100,如果大于则执行限流拒绝策略,否则插入本次请求记录并返回可以正常执行的标识给客户端。


滑动时间窗口如下图所示:


image.png


其中每一小个表示 10s,被红色虚线包围的时间段则为需要判断的时间间隔,比如 60s 秒允许 100 次请求,那么红色虚线部分则为 60s。


我们可以借助 Redis 的有序集合 ZSet 来实现时间窗口算法限流,实现的过程是先使用 ZSet 的 key 存储限流的 ID,score 用来存储请求的时间,每次有请求访问来了之后,先清空之前时间窗口的访问量,统计现在时间窗口的个数和最大允许访问量对比,如果大于等于最大访问量则返回 false 执行限流操作,负责允许执行业务逻辑,并且在 ZSet 中添加一条有效的访问记录,具体实现代码如下。


我们借助 Jedis 包来操作 Redis,实现在 pom.xml 添加 Jedis 框架的引用,配置如下:


<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.3.0</version>
</dependency>


具体的 Java 实现代码如下:


import redis.clients.jedis.Jedis;
public class RedisLimit {
    // Redis 操作客户端
    static Jedis jedis = new Jedis("127.0.0.1", 6379);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 15; i++) {
            boolean res = isPeriodLimiting("java", 3, 10);
            if (res) {
                System.out.println("正常执行请求:" + i);
            } else {
                System.out.println("被限流:" + i);
            }
        }
        // 休眠 4s
        Thread.sleep(4000);
        // 超过最大执行时间之后,再从发起请求
        boolean res = isPeriodLimiting("java", 3, 10);
        if (res) {
            System.out.println("休眠后,正常执行请求");
        } else {
            System.out.println("休眠后,被限流");
        }
    }
    /**
     * 限流方法(滑动时间算法)
     * @param key      限流标识
     * @param period   限流时间范围(单位:秒)
     * @param maxCount 最大运行访问次数
     * @return
     */
    private static boolean isPeriodLimiting(String key, int period, int maxCount) {
        long nowTs = System.currentTimeMillis(); // 当前时间戳
        // 删除非时间段内的请求数据(清除老访问数据,比如 period=60 时,标识清除 60s 以前的请求记录)
        jedis.zremrangeByScore(key, 0, nowTs - period * 1000);
        long currCount = jedis.zcard(key); // 当前请求次数
        if (currCount >= maxCount) {
            // 超过最大请求次数,执行限流
            return false;
        }
        // 未达到最大请求数,正常执行业务
        jedis.zadd(key, nowTs, "" + nowTs); // 请求记录 +1
        return true;
    }
}


以上程序的执行结果为:


正常执行请求:0

正常执行请求:1

正常执行请求:2

正常执行请求:3

正常执行请求:4

正常执行请求:5

正常执行请求:6

正常执行请求:7

正常执行请求:8

正常执行请求:9

被限流:10

被限流:11

被限流:12

被限流:13

被限流:14

休眠后,正常执行请求


此实现方式存在的缺点有两个:


  • 使用 ZSet 存储有每次的访问记录,如果数据量比较大时会占用大量的空间,比如 60s 允许 100W 访问时;
  • 此代码的执行非原子操作,先判断后增加,中间空隙可穿插其他业务逻辑的执行,最终导致结果不准确。


2.漏桶算法


漏桶算法的灵感源于漏斗,如下图所示:


image.png


滑动时间算法有一个问题就是在一定范围内,比如 60s 内只能有 10 个请求,当第一秒时就到达了 10 个请求,那么剩下的 59s 只能把所有的请求都给拒绝掉,而漏桶算法可以解决这个问题。


漏桶算法类似于生活中的漏斗,无论上面的水流倒入漏斗有多大,也就是无论请求有多少,它都是以均匀的速度慢慢流出的。当上面的水流速度大于下面的流出速度时,漏斗会慢慢变满,当漏斗满了之后就会丢弃新来的请求;当上面的水流速度小于下面流出的速度的话,漏斗永远不会被装满,并且可以一直流出。


漏桶算法的实现步骤是,先声明一个队列用来保存请求,这个队列相当于漏斗,当队列容量满了之后就放弃新来的请求,然后重新声明一个线程定期从任务队列中获取一个或多个任务进行执行,这样就实现了漏桶算法。


上面我们演示 Nginx 的控制速率其实使用的就是漏桶算法,当然我们也可以借助 Redis 很方便的实现漏桶算法。


我们可以使用 Redis 4.0 版本中提供的 Redis-Cell 模块,该模块使用的是漏斗算法,并且提供了原子的限流指令,而且依靠 Redis 这个天生的分布式程序就可以实现比较完美的限流了。


Redis-Cell 实现限流的方法也很简单,只需要使用一条指令 cl.throttle 即可,使用示例如下:


> cl.throttle mylimit 15 30 60
1)(integer)0 # 0 表示获取成功,1 表示拒绝
2)(integer)15 # 漏斗容量
3)(integer)14 # 漏斗剩余容量
4)(integer)-1 # 被拒绝之后,多长时间之后再试(单位:秒)-1 表示无需重试
5)(integer)2 # 多久之后漏斗完全空出来


其中 15 为漏斗的容量,30 / 60s 为漏斗的速率。


3.令牌算法


在令牌桶算法中有一个程序以某种恒定的速度生成令牌,并存入令牌桶中,而每个请求需要先获取令牌才能执行,如果没有获取到令牌的请求可以选择等待或者放弃执行,如下图所示:


image.png


我们可以使用 Google 开源的 guava 包,很方便的实现令牌桶算法,首先在 pom.xml 添加 guava 引用,配置如下:


<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.2-jre</version>
</dependency>


具体实现代码如下:


import com.google.common.util.concurrent.RateLimiter;
import java.time.Instant;
/**
 * Guava 实现限流
 */
public class RateLimiterExample {
    public static void main(String[] args) {
        // 每秒产生 10 个令牌(每 100 ms 产生一个)
        RateLimiter rt = RateLimiter.create(10);
        for (int i = 0; i < 11; i++) {
            new Thread(() -> {
                // 获取 1 个令牌
                rt.acquire();
                System.out.println("正常执行方法,ts:" + Instant.now());
            }).start();
        }
    }
}


以上程序的执行结果为:


正常执行方法,ts:2020-05-15T14:46:37.175Z
正常执行方法,ts:2020-05-15T14:46:37.237Z
正常执行方法,ts:2020-05-15T14:46:37.339Z
正常执行方法,ts:2020-05-15T14:46:37.442Z
正常执行方法,ts:2020-05-15T14:46:37.542Z
正常执行方法,ts:2020-05-15T14:46:37.640Z
正常执行方法,ts:2020-05-15T14:46:37.741Z
正常执行方法,ts:2020-05-15T14:46:37.840Z
正常执行方法,ts:2020-05-15T14:46:37.942Z
正常执行方法,ts:2020-05-15T14:46:38.042Z
正常执行方法,ts:2020-05-15T14:46:38.142Z


从以上结果可以看出令牌确实是每 100ms 产生一个,而 acquire() 方法为阻塞等待获取令牌,它可以传递一个 int 类型的参数,用于指定获取令牌的个数。它的替代方法还有 tryAcquire(),此方法在没有可用令牌时就会返回 false 这样就不会阻塞等待了。当然 tryAcquire() 方法也可以设置超时时间,未超过最大等待时间会阻塞等待获取令牌,如果超过了最大等待时间,还没有可用的令牌就会返回 false。


注意:使用 guava 实现的令牌算法属于程序级别的单机限流方案,而上面使用 Redis-Cell 的是分布式的限流方案。


总结


本文提供了 6 种具体的实现限流的手段,他们分别是:Tomcat 使用 maxThreads 来实现限流;Nginx 提供了两种限流方式,一是通过 limit_req_zoneburst 来实现速率限流,二是通过 limit_conn_zonelimit_conn 两个指令控制并发连接的总数。最后我们讲了时间窗口算法借助 Redis 的有序集合可以实现,还有漏桶算法可以使用 Redis-Cell 来实现,以及令牌算法可以解决 Google 的 guava 包来实现。


需要注意的是借助 Redis 实现的限流方案可用于分布式系统,而 guava 实现的限流只能应用于单机环境。如果你嫌弃服务器端限流麻烦,甚至可以在不改代码的情况下直接使用容器限流(Nginx 或 Tomcat),但前提是能满足你的业务需求。


好了,文章到这里就结束了,期待我们下期再会~

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
12月前
|
NoSQL 算法 Java
面试官:网关如何实现限流?
面试官:网关如何实现限流?
535 2
面试官:网关如何实现限流?
|
3月前
|
搜索推荐 中间件 微服务
常见的降级思路 面试准备
【8月更文挑战第16天】
46 2
|
4月前
|
算法 NoSQL Java
常用的限流算法有哪些?你听说过几种?
限流,就是指限制流量请求的频次。在高并发情况下,它是一种保护系统的策略,避免了在流量高峰时系统崩溃,造成系统的不可用。
66 1
|
4月前
|
缓存 算法 Java
高并发架构设计三大利器:缓存、限流和降级问题之使用代码实现漏桶算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之使用代码实现漏桶算法问题如何解决
|
4月前
|
存储 算法 Java
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
|
4月前
|
存储 算法 缓存
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法的原理是什么
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法的原理是什么
|
4月前
|
算法 API 缓存
高并发架构设计三大利器:缓存、限流和降级问题之固定窗口限流算法的原理是什么
高并发架构设计三大利器:缓存、限流和降级问题之固定窗口限流算法的原理是什么
|
6月前
|
算法 NoSQL 应用服务中间件
你们公司用的限流方案,可以讲讲吗
面试官:听说你是公司技术一号位,那我就考考你吧😊。对于ip的限流,我们是直接使用了Nginx的限流,Nginx的limit_req_zone可以设置每个IP地址在单位时间内所允许发起的请求数。
|
6月前
|
Java 应用服务中间件 nginx
面试官:限流的实现方式有哪些?
面试官:限流的实现方式有哪些?
135 5
|
算法 NoSQL 网络协议
没有10年的功力,根本不可能设计出这么好的高并发限流方案!
没有10年的功力,根本不可能设计出这么好的高并发限流方案!