redis实现漏桶算法--https://blog.csdn.net/m0_74908430/article/details/155076710

其他限流算法实现--参考

好的,我们来探讨如何基于 Redis 实现漏桶算法。

漏桶算法(Leaky Bucket Algorithm)是一种经典的限流算法,它可以控制请求的速率,平滑突发流量,防止系统因瞬时高并发而被击垮。

算法原理

  1. 想象一个漏斗:请求就像水一样倒入漏斗。
  2. 固定流出速率:漏斗底部有一个小孔,水会以一个固定的速率(比如每秒 10 滴)流出。
  3. 缓存与丢弃
    • 如果水流倒入的速度小于或等于流出速度,那么漏斗里不会积水,所有请求都会被平稳处理。
    • 如果水流倒入的速度大于流出速度,多余的水就会暂时存留在漏斗中。
    • 如果漏斗被倒满了,再继续倒入的水就会溢出(被丢弃),这对应于请求被限流。

Redis 实现方案

利用 Redis 的 Hash 数据结构和 Lua 脚本可以高效地实现漏桶算法。Hash 用于存储漏桶的状态,Lua 脚本保证了多个命令执行的原子性。

1. 数据结构设计

我们用一个 Redis Hash 来存储一个漏桶的状态,key 可以是限流的对象(如用户ID、接口名等)。Hashfield 包括:

  • capacity: 漏桶的总容量。
  • rate: 漏桶的漏水速率(单位:请求/秒)。
  • water: 当前漏桶中的水量(即等待处理的请求数)。
  • last_leak_time: 上一次漏水的时间戳(单位:毫秒)。
2. Lua 脚本实现核心逻辑

Lua 脚本是实现这个功能的关键,因为它可以在 Redis 服务器端原子地执行一系列命令,避免了在高并发下出现 race condition(竞态条件)。

脚本逻辑如下

  1. 获取当前时间戳。
  2. 计算从上一次漏水到现在,应该漏掉多少水。
  3. 更新当前的水量(减去漏掉的水量,但不能小于 0)。
  4. 检查如果再加入一滴水(当前请求),是否会超过桶的容量。
  5. 如果没超过,就将水量加 1,并更新最后操作时间,返回 1 表示请求允许通过。
  6. 如果超过了,返回 0 表示请求被限流。

以下是 Lua 脚本代码

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 中,你可以使用 JedisSpring Data Redis 来执行这个 Lua 脚本。

使用 Jedis 的示例

java 复制代码
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();
                }
            }
        }
    }
}

总结与注意事项

  1. 原子性:Lua 脚本确保了整个限流判断和状态更新是原子操作,这在分布式环境中至关重要。
  2. 性能 :Redis 处理 Lua 脚本非常快,足以应对高并发场景。预加载脚本(scriptLoadevalsha)可以进一步提升性能。
  3. 内存占用 :每个限流对象都会在 Redis 中创建一个 Hash,如果限流对象非常多,需要注意 Redis 的内存使用情况。可以考虑为 Hash 设置过期时间,或者定期清理长时间无访问的漏桶状态。
  4. 精度:该实现依赖于 Redis 的系统时间。如果 Redis 服务器之间时间不同步,可能会导致限流精度出现微小偏差,但通常在可接受范围内。
  5. 与令牌桶的区别:漏桶算法流出速率恒定,能有效平滑流量。而令牌桶算法可以在令牌积累后允许一定的突发流量。选择哪种算法取决于具体的业务需求。
相关推荐
Jerry10 分钟前
KeetCode 44. 开发商购买土地
算法
Jerry36 分钟前
KeetCode 58. 区间和
算法
宠友信息40 分钟前
多端数据互通场景下Spring Boot仿小红书源码结构设计
数据库·spring boot·redis·缓存·架构
长不胖的路人甲1 小时前
Redis 缓存的数据持久化方案讲解
数据库·redis·缓存
Jerry1 小时前
LeetCode 209. 长度最小的子数组
算法
长不胖的路人甲1 小时前
Redis 单线程为什么速度很快
数据库·redis·缓存
彦为君2 小时前
算法思维与经典智力题
java·前端·redis·算法
智能优化与强化学习2 小时前
Gym(Gymnasium)仿真环境详解(二):环境简介、入门算法、调参要点、核心挑战
算法·强化学习·gym·零基础入门·算法评估
mxwin2 小时前
Unity Shader exp 函数的算法与渲染应用
算法·unity·游戏引擎·shader
“码”力全开2 小时前
AI视频分析误报优化完整流程
算法·架构·边缘计算