redis实现漏桶算法--https://blog.youkuaiyun.com/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 脚本
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 的示例

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. 与令牌桶的区别:漏桶算法流出速率恒定,能有效平滑流量。而令牌桶算法可以在令牌积累后允许一定的突发流量。选择哪种算法取决于具体的业务需求。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值