Redis令牌桶(Token Bucket)是一种常用的限流算法,可以用来控制请求的速率,防止系统因过多请求而崩溃。令牌桶算法基于一个简单的概念:在固定的时间间隔内生成一定数量的令牌,每个请求需要消耗一个令牌,如果令牌不足,请求将被拒绝或等待。下面是Redis令牌桶的详细讲解。
令牌桶算法原理
- 令牌桶:令牌桶是一个容器,用于存放令牌。令牌以固定的速率被加入到桶中,桶有一个最大容量,超过这个容量的令牌将被丢弃。
- 请求处理:每个请求需要从桶中获取一个令牌才能被处理。如果桶中没有令牌,请求要么被拒绝,要么等待直到有令牌可用。
- 生成令牌:令牌按照固定的时间间隔生成,例如每秒生成10个令牌。
- 消耗令牌:每个请求消耗一个令牌,当令牌不足时,请求会被限流。
实现Redis令牌桶
通过Redis的INCR
和EXPIRE
等原子操作,可以实现分布式环境下的令牌桶算法。下面是一个示例实现:
Lua脚本实现
Lua脚本可以保证操作的原子性,在Redis中执行多个命令时避免竞争条件。
-- redis_token_bucket.lua
local key = KEYS[1] -- 令牌桶的键名
local rate = tonumber(ARGV[1]) -- 令牌生成速率 (每秒生成的令牌数)
local capacity = tonumber(ARGV[2]) -- 令牌桶的容量
local now = tonumber(ARGV[3]) -- 当前时间戳(毫秒)
local requested = tonumber(ARGV[4]) -- 请求消耗的令牌数
-- 获取桶的信息
local last_tokens = tonumber(redis.call('get', key .. ':tokens')) or capacity
local last_refreshed = tonumber(redis.call('get', key .. ':timestamp')) or now
-- 计算生成的令牌数
local delta = math.max(0, now - last_refreshed) * rate / 1000
local filled_tokens = math.min(capacity, last_tokens + delta)
-- 检查是否有足够的令牌
if filled_tokens < requested then
return {0, filled_tokens} -- 令牌不足,返回失败
else
-- 更新令牌桶信息
filled_tokens = filled_tokens - requested
redis.call('set', key .. ':tokens', filled_tokens)
redis.call('set', key .. ':timestamp', now)
return {1, filled_tokens} -- 请求成功,返回剩余令牌数
end
Java代码调用Lua脚本
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class TokenBucket {
private final JedisPool jedisPool;
private final String script;
public TokenBucket(JedisPool jedisPool) {
this.jedisPool = jedisPool;
// Lua脚本内容
this.script = "local key = KEYS[1] " +
"local rate = tonumber(ARGV[1]) " +
"local capacity = tonumber(ARGV[2]) " +
"local now = tonumber(ARGV[3]) " +
"local requested = tonumber(ARGV[4]) " +
"local last_tokens = tonumber(redis.call('get', key .. ':tokens')) or capacity " +
"local last_refreshed = tonumber(redis.call('get', key .. ':timestamp')) or now " +
"local delta = math.max(0, now - last_refreshed) * rate / 1000 " +
"local filled_tokens = math.min(capacity, last_tokens + delta) " +
"if filled_tokens < requested then " +
" return {0, filled_tokens} " +
"else " +
" filled_tokens = filled_tokens - requested " +
" redis.call('set', key .. ':tokens', filled_tokens) " +
" redis.call('set', key .. ':timestamp', now) " +
" return {1, filled_tokens} " +
"end";
}
public boolean acquireToken(String key, int rate, int capacity, int requested) {
try (Jedis jedis = jedisPool.getResource()) {
long now = System.currentTimeMillis();
Object result = jedis.eval(script, 1, key, String.valueOf(rate), String.valueOf(capacity), String.valueOf(now), String.valueOf(requested));
List<Long> results = (List<Long>) result;
return results.get(0) == 1;
}
}
}
使用示例
public class Main {
public static void main(String[] args) {
JedisPool jedisPool = new JedisPool("localhost", 6379);
TokenBucket tokenBucket = new TokenBucket(jedisPool);
String key = "my_rate_limiter";
int rate = 10; // 每秒生成10个令牌
int capacity = 100; // 令牌桶的容量
int requested = 1; // 每次请求消耗1个令牌
boolean allowed = tokenBucket.acquireToken(key, rate, capacity, requested);
if (allowed) {
System.out.println("Request allowed");
} else {
System.out.println("Request denied");
}
jedisPool.close();
}
}