令牌桶限流

李子捌 发表于 2021/10/15 13:21:17 2021/10/15
【摘要】 令牌桶算法比较简单,它就好比摇号买房,拿到号的人才有资格买,没拿到号的就只能等下次了(还好小编不需摇号,因为买不起!)。

1、简介

令牌桶算法比较简单,它就好比摇号买房,拿到号的人才有资格买,没拿到号的就只能等下次了(还好小编不需摇号,因为买不起!)。

在实际的开发中,系统会维护一个容器用于存放令牌(token),并且系统以一个固定速率往容器中添加令牌(token),这个速率通常更加系统的处理能力来权衡。当客户端的请求打过来时,需要从令牌桶中获取到令牌(token)之后,这个请求才会被处理,否则直接拒绝服务。

令牌桶限流的关键在于发放令牌的速率和令牌桶的容量。

实现令牌桶限流的方式有很多种,本文讲述的是基于Redis的Redis-Cell限流模块,这是Redis提供的适用于分布式系统、高效、准确的限流方式,使用十分广泛,而且非常简单!


2、Redis-Cell的安装

Redis默认是没有集成Redis-Cell这个限流模块的,就好比Redis使用布隆过滤器一样,我们也需要对该模块进行安装与集成。


2.1 GitHub源码&安装包

Redis-Cell的GitHub地址:

Redis-Cell基于Rust语言开发,如果不想花费精力去搞Rust环境,那么可以直接下载与你的操作系统对应的安装包(这个很关键,我就安装了挺多次的,如果安装的问题比较多的话,也建议降低一个release版本!)


下载对应的安装包:

如果不清楚自己的服务器(Linux)版本的,可以事先查看后再下载安装包:

# Linux 查看当前操作系统的内核信息
uname -a
# 查看当前操作系统系统的版本信息
cat /proc/version


2.2 安装&异常处理

  • 在Redis的安装目录的同级目录下,新建文件夹Redis-Cell,将压缩包上传后解压
tar -zxvf redis-cell-v0.2.5-powerpc64-unknown-linux-gnu.tar.gz
  • 解压后出现如下文件,复制libredis_cell.so文件的路径(pwd查看当前路径

  • 修改Redis配置文件,redis.conf,添加完成后记得保存后再退出

  • 重启Redis,如果启动正常,进入redis客户端,通过module list查看挂载的模块是否有Redis-Cell

  • 测试指令,出现如下情况说明集成Redis-Cell成功

  • 如果重启Redis后,客户端无法连接成功,说明Redis启动失败,这个时候我们需要查看Redis的启动日志,如果已经配置日志文件的可以直接查看日志定位问题,如果还未配置日志文件的需要先配置日志文件,redis.conf添加日志文件路径地址,再次重启,查看日志文件输出的错误日志

  • 错误可能千奇百怪,问题不大搞技术就不要心急,一个个解决,我这里记录下我最后遇到的问题,/lib64/libc.so.6: version `GLIBC_2.18‘ not found
43767:M 08 Sep 2021 21:39:39.643 # Module /usr/local/soft/Redis-Cell-0.3.0/libredis_cell.so failed to load: /lib64/libc.so.6: version `GLIBC_2.18' not found (required by /usr/local/soft/Redis-Cell-0.3.0/libredis_cell.so)
43767:M 08 Sep 2021 21:39:39.643 # Can't load module from /usr/local/soft/Redis-Cell-0.3.0/libredis_cell.so: server aborting
  • 缺失GLIBC_2.18,那就安装它(最后两个编译的过程时间比较长,耐心等待几分钟)
yum install gcc
wget http://ftp.gnu.org/gnu/glibc/glibc-2.18.tar.gz
tar zxf glibc-2.18.tar.gz 
cd glibc-2.18/
mkdir build
cd build/
../configure --prefix=/usr
make -j4
make install
  • 安装完成后,重启Redis,测试是否安装成功,循环上面的过程,通过日志分析错误即可


3、CL.THROTTLE指令

指令CL.THROTTLE参数含义

CL.THROTTLE liziba  10  5 60 1
               ▲     ▲  ▲  ▲ ▲
               |     |  |  | └───── apply 1 token (default if omitted) (本次申请一个token)
               |     |  └──┴─────── 5 tokens / 60 seconds  (60秒添加5个token到令牌桶中)
               |     └───────────── 10 max_burst	(最大的突发请求,不是令牌桶的最大容量)
               └─────────────────── key "liziba" (限流key)

输出参数值含义

127.0.0.1:6379> cl.throttle liziba 10 5 60 1
1) (integer) 0					# 当前请求是否被允许,0表示允许,1表示不允许
2) (integer) 11					# 令牌桶的最大容量,令牌桶中令牌数的最大值
3) (integer) 10				  # 令牌桶中当前的令牌数
4) (integer) -1					# 如果被拒绝,需要多长时间后在重试,如果当前被允许则为-1
5) (integer) 12					# 多长时间后令牌桶中的令牌会满

这里唯一有歧义的可能是max_burst,这个并不是令牌桶的最大容量,从作者的README.md中的解释也可以看出来

The total limit of the key (max_burst + 1). This is equivalent to the common X-RateLimit-Limit HTTP header.


4、Java调用Redis-Cell模块实现限流

4.1 导入依赖

<dependency>
  <groupId>io.lettuce</groupId>
  <artifactId>lettuce-core</artifactId>
  <version>5.3.4.RELEASE</version>
  <!--排除 netty 包冲突-->
  <exclusions>
    <exclusion>
      <groupId>io.netty</groupId>
      <artifactId>netty-buffer</artifactId>
    </exclusion>
    <exclusion>
      <groupId>io.netty</groupId>
      <artifactId>netty-common</artifactId>
    </exclusion>
    <exclusion>
      <groupId>io.netty</groupId>
      <artifactId>netty-codec</artifactId>
    </exclusion>
    <exclusion>
      <groupId>io.netty</groupId>
      <artifactId>netty-transport</artifactId>
    </exclusion>
  </exclusions>
</dependency>

4.2 实现代码

Redis命令接口定义:

package com.lizba.redis.limit.tokenbucket;

import io.lettuce.core.dynamic.Commands;
import io.lettuce.core.dynamic.annotation.Command;

import java.util.List;

/**
 * <p>
 *      Redis命令接口定义
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/8 23:50
 */
public interface IRedisCommand extends Commands {


    /**
     * 定义限流方法
     *
     * @param key           限流key
     * @param maxBurst      最大的突发请求,桶容量等于maxBurst + 1
     * @param tokens        tokens 与 seconds 是组合参数,表示seconds秒内添加个tokens
     * @param seconds       tokens 与 seconds 是组合参数,表示seconds秒内添加个tokens
     * @param apply         当前申请的token数
     * @return
     */
    @Command("CL.THROTTLE ?0 ?1 ?2 ?3 ?4")
    List<Object> throttle(String key, long maxBurst, long tokens, long seconds, long apply);

}

Redis-Cell令牌桶限流类定义:

package com.lizba.redis.limit.tokenbucket;

import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.dynamic.RedisCommandFactory;

import java.util.List;

/**
 * <p>
 *      Redis-Cell令牌桶限流
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/8 23:47
 */
public class TokenBucketRateLimiter {

    private static final String SUCCESS = "0";
    private RedisClient client;
    private StatefulRedisConnection<String, String> connection;
    private IRedisCommand command;

    public TokenBucketRateLimiter(RedisClient client) {
        this.client = client;
        this.connection = client.connect();
        this.command = new RedisCommandFactory(connection).getCommands(IRedisCommand.class);
    }


    /**
     * 请是否被允许
     *
     * @param key
     * @param maxBurst
     * @param tokens
     * @param seconds
     * @param apply
     * @return
     */
    public boolean isActionAllowed(String key, long maxBurst, long tokens, long seconds, long apply) {
        List<Object> result = command.throttle(key, maxBurst, tokens, seconds, apply);
        if (result != null && result.size() > 0) {
            return SUCCESS.equals(result.get(0).toString());
        }
        return false;
    }

}

测试代码:

package com.lizba.redis.limit.tokenbucket;

import io.lettuce.core.RedisClient;

/**
 * <p>
 *      测试令牌桶限流
 *      测试参数 cl.throttle liziba 10 5 60 1
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/9 0:02
 */
public class TestTokenBucketRateLimiter {

    public static void main(String[] args) {
        RedisClient client = RedisClient.create("redis://192.168.211.108:6379");
        TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(client);
        // cl.throttle liziba 10 5 60 1
        for (int i = 1; i <= 15; i++) {
            boolean success = limiter.isActionAllowed("liziba", 10, 5, 60, 1);
            System.out.println("第" + i + "次请求" + (success ? "成功" : "失败"));
        }

    }

}

测试结果(这里也说明了令牌桶的容量是max_burst + 1):

第0次请求成功
第1次请求成功
第2次请求成功
第3次请求成功
第4次请求成功
第5次请求成功
第6次请求成功
第7次请求成功
第8次请求成功
第9次请求成功
第10次请求成功
第11次请求成功
第14次请求失败
第15次请求失败
第14次请求失败
第15次请求失败
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。