其他限流算法实现–参考
好的,我们来探讨如何基于 Redis 实现漏桶算法。
漏桶算法(Leaky Bucket Algorithm)是一种经典的限流算法,它可以控制请求的速率,平滑突发流量,防止系统因瞬时高并发而被击垮。
算法原理
- 想象一个漏斗:请求就像水一样倒入漏斗。
- 固定流出速率:漏斗底部有一个小孔,水会以一个固定的速率(比如每秒 10 滴)流出。
- 缓存与丢弃:
- 如果水流倒入的速度小于或等于流出速度,那么漏斗里不会积水,所有请求都会被平稳处理。
- 如果水流倒入的速度大于流出速度,多余的水就会暂时存留在漏斗中。
- 如果漏斗被倒满了,再继续倒入的水就会溢出(被丢弃),这对应于请求被限流。
Redis 实现方案
利用 Redis 的 Hash 数据结构和 Lua 脚本可以高效地实现漏桶算法。Hash 用于存储漏桶的状态,Lua 脚本保证了多个命令执行的原子性。
1. 数据结构设计
我们用一个 Redis Hash 来存储一个漏桶的状态,key 可以是限流的对象(如用户ID、接口名等)。Hash 的 field 包括:
capacity: 漏桶的总容量。rate: 漏桶的漏水速率(单位:请求/秒)。water: 当前漏桶中的水量(即等待处理的请求数)。last_leak_time: 上一次漏水的时间戳(单位:毫秒)。
2. Lua 脚本实现核心逻辑
Lua 脚本是实现这个功能的关键,因为它可以在 Redis 服务器端原子地执行一系列命令,避免了在高并发下出现 race condition(竞态条件)。
脚本逻辑如下:
- 获取当前时间戳。
- 计算从上一次漏水到现在,应该漏掉多少水。
- 更新当前的水量(减去漏掉的水量,但不能小于 0)。
- 检查如果再加入一滴水(当前请求),是否会超过桶的容量。
- 如果没超过,就将水量加 1,并更新最后操作时间,返回
1表示请求允许通过。 - 如果超过了,返回
0表示请求被限流。
以下是 Lua 脚本代码:
-- 漏桶限流 Lua 脚本
local bucket_key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
-- 获取当前时间戳(毫秒)
local now = tonumber(redis.call('time')[1]) * 1000 + tonumber(redis.call('time')[2])
-- 初始化漏桶(如果不存在)
local bucket = redis.call('hgetall', bucket_key)
if next(bucket) == nil then
redis.call('hmset', bucket_key,
'capacity', capacity,
'rate', rate,
'water', 0,
'last_leak_time', now
)
-- 首次请求,直接允许通过
redis.call('hincrby', bucket_key, 'water', 1)
return 1
end
-- 解析漏桶当前状态
local bucket_map = {}
for i = 1, #bucket, 2 do
bucket_map[bucket[i]] = bucket[i + 1]
end
local current_water = tonumber(bucket_map['water'])
local last_leak_time = tonumber(bucket_map['last_leak_time'])
-- 计算应该漏掉的水量
local time_passed = now - last_leak_time
-- 漏水速率是 每秒 rate 个,所以先将时间差转为秒
local leaks = math.floor(time_passed / 1000 * rate)
-- 更新水量和最后漏水时间
current_water = math.max(0, current_water - leaks)
redis.call('hmset', bucket_key,
'water', current_water,
'last_leak_time', now
)
-- 检查是否可以加入新的请求
if current_water + 1 <= capacity then
redis.call('hincrby', bucket_key, 'water', 1)
return 1 -- 允许通过
else
return 0 -- 限流
end
3. Java 代码调用示例
在 Java 中,你可以使用 Jedis 或 Spring Data Redis 来执行这个 Lua 脚本。
使用 Jedis 的示例:
import redis.clients.jedis.Jedis;
public class RedisLeakyBucketLimiter {
// Lua 脚本内容
private static final String LEAKY_BUCKET_LUA_SCRIPT = "local bucket_key = KEYS[1] ... "; // (此处省略完整脚本,请替换为上面的 Lua 代码)
private final Jedis jedis;
private final String scriptSha1;
public RedisLeakyBucketLimiter(Jedis jedis) {
this.jedis = jedis;
// 加载脚本并获取其 SHA1 校验和,这样可以避免每次都传输完整脚本
this.scriptSha1 = jedis.scriptLoad(LEAKY_BUCKET_LUA_SCRIPT);
}
/**
* 尝试获取令牌
* @param bucketKey 漏桶的 key,用于区分不同的限流对象
* @param capacity 漏桶容量
* @param rate 漏水速率(请求/秒)
* @return true if allowed, false otherwise
*/
public boolean tryAcquire(String bucketKey, int capacity, int rate) {
// 执行 Lua 脚本
Object result = jedis.evalsha(scriptSha1, 1, bucketKey, String.valueOf(capacity), String.valueOf(rate));
return Integer.parseInt(result.toString()) == 1;
}
public static void main(String[] args) {
// 示例
try (Jedis jedis = new Jedis("localhost", 6379)) {
RedisLeakyBucketLimiter limiter = new RedisLeakyBucketLimiter(jedis);
String userId = "user_123";
int capacity = 10; // 桶容量
int rate = 2; // 每秒允许 2 个请求
for (int i = 0; i < 15; i++) {
boolean allowed = limiter.tryAcquire(userId, capacity, rate);
System.out.println("Request " + (i + 1) + ": " + (allowed ? "Allowed" : "Denied"));
try {
Thread.sleep(200); // 模拟请求间隔
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}
总结与注意事项
- 原子性:Lua 脚本确保了整个限流判断和状态更新是原子操作,这在分布式环境中至关重要。
- 性能:Redis 处理 Lua 脚本非常快,足以应对高并发场景。预加载脚本(
scriptLoad和evalsha)可以进一步提升性能。 - 内存占用:每个限流对象都会在 Redis 中创建一个
Hash,如果限流对象非常多,需要注意 Redis 的内存使用情况。可以考虑为Hash设置过期时间,或者定期清理长时间无访问的漏桶状态。 - 精度:该实现依赖于 Redis 的系统时间。如果 Redis 服务器之间时间不同步,可能会导致限流精度出现微小偏差,但通常在可接受范围内。
- 与令牌桶的区别:漏桶算法流出速率恒定,能有效平滑流量。而令牌桶算法可以在令牌积累后允许一定的突发流量。选择哪种算法取决于具体的业务需求。
1万+






