1、简介
令牌桶算法比较简单,它就好比摇号买房,拿到号的人才有资格买,没拿到号的就只能等下次了(还好小编不需摇号,因为买不起!)。在实际的开发中,系统会维护一个容器用于存放令牌(token),并且系统以一个固定速率往容器中添加令牌(token),这个速率通常更加系统的处理能力来权衡。当客户端的请求打过来时,需要从令牌桶中获取到令牌(token)之后,这个请求才会被处理,否则直接拒绝服务。令牌桶限流的关键在于发放令牌的速率和令牌桶的容量。
实现令牌桶限流的方式有很多种,本文讲述的是基于 Redis 的 Redis-Cell 限流模块,这是 Redis 提供的适用于分布式系统、高效、准确的限流方式,使用十分广泛,而且非常简单!
2、Redis-Cell 的安装
Redis 默认是没有集成 Redis-Cell 这个限流模块的,就好比 Redis 使用布隆过滤器一样,我们也需要对该模块进行安装与集成。
2.1 GitHub 源码 &安装包
Redis-Cell 的 GitHub 地址:
https://github.com/brandur/redis-cell
Redis-Cell 基于 Rust 语言开发,如果不想花费精力去搞 Rust 环境,那么可以直接下载与你的操作系统对应的安装包(这个很关键,我就安装了挺多次的,如果安装的问题比较多的话,也建议降低一个 release 版本!)
下载对应的安装包:
https://github.com/brandur/redis-cell/releases/download/v0.3.0/redis-cell-v0.3.0-armv7-unknown-linux-gnueabihf.tar.gz
如果不清楚自己的服务器(Linux)版本的,可以事先查看后再下载安装包:
# Linux 查看当前操作系统的内核信息uname -a# 查看当前操作系统系统的版本信息cat /proc/version
复制代码
2.2 安装 &异常处理
tar -zxvf redis-cell-v0.2.5-powerpc64-unknown-linux-gnu.tar.gz
复制代码
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
复制代码
yum install gccwget http://ftp.gnu.org/gnu/glibc/glibc-2.18.tar.gztar zxf glibc-2.18.tar.gz cd glibc-2.18/mkdir buildcd build/../configure --prefix=/usrmake -j4make install
复制代码
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 11) (integer) 0 # 当前请求是否被允许,0表示允许,1表示不允许2) (integer) 11 # 令牌桶的最大容量,令牌桶中令牌数的最大值3) (integer) 10 # 令牌桶中当前的令牌数4) (integer) -1 # 如果被拒绝,需要多长时间后在重试,如果当前被允许则为-15) (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次请求失败
复制代码
评论